Files
grafana/pkg/plugins/pluginassets/modulehash.go
Will Browne aa9b587cc1 Plugins: Add module hash field to plugin model (#116119)
* add module hash field to plugin model

* fix tests

* fix lint issues
2026-01-13 14:35:45 +00:00

91 lines
2.5 KiB
Go

package pluginassets
import (
"encoding/base64"
"encoding/hex"
"path"
"path/filepath"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
)
// CalculateModuleHash calculates the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
// The module hash is read from the plugin's cached manifest.
// For nested plugins, the module hash is read from the root parent plugin's manifest.
// If the plugin is unsigned or not a CDN plugin, an empty string is returned.
func CalculateModuleHash(p *plugins.Plugin, cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) string {
if cfg == nil || !cfg.Features.SriChecksEnabled {
return ""
}
if !p.Signature.IsValid() {
return ""
}
rootParent := findRootParent(p)
if rootParent.Manifest == nil {
return ""
}
if !rootParent.Manifest.IsV2() {
return ""
}
if !cdnEnabled(rootParent, cdn) {
return ""
}
modulePath := getModulePathInManifest(p, rootParent)
moduleHash, ok := rootParent.Manifest.Files[modulePath]
if !ok {
return ""
}
return convertHashForSRI(moduleHash)
}
// findRootParent returns the root parent plugin (the one that contains the manifest).
// For non-nested plugins, it returns the plugin itself.
func findRootParent(p *plugins.Plugin) *plugins.Plugin {
root := p
for root.Parent != nil {
root = root.Parent
}
return root
}
// getModulePathInManifest returns the path to module.js as it appears in the manifest.
// For nested plugins, this is the relative path from the root parent to the plugin's module.js.
// For non-nested plugins, this is simply "module.js".
func getModulePathInManifest(p *plugins.Plugin, rootParent *plugins.Plugin) string {
if p == rootParent {
return "module.js"
}
// Calculate the relative path from root parent to this plugin
relPath, err := rootParent.FS.Rel(p.FS.Base())
if err != nil {
return ""
}
// MANIFEST.txt uses forward slashes as path separators
pluginRootPath := filepath.ToSlash(relPath)
return path.Join(pluginRootPath, "module.js")
}
// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks.
func convertHashForSRI(h string) string {
hb, err := hex.DecodeString(h)
if err != nil {
return ""
}
return "sha256-" + base64.StdEncoding.EncodeToString(hb)
}
// cdnEnabled checks if a plugin is loaded via CDN
func cdnEnabled(p *plugins.Plugin, cdn *pluginscdn.Service) bool {
return p.FS.Type().CDN() || cdn.PluginSupported(p.ID)
}