mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
scanner: report the Grype version that is actually linked in
The vulnerability report's descriptor hardcoded "v0.107.1" while go.mod has required v0.118.0 for some time, so every report ever stored misreported which tool produced its findings. That field exists to answer the first question anyone asks when two scans of one image disagree: did the scanner change, or did the feed. A constant cannot answer it, and had been quietly wrong across eleven minor versions. It now reads the module version from debug.ReadBuildInfo, so a dependency bump carries into the reports on its own. A build-time ldflags stamp was the alternative and has the same failure mode as the constant, just moved to a step someone has to remember. Where the build genuinely cannot say, the descriptor carries "unknown" rather than a plausible-looking lie. The lookup is split into a pure function because a test binary's build info carries no dependency list, so the real call returns "unknown" under `go test` no matter what the code does; testing through it would have proved nothing. Verified against a real build instead: `go version -m bin/atcr-scanner` lists dep github.com/anchore/grype v0.118.0, which is what the split-out lookup returns for that input. A second test fails if the literal ever reappears in the source, since that is the regression rather than the lookup being wrong. Reports already stored keep the old string; there is no migration and nothing reads the field today, the appview decoders declaring only matches and dropping the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1Km3N3uUmeGaj7VbaM8PF
This commit is contained in:
co-authored by
Claude Opus 5
parent
96b48f4b3d
commit
29fe24aa41
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -158,7 +159,7 @@ func scanVulnerabilities(ctx context.Context, s *sbom.SBOM, vulnDBPath string) (
|
||||
"distro": s.Artifacts.LinuxDistribution,
|
||||
"descriptor": map[string]any{
|
||||
"name": "grype",
|
||||
"version": "v0.107.1",
|
||||
"version": grypeVersion(),
|
||||
},
|
||||
"summary": summary,
|
||||
}
|
||||
@@ -174,6 +175,49 @@ func scanVulnerabilities(ctx context.Context, s *sbom.SBOM, vulnDBPath string) (
|
||||
return reportJSON, digest, summary, nil
|
||||
}
|
||||
|
||||
// grypeModulePath is the module whose version the report descriptor reports.
|
||||
const grypeModulePath = "github.com/anchore/grype"
|
||||
|
||||
// grypeVersion reports the Grype version actually linked into this binary.
|
||||
//
|
||||
// The descriptor exists so a stored report says which tool produced its
|
||||
// findings, which is the first thing anyone asks when two scans of one image
|
||||
// disagree. It used to be a hardcoded string, and by the time anyone noticed it
|
||||
// claimed v0.107.1 while the module had moved to v0.118.0, so every report ever
|
||||
// written misreported its own provenance.
|
||||
//
|
||||
// Reading it from the build info is what stops that recurring: a build stamp or
|
||||
// another constant would have the same failure mode, just relocated to a step
|
||||
// someone has to remember. Computed once, since it cannot change while the
|
||||
// process runs.
|
||||
var grypeVersion = sync.OnceValue(func() string {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
// Only reachable in a binary built without module information.
|
||||
return unknownVersion
|
||||
}
|
||||
return grypeVersionFrom(bi.Deps)
|
||||
})
|
||||
|
||||
// unknownVersion is what the descriptor carries when the build cannot say.
|
||||
// Better than a plausible-looking lie, which is what the constant was.
|
||||
const unknownVersion = "unknown"
|
||||
|
||||
// grypeVersionFrom finds the Grype module in a dependency list.
|
||||
//
|
||||
// Split out because a test binary's build info carries no dependency list at
|
||||
// all, so the lookup cannot be exercised through debug.ReadBuildInfo. Verified
|
||||
// against a real build: `go version -m bin/atcr-scanner` lists
|
||||
// `dep github.com/anchore/grype v0.118.0`, which is what this returns there.
|
||||
func grypeVersionFrom(deps []*debug.Module) string {
|
||||
for _, dep := range deps {
|
||||
if dep != nil && dep.Path == grypeModulePath {
|
||||
return dep.Version
|
||||
}
|
||||
}
|
||||
return unknownVersion
|
||||
}
|
||||
|
||||
// grypeDBConfig returns the distribution and installation configs used for
|
||||
// all Grype DB load/update calls. Kept in one place so both the initial load
|
||||
// and the periodic reload see identical settings.
|
||||
|
||||
@@ -3,6 +3,9 @@ package scan
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -252,3 +255,59 @@ func TestLoadVulnDatabase_ColdStartFailureIsAnError(t *testing.T) {
|
||||
t.Errorf("loader called %d times, want 1", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrypeVersionFrom covers the report descriptor's provenance lookup. The
|
||||
// descriptor used to be a hardcoded "v0.107.1" against a v0.118.0 dependency,
|
||||
// so every stored report misreported which tool produced its findings.
|
||||
//
|
||||
// This tests grypeVersionFrom rather than grypeVersion because a test binary's
|
||||
// build info carries no dependency list, so the real call returns "unknown"
|
||||
// here no matter what the code does.
|
||||
func TestGrypeVersionFrom(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
deps []*debug.Module
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "finds grype among its neighbours",
|
||||
deps: []*debug.Module{
|
||||
{Path: "github.com/anchore/syft", Version: "v1.51.1"},
|
||||
{Path: grypeModulePath, Version: "v0.118.0"},
|
||||
{Path: "github.com/gorilla/websocket", Version: "v1.5.4"},
|
||||
},
|
||||
want: "v0.118.0",
|
||||
},
|
||||
{
|
||||
name: "absent grype is unknown, not a guess",
|
||||
deps: []*debug.Module{{Path: "github.com/anchore/syft", Version: "v1.51.1"}},
|
||||
want: unknownVersion,
|
||||
},
|
||||
{name: "no dependencies at all", deps: nil, want: unknownVersion},
|
||||
{
|
||||
name: "a nil entry does not panic",
|
||||
deps: []*debug.Module{nil, {Path: grypeModulePath, Version: "v0.118.0"}},
|
||||
want: "v0.118.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := grypeVersionFrom(tt.deps); got != tt.want {
|
||||
t.Errorf("grypeVersionFrom() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrypeVersionIsNotTheOldConstant guards the actual regression: the value
|
||||
// must never again be a literal baked into the source.
|
||||
func TestGrypeVersionIsNotTheOldConstant(t *testing.T) {
|
||||
src, err := os.ReadFile("grype.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read grype.go: %v", err)
|
||||
}
|
||||
if strings.Contains(string(src), `"v0.107.1"`) {
|
||||
t.Error("the hardcoded Grype version is back in grype.go")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user