From 02e4ca3f0408bf73b5d1354b1c4d04616daeceff Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Mon, 12 Oct 2026 15:12:32 +0200 Subject: [PATCH 1/2] feat: marketplace dev mode analyzer --- README.md | 1 + config/publishing.yaml | 3 + pkg/analysis/passes/analysis.go | 2 + .../marketplacelicense/marketplacelicense.go | 979 +++++++++++++++++ .../marketplacelicense_test.go | 983 ++++++++++++++++++ pkg/runner/runner_test.go | 1 + 6 files changed, 1969 insertions(+) create mode 100644 pkg/analysis/passes/marketplacelicense/marketplacelicense.go create mode 100644 pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go diff --git a/README.md b/README.md index 495b878f..55f9021f 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,7 @@ Run "mage gen:readme" to regenerate this section. | LLM Review / `llmreview` | Runs the code through an LLM to check for security issues or disallowed usage. | API key for one of: Anthropic (ANTHROPIC_API_KEY), OpenAI (OPENAI_API_KEY), or Google (GEMINI_API_KEY) | | Logos / `logos` | Detects whether the plugin includes small and large logos to display in the plugin catalog. | None | | Manifest (Signing) / `manifest` | When a plugin is signed, the zip file will contain a signed `MANIFEST.txt` file. | None | +| Marketplace License / `marketplacelicense` | Rejects backend executables built with marketplace development-mode license checks disabled. | None | | Metadata / `metadata` | Checks that `plugin.json` exists and is valid. | None | | Metadata Grafana Dependency / `grafanadependency` | Checks that dependencies.grafanaDependency in `plugin.json` is valid. | None | | Metadata Paths / `metadatapaths` | Ensures all paths are valid and images referenced exist. | None | diff --git a/config/publishing.yaml b/config/publishing.yaml index 25af19fe..769fe8a0 100644 --- a/config/publishing.yaml +++ b/config/publishing.yaml @@ -3,6 +3,9 @@ global: severity: error analyzers: + marketplacelicense: + enabled: true + severity: error htmlreadme: severity: warning legacyplatform: diff --git a/pkg/analysis/passes/analysis.go b/pkg/analysis/passes/analysis.go index bb7f3f4e..844fa1c3 100644 --- a/pkg/analysis/passes/analysis.go +++ b/pkg/analysis/passes/analysis.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/plugin-validator/pkg/analysis/passes/llmreview" "github.com/grafana/plugin-validator/pkg/analysis/passes/logos" "github.com/grafana/plugin-validator/pkg/analysis/passes/manifest" + "github.com/grafana/plugin-validator/pkg/analysis/passes/marketplacelicense" "github.com/grafana/plugin-validator/pkg/analysis/passes/metadata" "github.com/grafana/plugin-validator/pkg/analysis/passes/metadatapaths" "github.com/grafana/plugin-validator/pkg/analysis/passes/metadatavalid" @@ -83,6 +84,7 @@ var Analyzers = []*analysis.Analyzer{ llmreview.Analyzer, logos.Analyzer, manifest.Analyzer, + marketplacelicense.Analyzer, metadata.Analyzer, metadatapaths.Analyzer, metadatavalid.Analyzer, diff --git a/pkg/analysis/passes/marketplacelicense/marketplacelicense.go b/pkg/analysis/passes/marketplacelicense/marketplacelicense.go new file mode 100644 index 00000000..d6788825 --- /dev/null +++ b/pkg/analysis/passes/marketplacelicense/marketplacelicense.go @@ -0,0 +1,979 @@ +package marketplacelicense + +import ( + "bufio" + "bytes" + "debug/buildinfo" + "fmt" + "go/ast" + "go/build" + "go/build/constraint" + "go/parser" + "go/token" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "unicode" + + "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" +) + +const ( + marketplaceDevelopmentTag = "marketplace_dev" + marketplaceDevelopmentMarker = "GRAFANA_MARKETPLACE_DEV_BUILD_NO_LICENSE_CHECKS_V1" + marketplaceSDKModule = "github.com/grafana/grafana-plugin-sdk-go" + marketplacePackage = marketplaceSDKModule + "/experimental/marketplace" + markerScanBufferSize = 32 * 1024 + maxSourceFileSize = 8 * 1024 * 1024 + maxGoModSize = 2 * 1024 * 1024 +) + +var ( + marketplaceDevelopmentBuild = &analysis.Rule{ + Name: "marketplace-development-build", + Severity: analysis.Error, + } + marketplaceBuildUnverified = &analysis.Rule{ + Name: "marketplace-build-unverified", + Severity: analysis.Error, + } +) + +var Analyzer = &analysis.Analyzer{ + Name: "marketplacelicense", + Requires: []*analysis.Analyzer{ + archive.Analyzer, + nestedmetadata.Analyzer, + sourcecode.Analyzer, + }, + Run: run, + Rules: []*analysis.Rule{ + marketplaceDevelopmentBuild, + marketplaceBuildUnverified, + }, + ReadmeInfo: analysis.ReadmeInfo{ + Name: "Marketplace License", + Description: "Rejects backend executables built with marketplace development-mode license checks disabled.", + }, +} + +type targetBuild struct { + goos string + goarch string + tags map[string]bool +} + +type binaryInspection struct { + buildInfoReadable bool + developmentTag bool + developmentMarker bool + sdkDependency bool + sdkVersion string + sdkReplaced bool + buildInfoError error + markerScanError error + target targetBuild +} + +func run(pass *analysis.Pass) (interface{}, error) { + archiveDir, ok := pass.ResultOf[archive.Analyzer].(string) + if !ok { + reportUnverified( + pass, + "Could not discover marketplace backend executables", + "The plugin archive structure could not be determined", + ) + return nil, nil + } + + metadataMap, ok := pass.ResultOf[nestedmetadata.Analyzer].(nestedmetadata.Metadatamap) + if !ok { + reportUnverified( + pass, + "Could not discover marketplace backend executables", + "The plugin metadata could not be read", + ) + return nil, nil + } + + sourceCodeDir := marketplaceSourceDirectory(pass) + + pluginJSONPaths := make([]string, 0, len(metadataMap)) + for pluginJSONPath := range metadataMap { + pluginJSONPaths = append(pluginJSONPaths, pluginJSONPath) + } + sort.Strings(pluginJSONPaths) + + for _, pluginJSONPath := range pluginJSONPaths { + data := metadataMap[pluginJSONPath] + if !data.Backend { + if data.Executable != "" { + reportUnverified( + pass, + fmt.Sprintf("Could not verify backend declaration for %s", pluginJSONPath), + "plugin.json declares an executable but sets backend to false", + ) + } + continue + } + if data.Executable == "" { + reportUnverified( + pass, + fmt.Sprintf("Could not discover backend executables for %s", pluginJSONPath), + "The backend plugin does not declare an executable", + ) + continue + } + + executableParentDir := filepath.Join( + archiveDir, + filepath.Dir(pluginJSONPath), + filepath.Dir(data.Executable), + ) + if !pathWithinRoot(archiveDir, executableParentDir) { + reportUnverified( + pass, + fmt.Sprintf("Could not verify marketplace license mode for %s", pluginJSONPath), + "The backend executable path resolves outside the plugin archive", + ) + continue + } + + safeExecutableParentDir, err := resolveArchiveDirectory(archiveDir, executableParentDir) + if err != nil { + reportUnverified( + pass, + fmt.Sprintf("Could not discover backend executables for %s", pluginJSONPath), + err.Error(), + ) + continue + } + + entries, err := os.ReadDir(safeExecutableParentDir) + if err != nil { + reportUnverified( + pass, + fmt.Sprintf("Could not discover backend executables for %s", pluginJSONPath), + "The backend executable directory could not be read: "+err.Error(), + ) + continue + } + + executableBase := filepath.Base(data.Executable) + binaries := make([]string, 0) + for _, entry := range entries { + if entry.IsDir() || !isExecutableCandidate(executableBase, entry.Name()) { + continue + } + binaries = append(binaries, filepath.Join(safeExecutableParentDir, entry.Name())) + } + sort.Strings(binaries) + if len(binaries) == 0 { + reportUnverified( + pass, + fmt.Sprintf("Could not discover backend executables for %s", pluginJSONPath), + "No packaged backend executable matched the executable declared in plugin.json", + ) + continue + } + + for _, binaryPath := range binaries { + inspectAndReport(pass, archiveDir, binaryPath, sourceCodeDir) + } + } + + return nil, nil +} + +func reportUnverified(pass *analysis.Pass, title, detail string) { + pass.ReportResult( + pass.AnalyzerName, + marketplaceBuildUnverified, + title, + detail+". Manual review is required before submission can proceed.", + ) +} + +func marketplaceSourceDirectory(pass *analysis.Pass) string { + sourceCodeDir, _ := pass.ResultOf[sourcecode.Analyzer].(string) + return sourceCodeDir +} + +func inspectSourceSDK(pass *analysis.Pass, sourceCodeDir string) string { + goModPath := filepath.Join(sourceCodeDir, "go.mod") + contents, err := readConfinedRegularFile(sourceCodeDir, goModPath, maxGoModSize) + if err != nil { + reportUnverified( + pass, + "Could not verify the marketplace SDK dependency in source code", + "go.mod could not be read: "+err.Error(), + ) + return "" + } + parsed, err := modfile.Parse(goModPath, contents, nil) + if err != nil { + reportUnverified( + pass, + "Could not verify the marketplace SDK dependency in source code", + "go.mod could not be parsed: "+err.Error(), + ) + return "" + } + + sdkVersion := "" + for _, requirement := range parsed.Require { + if requirement.Mod.Path == marketplaceSDKModule { + sdkVersion = requirement.Mod.Version + break + } + } + if sdkVersion == "" { + reportUnverified( + pass, + "Could not verify the marketplace SDK dependency in source code", + "go.mod does not require the Grafana Plugin SDK", + ) + return "" + } + + vendoredSDKPath := filepath.Join( + sourceCodeDir, + "vendor", + filepath.FromSlash(marketplaceSDKModule), + ) + if _, err := os.Lstat(vendoredSDKPath); err == nil { + reportUnverified( + pass, + "Could not verify the vendored marketplace SDK dependency", + "Vendored Grafana Plugin SDK source requires an explicit integrity review", + ) + } else if !os.IsNotExist(err) { + reportUnverified( + pass, + "Could not verify the vendored marketplace SDK dependency", + "The vendored SDK path could not be inspected: "+err.Error(), + ) + } + + var selectedReplacement *modfile.Replace + for _, replacement := range parsed.Replace { + if replacement.Old.Path != marketplaceSDKModule { + continue + } + if replacement.Old.Version == sdkVersion { + selectedReplacement = replacement + break + } + if replacement.Old.Version == "" { + selectedReplacement = replacement + } + } + if selectedReplacement == nil { + return sdkVersion + } + if selectedReplacement.New.Path != marketplaceSDKModule || selectedReplacement.New.Version == "" { + reportUnverified( + pass, + "Unsupported marketplace SDK replacement found in source code", + "go.mod replaces the Grafana Plugin SDK with a local path or fork", + ) + return sdkVersion + } + return selectedReplacement.New.Version +} + +func readConfinedRegularFile(root, path string, maxSize int64) ([]byte, error) { + if !pathWithinRoot(root, path) { + return nil, fmt.Errorf("source path resolves outside the trusted checkout") + } + resolvedRoot, err := filepath.EvalSymlinks(root) + if err != nil { + return nil, fmt.Errorf("resolve source checkout path: %w", err) + } + resolvedPath, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, fmt.Errorf("resolve source file path: %w", err) + } + if !pathWithinRoot(resolvedRoot, resolvedPath) { + return nil, fmt.Errorf("source file symlink resolves outside the trusted checkout") + } + pathInfo, err := os.Stat(resolvedPath) + if err != nil { + return nil, fmt.Errorf("read source file information: %w", err) + } + if !pathInfo.Mode().IsRegular() { + return nil, fmt.Errorf("source file is not a regular file") + } + if pathInfo.Size() > maxSize { + return nil, fmt.Errorf("source file exceeds the %d-byte inspection limit", maxSize) + } + + file, err := os.Open(resolvedPath) + if err != nil { + return nil, fmt.Errorf("open source file: %w", err) + } + defer file.Close() + fileInfo, err := file.Stat() + if err != nil { + return nil, fmt.Errorf("read source file information: %w", err) + } + if !fileInfo.Mode().IsRegular() { + return nil, fmt.Errorf("source file is not a regular file") + } + if fileInfo.Size() > maxSize { + return nil, fmt.Errorf("source file exceeds the %d-byte inspection limit", maxSize) + } + contents, err := io.ReadAll(io.LimitReader(file, maxSize+1)) + if err != nil { + return nil, fmt.Errorf("read source file: %w", err) + } + if int64(len(contents)) > maxSize { + return nil, fmt.Errorf("source file exceeds the %d-byte inspection limit", maxSize) + } + return contents, nil +} + +func sourceUsesMarketplaceLicense(root string, target targetBuild) (bool, bool, error) { + type sourceFunction struct { + body *ast.BlockStmt + aliases map[string]struct{} + dotImported bool + } + type sourcePackage struct { + functions map[string][]sourceFunction + mains []sourceFunction + inits []sourceFunction + } + + packages := make(map[string]*sourcePackage) + referencesMarketplace := false + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + switch entry.Name() { + case ".git", "dist", "node_modules", "vendor": + if path != root { + return filepath.SkipDir + } + } + return nil + } + if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") { + return nil + } + + contents, err := readConfinedRegularFile(root, path, maxSourceFileSize) + if err != nil { + return err + } + if !productionBuildEligible(path, contents, target) { + return nil + } + parsed, err := parser.ParseFile(token.NewFileSet(), path, contents, 0) + if err != nil { + return fmt.Errorf("parse %s: %w", path, err) + } + if parsed.Name.Name != "main" || (!target.tags["cgo"] && importsC(parsed)) { + return nil + } + + aliases := make(map[string]struct{}) + dotImported := false + for _, importSpec := range parsed.Imports { + importPath, err := strconv.Unquote(importSpec.Path.Value) + if err != nil || importPath != marketplacePackage { + continue + } + referencesMarketplace = true + if importSpec.Name == nil { + aliases["marketplace"] = struct{}{} + continue + } + switch importSpec.Name.Name { + case ".": + dotImported = true + case "_": + default: + aliases[importSpec.Name.Name] = struct{}{} + } + } + + packageDir := filepath.Dir(path) + packageInfo := packages[packageDir] + if packageInfo == nil { + packageInfo = &sourcePackage{functions: make(map[string][]sourceFunction)} + packages[packageDir] = packageInfo + } + for _, declaration := range parsed.Decls { + function, ok := declaration.(*ast.FuncDecl) + if !ok || function.Recv != nil || function.Body == nil { + continue + } + candidate := sourceFunction{ + body: function.Body, + aliases: aliases, + dotImported: dotImported, + } + packageInfo.functions[function.Name.Name] = append( + packageInfo.functions[function.Name.Name], + candidate, + ) + switch function.Name.Name { + case "main": + packageInfo.mains = append(packageInfo.mains, candidate) + case "init": + packageInfo.inits = append(packageInfo.inits, candidate) + } + } + return nil + }) + if err != nil { + return false, false, err + } + + reachesMarketplaceLicense := func(root sourceFunction, functions map[string][]sourceFunction) bool { + queue := []sourceFunction{root} + visited := make(map[*ast.BlockStmt]bool) + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + if visited[current.body] { + continue + } + visited[current.body] = true + + found := false + calledFunctions := make(map[string]struct{}) + ast.Inspect(current.body, func(node ast.Node) bool { + if found { + return false + } + if _, ok := node.(*ast.FuncLit); ok { + return false + } + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + switch function := call.Fun.(type) { + case *ast.SelectorExpr: + identifier, ok := function.X.(*ast.Ident) + if ok && (identifier.Obj == nil || identifier.Obj.Kind == ast.Pkg) && + isMarketplaceLicenseFunction(function.Sel.Name) { + _, found = current.aliases[identifier.Name] + } + case *ast.Ident: + if function.Obj == nil && current.dotImported && + isMarketplaceLicenseFunction(function.Name) { + found = true + } else if function.Obj == nil || function.Obj.Kind == ast.Fun { + calledFunctions[function.Name] = struct{}{} + } + } + return !found + }) + if found { + return true + } + for functionName := range calledFunctions { + queue = append(queue, functions[functionName]...) + } + } + return false + } + + foundMain := false + for _, packageInfo := range packages { + if len(packageInfo.mains) == 0 { + continue + } + foundMain = true + initEnforces := false + for _, initializer := range packageInfo.inits { + if reachesMarketplaceLicense(initializer, packageInfo.functions) { + initEnforces = true + break + } + } + if initEnforces { + continue + } + for _, mainFunction := range packageInfo.mains { + if !reachesMarketplaceLicense(mainFunction, packageInfo.functions) { + return referencesMarketplace, false, nil + } + } + } + return referencesMarketplace, foundMain, nil +} + +func productionBuildEligible(path string, contents []byte, target targetBuild) bool { + if !filenameMatchesTarget(path, target) { + return false + } + + scanner := bufio.NewScanner(bytes.NewReader(contents)) + expressions := make([]constraint.Expr, 0, 1) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "package ") { + break + } + if !strings.HasPrefix(line, "//go:build ") && !strings.HasPrefix(line, "// +build ") { + continue + } + expression, err := constraint.Parse(line) + if err != nil { + return false + } + expressions = append(expressions, expression) + } + if scanner.Err() != nil { + return false + } + for _, expression := range expressions { + if !expression.Eval(func(tag string) bool { + return targetHasBuildTag(target, tag) + }) { + return false + } + } + return true +} + +func filenameMatchesTarget(path string, target targetBuild) bool { + name := strings.TrimSuffix(filepath.Base(path), ".go") + if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { + return false + } + parts := strings.Split(name, "_") + if len(parts) < 2 { + return true + } + last := parts[len(parts)-1] + if supportedGOARCH(last) { + if last != target.goarch { + return false + } + if len(parts) >= 3 { + penultimate := parts[len(parts)-2] + if supportedGOOS(penultimate) { + return penultimate == target.goos + } + } + return true + } + if supportedGOOS(last) { + return last == target.goos + } + return true +} + +func targetHasBuildTag(target targetBuild, tag string) bool { + if tag == marketplaceDevelopmentTag || tag == "ignore" { + return false + } + if tag == target.goos || tag == target.goarch || tag == "gc" { + return true + } + if tag == "unix" { + switch target.goos { + case "aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", + "ios", "linux", "netbsd", "openbsd", "solaris": + return true + } + } + if target.tags[tag] { + return true + } + for _, releaseTag := range build.Default.ReleaseTags { + if tag == releaseTag { + return true + } + } + return false +} + +func importsC(file *ast.File) bool { + for _, importSpec := range file.Imports { + if importSpec.Path.Value == `"C"` { + return true + } + } + return false +} + +func isMarketplaceLicenseFunction(name string) bool { + return name == "Manage" || name == "CheckMarketplacePluginLicense" +} + +func isExecutableCandidate(base, name string) bool { + if name == base { + return true + } + if !strings.HasPrefix(name, base+"_") { + return false + } + + suffix := strings.TrimPrefix(name, base+"_") + if strings.HasSuffix(strings.ToLower(suffix), ".exe") { + suffix = suffix[:len(suffix)-len(".exe")] + } + platform := strings.Split(suffix, "_") + return len(platform) == 2 && supportedGOOS(platform[0]) && supportedGOARCH(platform[1]) +} + +func supportedGOOS(value string) bool { + switch value { + case "aix", "android", "darwin", "dragonfly", "freebsd", "illumos", "ios", "js", + "linux", "netbsd", "openbsd", "plan9", "solaris", "wasip1", "windows": + return true + default: + return false + } +} + +func supportedGOARCH(value string) bool { + switch value { + case "386", "amd64", "arm", "arm64", "loong64", "mips", "mipsle", "mips64", + "mips64le", "ppc64", "ppc64le", "riscv64", "s390x", "wasm": + return true + default: + return false + } +} + +func inspectAndReport( + pass *analysis.Pass, + archiveDir string, + binaryPath string, + sourceCodeDir string, +) { + displayRoot := archiveDir + if resolvedRoot, err := filepath.EvalSymlinks(archiveDir); err == nil { + displayRoot = resolvedRoot + } + relativePath, err := filepath.Rel(displayRoot, binaryPath) + if err != nil { + relativePath = binaryPath + } + relativePath = filepath.ToSlash(relativePath) + + safeBinaryPath, err := resolveArchivePath(archiveDir, binaryPath) + if err != nil { + reportUnverified( + pass, + fmt.Sprintf("Could not verify marketplace license mode in %s", relativePath), + err.Error(), + ) + return + } + + inspection := inspectBinary(safeBinaryPath) + + if inspection.developmentTag || inspection.developmentMarker { + signals := make([]string, 0, 2) + if inspection.developmentTag { + signals = append(signals, fmt.Sprintf("the %q build tag", marketplaceDevelopmentTag)) + } + if inspection.developmentMarker { + signals = append(signals, "the marketplace development marker") + } + + pass.ReportResult( + pass.AnalyzerName, + marketplaceDevelopmentBuild, + fmt.Sprintf("Marketplace development build found in %s", relativePath), + fmt.Sprintf( + "The backend executable contains %s. Rebuild it without marketplace development mode before submission.", + strings.Join(signals, " and "), + ), + ) + return + } + + sourceSDKVersion := "" + if inspection.buildInfoReadable && sourceCodeDir != "" && + inspection.target.goos != "" && inspection.target.goarch != "" { + referencesMarketplace, usesMarketplaceLicense, sourceErr := sourceUsesMarketplaceLicense( + sourceCodeDir, + inspection.target, + ) + if sourceErr != nil { + reportUnverified( + pass, + fmt.Sprintf("Could not inspect marketplace source for %s", relativePath), + "The source code could not be inspected: "+sourceErr.Error(), + ) + } else if referencesMarketplace { + sourceSDKVersion = inspectSourceSDK(pass, sourceCodeDir) + if !usesMarketplaceLicense { + reportUnverified( + pass, + fmt.Sprintf("Could not verify marketplace license entrypoint for %s", relativePath), + fmt.Sprintf( + "No reachable production call to marketplace.Manage or marketplace.CheckMarketplacePluginLicense was found for %s/%s", + inspection.target.goos, + inspection.target.goarch, + ), + ) + } + } + } + + sdkVersionMatches := sourceSDKVersion == "" || inspection.sdkVersion == sourceSDKVersion + if inspection.buildInfoReadable && inspection.markerScanError == nil && inspection.sdkDependency && + !inspection.sdkReplaced && sdkVersionMatches { + return + } + + reasons := make([]string, 0, 5) + if inspection.buildInfoError != nil { + reasons = append(reasons, "Go build metadata could not be read: "+inspection.buildInfoError.Error()) + } + if inspection.markerScanError != nil { + reasons = append(reasons, "the development marker scan failed: "+inspection.markerScanError.Error()) + } + if inspection.buildInfoReadable && !inspection.sdkDependency { + reasons = append(reasons, "the Grafana Plugin SDK dependency is not present in Go build metadata") + } + if inspection.sdkReplaced { + reasons = append(reasons, "Go build metadata records a local or forked Grafana Plugin SDK replacement") + } + if inspection.sdkDependency && !sdkVersionMatches { + reasons = append(reasons, fmt.Sprintf( + "the source requires Grafana Plugin SDK %s but the executable records %s", + sourceSDKVersion, + inspection.sdkVersion, + )) + } + + reportUnverified( + pass, + fmt.Sprintf("Could not verify marketplace license mode in %s", relativePath), + strings.Join(reasons, "; "), + ) +} + +func pathWithinRoot(root, path string) bool { + rootPath, err := filepath.Abs(root) + if err != nil { + return false + } + candidatePath, err := filepath.Abs(path) + if err != nil { + return false + } + relativePath, err := filepath.Rel(rootPath, candidatePath) + if err != nil { + return false + } + return relativePath != ".." && + !strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) && + !filepath.IsAbs(relativePath) +} + +func resolveArchiveDirectory(root, path string) (string, error) { + resolvedPath, err := resolveArchiveEntry(root, path, "backend executable directory") + if err != nil { + return "", err + } + fileInfo, err := os.Stat(resolvedPath) + if err != nil { + return "", fmt.Errorf("read backend executable directory information: %w", err) + } + if !fileInfo.IsDir() { + return "", fmt.Errorf("backend executable parent is not a directory") + } + return resolvedPath, nil +} + +func resolveArchivePath(root, path string) (string, error) { + resolvedPath, err := resolveArchiveEntry(root, path, "backend executable") + if err != nil { + return "", err + } + fileInfo, err := os.Stat(resolvedPath) + if err != nil { + return "", fmt.Errorf("read backend executable file information: %w", err) + } + if !fileInfo.Mode().IsRegular() { + return "", fmt.Errorf("backend executable is not a regular file") + } + return resolvedPath, nil +} + +func resolveArchiveEntry(root, path, description string) (string, error) { + resolvedRoot, err := filepath.EvalSymlinks(root) + if err != nil { + return "", fmt.Errorf("resolve plugin archive path: %w", err) + } + if !pathWithinRoot(root, path) && !pathWithinRoot(resolvedRoot, path) { + return "", fmt.Errorf("%s path resolves outside the plugin archive", description) + } + + resolvedPath, err := filepath.EvalSymlinks(path) + if err != nil { + return "", fmt.Errorf("resolve %s path: %w", description, err) + } + if !pathWithinRoot(resolvedRoot, resolvedPath) { + return "", fmt.Errorf("%s symlink resolves outside the plugin archive", description) + } + return resolvedPath, nil +} + +func inspectBinary(path string) binaryInspection { + inspection := binaryInspection{} + info, err := buildinfo.ReadFile(path) + if err != nil { + inspection.buildInfoError = err + } else { + inspection.buildInfoReadable = true + inspection.developmentTag = hasBuildTag(info, marketplaceDevelopmentTag) + inspection.sdkDependency, inspection.sdkVersion, inspection.sdkReplaced = inspectSDKDependency(info) + inspection.target = inspectTargetBuild(info) + } + + inspection.developmentMarker, inspection.markerScanError = fileContainsMarker( + path, + []byte(marketplaceDevelopmentMarker), + ) + return inspection +} + +func inspectSDKDependency(info *buildinfo.BuildInfo) (found bool, version string, replaced bool) { + for _, dependency := range info.Deps { + if dependency.Path != marketplaceSDKModule { + continue + } + if dependency.Replace == nil { + return true, dependency.Version, false + } + if dependency.Replace.Path != marketplaceSDKModule || dependency.Replace.Version == "" { + return true, dependency.Version, true + } + return true, dependency.Replace.Version, false + } + return false, "", false +} + +func inspectTargetBuild(info *buildinfo.BuildInfo) targetBuild { + target := targetBuild{tags: make(map[string]bool)} + settings := make(map[string]string) + for _, setting := range info.Settings { + settings[setting.Key] = setting.Value + switch setting.Key { + case "GOOS": + target.goos = setting.Value + case "GOARCH": + target.goarch = setting.Value + case "CGO_ENABLED": + target.tags["cgo"] = setting.Value == "1" + case "GOEXPERIMENT": + for _, experiment := range splitBuildTags(setting.Value) { + target.tags["goexperiment."+experiment] = true + } + case "-tags": + for _, tag := range splitBuildTags(setting.Value) { + target.tags[tag] = true + } + } + } + addArchitectureBuildTags(&target, settings) + return target +} + +func addArchitectureBuildTags(target *targetBuild, settings map[string]string) { + featureSettings := map[string]string{ + "386": "GO386", + "amd64": "GOAMD64", + "arm": "GOARM", + "arm64": "GOARM64", + "mips": "GOMIPS", + "mips64": "GOMIPS64", + "mips64le": "GOMIPS64", + "mipsle": "GOMIPS", + "ppc64": "GOPPC64", + "ppc64le": "GOPPC64", + "riscv64": "GORISCV64", + } + settingKey := featureSettings[target.goarch] + value := strings.Split(settings[settingKey], ",")[0] + if settingKey == "" || value == "" { + return + } + + target.tags[target.goarch+"."+value] = true + if target.goarch != "amd64" || len(value) != 2 || value[0] != 'v' { + return + } + level := int(value[1] - '0') + for current := 1; current <= level && current <= 4; current++ { + target.tags[fmt.Sprintf("amd64.v%d", current)] = true + } +} + +func hasBuildTag(info *buildinfo.BuildInfo, wanted string) bool { + for _, setting := range info.Settings { + if setting.Key != "-tags" { + continue + } + for _, tag := range splitBuildTags(setting.Value) { + if tag == wanted { + return true + } + } + } + return false +} + +func splitBuildTags(value string) []string { + return strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || unicode.IsSpace(r) + }) +} + +func fileContainsMarker(path string, marker []byte) (bool, error) { + file, err := os.Open(path) + if err != nil { + return false, err + } + defer file.Close() + + buffer := make([]byte, markerScanBufferSize) + tail := make([]byte, 0, len(marker)-1) + for { + n, readErr := file.Read(buffer) + if n > 0 { + window := make([]byte, 0, len(tail)+n) + window = append(window, tail...) + window = append(window, buffer[:n]...) + if bytes.Contains(window, marker) { + return true, nil + } + + keep := len(marker) - 1 + if keep > len(window) { + keep = len(window) + } + tail = append(tail[:0], window[len(window)-keep:]...) + } + if readErr != nil { + if readErr == io.EOF { + return false, nil + } + return false, readErr + } + } +} diff --git a/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go b/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go new file mode 100644 index 00000000..5dcf993a --- /dev/null +++ b/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go @@ -0,0 +1,983 @@ +package marketplacelicense + +import ( + "debug/buildinfo" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "runtime/debug" + "strings" + "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/metadata" + "github.com/grafana/plugin-validator/pkg/analysis/passes/nestedmetadata" + "github.com/grafana/plugin-validator/pkg/analysis/passes/sourcecode" + "github.com/grafana/plugin-validator/pkg/runner" + "github.com/grafana/plugin-validator/pkg/testpassinterceptor" +) + +func TestMarketplaceBuildSignals(t *testing.T) { + tests := []struct { + name string + goos string + goarch string + tags string + includeMarker bool + wantRule string + wantDetail string + }{ + { + name: "readable metadata without tags", + goos: "linux", + goarch: "amd64", + }, + { + name: "tag only", + goos: "darwin", + goarch: "arm64", + tags: marketplaceDevelopmentTag, + wantRule: marketplaceDevelopmentBuild.Name, + wantDetail: "build tag", + }, + { + name: "marker only", + goos: "windows", + goarch: "amd64", + includeMarker: true, + wantRule: marketplaceDevelopmentBuild.Name, + wantDetail: "development marker", + }, + { + name: "tag and marker", + goos: "linux", + goarch: "arm64", + tags: "other," + marketplaceDevelopmentTag, + includeMarker: true, + wantRule: marketplaceDevelopmentBuild.Name, + wantDetail: " and ", + }, + { + name: "tag prefix is not an exact match", + goos: "linux", + goarch: "amd64", + tags: marketplaceDevelopmentTag + "_extra,other", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + archiveDir := t.TempDir() + binaryPath := filepath.Join(archiveDir, executableName(test.goos, test.goarch)) + buildTestBinary(t, binaryPath, test.goos, test.goarch, test.tags, test.includeMarker) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + if test.wantRule == "" { + require.Empty(t, diagnostics) + return + } + require.Len(t, diagnostics, 1) + require.Equal(t, test.wantRule, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, test.wantDetail) + }) + } +} + +func TestUnreadableBuildMetadata(t *testing.T) { + t.Run("without marker requires manual review", func(t *testing.T) { + archiveDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(archiveDir, "marketplace_test_linux_amd64"), + []byte("not a Go executable"), + 0o755, + )) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + }) + + t.Run("marker still rejects", func(t *testing.T) { + archiveDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(archiveDir, "marketplace_test_linux_amd64"), + []byte("prefix"+marketplaceDevelopmentMarker+"suffix"), + 0o755, + )) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "development marker") + }) +} + +func TestMixedPlatformArchiveInspectsEveryBinary(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("windows", "amd64")), + "windows", + "amd64", + marketplaceDevelopmentTag, + true, + ) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Title, "marketplace_test_windows_amd64.exe") +} + +func TestNestedPluginExecutablesAreInspected(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + buildTestBinary( + t, + filepath.Join(archiveDir, "nested", executableName("darwin", "arm64")), + "darwin", + "arm64", + marketplaceDevelopmentTag, + true, + ) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + "nested/plugin.json": backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Title, "nested/marketplace_test_darwin_arm64") +} + +func TestRunnerDiscoversPackagedBackendExecutables(t *testing.T) { + t.Cleanup(func() { + for _, rule := range Analyzer.Rules { + rule.Disabled = false + } + }) + + extractedArchiveDir := t.TempDir() + pluginDir := filepath.Join(extractedArchiveDir, "myorg-marketplace-datasource") + require.NoError(t, os.MkdirAll(pluginDir, 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(pluginDir, "plugin.json"), + []byte(`{"id":"myorg-marketplace-datasource","type":"datasource","backend":true,"executable":"marketplace_test"}`), + 0o644, + )) + buildTestBinary( + t, + filepath.Join(pluginDir, executableName("linux", "amd64")), + "linux", + "amd64", + marketplaceDevelopmentTag, + true, + ) + + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + + diagnostics, err := runner.Check( + []*analysis.Analyzer{Analyzer}, + analysis.CheckParams{ + ArchiveDir: extractedArchiveDir, + SourceCodeDir: sourceDir, + }, + runner.Config{Global: runner.GlobalConfig{Enabled: true}}, + analysis.Severity(""), + ) + require.NoError(t, err) + require.Len(t, diagnostics[Analyzer.Name], 1) + require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[Analyzer.Name][0].Name) + require.Contains(t, diagnostics[Analyzer.Name][0].Title, "marketplace_test_linux_amd64") + + disabled := false + diagnostics, err = runner.Check( + []*analysis.Analyzer{Analyzer}, + analysis.CheckParams{ + ArchiveDir: extractedArchiveDir, + SourceCodeDir: sourceDir, + }, + runner.Config{ + Global: runner.GlobalConfig{Enabled: true}, + Analyzers: map[string]runner.AnalyzerConfig{ + Analyzer.Name: {Enabled: &disabled}, + }, + }, + analysis.Severity(""), + ) + require.NoError(t, err) + require.Empty(t, diagnostics[Analyzer.Name]) +} + +func TestOrdinaryBackendDoesNotRequireMarketplaceEntrypoint(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + sourceDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "main.go"), + []byte("package main\nfunc main() {}\n"), + 0o644, + )) + + diagnostics := runAnalyzerWithSource( + t, + archiveDir, + map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }, + sourceDir, + ) + require.Empty(t, diagnostics) +} + +func TestDevelopmentBuildIsRejectedWithoutMarketplaceClassification(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + marketplaceDevelopmentTag, + true, + ) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[0].Name) +} + +func TestMissingAnalysisDependenciesFailClosed(t *testing.T) { + tests := []struct { + name string + resultOf map[*analysis.Analyzer]interface{} + }{ + { + name: "archive result", + resultOf: map[*analysis.Analyzer]interface{}{ + nestedmetadata.Analyzer: nestedmetadata.Metadatamap{}, + }, + }, + { + name: "metadata result", + resultOf: map[*analysis.Analyzer]interface{}{ + archive.Analyzer: t.TempDir(), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + AnalyzerName: Analyzer.Name, + ResultOf: test.resultOf, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, interceptor.Diagnostics[0].Name) + }) + } +} + +func TestBackendExecutableDiscoveryFailsClosed(t *testing.T) { + t.Run("missing executable declaration", func(t *testing.T) { + diagnostics := runAnalyzer(t, t.TempDir(), map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: {Backend: true}, + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "does not declare an executable") + }) + + t.Run("missing packaged executable", func(t *testing.T) { + diagnostics := runAnalyzer(t, t.TempDir(), map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "No packaged backend executable matched") + }) + + t.Run("executable with backend disabled", func(t *testing.T) { + diagnostics := runAnalyzer(t, t.TempDir(), map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: { + Backend: false, + Executable: "marketplace_test", + }, + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "sets backend to false") + }) +} + +func TestBuildManifestIsNotTreatedAsAnExecutable(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, "go_plugin_linux_amd64"), + "linux", + "amd64", + "", + false, + ) + require.NoError(t, os.WriteFile( + filepath.Join(archiveDir, "go_plugin_build_manifest"), + []byte("manifest"), + 0o644, + )) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("go_plugin"), + }) + require.Empty(t, diagnostics) +} + +func TestExecutableNameTreatsGlobMetacharactersLiterally(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, "marketplace[abc]_linux_amd64"), + "linux", + "amd64", + "", + false, + ) + require.NoError(t, os.WriteFile( + filepath.Join(archiveDir, "marketplacea_linux_amd64"), + []byte(marketplaceDevelopmentMarker), + 0o755, + )) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace[abc]"), + }) + require.Empty(t, diagnostics) +} + +func TestExecutablePathCannotEscapeArchive(t *testing.T) { + parentDir := t.TempDir() + archiveDir := filepath.Join(parentDir, "archive") + require.NoError(t, os.MkdirAll(archiveDir, 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(parentDir, "escaped_linux_amd64"), + []byte(marketplaceDevelopmentMarker), + 0o755, + )) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("../escaped"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.NotContains(t, diagnostics[0].Title, "development build") +} + +func TestExecutableSymlinkCannotEscapeArchive(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("creating symlinks requires additional privileges on Windows") + } + + parentDir := t.TempDir() + archiveDir := filepath.Join(parentDir, "archive") + require.NoError(t, os.MkdirAll(archiveDir, 0o755)) + outsidePath := filepath.Join(parentDir, "outside") + require.NoError(t, os.WriteFile(outsidePath, []byte(marketplaceDevelopmentMarker), 0o755)) + require.NoError(t, os.Symlink( + outsidePath, + filepath.Join(archiveDir, "marketplace_test_linux_amd64"), + )) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "symlink resolves outside") +} + +func TestExecutableDirectorySymlinkCannotEscapeArchive(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("creating symlinks requires additional privileges on Windows") + } + + parentDir := t.TempDir() + archiveDir := filepath.Join(parentDir, "archive") + outsideDir := filepath.Join(parentDir, "outside") + require.NoError(t, os.MkdirAll(archiveDir, 0o755)) + require.NoError(t, os.MkdirAll(outsideDir, 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(outsideDir, "marketplace_test_linux_amd64"), + []byte(marketplaceDevelopmentMarker), + 0o755, + )) + require.NoError(t, os.Symlink(outsideDir, filepath.Join(archiveDir, "bin"))) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("bin/marketplace_test"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "directory symlink resolves outside") +} + +func TestNonRegularExecutableIsNotScanned(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Unix domain socket fixture is not portable to Windows") + } + + archiveDir := t.TempDir() + socketPath := filepath.Join(archiveDir, "x") + listener, err := net.Listen("unix", socketPath) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, listener.Close()) }) + + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("x"), + }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "not a regular file") +} + +func TestMarketplaceSourceSymlinkCannotEscapeCheckout(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("creating symlinks requires additional privileges on Windows") + } + + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + require.NoError(t, os.Remove(filepath.Join(sourceDir, "main.go"))) + require.NoError(t, os.Symlink("/dev/zero", filepath.Join(sourceDir, "main.go"))) + + diagnostics := runAnalyzerWithSource( + t, + archiveDir, + map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }, + sourceDir, + ) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "symlink resolves outside") +} + +func TestSDKDependencyUsesEffectiveReplacementVersion(t *testing.T) { + found, version, replaced := inspectSDKDependency(&buildinfo.BuildInfo{ + Deps: []*debug.Module{ + { + Path: marketplaceSDKModule, + Version: "v1.0.0", + Replace: &debug.Module{ + Path: marketplaceSDKModule, + Version: "v2.0.0", + }, + }, + }, + }) + require.True(t, found) + require.Equal(t, "v2.0.0", version) + require.False(t, replaced) +} + +func TestSourceSDKUsesVersionSpecificReplacementBeforeWildcard(t *testing.T) { + archiveDir := t.TempDir() + binaryPath := filepath.Join(archiveDir, executableName("linux", "amd64")) + buildTestBinary( + t, + binaryPath, + "linux", + "amd64", + "", + false, + ) + binaryBuildInfo, err := buildinfo.ReadFile(binaryPath) + require.NoError(t, err) + found, binarySDKVersion, replaced := inspectSDKDependency(binaryBuildInfo) + require.True(t, found) + require.False(t, replaced) + require.NotEmpty(t, binarySDKVersion) + + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + goMod := fmt.Sprintf(`module marketplace-validator-test + +go 1.21 + +require %[1]s v1.2.3 +replace %[1]s => %[1]s v0.0.1 +replace %[1]s v1.2.3 => %[1]s %[2]s +`, marketplaceSDKModule, binarySDKVersion) + require.NoError(t, os.WriteFile(filepath.Join(sourceDir, "go.mod"), []byte(goMod), 0o644)) + + diagnostics := runAnalyzerWithSource( + t, + archiveDir, + map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }, + sourceDir, + ) + require.Empty(t, diagnostics) +} + +func TestVendoredMarketplaceSDKRequiresIntegrityReview(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + require.NoError(t, os.MkdirAll( + filepath.Join(sourceDir, "vendor", filepath.FromSlash(marketplaceSDKModule)), + 0o755, + )) + + diagnostics := runAnalyzerWithSource( + t, + archiveDir, + map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }, + sourceDir, + ) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "integrity review") +} + +func TestMarketplaceSourceAndBinarySDKMustMatch(t *testing.T) { + tests := []struct { + name string + goModSuffix string + wantDetail string + }{ + { + name: "source SDK replacement", + goModSuffix: "\nreplace github.com/grafana/grafana-plugin-sdk-go => ../fork\n", + wantDetail: "local path or fork", + }, + { + name: "source and binary SDK versions differ", + wantDetail: "source requires Grafana Plugin SDK v1.2.3", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + if test.goModSuffix != "" { + contents, err := os.ReadFile(filepath.Join(sourceDir, "go.mod")) + require.NoError(t, err) + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "go.mod"), + append(contents, test.goModSuffix...), + 0o644, + )) + } else { + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "go.mod"), + []byte("module marketplace-validator-test\n\ngo 1.21\n\nrequire github.com/grafana/grafana-plugin-sdk-go v1.2.3\n"), + 0o644, + )) + } + + diagnostics := runAnalyzerWithSource( + t, + archiveDir, + map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }, + sourceDir, + ) + require.NotEmpty(t, diagnostics) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, test.wantDetail) + }) + } +} + +func TestMarketplaceSourceEntrypointDetection(t *testing.T) { + tests := []struct { + name string + source string + want bool + }{ + { + name: "Manage with alias", + source: `package main +import licensing "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func main() { licensing.Manage("plugin-id") } +`, + want: true, + }, + { + name: "direct checker with dot import", + source: `package main +import . "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func main() { CheckMarketplacePluginLicense("plugin-id") } +`, + want: true, + }, + { + name: "import without enforcement call", + source: `package main +import _ "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func main() {} +`, + }, + { + name: "unreachable call", + source: `package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func unused() { marketplace.Manage("plugin-id") } +func main() {} +`, + }, + { + name: "development-only entrypoint", + source: `//go:build marketplace_dev + +package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func main() { marketplace.Manage("plugin-id") } +`, + }, + { + name: "reachable wrapper", + source: `package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func enforce() { marketplace.Manage("plugin-id") } +func main() { enforce() } +`, + want: true, + }, + { + name: "shadowed wrapper", + source: `package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func enforce() { marketplace.Manage("plugin-id") } +func main() { + enforce := func() {} + enforce() +} +`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sourceDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "main.go"), + []byte(test.source), + 0o644, + )) + _, found, err := sourceUsesMarketplaceLicense(sourceDir, targetBuild{ + goos: "linux", + goarch: "amd64", + tags: make(map[string]bool), + }) + require.NoError(t, err) + require.Equal(t, test.want, found) + }) + } +} + +func TestSourceUsesMarketplaceLicenseMatchesTargetBuild(t *testing.T) { + sourceDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "main.go"), + []byte("package main\nfunc main() {}\n"), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "license_windows.go"), + []byte(`package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func init() { marketplace.Manage("plugin-id") } +`), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "license_ignore.go"), + []byte(`//go:build ignore + +package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func init() { marketplace.Manage("plugin-id") } +`), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "license_cgo.go"), + []byte(`package main +import "C" +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func init() { marketplace.Manage("plugin-id") } +`), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "license_amd64.go"), + []byte(`//go:build !amd64.v1 + +package main +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" +func init() { marketplace.Manage("plugin-id") } +`), + 0o644, + )) + + _, linux, err := sourceUsesMarketplaceLicense(sourceDir, targetBuild{ + goos: "linux", + goarch: "amd64", + tags: map[string]bool{"amd64.v1": true}, + }) + require.NoError(t, err) + require.False(t, linux) + + _, windows, err := sourceUsesMarketplaceLicense(sourceDir, targetBuild{ + goos: "windows", + goarch: "amd64", + tags: map[string]bool{"amd64.v1": true}, + }) + require.NoError(t, err) + require.True(t, windows) +} + +func TestMarketplaceSourceFIFOIsRejectedBeforeOpening(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("named pipes require a Unix-like system") + } + + archiveDir := t.TempDir() + buildTestBinary( + t, + filepath.Join(archiveDir, executableName("linux", "amd64")), + "linux", + "amd64", + "", + false, + ) + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + require.NoError(t, os.Remove(filepath.Join(sourceDir, "main.go"))) + require.NoError(t, exec.Command("mkfifo", filepath.Join(sourceDir, "main.go")).Run()) + + diagnostics := runAnalyzerWithSource( + t, + archiveDir, + map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), + }, + sourceDir, + ) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Contains(t, diagnostics[0].Detail, "not a regular file") +} + +func TestMarkerScanAcrossBufferBoundary(t *testing.T) { + path := filepath.Join(t.TempDir(), "binary") + prefix := strings.Repeat("x", markerScanBufferSize-len(marketplaceDevelopmentMarker)/2) + require.NoError(t, os.WriteFile( + path, + []byte(prefix+marketplaceDevelopmentMarker+"suffix"), + 0o644, + )) + + found, err := fileContainsMarker(path, []byte(marketplaceDevelopmentMarker)) + require.NoError(t, err) + require.True(t, found) +} + +func runAnalyzer( + t *testing.T, + archiveDir string, + metadataMap nestedmetadata.Metadatamap, +) []*analysis.Diagnostic { + t.Helper() + sourceDir := t.TempDir() + writeMarketplaceSourceFixture(t, sourceDir) + + return runAnalyzerWithSource(t, archiveDir, metadataMap, sourceDir) +} + +func runAnalyzerWithSource( + t *testing.T, + archiveDir string, + metadataMap nestedmetadata.Metadatamap, + sourceDir string, +) []*analysis.Diagnostic { + t.Helper() + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + AnalyzerName: Analyzer.Name, + ResultOf: map[*analysis.Analyzer]interface{}{ + archive.Analyzer: archiveDir, + nestedmetadata.Analyzer: metadataMap, + sourcecode.Analyzer: sourceDir, + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + return interceptor.Diagnostics +} + +func writeMarketplaceSourceFixture(t *testing.T, sourceDir string) { + t.Helper() + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "go.mod"), + []byte("module marketplace-validator-test\n\ngo 1.21\n\nrequire github.com/grafana/grafana-plugin-sdk-go v0.0.0\n"), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(sourceDir, "main.go"), + []byte(`package main + +import "github.com/grafana/grafana-plugin-sdk-go/experimental/marketplace" + +func main() { marketplace.Manage("plugin-id") } +`), + 0o644, + )) +} + +func backendMetadata(executable string) metadata.Metadata { + return metadata.Metadata{ + Backend: true, + Executable: executable, + } +} + +func executableName(goos, goarch string) string { + name := "marketplace_test_" + goos + "_" + goarch + if goos == "windows" { + name += ".exe" + } + return name +} + +func buildTestBinary( + t *testing.T, + destination string, + goos string, + goarch string, + tags string, + includeMarker bool, +) { + t.Helper() + moduleDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(moduleDir, "go.mod"), + []byte("module marketplace-validator-test\n\ngo 1.21\n\nrequire github.com/grafana/grafana-plugin-sdk-go v0.0.0\n"), + 0o644, + )) + vendorSDKDir := filepath.Join( + moduleDir, + "vendor", + "github.com", + "grafana", + "grafana-plugin-sdk-go", + ) + require.NoError(t, os.MkdirAll(vendorSDKDir, 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(moduleDir, "vendor", "modules.txt"), + []byte("# github.com/grafana/grafana-plugin-sdk-go v0.0.0\n## explicit; go 1.21\ngithub.com/grafana/grafana-plugin-sdk-go\n"), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(vendorSDKDir, "sdk.go"), + []byte("package sdk\n\nfunc Keep() {}\n"), + 0o644, + )) + + source := "package main\n\nimport sdk \"github.com/grafana/grafana-plugin-sdk-go\"\n\nfunc main() { sdk.Keep() }\n" + if includeMarker { + source = "package main\n\nimport (\n\t\"fmt\"\n\tsdk \"github.com/grafana/grafana-plugin-sdk-go\"\n)\n\nconst marker = \"" + + marketplaceDevelopmentMarker + "\"\n\nfunc main() { sdk.Keep(); fmt.Print(marker) }\n" + } + require.NoError(t, os.WriteFile(filepath.Join(moduleDir, "main.go"), []byte(source), 0o644)) + require.NoError(t, os.MkdirAll(filepath.Dir(destination), 0o755)) + + args := []string{"build", "-mod=vendor", "-trimpath", "-ldflags=-s -w", "-o", destination} + if tags != "" { + args = append(args, "-tags="+tags) + } + args = append(args, ".") + cmd := exec.Command("go", args...) + cmd.Dir = moduleDir + cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch, "CGO_ENABLED=0") + output, err := cmd.CombinedOutput() + require.NoError(t, err, "failed to build test binary: %s", output) +} diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 86fc8eb7..794ed52e 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -19,6 +19,7 @@ var tests = []struct { "Archive is empty", "LLM review skipped due to errors in archive", "Code diff skipped due to errors in archive", + "Could not discover marketplace backend executables", }}, {Dir: "EmptyDirectory", Messages: []string{ "missing plugin.json", From 7497db4aee3bbf5435200d303b607925b15e190d Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Mon, 12 Oct 2026 15:15:18 +0200 Subject: [PATCH 2/2] remove development marker --- .../marketplacelicense/marketplacelicense.go | 87 +++-------- .../marketplacelicense_test.go | 141 ++++++------------ 2 files changed, 64 insertions(+), 164 deletions(-) diff --git a/pkg/analysis/passes/marketplacelicense/marketplacelicense.go b/pkg/analysis/passes/marketplacelicense/marketplacelicense.go index d6788825..9e907da9 100644 --- a/pkg/analysis/passes/marketplacelicense/marketplacelicense.go +++ b/pkg/analysis/passes/marketplacelicense/marketplacelicense.go @@ -28,13 +28,11 @@ import ( ) const ( - marketplaceDevelopmentTag = "marketplace_dev" - marketplaceDevelopmentMarker = "GRAFANA_MARKETPLACE_DEV_BUILD_NO_LICENSE_CHECKS_V1" - marketplaceSDKModule = "github.com/grafana/grafana-plugin-sdk-go" - marketplacePackage = marketplaceSDKModule + "/experimental/marketplace" - markerScanBufferSize = 32 * 1024 - maxSourceFileSize = 8 * 1024 * 1024 - maxGoModSize = 2 * 1024 * 1024 + marketplaceDevelopmentTag = "marketplace_dev" + marketplaceSDKModule = "github.com/grafana/grafana-plugin-sdk-go" + marketplacePackage = marketplaceSDKModule + "/experimental/marketplace" + maxSourceFileSize = 8 * 1024 * 1024 + maxGoModSize = 2 * 1024 * 1024 ) var ( @@ -75,12 +73,10 @@ type targetBuild struct { type binaryInspection struct { buildInfoReadable bool developmentTag bool - developmentMarker bool sdkDependency bool sdkVersion string sdkReplaced bool buildInfoError error - markerScanError error target targetBuild } @@ -682,22 +678,14 @@ func inspectAndReport( inspection := inspectBinary(safeBinaryPath) - if inspection.developmentTag || inspection.developmentMarker { - signals := make([]string, 0, 2) - if inspection.developmentTag { - signals = append(signals, fmt.Sprintf("the %q build tag", marketplaceDevelopmentTag)) - } - if inspection.developmentMarker { - signals = append(signals, "the marketplace development marker") - } - + if inspection.developmentTag { pass.ReportResult( pass.AnalyzerName, marketplaceDevelopmentBuild, fmt.Sprintf("Marketplace development build found in %s", relativePath), fmt.Sprintf( - "The backend executable contains %s. Rebuild it without marketplace development mode before submission.", - strings.Join(signals, " and "), + "The backend executable was built with the %q build tag. Rebuild it without marketplace development mode before submission.", + marketplaceDevelopmentTag, ), ) return @@ -733,18 +721,15 @@ func inspectAndReport( } sdkVersionMatches := sourceSDKVersion == "" || inspection.sdkVersion == sourceSDKVersion - if inspection.buildInfoReadable && inspection.markerScanError == nil && inspection.sdkDependency && + if inspection.buildInfoReadable && inspection.sdkDependency && !inspection.sdkReplaced && sdkVersionMatches { return } - reasons := make([]string, 0, 5) + reasons := make([]string, 0, 4) if inspection.buildInfoError != nil { reasons = append(reasons, "Go build metadata could not be read: "+inspection.buildInfoError.Error()) } - if inspection.markerScanError != nil { - reasons = append(reasons, "the development marker scan failed: "+inspection.markerScanError.Error()) - } if inspection.buildInfoReadable && !inspection.sdkDependency { reasons = append(reasons, "the Grafana Plugin SDK dependency is not present in Go build metadata") } @@ -834,21 +819,17 @@ func resolveArchiveEntry(root, path, description string) (string, error) { } func inspectBinary(path string) binaryInspection { - inspection := binaryInspection{} info, err := buildinfo.ReadFile(path) if err != nil { - inspection.buildInfoError = err - } else { - inspection.buildInfoReadable = true - inspection.developmentTag = hasBuildTag(info, marketplaceDevelopmentTag) - inspection.sdkDependency, inspection.sdkVersion, inspection.sdkReplaced = inspectSDKDependency(info) - inspection.target = inspectTargetBuild(info) + return binaryInspection{buildInfoError: err} } - inspection.developmentMarker, inspection.markerScanError = fileContainsMarker( - path, - []byte(marketplaceDevelopmentMarker), - ) + inspection := binaryInspection{ + buildInfoReadable: true, + developmentTag: hasBuildTag(info, marketplaceDevelopmentTag), + target: inspectTargetBuild(info), + } + inspection.sdkDependency, inspection.sdkVersion, inspection.sdkReplaced = inspectSDKDependency(info) return inspection } @@ -943,37 +924,3 @@ func splitBuildTags(value string) []string { return r == ',' || unicode.IsSpace(r) }) } - -func fileContainsMarker(path string, marker []byte) (bool, error) { - file, err := os.Open(path) - if err != nil { - return false, err - } - defer file.Close() - - buffer := make([]byte, markerScanBufferSize) - tail := make([]byte, 0, len(marker)-1) - for { - n, readErr := file.Read(buffer) - if n > 0 { - window := make([]byte, 0, len(tail)+n) - window = append(window, tail...) - window = append(window, buffer[:n]...) - if bytes.Contains(window, marker) { - return true, nil - } - - keep := len(marker) - 1 - if keep > len(window) { - keep = len(window) - } - tail = append(tail[:0], window[len(window)-keep:]...) - } - if readErr != nil { - if readErr == io.EOF { - return false, nil - } - return false, readErr - } - } -} diff --git a/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go b/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go index 5dcf993a..f0ee5ca3 100644 --- a/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go +++ b/pkg/analysis/passes/marketplacelicense/marketplacelicense_test.go @@ -9,7 +9,6 @@ import ( "path/filepath" "runtime" "runtime/debug" - "strings" "testing" "github.com/stretchr/testify/require" @@ -29,9 +28,7 @@ func TestMarketplaceBuildSignals(t *testing.T) { goos string goarch string tags string - includeMarker bool - wantRule string - wantDetail string + wantRejection bool }{ { name: "readable metadata without tags", @@ -39,29 +36,25 @@ func TestMarketplaceBuildSignals(t *testing.T) { goarch: "amd64", }, { - name: "tag only", - goos: "darwin", - goarch: "arm64", - tags: marketplaceDevelopmentTag, - wantRule: marketplaceDevelopmentBuild.Name, - wantDetail: "build tag", + name: "linux tag among other tags", + goos: "linux", + goarch: "arm64", + tags: "other," + marketplaceDevelopmentTag, + wantRejection: true, }, { - name: "marker only", - goos: "windows", - goarch: "amd64", - includeMarker: true, - wantRule: marketplaceDevelopmentBuild.Name, - wantDetail: "development marker", + name: "darwin tag", + goos: "darwin", + goarch: "arm64", + tags: marketplaceDevelopmentTag, + wantRejection: true, }, { - name: "tag and marker", - goos: "linux", - goarch: "arm64", - tags: "other," + marketplaceDevelopmentTag, - includeMarker: true, - wantRule: marketplaceDevelopmentBuild.Name, - wantDetail: " and ", + name: "windows tag", + goos: "windows", + goarch: "amd64", + tags: marketplaceDevelopmentTag, + wantRejection: true, }, { name: "tag prefix is not an exact match", @@ -75,53 +68,46 @@ func TestMarketplaceBuildSignals(t *testing.T) { t.Run(test.name, func(t *testing.T) { archiveDir := t.TempDir() binaryPath := filepath.Join(archiveDir, executableName(test.goos, test.goarch)) - buildTestBinary(t, binaryPath, test.goos, test.goarch, test.tags, test.includeMarker) + buildTestBinary(t, binaryPath, test.goos, test.goarch, test.tags) + + if test.tags == "" { + info, err := buildinfo.ReadFile(binaryPath) + require.NoError(t, err) + for _, setting := range info.Settings { + require.NotEqual(t, "-tags", setting.Key) + } + } diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), }) - if test.wantRule == "" { + if !test.wantRejection { require.Empty(t, diagnostics) return } require.Len(t, diagnostics, 1) - require.Equal(t, test.wantRule, diagnostics[0].Name) - require.Contains(t, diagnostics[0].Detail, test.wantDetail) + require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[0].Name) + require.Equal(t, analysis.Error, diagnostics[0].Severity) + require.Contains(t, diagnostics[0].Detail, `"marketplace_dev" build tag`) }) } } -func TestUnreadableBuildMetadata(t *testing.T) { - t.Run("without marker requires manual review", func(t *testing.T) { - archiveDir := t.TempDir() - require.NoError(t, os.WriteFile( - filepath.Join(archiveDir, "marketplace_test_linux_amd64"), - []byte("not a Go executable"), - 0o755, - )) - - diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ - nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), - }) - require.Len(t, diagnostics, 1) - require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) - }) - - t.Run("marker still rejects", func(t *testing.T) { - archiveDir := t.TempDir() - require.NoError(t, os.WriteFile( - filepath.Join(archiveDir, "marketplace_test_linux_amd64"), - []byte("prefix"+marketplaceDevelopmentMarker+"suffix"), - 0o755, - )) +func TestUnreadableBuildMetadataRequiresManualReview(t *testing.T) { + archiveDir := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(archiveDir, "marketplace_test_linux_amd64"), + []byte("not a Go executable"), + 0o755, + )) - diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ - nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), - }) - require.Len(t, diagnostics, 1) - require.Equal(t, marketplaceDevelopmentBuild.Name, diagnostics[0].Name) - require.Contains(t, diagnostics[0].Detail, "development marker") + diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ + nestedmetadata.MainPluginJson: backendMetadata("marketplace_test"), }) + require.Len(t, diagnostics, 1) + require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) + require.Equal(t, analysis.Error, diagnostics[0].Severity) + require.Contains(t, diagnostics[0].Detail, "Go build metadata could not be read") } func TestMixedPlatformArchiveInspectsEveryBinary(t *testing.T) { @@ -132,7 +118,6 @@ func TestMixedPlatformArchiveInspectsEveryBinary(t *testing.T) { "linux", "amd64", "", - false, ) buildTestBinary( t, @@ -140,7 +125,6 @@ func TestMixedPlatformArchiveInspectsEveryBinary(t *testing.T) { "windows", "amd64", marketplaceDevelopmentTag, - true, ) diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ @@ -159,7 +143,6 @@ func TestNestedPluginExecutablesAreInspected(t *testing.T) { "linux", "amd64", "", - false, ) buildTestBinary( t, @@ -167,7 +150,6 @@ func TestNestedPluginExecutablesAreInspected(t *testing.T) { "darwin", "arm64", marketplaceDevelopmentTag, - true, ) diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ @@ -200,7 +182,6 @@ func TestRunnerDiscoversPackagedBackendExecutables(t *testing.T) { "linux", "amd64", marketplaceDevelopmentTag, - true, ) sourceDir := t.TempDir() @@ -247,7 +228,6 @@ func TestOrdinaryBackendDoesNotRequireMarketplaceEntrypoint(t *testing.T) { "linux", "amd64", "", - false, ) sourceDir := t.TempDir() require.NoError(t, os.WriteFile( @@ -275,7 +255,6 @@ func TestDevelopmentBuildIsRejectedWithoutMarketplaceClassification(t *testing.T "linux", "amd64", marketplaceDevelopmentTag, - true, ) diagnostics := runAnalyzer(t, archiveDir, map[string]metadata.Metadata{ @@ -361,7 +340,6 @@ func TestBuildManifestIsNotTreatedAsAnExecutable(t *testing.T) { "linux", "amd64", "", - false, ) require.NoError(t, os.WriteFile( filepath.Join(archiveDir, "go_plugin_build_manifest"), @@ -383,11 +361,10 @@ func TestExecutableNameTreatsGlobMetacharactersLiterally(t *testing.T) { "linux", "amd64", "", - false, ) require.NoError(t, os.WriteFile( filepath.Join(archiveDir, "marketplacea_linux_amd64"), - []byte(marketplaceDevelopmentMarker), + []byte("not a Go executable"), 0o755, )) @@ -403,7 +380,7 @@ func TestExecutablePathCannotEscapeArchive(t *testing.T) { require.NoError(t, os.MkdirAll(archiveDir, 0o755)) require.NoError(t, os.WriteFile( filepath.Join(parentDir, "escaped_linux_amd64"), - []byte(marketplaceDevelopmentMarker), + []byte("not a Go executable"), 0o755, )) @@ -412,7 +389,7 @@ func TestExecutablePathCannotEscapeArchive(t *testing.T) { }) require.Len(t, diagnostics, 1) require.Equal(t, marketplaceBuildUnverified.Name, diagnostics[0].Name) - require.NotContains(t, diagnostics[0].Title, "development build") + require.Contains(t, diagnostics[0].Detail, "path resolves outside the plugin archive") } func TestExecutableSymlinkCannotEscapeArchive(t *testing.T) { @@ -424,7 +401,7 @@ func TestExecutableSymlinkCannotEscapeArchive(t *testing.T) { archiveDir := filepath.Join(parentDir, "archive") require.NoError(t, os.MkdirAll(archiveDir, 0o755)) outsidePath := filepath.Join(parentDir, "outside") - require.NoError(t, os.WriteFile(outsidePath, []byte(marketplaceDevelopmentMarker), 0o755)) + require.NoError(t, os.WriteFile(outsidePath, []byte("not a Go executable"), 0o755)) require.NoError(t, os.Symlink( outsidePath, filepath.Join(archiveDir, "marketplace_test_linux_amd64"), @@ -450,7 +427,7 @@ func TestExecutableDirectorySymlinkCannotEscapeArchive(t *testing.T) { require.NoError(t, os.MkdirAll(outsideDir, 0o755)) require.NoError(t, os.WriteFile( filepath.Join(outsideDir, "marketplace_test_linux_amd64"), - []byte(marketplaceDevelopmentMarker), + []byte("not a Go executable"), 0o755, )) require.NoError(t, os.Symlink(outsideDir, filepath.Join(archiveDir, "bin"))) @@ -494,7 +471,6 @@ func TestMarketplaceSourceSymlinkCannotEscapeCheckout(t *testing.T) { "linux", "amd64", "", - false, ) sourceDir := t.TempDir() writeMarketplaceSourceFixture(t, sourceDir) @@ -541,7 +517,6 @@ func TestSourceSDKUsesVersionSpecificReplacementBeforeWildcard(t *testing.T) { "linux", "amd64", "", - false, ) binaryBuildInfo, err := buildinfo.ReadFile(binaryPath) require.NoError(t, err) @@ -581,7 +556,6 @@ func TestVendoredMarketplaceSDKRequiresIntegrityReview(t *testing.T) { "linux", "amd64", "", - false, ) sourceDir := t.TempDir() writeMarketplaceSourceFixture(t, sourceDir) @@ -629,7 +603,6 @@ func TestMarketplaceSourceAndBinarySDKMustMatch(t *testing.T) { "linux", "amd64", "", - false, ) sourceDir := t.TempDir() writeMarketplaceSourceFixture(t, sourceDir) @@ -825,7 +798,6 @@ func TestMarketplaceSourceFIFOIsRejectedBeforeOpening(t *testing.T) { "linux", "amd64", "", - false, ) sourceDir := t.TempDir() writeMarketplaceSourceFixture(t, sourceDir) @@ -845,20 +817,6 @@ func TestMarketplaceSourceFIFOIsRejectedBeforeOpening(t *testing.T) { require.Contains(t, diagnostics[0].Detail, "not a regular file") } -func TestMarkerScanAcrossBufferBoundary(t *testing.T) { - path := filepath.Join(t.TempDir(), "binary") - prefix := strings.Repeat("x", markerScanBufferSize-len(marketplaceDevelopmentMarker)/2) - require.NoError(t, os.WriteFile( - path, - []byte(prefix+marketplaceDevelopmentMarker+"suffix"), - 0o644, - )) - - found, err := fileContainsMarker(path, []byte(marketplaceDevelopmentMarker)) - require.NoError(t, err) - require.True(t, found) -} - func runAnalyzer( t *testing.T, archiveDir string, @@ -934,7 +892,6 @@ func buildTestBinary( goos string, goarch string, tags string, - includeMarker bool, ) { t.Helper() moduleDir := t.TempDir() @@ -963,10 +920,6 @@ func buildTestBinary( )) source := "package main\n\nimport sdk \"github.com/grafana/grafana-plugin-sdk-go\"\n\nfunc main() { sdk.Keep() }\n" - if includeMarker { - source = "package main\n\nimport (\n\t\"fmt\"\n\tsdk \"github.com/grafana/grafana-plugin-sdk-go\"\n)\n\nconst marker = \"" + - marketplaceDevelopmentMarker + "\"\n\nfunc main() { sdk.Keep(); fmt.Print(marker) }\n" - } require.NoError(t, os.WriteFile(filepath.Join(moduleDir, "main.go"), []byte(source), 0o644)) require.NoError(t, os.MkdirAll(filepath.Dir(destination), 0o755))