Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions alias.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package claircore

import (
"encoding/json"
"strings"
"unique"
)
Expand Down Expand Up @@ -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
}
41 changes: 41 additions & 0 deletions alias_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package claircore

import (
"encoding/json"
"fmt"
"testing"
"unique"
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this isn't going to be totally symmetrical, i.e unmarshaling {"Space":"","Name":""} will return something different from Alias{} should. Do you think it's worth calling unmarshal and checking Valid() is false?

}

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)
}
})
}
94 changes: 94 additions & 0 deletions datastore/postgres/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"strconv"
"time"
"unique"

"github.com/jackc/pgx/v5"
"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -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
}
Loading