From eae5f20b3d485080516191afa300daa9af81f78a Mon Sep 17 00:00:00 2001 From: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:05:40 +0200 Subject: [PATCH 1/2] fix(ingest): accept Grype source.target as string or object Newer Grype emits source.target as a plain path string for file/directory/tarball scans. Unmarshaling that into the image struct broke release dogfood (ingest of release-grype.json). --- internal/ingest/ingest.go | 38 ++++++++++++++++++++++++++++++---- internal/ingest/ingest_test.go | 27 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index 4541be7..899f08c 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -1,6 +1,7 @@ package ingest import ( + "bytes" "encoding/json" "fmt" "os" @@ -81,13 +82,42 @@ type GrypeMatch struct { } `json:"artifact"` } +// grypeSourceTarget accepts both Grype image scans (object with userInput/imageID) +// and file/directory scans (plain string path), which newer Grype emits for tarballs. +type grypeSourceTarget struct { + UserInput string + ImageID string +} + +func (t *grypeSourceTarget) UnmarshalJSON(data []byte) error { + data = bytes.TrimSpace(data) + if len(data) == 0 || string(data) == "null" { + return nil + } + if data[0] == '"' { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + t.UserInput = s + return nil + } + var obj struct { + UserInput string `json:"userInput"` + ImageID string `json:"imageID"` + } + if err := json.Unmarshal(data, &obj); err != nil { + return err + } + t.UserInput = obj.UserInput + t.ImageID = obj.ImageID + return nil +} + type Grype struct { Matches []GrypeMatch `json:"matches"` Source *struct { - Target *struct { - UserInput string `json:"userInput"` - ImageID string `json:"imageID"` - } `json:"target"` + Target *grypeSourceTarget `json:"target"` } `json:"source"` } diff --git a/internal/ingest/ingest_test.go b/internal/ingest/ingest_test.go index a55e019..fff5b5c 100644 --- a/internal/ingest/ingest_test.go +++ b/internal/ingest/ingest_test.go @@ -58,6 +58,33 @@ func TestIngestGrype(t *testing.T) { } } + +func TestIngestGrypeFileSourceTarget(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "grype-file.json") + // Newer Grype emits source.target as a string for file/directory/tarball scans. + payload := `{ + "matches": [{ + "vulnerability": {"id": "CVE-2024-0001", "severity": "HIGH", "description": "test", "fix": {"versions": ["1.2.4"], "state": "fixed"}}, + "artifact": {"name": "demo", "version": "1.2.3", "type": "go-module", "purl": "pkg:golang/demo@1.2.3"} + }], + "source": {"type": "file", "target": "dist/blastguard_0.2.0_linux_amd64.tar.gz"} + }` + if err := os.WriteFile(path, []byte(payload), 0o644); err != nil { + t.Fatal(err) + } + b := NewBuilder() + if err := b.IngestGrype(path); err != nil { + t.Fatalf("IngestGrype file target failed: %v", err) + } + if b.imageRef != "dist/blastguard_0.2.0_linux_amd64.tar.gz" { + t.Fatalf("imageRef=%q, want tarball path", b.imageRef) + } + if len(b.cves) != 1 { + t.Fatalf("cves=%d, want 1", len(b.cves)) + } +} + func TestIngestOSV(t *testing.T) { wd, _ := os.Getwd() testdata := filepath.Join(wd, "..", "..", "testdata", "alpine-3.20-osv.json") From d11332980f562e49b0876587015d4080f22b07a7 Mon Sep 17 00:00:00 2001 From: Andrey Lesnikov <52860508+justrunme@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:08:25 +0200 Subject: [PATCH 2/2] style: gofmt ingest_test (drop extra blank line) --- internal/ingest/ingest_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/ingest/ingest_test.go b/internal/ingest/ingest_test.go index fff5b5c..36bc805 100644 --- a/internal/ingest/ingest_test.go +++ b/internal/ingest/ingest_test.go @@ -58,7 +58,6 @@ func TestIngestGrype(t *testing.T) { } } - func TestIngestGrypeFileSourceTarget(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "grype-file.json")