From b55891bdee17418c07172d862471392c87081587 Mon Sep 17 00:00:00 2001 From: Todd Treece Date: Wed, 25 Mar 2026 08:58:58 -0400 Subject: [PATCH 1/3] Add stale backend build check --- pkg/analysis/passes/analysis.go | 2 + .../passes/gobuildinfo/gobuildinfo.go | 322 ++++++++++++++++++ .../passes/gobuildinfo/gobuildinfo_test.go | 275 +++++++++++++++ 3 files changed, 599 insertions(+) create mode 100644 pkg/analysis/passes/gobuildinfo/gobuildinfo.go create mode 100644 pkg/analysis/passes/gobuildinfo/gobuildinfo_test.go diff --git a/pkg/analysis/passes/analysis.go b/pkg/analysis/passes/analysis.go index 874354cb..51b161ac 100644 --- a/pkg/analysis/passes/analysis.go +++ b/pkg/analysis/passes/analysis.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/plugin-validator/pkg/analysis/passes/codediff" "github.com/grafana/plugin-validator/pkg/analysis/passes/coderules" "github.com/grafana/plugin-validator/pkg/analysis/passes/discoverability" + "github.com/grafana/plugin-validator/pkg/analysis/passes/gobuildinfo" "github.com/grafana/plugin-validator/pkg/analysis/passes/gomanifest" "github.com/grafana/plugin-validator/pkg/analysis/passes/gosec" "github.com/grafana/plugin-validator/pkg/analysis/passes/grafanadependency" @@ -67,6 +68,7 @@ var Analyzers = []*analysis.Analyzer{ checksum.Analyzer, coderules.Analyzer, discoverability.Analyzer, + gobuildinfo.Analyzer, gomanifest.Analyzer, gosec.Analyzer, includesnested.Analyzer, diff --git a/pkg/analysis/passes/gobuildinfo/gobuildinfo.go b/pkg/analysis/passes/gobuildinfo/gobuildinfo.go new file mode 100644 index 00000000..d1964240 --- /dev/null +++ b/pkg/analysis/passes/gobuildinfo/gobuildinfo.go @@ -0,0 +1,322 @@ +package gobuildinfo + +import ( + "debug/buildinfo" + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime/debug" + "strings" + + "github.com/bmatcuk/doublestar/v4" + "golang.org/x/mod/modfile" + + "github.com/grafana/plugin-validator/pkg/analysis" + "github.com/grafana/plugin-validator/pkg/analysis/passes/archive" + "github.com/grafana/plugin-validator/pkg/analysis/passes/nestedmetadata" + "github.com/grafana/plugin-validator/pkg/analysis/passes/sourcecode" + "github.com/grafana/plugin-validator/pkg/logme" +) + +var ( + binarySourceCodeRequired = &analysis.Rule{ + Name: "binary-source-code-required", + Severity: analysis.Warning, + } + binaryNoBuildInfo = &analysis.Rule{ + Name: "binary-no-build-info", + Severity: analysis.Error, + } + binaryDirtyBuild = &analysis.Rule{ + Name: "binary-dirty-build", + Severity: analysis.Error, + } + binaryPluginIDMismatch = &analysis.Rule{ + Name: "binary-plugin-id-mismatch", + Severity: analysis.Error, + } + binaryCGOEnabled = &analysis.Rule{ + Name: "binary-cgo-enabled", + Severity: analysis.Warning, + } + // binary-build-info-json-plugin-id-mismatch: the pluginID field in the SDK's + // embedded buildInfoJSON does not match the plugin.json ID. + binaryBuildInfoJSONPluginIDMismatch = &analysis.Rule{ + Name: "binary-build-info-json-plugin-id-mismatch", + Severity: analysis.Error, + } + // binary-build-info-json-version-mismatch: the version field in the SDK's + // embedded buildInfoJSON does not match the plugin.json version. + binaryBuildInfoJSONVersionMismatch = &analysis.Rule{ + Name: "binary-build-info-json-version-mismatch", + Severity: analysis.Error, + } + // binary-dep-not-in-gomod: a dependency compiled into the binary has no + // entry in the submitted go.mod. + binaryDepNotInGoMod = &analysis.Rule{ + Name: "binary-dep-not-in-gomod", + Severity: analysis.Error, + } + // binary-dep-gomod-version-mismatch: a dependency compiled into the binary + // is at a different version than declared in the submitted go.mod. + binaryDepGoModVersionMismatch = &analysis.Rule{ + Name: "binary-dep-gomod-version-mismatch", + Severity: analysis.Error, + } +) + +var Analyzer = &analysis.Analyzer{ + Name: "gobuildinfo", + Requires: []*analysis.Analyzer{archive.Analyzer, nestedmetadata.Analyzer, sourcecode.Analyzer}, + Run: run, + Rules: []*analysis.Rule{ + binarySourceCodeRequired, + binaryNoBuildInfo, + binaryDirtyBuild, + binaryPluginIDMismatch, + binaryCGOEnabled, + binaryBuildInfoJSONPluginIDMismatch, + binaryBuildInfoJSONVersionMismatch, + binaryDepNotInGoMod, + binaryDepGoModVersionMismatch, + }, + ReadmeInfo: analysis.ReadmeInfo{ + Name: "Go Build Info", + Description: "Validates embedded Go build metadata in backend plugin binaries.", + }, +} + +// sdkBuildInfo represents the JSON struct embedded via -ldflags by the Grafana +// plugin SDK. Older SDK versions include additional fields (repo, branch, hash, +// build) that are not present in newer versions. +type sdkBuildInfo struct { + PluginID string `json:"pluginID"` + Version string `json:"version"` +} + +func run(pass *analysis.Pass) (interface{}, error) { + archiveDir, ok := pass.ResultOf[archive.Analyzer].(string) + if !ok { + return nil, nil + } + + metadatamap, ok := pass.ResultOf[nestedmetadata.Analyzer].(nestedmetadata.Metadatamap) + if !ok { + return nil, nil + } + + sourceCodeDir, ok := pass.ResultOf[sourcecode.Analyzer].(string) + + hasBackend := false + for _, data := range metadatamap { + if data.Backend && data.Executable != "" { + hasBackend = true + break + } + } + + if hasBackend && (!ok || sourceCodeDir == "") { + pass.ReportResult( + pass.AnalyzerName, + binarySourceCodeRequired, + "source code is required to validate backend binaries", + "Provide the plugin source code to enable backend binary validation checks.", + ) + return nil, nil + } + + if !ok || sourceCodeDir == "" { + return nil, nil + } + + goMod := parseGoMod(filepath.Join(sourceCodeDir, "go.mod")) + + for pluginJSONPath, data := range metadatamap { + if !data.Backend || data.Executable == "" { + continue + } + + pluginRootDir := filepath.Join(archiveDir, filepath.Dir(pluginJSONPath)) + executableParentDir := filepath.Join(pluginRootDir, filepath.Dir(data.Executable)) + + binaries, err := doublestar.FilepathGlob( + executableParentDir + "/" + filepath.Base(data.Executable) + "*", + ) + if err != nil { + logme.Debugln("gobuildinfo: error finding binaries:", err) + continue + } + + for _, binary := range binaries { + checkBinary(pass, binary, data.ID, data.Info.Version, goMod) + } + } + + return nil, nil +} + +func checkBinary(pass *analysis.Pass, binaryPath, pluginID, pluginVersion string, goMod *modfile.File) { + info, err := buildinfo.ReadFile(binaryPath) + if err != nil { + pass.ReportResult( + pass.AnalyzerName, + binaryNoBuildInfo, + fmt.Sprintf("could not read build info from %s", filepath.Base(binaryPath)), + "The binary may have been stripped of Go build information. Ensure binaries are built without stripping build metadata.", + ) + return + } + + binaryName := filepath.Base(binaryPath) + + for _, setting := range info.Settings { + switch setting.Key { + case "vcs.modified": + if setting.Value == "true" { + pass.ReportResult( + pass.AnalyzerName, + binaryDirtyBuild, + fmt.Sprintf("%s: built from a dirty working tree", binaryName), + "The binary was built with uncommitted changes (vcs.modified=true). Binaries submitted for signing should be built from a clean git working tree.", + ) + } + case "CGO_ENABLED": + if setting.Value == "1" { + pass.ReportResult( + pass.AnalyzerName, + binaryCGOEnabled, + fmt.Sprintf("%s: built with CGO_ENABLED=1", binaryName), + "Building with CGO enabled makes builds harder to reproduce and verify. Consider building with CGO_ENABLED=0.", + ) + } + case "-ldflags": + checkPluginIDInLDFlags(pass, binaryName, setting.Value, pluginID) + checkBuildInfoJSON(pass, binaryName, setting.Value, pluginID, pluginVersion) + } + } + + if goMod != nil { + required := make(map[string]string, len(goMod.Require)) + for _, r := range goMod.Require { + required[r.Mod.Path] = r.Mod.Version + } + for _, dep := range info.Deps { + checkDepGoMod(pass, binaryName, dep, required) + } + } +} + +func checkPluginIDInLDFlags(pass *analysis.Pass, binaryName, ldflags, expectedID string) { + _, after, ok := strings.Cut(ldflags, "main.pluginID=") + if !ok { + return + } + after = strings.TrimPrefix(after, "'") + if end := strings.IndexAny(after, "' \t"); end >= 0 { + after = after[:end] + } + embeddedID := after + if embeddedID != expectedID { + pass.ReportResult( + pass.AnalyzerName, + binaryPluginIDMismatch, + fmt.Sprintf("%s: embedded plugin ID %q does not match plugin.json ID %q", binaryName, embeddedID, expectedID), + "The plugin ID embedded in the binary at build time does not match the plugin.json ID. Ensure the binary was built for this plugin.", + ) + } +} + +// extractBuildInfoJSON extracts the SDK buildInfoJSON value from ldflags. +// The package path changed between SDK versions: +// - old: github.com/grafana/grafana-plugin-sdk-go/build.buildInfoJSON +// - new: github.com/grafana/grafana-plugin-sdk-go/build/buildinfo.buildInfoJSON +func extractBuildInfoJSON(ldflags string) string { + for _, prefix := range []string{ + "github.com/grafana/grafana-plugin-sdk-go/build/buildinfo.buildInfoJSON=", + "github.com/grafana/grafana-plugin-sdk-go/build.buildInfoJSON=", + } { + _, after, ok := strings.Cut(ldflags, prefix) + if !ok { + continue + } + after = strings.TrimPrefix(after, "'") + if end := strings.IndexByte(after, '\''); end >= 0 { + return after[:end] + } + return after + } + return "" +} + +func checkBuildInfoJSON(pass *analysis.Pass, binaryName, ldflags, expectedPluginID, expectedVersion string) { + raw := extractBuildInfoJSON(ldflags) + if raw == "" { + return + } + var info sdkBuildInfo + if err := json.Unmarshal([]byte(raw), &info); err != nil { + return + } + if info.PluginID != "" && info.PluginID != expectedPluginID { + pass.ReportResult( + pass.AnalyzerName, + binaryBuildInfoJSONPluginIDMismatch, + fmt.Sprintf("%s: buildInfoJSON plugin ID %q does not match plugin.json ID %q", binaryName, info.PluginID, expectedPluginID), + "The plugin ID embedded in the SDK build info JSON does not match the plugin.json ID. Ensure the binary was built for this plugin.", + ) + } + if info.Version != "" && expectedVersion != "" && info.Version != expectedVersion { + pass.ReportResult( + pass.AnalyzerName, + binaryBuildInfoJSONVersionMismatch, + fmt.Sprintf("%s: buildInfoJSON version %q does not match plugin.json version %q", binaryName, info.Version, expectedVersion), + fmt.Sprintf("The binary was built for version %q but plugin.json declares version %q. Rebuild the backend binary.", info.Version, expectedVersion), + ) + } +} + +// parseGoMod reads and parses a go.mod file. +func parseGoMod(path string) *modfile.File { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + f, err := modfile.ParseLax(path, data, nil) + if err != nil { + return nil + } + return f +} + +func checkDepGoMod(pass *analysis.Pass, binaryName string, dep *debug.Module, goMod map[string]string) { + if dep.Replace != nil { + dep = dep.Replace + } + modVersion, ok := goMod[dep.Path] + if !ok { + pass.ReportResult( + pass.AnalyzerName, + binaryDepNotInGoMod, + fmt.Sprintf("%s: dependency %s@%s is not in go.mod", binaryName, dep.Path, dep.Version), + fmt.Sprintf( + "The binary was compiled with %s but it is not declared in go.mod. "+ + "Rebuild the backend binary after updating go.mod.", + dep.Path, + ), + ) + return + } + if dep.Version != modVersion { + pass.ReportResult( + pass.AnalyzerName, + binaryDepGoModVersionMismatch, + fmt.Sprintf("%s: dependency %s version mismatch: binary has %s, go.mod requires %s", binaryName, dep.Path, dep.Version, modVersion), + fmt.Sprintf( + "The binary was compiled with %s@%s but go.mod requires %s. "+ + "Rebuild the backend binary.", + dep.Path, dep.Version, modVersion, + ), + ) + } +} diff --git a/pkg/analysis/passes/gobuildinfo/gobuildinfo_test.go b/pkg/analysis/passes/gobuildinfo/gobuildinfo_test.go new file mode 100644 index 00000000..b7aef444 --- /dev/null +++ b/pkg/analysis/passes/gobuildinfo/gobuildinfo_test.go @@ -0,0 +1,275 @@ +package gobuildinfo + +import ( + "os" + "os/exec" + "path/filepath" + "runtime/debug" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/plugin-validator/pkg/analysis" + "github.com/grafana/plugin-validator/pkg/analysis/passes/archive" + "github.com/grafana/plugin-validator/pkg/analysis/passes/nestedmetadata" + "github.com/grafana/plugin-validator/pkg/analysis/passes/sourcecode" + "github.com/grafana/plugin-validator/pkg/testpassinterceptor" +) + +// buildTestBinary compiles a minimal Go binary into dir and returns its path. +// The binary embeds pluginID via -ldflags so checkPluginIDInLDFlags can be exercised. +func buildTestBinary(t *testing.T, dir, pluginID string) string { + t.Helper() + src := filepath.Join(dir, "main.go") + err := os.WriteFile(src, []byte(`package main +import _ "runtime/debug" +func main() {} +`), 0644) + require.NoError(t, err) + + out := filepath.Join(dir, "gpx_test_linux_amd64") + ldflags := "-X 'main.pluginID=" + pluginID + "'" + cmd := exec.Command("go", "build", "-ldflags", ldflags, "-o", out, src) + cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64") + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("build output: %s", output) + } + require.NoError(t, err) + return out +} + +func makePass(t *testing.T, archiveDir, sourceDir string, meta nestedmetadata.Metadatamap) (*analysis.Pass, *testpassinterceptor.TestPassInterceptor) { + t.Helper() + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: "./", + ResultOf: map[*analysis.Analyzer]interface{}{ + archive.Analyzer: archiveDir, + nestedmetadata.Analyzer: meta, + sourcecode.Analyzer: sourceDir, + }, + Report: interceptor.ReportInterceptor(), + } + return pass, &interceptor +} + +func TestNonBackendPluginSkipped(t *testing.T) { + meta := nestedmetadata.Metadatamap{ + "plugin.json": {ID: "test-plugin", Backend: false}, + } + pass, interceptor := makePass(t, t.TempDir(), "", meta) + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Empty(t, interceptor.Diagnostics) +} + +func TestCleanBinary(t *testing.T) { + dir := t.TempDir() + pluginID := "myorg-myplugin-datasource" + binary := buildTestBinary(t, dir, pluginID) + + meta := nestedmetadata.Metadatamap{ + "plugin.json": {ID: pluginID, Backend: true, Executable: "gpx_test"}, + } + pass, interceptor := makePass(t, dir, "", meta) + _, err := Analyzer.Run(pass) + require.NoError(t, err) + // A clean local build may have vcs.modified=true or no vcs info at all — only + // assert there's no plugin-id-mismatch or go-sum-mismatch. + for _, d := range interceptor.Diagnostics { + require.NotEqual(t, "binary-plugin-id-mismatch", d.Name) + require.NotEqual(t, "binary-go-sum-mismatch", d.Name) + } + _ = binary +} + + +func TestPluginIDMismatch(t *testing.T) { + dir := t.TempDir() + binary := buildTestBinary(t, dir, "myorg-otherplugin-datasource") + + meta := nestedmetadata.Metadatamap{ + "plugin.json": {ID: "myorg-myplugin-datasource", Backend: true, Executable: "gpx_test"}, + } + pass, interceptor := makePass(t, dir, dir, meta) + _, err := Analyzer.Run(pass) + require.NoError(t, err) + + var names []string + for _, d := range interceptor.Diagnostics { + names = append(names, d.Name) + } + require.Contains(t, names, "binary-plugin-id-mismatch") + _ = binary +} + +func TestNoBuildInfo(t *testing.T) { + dir := t.TempDir() + // Write a file that is not a Go binary. + notABinary := filepath.Join(dir, "gpx_fake_linux_amd64") + err := os.WriteFile(notABinary, []byte("not a go binary"), 0755) + require.NoError(t, err) + + meta := nestedmetadata.Metadatamap{ + "plugin.json": {ID: "myorg-fake-datasource", Backend: true, Executable: "gpx_fake"}, + } + pass, interceptor := makePass(t, dir, dir, meta) + _, err = Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, "binary-no-build-info", interceptor.Diagnostics[0].Name) +} + + +func TestCheckPluginIDInLDFlagsMatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + ldflags := `-w -s -X 'main.pluginID=myorg-myplugin-datasource' -X 'main.version=1.0.0'` + checkPluginIDInLDFlags(pass, "gpx_test", ldflags, "myorg-myplugin-datasource") + require.Empty(t, interceptor.Diagnostics) +} + +func TestCheckPluginIDInLDFlagsMismatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + ldflags := `-w -s -X 'main.pluginID=myorg-otherplugin-datasource'` + checkPluginIDInLDFlags(pass, "gpx_test", ldflags, "myorg-myplugin-datasource") + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, "binary-plugin-id-mismatch", interceptor.Diagnostics[0].Name) +} + +func TestCheckBuildInfoJSONMatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + // new SDK path + ldflags := `-w -s -X 'github.com/grafana/grafana-plugin-sdk-go/build/buildinfo.buildInfoJSON={"pluginID":"myorg-myplugin-datasource","version":"1.0.0"}'` + checkBuildInfoJSON(pass, "gpx_test", ldflags, "myorg-myplugin-datasource", "1.0.0") + require.Empty(t, interceptor.Diagnostics) +} + +func TestCheckBuildInfoJSONOldSDKMatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + // old SDK path with extra fields + ldflags := `-w -s -X 'github.com/grafana/grafana-plugin-sdk-go/build.buildInfoJSON={"time":1714395852089,"pluginID":"myorg-myplugin-datasource","version":"1.0.0","repo":"https://github.com/example/repo","branch":"main","hash":"abc123","build":42}'` + checkBuildInfoJSON(pass, "gpx_test", ldflags, "myorg-myplugin-datasource", "1.0.0") + require.Empty(t, interceptor.Diagnostics) +} + +func TestCheckBuildInfoJSONPluginIDMismatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + ldflags := `-w -s -X 'github.com/grafana/grafana-plugin-sdk-go/build/buildinfo.buildInfoJSON={"pluginID":"myorg-otherplugin-datasource","version":"1.0.0"}'` + checkBuildInfoJSON(pass, "gpx_test", ldflags, "myorg-myplugin-datasource", "1.0.0") + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, "binary-build-info-json-plugin-id-mismatch", interceptor.Diagnostics[0].Name) +} + +func TestCheckBuildInfoJSONVersionMismatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + ldflags := `-w -s -X 'github.com/grafana/grafana-plugin-sdk-go/build/buildinfo.buildInfoJSON={"pluginID":"myorg-myplugin-datasource","version":"1.0.0"}'` + checkBuildInfoJSON(pass, "gpx_test", ldflags, "myorg-myplugin-datasource", "2.0.0") + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, "binary-build-info-json-version-mismatch", interceptor.Diagnostics[0].Name) +} + +func TestCheckBuildInfoJSONAbsent(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + // binary built without SDK (no buildInfoJSON) — should be a no-op + ldflags := `-w -s -X 'main.pluginID=myorg-myplugin-datasource'` + checkBuildInfoJSON(pass, "gpx_test", ldflags, "myorg-myplugin-datasource", "1.0.0") + require.Empty(t, interceptor.Diagnostics) +} + +func TestParseGoMod(t *testing.T) { + dir := t.TempDir() + content := `module github.com/example/plugin + +go 1.21 + +require ( + github.com/foo/bar v1.2.3 + github.com/baz/qux v0.1.0 // indirect +) +` + err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(content), 0644) + require.NoError(t, err) + + f := parseGoMod(filepath.Join(dir, "go.mod")) + require.NotNil(t, f) + versions := make(map[string]string) + for _, r := range f.Require { + versions[r.Mod.Path] = r.Mod.Version + } + require.Equal(t, "v1.2.3", versions["github.com/foo/bar"]) + require.Equal(t, "v0.1.0", versions["github.com/baz/qux"]) +} + +func TestCheckDepGoModMatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + dep := &debug.Module{Path: "github.com/foo/bar", Version: "v1.2.3"} + goMod := map[string]string{"github.com/foo/bar": "v1.2.3"} + checkDepGoMod(pass, "gpx_test", dep, goMod) + require.Empty(t, interceptor.Diagnostics) +} + +func TestCheckDepGoModNotInGoMod(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + dep := &debug.Module{Path: "github.com/foo/bar", Version: "v1.2.3"} + goMod := map[string]string{} + checkDepGoMod(pass, "gpx_test", dep, goMod) + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, "binary-dep-not-in-gomod", interceptor.Diagnostics[0].Name) +} + +func TestCheckDepGoModVersionMismatch(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + dep := &debug.Module{Path: "github.com/foo/bar", Version: "v1.3.0"} + goMod := map[string]string{"github.com/foo/bar": "v1.2.3"} + checkDepGoMod(pass, "gpx_test", dep, goMod) + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, "binary-dep-gomod-version-mismatch", interceptor.Diagnostics[0].Name) +} + +func TestCheckDepGoModReplace(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{Report: interceptor.ReportInterceptor()} + pass.AnalyzerName = Analyzer.Name + + // Replace directive: check is against the replacement module + dep := &debug.Module{ + Path: "github.com/foo/bar", + Version: "v1.2.3", + Replace: &debug.Module{Path: "github.com/fork/bar", Version: "v1.2.3"}, + } + goMod := map[string]string{"github.com/fork/bar": "v1.2.3"} + checkDepGoMod(pass, "gpx_test", dep, goMod) + require.Empty(t, interceptor.Diagnostics) +} + From 2f151531ac36ed5aa30d1438791b508bcbc633ce Mon Sep 17 00:00:00 2001 From: Todd Treece Date: Wed, 25 Mar 2026 09:39:22 -0400 Subject: [PATCH 2/3] address pr feedback --- .../passes/gobuildinfo/gobuildinfo.go | 29 ++----------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/pkg/analysis/passes/gobuildinfo/gobuildinfo.go b/pkg/analysis/passes/gobuildinfo/gobuildinfo.go index d1964240..56bd175a 100644 --- a/pkg/analysis/passes/gobuildinfo/gobuildinfo.go +++ b/pkg/analysis/passes/gobuildinfo/gobuildinfo.go @@ -20,10 +20,6 @@ import ( ) var ( - binarySourceCodeRequired = &analysis.Rule{ - Name: "binary-source-code-required", - Severity: analysis.Warning, - } binaryNoBuildInfo = &analysis.Rule{ Name: "binary-no-build-info", Severity: analysis.Error, @@ -38,7 +34,7 @@ var ( } binaryCGOEnabled = &analysis.Rule{ Name: "binary-cgo-enabled", - Severity: analysis.Warning, + Severity: analysis.Error, } // binary-build-info-json-plugin-id-mismatch: the pluginID field in the SDK's // embedded buildInfoJSON does not match the plugin.json ID. @@ -71,7 +67,6 @@ var Analyzer = &analysis.Analyzer{ Requires: []*analysis.Analyzer{archive.Analyzer, nestedmetadata.Analyzer, sourcecode.Analyzer}, Run: run, Rules: []*analysis.Rule{ - binarySourceCodeRequired, binaryNoBuildInfo, binaryDirtyBuild, binaryPluginIDMismatch, @@ -108,24 +103,6 @@ func run(pass *analysis.Pass) (interface{}, error) { sourceCodeDir, ok := pass.ResultOf[sourcecode.Analyzer].(string) - hasBackend := false - for _, data := range metadatamap { - if data.Backend && data.Executable != "" { - hasBackend = true - break - } - } - - if hasBackend && (!ok || sourceCodeDir == "") { - pass.ReportResult( - pass.AnalyzerName, - binarySourceCodeRequired, - "source code is required to validate backend binaries", - "Provide the plugin source code to enable backend binary validation checks.", - ) - return nil, nil - } - if !ok || sourceCodeDir == "" { return nil, nil } @@ -178,7 +155,7 @@ func checkBinary(pass *analysis.Pass, binaryPath, pluginID, pluginVersion string pass.AnalyzerName, binaryDirtyBuild, fmt.Sprintf("%s: built from a dirty working tree", binaryName), - "The binary was built with uncommitted changes (vcs.modified=true). Binaries submitted for signing should be built from a clean git working tree.", + "The binary was built with uncommitted changes (vcs.modified=true). Binaries submitted for signing must be built from a clean git working tree. See https://grafana.com/developers/plugin-tools/publish-a-plugin/build-automation for more information.", ) } case "CGO_ENABLED": @@ -187,7 +164,7 @@ func checkBinary(pass *analysis.Pass, binaryPath, pluginID, pluginVersion string pass.AnalyzerName, binaryCGOEnabled, fmt.Sprintf("%s: built with CGO_ENABLED=1", binaryName), - "Building with CGO enabled makes builds harder to reproduce and verify. Consider building with CGO_ENABLED=0.", + "Grafana plugins must be built with CGO_ENABLED=0. CGO is not supported in the plugin runtime environment.", ) } case "-ldflags": From e0f209cc7a0eb0e59a541f9e284019ae1cd20525 Mon Sep 17 00:00:00 2001 From: Todd Treece Date: Wed, 25 Mar 2026 10:17:54 -0400 Subject: [PATCH 3/3] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index daed6ded..5560796a 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ Run "mage gen:readme" to regenerate this section. | Code Rules / `code-rules` | Checks for forbidden access to environment variables, file system or use of syscall module. | [semgrep](https://github.com/returntocorp/semgrep), `sourceCodeUri` | | Developer Jargon / `jargon` | Generally discourages use of code jargon in the documentation. | None | | Discoverability / `discoverability` | Warns about missing keywords and description that are used for plugin indexing in the catalog. | None | +| Go Build Info / `gobuildinfo` | Validates embedded Go build metadata in backend plugin binaries. | None | | Go Manifest / `go-manifest` | Validates the build manifest. | None | | Go Security Checker / `go-sec` | Inspects source code for security problems by scanning the Go AST. | [gosec](https://github.com/securego/gosec), `sourceCodeUri` | | JS Source Map / `jsMap` | Checks for required `module.js.map` file(s) in archive. | `sourceCodeUri` |