From d127385f3010cc941067a9aa51de07aadb1e11c2 Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Thu, 4 Jun 2026 16:39:16 -0700 Subject: [PATCH 1/2] claircore: add JSON marshaling for Alias type unique.Handle[string] does not implement json.Marshaler, causing Alias.Space to serialize as {} instead of the actual namespace value. Add MarshalJSON and UnmarshalJSON to preserve Space values through JSON round-trips. Signed-off-by: J. Victor Martins --- alias.go | 31 +++++++++++++++++++++++++++++++ alias_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/alias.go b/alias.go index d0d6b08d0..96b8459d5 100644 --- a/alias.go +++ b/alias.go @@ -1,6 +1,7 @@ package claircore import ( + "encoding/json" "strings" "unique" ) @@ -49,3 +50,33 @@ func (a Alias) Equal(b Alias) bool { func (a Alias) Valid() bool { return a.Space != unique.Handle[string]{} && a.Space.Value() != "" && a.Name != "" } + +type aliasJSON struct { + Space string `json:"space"` + Name string `json:"name"` +} + +// MarshalJSON implements [json.Marshaler]. +func (a Alias) MarshalJSON() ([]byte, error) { + var space string + if a.Space != (unique.Handle[string]{}) { + space = a.Space.Value() + } + return json.Marshal(aliasJSON{ + Space: space, + Name: a.Name, + }) +} + +// UnmarshalJSON implements [json.Unmarshaler]. +func (a *Alias) UnmarshalJSON(data []byte) error { + var v aliasJSON + if err := json.Unmarshal(data, &v); err != nil { + return err + } + if v.Space != "" { + a.Space = unique.Make(v.Space) + } + a.Name = v.Name + return nil +} diff --git a/alias_test.go b/alias_test.go index 81d962794..297ac0a5b 100644 --- a/alias_test.go +++ b/alias_test.go @@ -1,6 +1,7 @@ package claircore import ( + "encoding/json" "fmt" "testing" "unique" @@ -71,4 +72,44 @@ func TestAlias(t *testing.T) { } }) }) + + t.Run("JSON", func(t *testing.T) { + a := Alias{Space: unique.Make("CVE"), Name: "2024-24786"} + data, err := json.Marshal(a) + if err != nil { + t.Fatal(err) + } + want := `{"space":"CVE","name":"2024-24786"}` + if got := string(data); got != want { + t.Fatalf("marshal: got %s, want %s", got, want) + } + + var b Alias + if err := json.Unmarshal(data, &b); err != nil { + t.Fatal(err) + } + if !a.Equal(b) { + t.Errorf("unmarshal: got %v, want %v", b, a) + } + }) + + t.Run("JSONZero", func(t *testing.T) { + a := Alias{} + data, err := json.Marshal(a) + if err != nil { + t.Fatal(err) + } + want := `{"space":"","name":""}` + if got := string(data); got != want { + t.Errorf("marshal: got %s, want %s", got, want) + } + + var b Alias + if err := json.Unmarshal(data, &b); err != nil { + t.Fatal(err) + } + if b.Valid() { + t.Errorf("unmarshal zero: expected invalid alias, got valid: %v", b) + } + }) } From 4629529aec4df32e43e1f363e937e60c4f45fe3d Mon Sep 17 00:00:00 2001 From: "J. Victor Martins" Date: Thu, 4 Jun 2026 16:39:20 -0700 Subject: [PATCH 2/2] datastore/postgres: populate Aliases and Self when retrieving vulnerabilities Signed-off-by: J. Victor Martins --- datastore/postgres/get.go | 94 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/datastore/postgres/get.go b/datastore/postgres/get.go index 67c3578e1..78aa49ade 100644 --- a/datastore/postgres/get.go +++ b/datastore/postgres/get.go @@ -6,6 +6,7 @@ import ( "log/slog" "strconv" "time" + "unique" "github.com/jackc/pgx/v5" "github.com/prometheus/client_golang/prometheus" @@ -172,8 +173,101 @@ func (s *MatcherStore) Get(ctx context.Context, records []*claircore.IndexRecord getVulnerabilitiesCounter.WithLabelValues("query_batch").Add(1) getVulnerabilitiesDuration.WithLabelValues("query_batch").Observe(time.Since(start).Seconds()) + if err := populateAliases(ctx, tx, results); err != nil { + return nil, fmt.Errorf("populating aliases: %w", err) + } + if err := tx.Commit(ctx); err != nil { return nil, fmt.Errorf("failed to commit tx: %v", err) } return results, nil } + +// populateAliases fetches aliases and self references for all vulnerabilities +// in the results map and populates the Aliases and Self fields. +func populateAliases(ctx context.Context, tx pgx.Tx, results map[string][]*claircore.Vulnerability) error { + vulnByID := make(map[string]*claircore.Vulnerability) + for _, vulns := range results { + for _, v := range vulns { + vulnByID[v.ID] = v + } + } + if len(vulnByID) == 0 { + return nil + } + + ids := make([]int64, 0, len(vulnByID)) + for id := range vulnByID { + n, err := strconv.ParseInt(id, 10, 64) + if err != nil { + continue + } + ids = append(ids, n) + } + + const aliasQuery = ` + SELECT va.vulnerability, ns.namespace, a.name + FROM vulnerability_alias va + JOIN alias a ON va.alias = a.id + JOIN alias_namespace ns ON a.namespace = ns.id + WHERE va.vulnerability = ANY($1) + ` + aliasRows, err := tx.Query(ctx, aliasQuery, ids) + if err != nil { + return fmt.Errorf("querying aliases: %w", err) + } + defer aliasRows.Close() + + for aliasRows.Next() { + var vulnID int64 + var namespace, name string + if err := aliasRows.Scan(&vulnID, &namespace, &name); err != nil { + return fmt.Errorf("scanning alias row: %w", err) + } + v := vulnByID[strconv.FormatInt(vulnID, 10)] + if v == nil { + continue + } + v.Aliases = append(v.Aliases, claircore.Alias{ + Space: unique.Make(namespace), + Name: name, + }) + } + if err := aliasRows.Err(); err != nil { + return fmt.Errorf("iterating alias rows: %w", err) + } + + const selfQuery = ` + SELECT vs.vulnerability, ns.namespace, a.name + FROM vulnerability_self vs + JOIN alias a ON vs.self = a.id + JOIN alias_namespace ns ON a.namespace = ns.id + WHERE vs.vulnerability = ANY($1) + ` + selfRows, err := tx.Query(ctx, selfQuery, ids) + if err != nil { + return fmt.Errorf("querying self aliases: %w", err) + } + defer selfRows.Close() + + for selfRows.Next() { + var vulnID int64 + var namespace, name string + if err := selfRows.Scan(&vulnID, &namespace, &name); err != nil { + return fmt.Errorf("scanning self row: %w", err) + } + v := vulnByID[strconv.FormatInt(vulnID, 10)] + if v == nil { + continue + } + v.Self = claircore.Alias{ + Space: unique.Make(namespace), + Name: name, + } + } + if err := selfRows.Err(); err != nil { + return fmt.Errorf("iterating self rows: %w", err) + } + + return nil +}