Skip to content
Merged
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
9 changes: 6 additions & 3 deletions grype/db/internal/provider/unmarshal/os_vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ type OSFixedIn struct {
} `json:"AdvisorySummary"`
NoAdvisory bool `json:"NoAdvisory"`
} `json:"VendorAdvisory"`
Version string `json:"Version"`
VersionFormat string `json:"VersionFormat"`
VulnerableRange string `json:"VulnerableRange"`
Version string `json:"Version"`
VersionFormat string `json:"VersionFormat"`
// Arch is set only when an advisory ships a different fix per architecture (e.g. an x86_64 and
// aarch64 build respun at different revisions); nil/absent means the fix applies to all arches.
Arch *string `json:"Arch,omitempty"`
VulnerableRange string `json:"VulnerableRange"`
Available struct {
Date string `json:"Date,omitempty"`
Kind string `json:"Kind,omitempty"`
Expand Down
42 changes: 42 additions & 0 deletions grype/db/v6/architecture_alias_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package v6

import (
"fmt"

"gorm.io/gorm"
)

type ArchitectureAliasStoreReader interface {
// GetArchitectureAliases returns the architecture alias table (alias spelling -> canonical
// token), used by the architecture qualifier to fold dialect spellings at match time.
GetArchitectureAliases() (map[string]string, error)
}

type architectureAliasStore struct {
db *gorm.DB
}

func newArchitectureAliasStore(db *gorm.DB) *architectureAliasStore {
return &architectureAliasStore{db: db}
}

// GetArchitectureAliases returns the architecture alias table as a map of alias spelling to
// canonical token. A database built before this table existed has no such table; that is not
// an error — an empty map is returned, which the architecture qualifier reads as "fall back to
// the built-in default aliases".
func (s *architectureAliasStore) GetArchitectureAliases() (map[string]string, error) {
if !s.db.Migrator().HasTable(&ArchitectureAlias{}) {
return map[string]string{}, nil
}

var rows []ArchitectureAlias
if err := s.db.Find(&rows).Error; err != nil {
return nil, fmt.Errorf("unable to read architecture aliases: %w", err)
}

out := make(map[string]string, len(rows))
for _, r := range rows {
out[r.Alias] = r.Canonical
}
return out, nil
}
31 changes: 31 additions & 0 deletions grype/db/v6/architecture_alias_store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package v6

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/anchore/grype/grype/pkg/qualifier/architecture"
)

func TestArchitectureAliasStore_GetArchitectureAliases_SeededDefaults(t *testing.T) {
// setupTestStore opens an empty+writable DB, which seeds InitialData (including the
// architecture aliases). The read-back must equal the build-time default table.
s := setupTestStore(t)

got, err := s.GetArchitectureAliases()
require.NoError(t, err)
require.Equal(t, architecture.DefaultAliases(), got)
}

func TestArchitectureAliasStore_GetArchitectureAliases_MissingTableIsEmptyNotError(t *testing.T) {
// a database built before the architecture_aliases table existed has no such table; that
// must read as an empty map (the qualifier's signal to fall back to built-in defaults),
// never an error.
s := setupTestStore(t)
require.NoError(t, s.db.Migrator().DropTable(&ArchitectureAlias{}))

got, err := s.GetArchitectureAliases()
require.NoError(t, err)
require.Empty(t, got)
}
9 changes: 5 additions & 4 deletions grype/db/v6/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,11 @@ type PackageQualifiers struct {
// Architecture is the architecture of the affected RPM, copied from the source PURL's
// `arch` qualifier when present (e.g. "src", "x86_64") or set to a synthesized sentinel
// like "binary-no-arch-specified" when a binary RPM was disclosed without an explicit
// arch. Upstream-indirected RPM matches drop entries tagged with any arch other than
// "src", so providers that disclose at binary granularity (e.g. hummingbird CSAF VEX)
// avoid FP-matching unrelated sibling binaries built from the same source. See
// pkg/qualifier/architecture for the constants.
// arch. At match time the architecture qualifier compares this value against the scanned
// package's arch (see pkg/qualifier/architecture, Satisfied): a "binary-no-arch-specified"
// entry matches any binary package but rejects the rpm matcher's synthesized "src" upstream,
// so providers that disclose at binary granularity (e.g. hummingbird CSAF VEX) avoid
// FP-matching unrelated sibling binaries built from the same source.
Architecture *string `json:"architecture,omitempty"`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This already shipped as a string, but maybe missed opportunity to do a Set[string] so that we could check "intersection of package arch with vuln arch is not empty" instead of equality.


// RootIO indicates that the vulnerability applies only to Root IO packages (packages with Root IO fixes).
Expand Down
15 changes: 15 additions & 0 deletions grype/db/v6/build/transformers/os/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ func getPackages(vuln unmarshal.OSVulnerability) ([]db.AffectedPackageHandle, []
qualifiers = &db.PackageQualifiers{
RpmModularity: &module,
}
// when the advisory scoped this fix to a specific architecture, carry it so the
// architecture qualifier only applies the fix to packages of that arch (see
// pkg/qualifier/architecture). Absent arch means the fix applies to all arches.
if group.arch != "" {
arch := group.arch
qualifiers.Architecture = &arch
}
}

aph := db.AffectedPackageHandle{
Expand Down Expand Up @@ -251,6 +258,7 @@ type groupIndex struct {
hasModule bool
module string
format string
arch string
}

func groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OSFixedIn {
Expand All @@ -262,6 +270,10 @@ func groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OS
if fixedIn.Module != nil {
mod = *fixedIn.Module
}
var arch string
if fixedIn.Arch != nil {
arch = *fixedIn.Arch
}
g := groupIndex{
name: fixedIn.Name,
id: oi.id,
Expand All @@ -271,6 +283,9 @@ func groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OS
hasModule: fixedIn.Module != nil,
module: mod,
format: fixedIn.VersionFormat,
// arch splits a per-arch fix into its own affected package handle so the architecture
// qualifier can scope it; empty means the fix applies to all arches.
arch: arch,
}

grouped[g] = append(grouped[g], fixedIn)
Expand Down
41 changes: 41 additions & 0 deletions grype/db/v6/build/transformers/os/transform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1825,3 +1825,44 @@ func timeRef(ti time.Time) *time.Time {
func strRef(s string) *string {
return &s
}

// Test_getPackages_perArchFix pins that an Oracle-style advisory with a different fix per
// architecture (the ELSA-2022-4803 false-positive shape) yields one affected package handle per
// arch, each carrying its own Architecture qualifier and fix version, while an arch-less FixedIn
// stays arch-agnostic. Without per-arch handles, the higher aarch64 revision would over-match a
// patched x86_64 package.
func Test_getPackages_perArchFix(t *testing.T) {
vuln := unmarshal.OSVulnerability{}
vuln.Vulnerability.Name = "ELSA-2022-4803"
vuln.Vulnerability.NamespaceName = "ol:7"
vuln.Vulnerability.FixedIn = []unmarshal.OSFixedIn{
{Name: "rsyslog", NamespaceName: "ol:7", Version: "0:8.24.0-57.0.1.el7_9.3", VersionFormat: "rpm", Arch: strRef("x86_64")},
{Name: "rsyslog", NamespaceName: "ol:7", Version: "0:8.24.0-57.0.4.el7_9.3", VersionFormat: "rpm", Arch: strRef("aarch64")},
{Name: "zlib", NamespaceName: "ol:7", Version: "0:1.2.7-21.el7", VersionFormat: "rpm"}, // arch-less: applies to all
}

affected, unaffected := getPackages(vuln)
require.Empty(t, unaffected)

type got struct {
name string
arch string // "" when no architecture qualifier
fixVers string
}
var results []got
for _, aph := range affected {
g := got{name: aph.Package.Name}
if aph.BlobValue.Qualifiers != nil && aph.BlobValue.Qualifiers.Architecture != nil {
g.arch = *aph.BlobValue.Qualifiers.Architecture
}
require.Len(t, aph.BlobValue.Ranges, 1)
g.fixVers = aph.BlobValue.Ranges[0].Fix.Version
results = append(results, g)
}

assert.ElementsMatch(t, []got{
{name: "rsyslog", arch: "x86_64", fixVers: "0:8.24.0-57.0.1.el7_9.3"},
{name: "rsyslog", arch: "aarch64", fixVers: "0:8.24.0-57.0.4.el7_9.3"},
{name: "zlib", arch: "", fixVers: "0:1.2.7-21.el7"},
}, results)
}
21 changes: 21 additions & 0 deletions grype/db/v6/data.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package v6

import (
"sort"
"strings"

"github.com/anchore/grype/grype/pkg/qualifier/architecture"
"github.com/anchore/syft/syft/pkg"
)

Expand Down Expand Up @@ -162,6 +164,25 @@ func KnownPackageSpecifierOverrides() []PackageSpecifierOverride {
return ret
}

// KnownArchitectureAliases is the default arch alias table seeded into the DB at build time.
// It is sourced from architecture.DefaultAliases so the build-time seed and the match-time
// fallback (used by clients whose DB predates this table) can never drift apart.
func KnownArchitectureAliases() []ArchitectureAlias {
defaults := architecture.DefaultAliases()

aliases := make([]string, 0, len(defaults))
for alias := range defaults {
aliases = append(aliases, alias)
}
sort.Strings(aliases) // deterministic seed order

ret := make([]ArchitectureAlias, 0, len(defaults))
for _, alias := range aliases {
ret = append(ret, ArchitectureAlias{Alias: alias, Canonical: defaults[alias]})
}
return ret
}

func ptr[T any](v T) *T {
return &v
}
7 changes: 6 additions & 1 deletion grype/db/v6/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (
Revision = 1

// Addition indicates how many changes have been introduced that are compatible with all historical data
Addition = 7
Addition = 8

// v6 model changelog:
// 6.0.0: Initial version 🎉
Expand All @@ -48,6 +48,10 @@ const (
// architecture). The field's semantics are unchanged; the rename drops the rpm-
// specific prefix because the value already lives in PackageQualifiers and can
// carry any architecture string for future arch-scoped advisories.
// 6.1.8: Add ArchitectureAlias table (architecture_aliases). The architecture qualifier
// reads it at match time to fold dialect arch spellings (e.g. "x86_64" <-> "amd64")
// onto a canonical token. Older clients ignore the table; clients reading a DB built
// before it existed fall back to the built-in default aliases.
)

const (
Expand Down Expand Up @@ -77,6 +81,7 @@ type Reader interface {
UnaffectedPackageStoreReader
AffectedCPEStoreReader
UnaffectedCPEStoreReader
ArchitectureAliasStoreReader
io.Closer
attachBlobValue(...blobable) error
}
Expand Down
17 changes: 17 additions & 0 deletions grype/db/v6/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func Models() []any {
&OperatingSystemSpecifierOverride{},
&Package{},
&PackageSpecifierOverride{},
&ArchitectureAlias{},

// CPE related search tables
&AffectedCPEHandle{}, // join on CPE
Expand Down Expand Up @@ -533,6 +534,22 @@ type PackageSpecifierOverride struct {
ReplacementEcosystem *string `gorm:"column:replacement_ecosystem;primaryKey"`
}

// ArchitectureAlias folds an architecture spelling onto a canonical token shared by every
// dialect spelling of the same CPU architecture (e.g. rpm "x86_64" and deb/OCI "amd64" both
// fold to "amd64"). The architecture qualifier reads this table at match time so cross-dialect
// arch comparisons don't drop real matches; the data lives in the DB rather than the grype
// binary so the folding can change without a release. See pkg/qualifier/architecture.
type ArchitectureAlias struct {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this complete? Do we need an "is platform independent" bool column or something? Right now the source code knows that noarch and all mean "this thing applies across all architectures" but the database doesn't?

We think that this case be added later if need be, and that it is probably not necessary.

// Alias is an architecture spelling as it may appear on a package or vulnerability entry
// (e.g. "x86_64"). Only non-canonical spellings are stored; a canonical token resolves to
// itself by absence.
Alias string `gorm:"column:alias;primaryKey"`

// Canonical is the token Alias folds to (e.g. "amd64"). The choice of token is arbitrary;
// only equivalence across spellings matters.
Canonical string `gorm:"column:canonical;not null"`
}

// OperatingSystem represents a specific release of an operating system. The resolution of the version is
// relative to the available data by the vulnerability data provider, so though there may be major.minor.patch OS
// releases, there may only be data available for major.minor.
Expand Down
7 changes: 7 additions & 0 deletions grype/db/v6/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type store struct {
*affectedCPEStore
*unaffectedCPEStore
*vulnerabilityDecoratorStore
*architectureAliasStore
blobStore *blobStore
db *gorm.DB
config Config
Expand All @@ -45,6 +46,11 @@ func InitialData() []any {
for i := range p {
data = append(data, &p[i])
}

a := KnownArchitectureAliases()
for i := range a {
data = append(data, &a[i])
}
return data
}

Expand Down Expand Up @@ -96,6 +102,7 @@ func newStore(cfg Config, empty, writable bool) (*store, error) {
affectedCPEStore: newAffectedCPEStore(db, bs),
vulnerabilityDecoratorStore: newVulnerabilityDecoratorStore(db, bs, dbVersion),
unaffectedCPEStore: newUnaffectedCPEStore(db, bs),
architectureAliasStore: newArchitectureAliasStore(db),
blobStore: bs,
db: db,
config: cfg,
Expand Down
Loading
Loading