From 224be590b37a42bfebe459caac8d7ce8386bdba9 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 17 Dec 2024 10:54:48 -0500 Subject: [PATCH] change store to support search Signed-off-by: Alex Goodman --- grype/db/v6/affected_cpe_store.go | 10 +- grype/db/v6/affected_package_store.go | 226 ++++++++++++++------- grype/db/v6/affected_package_store_test.go | 96 ++++----- grype/db/v6/blobs.go | 9 + grype/db/v6/models.go | 8 +- grype/db/v6/vulnerability_store.go | 90 +++++--- grype/db/v6/vulnerability_store_test.go | 72 +++++++ 7 files changed, 352 insertions(+), 159 deletions(-) diff --git a/grype/db/v6/affected_cpe_store.go b/grype/db/v6/affected_cpe_store.go index 4fd80e61338..214f6e3c92b 100644 --- a/grype/db/v6/affected_cpe_store.go +++ b/grype/db/v6/affected_cpe_store.go @@ -22,7 +22,7 @@ type GetAffectedCPEOptions struct { PreloadCPE bool PreloadVulnerability bool PreloadBlob bool - Vulnerability *VulnerabilitySpecifier + Vulnerabilities []VulnerabilitySpecifier } type affectedCPEStore struct { @@ -68,7 +68,7 @@ func (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetAffec query := s.handleCPE(s.db, cpe) var err error - query, err = s.handleVulnerabilityOptions(query, config.Vulnerability) + query, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities) if err != nil { return nil, err } @@ -110,14 +110,14 @@ func (s *affectedCPEStore) handleCPE(query *gorm.DB, c *cpe.Attributes) *gorm.DB return handleCPEOptions(query, c) } -func (s *affectedCPEStore) handleVulnerabilityOptions(query *gorm.DB, config *VulnerabilitySpecifier) (*gorm.DB, error) { - if config == nil { +func (s *affectedCPEStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier) (*gorm.DB, error) { + if len(configs) == 0 { return query, nil } query = query.Joins("JOIN vulnerability_handles ON affected_cpe_handles.vulnerability_id = vulnerability_handles.id") - return handleVulnerabilityOptions(query, config) + return handleVulnerabilityOptions(s.db, query, configs...) } func (s *affectedCPEStore) handlePreload(query *gorm.DB, config GetAffectedCPEOptions) *gorm.DB { diff --git a/grype/db/v6/affected_package_store.go b/grype/db/v6/affected_package_store.go index bebf827d720..5bd1cc64794 100644 --- a/grype/db/v6/affected_package_store.go +++ b/grype/db/v6/affected_package_store.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "sort" "strings" "gorm.io/gorm" @@ -12,9 +13,14 @@ import ( "github.com/anchore/syft/syft/cpe" ) -var NoDistroSpecified = &DistroSpecifier{} -var AnyDistroSpecified *DistroSpecifier -var ErrMissingDistroIdentification = errors.New("missing distro name or codename") +const ( + pkgNotSpecified = "not-specified" + anyOS = "any" +) + +var NoOSSpecified = &OSSpecifier{} +var AnyOSSpecified *OSSpecifier +var ErrMissingDistroIdentification = errors.New("missing os name or codename") var ErrDistroNotPresent = errors.New("distro not present") var ErrMultipleOSMatches = errors.New("multiple OS matches found but not allowed") @@ -24,10 +30,12 @@ type GetAffectedPackageOptions struct { PreloadPackageCPEs bool PreloadVulnerability bool PreloadBlob bool - Distro *DistroSpecifier - Vulnerability *VulnerabilitySpecifier + OSs OSSpecifiers + Vulnerabilities VulnerabilitySpecifiers } +type PackageSpecifiers []*PackageSpecifier + type PackageSpecifier struct { Name string Type string @@ -36,7 +44,7 @@ type PackageSpecifier struct { func (p *PackageSpecifier) String() string { if p == nil { - return "no-package-specified" + return pkgNotSpecified } var args []string @@ -53,14 +61,28 @@ func (p *PackageSpecifier) String() string { } if len(args) > 0 { - return fmt.Sprintf("pkg(%s)", strings.Join(args, ", ")) + return fmt.Sprintf("package(%s)", strings.Join(args, ", ")) + } + + return pkgNotSpecified +} + +func (p PackageSpecifiers) String() string { + if len(p) == 0 { + return pkgNotSpecified } - return "no-package-specified" + var parts []string + for _, v := range p { + parts = append(parts, v.String()) + } + return strings.Join(parts, ", ") } -// DistroSpecifier is a struct that represents a distro in a way that can be used to query the affected package store. -type DistroSpecifier struct { +type OSSpecifiers []*OSSpecifier + +// OSSpecifier is a struct that represents a distro in a way that can be used to query the affected package store. +type OSSpecifier struct { // Name of the distro as identified by the ID field in /etc/os-release Name string @@ -81,7 +103,37 @@ type DistroSpecifier struct { AllowMultiple bool } -func (d DistroSpecifier) version() string { +func (d *OSSpecifier) String() string { + if d == nil { + return anyOS + } + + if *d == *NoOSSpecified { + return "none" + } + + var version string + if d.MajorVersion != "" { + version = d.MajorVersion + if d.MinorVersion != "" { + version += "." + d.MinorVersion + } + } else { + version = d.Codename + } + + distroDisplayName := d.Name + if version != "" { + distroDisplayName += "@" + version + } + if version == d.MajorVersion && d.Codename != "" { + distroDisplayName += " (" + d.Codename + ")" + } + + return distroDisplayName +} + +func (d OSSpecifier) version() string { if d.MajorVersion != "" && d.MinorVersion != "" { return d.MajorVersion + "." + d.MinorVersion } @@ -101,7 +153,28 @@ func (d DistroSpecifier) version() string { return "" } -func (d DistroSpecifier) matchesVersionPattern(pattern string) bool { +func (d OSSpecifiers) String() string { + if d.IsAny() { + return anyOS + } + var parts []string + for _, v := range d { + parts = append(parts, v.String()) + } + return strings.Join(parts, ", ") +} + +func (d OSSpecifiers) IsAny() bool { + if len(d) == 0 { + return true + } + if len(d) == 1 && d[0] == AnyOSSpecified { + return true + } + return false +} + +func (d OSSpecifier) matchesVersionPattern(pattern string) bool { // check if version or version label matches the given regex r, err := regexp.Compile(pattern) if err != nil { @@ -156,17 +229,17 @@ func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config config = &GetAffectedPackageOptions{} } - log.WithFields("pkg", pkg.String(), "distro", distroDisplay(config.Distro)).Trace("fetching AffectedPackage record") + log.WithFields("pkg", pkg.String(), "distro", config.OSs, "vulns", config.Vulnerabilities).Trace("fetching AffectedPackage record") query := s.handlePackage(s.db, pkg) var err error - query, err = s.handleVulnerabilityOptions(query, config.Vulnerability) + query, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities) if err != nil { return nil, err } - query, err = s.handleDistroOptions(query, config.Distro) + query, err = s.handleOSOptions(query, config.OSs) if err != nil { return nil, err } @@ -221,57 +294,82 @@ func (s *affectedPackageStore) handlePackage(query *gorm.DB, config *PackageSpec return query } -func (s *affectedPackageStore) handleVulnerabilityOptions(query *gorm.DB, config *VulnerabilitySpecifier) (*gorm.DB, error) { - if config == nil { +func (s *affectedPackageStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier) (*gorm.DB, error) { + if len(configs) == 0 { return query, nil } query = query.Joins("JOIN vulnerability_handles ON affected_package_handles.vulnerability_id = vulnerability_handles.id") - return handleVulnerabilityOptions(query, config) + return handleVulnerabilityOptions(s.db, query, configs...) } -func (s *affectedPackageStore) handleDistroOptions(query *gorm.DB, config *DistroSpecifier) (*gorm.DB, error) { - var resolvedDistros []OperatingSystem - var err error +func (s *affectedPackageStore) handleOSOptions(query *gorm.DB, configs []*OSSpecifier) (*gorm.DB, error) { + resolvedDistroMap := make(map[int64]OperatingSystem) - switch { - case hasDistroSpecified(config): - resolvedDistros, err = s.resolveDistro(*config) - if err != nil { - return nil, fmt.Errorf("unable to resolve distro: %w", err) - } + if len(configs) == 0 { + configs = append(configs, AnyOSSpecified) + } + var hasAny, hasNone, hasSpecific bool + for _, config := range configs { switch { - case len(resolvedDistros) == 0: - return nil, ErrDistroNotPresent - case len(resolvedDistros) > 1 && !config.AllowMultiple: - return nil, ErrMultipleOSMatches + case hasDistroSpecified(config): + curResolvedDistros, err := s.resolveDistro(*config) + if err != nil { + return nil, fmt.Errorf("unable to resolve distro: %w", err) + } + + switch { + case len(curResolvedDistros) == 0: + return nil, ErrDistroNotPresent + case len(curResolvedDistros) > 1 && !config.AllowMultiple: + return nil, ErrMultipleOSMatches + } + hasSpecific = true + for _, d := range curResolvedDistros { + resolvedDistroMap[int64(d.ID)] = d + } + case config == AnyOSSpecified: + // TODO: one enhancement we may want to do later is "has OS defined but is not specific" which this does NOT cover. This is "may or may not have an OS defined" which is different. + hasAny = true + case *config == *NoOSSpecified: + hasNone = true } - case config == AnyDistroSpecified: - // TODO: one enhancement we may want to do later is "has OS defined but is not specific" which this does NOT cover. This is "may or may not have an OS defined" which is different. + } + + if (hasAny || hasNone) && hasSpecific { + return nil, fmt.Errorf("cannot mix specific distro with any or none distro specifiers") + } + + var resolvedDistros []OperatingSystem + switch { + case hasAny: return query, nil - case *config == *NoDistroSpecified: + case hasNone: return query.Where("operating_system_id IS NULL"), nil + case hasSpecific: + for _, d := range resolvedDistroMap { + resolvedDistros = append(resolvedDistros, d) + } + sort.Slice(resolvedDistros, func(i, j int) bool { + return resolvedDistros[i].ID < resolvedDistros[j].ID + }) } query = query.Joins("JOIN operating_systems ON affected_package_handles.operating_system_id = operating_systems.id") - var count int - for _, o := range resolvedDistros { - if o.ID != 0 { - if count == 0 { - query = query.Where("operating_systems.id = ?", o.ID) - } else { - query = query.Or("operating_systems.id = ?", o.ID) - } - count++ + if len(resolvedDistros) > 0 { + ids := make([]ID, len(resolvedDistros)) + for i, d := range resolvedDistros { + ids[i] = d.ID } + query = query.Where("operating_systems.id IN ?", ids) } return query, nil } -func (s *affectedPackageStore) resolveDistro(d DistroSpecifier) ([]OperatingSystem, error) { +func (s *affectedPackageStore) resolveDistro(d OSSpecifier) ([]OperatingSystem, error) { if d.Name == "" && d.Codename == "" { return nil, ErrMissingDistroIdentification } @@ -300,7 +398,7 @@ func (s *affectedPackageStore) resolveDistro(d DistroSpecifier) ([]OperatingSyst return s.searchForDistroVersionVariants(query, d) } -func (s *affectedPackageStore) searchForDistroVersionVariants(query *gorm.DB, d DistroSpecifier) ([]OperatingSystem, error) { +func (s *affectedPackageStore) searchForDistroVersionVariants(query *gorm.DB, d OSSpecifier) ([]OperatingSystem, error) { var allOs []OperatingSystem handleQuery := func(q *gorm.DB, desc string) ([]OperatingSystem, error) { @@ -354,7 +452,7 @@ func (s *affectedPackageStore) searchForDistroVersionVariants(query *gorm.DB, d return allOs, nil } -func (s *affectedPackageStore) applyAlias(d *DistroSpecifier) error { +func (s *affectedPackageStore) applyAlias(d *OSSpecifier) error { if d.Name == "" { return nil } @@ -473,42 +571,12 @@ func handleCPEOptions(query *gorm.DB, c *cpe.Attributes) *gorm.DB { return query } -func distroDisplay(d *DistroSpecifier) string { - if d == nil { - return "any" - } - - if *d == *NoDistroSpecified { - return "none" - } - - var version string - if d.MajorVersion != "" { - version = d.MajorVersion - if d.MinorVersion != "" { - version += "." + d.MinorVersion - } - } else { - version = d.Codename - } - - distroDisplayName := d.Name - if version != "" { - distroDisplayName += "@" + version - } - if version == d.MajorVersion && d.Codename != "" { - distroDisplayName += " (" + d.Codename + ")" - } - - return distroDisplayName -} - -func hasDistroSpecified(d *DistroSpecifier) bool { - if d == AnyDistroSpecified { +func hasDistroSpecified(d *OSSpecifier) bool { + if d == AnyOSSpecified { return false } - if *d == *NoDistroSpecified { + if *d == *NoOSSpecified { return false } return true diff --git a/grype/db/v6/affected_package_store_test.go b/grype/db/v6/affected_package_store_test.go index e3d7b877100..3045401c1ce 100644 --- a/grype/db/v6/affected_package_store_test.go +++ b/grype/db/v6/affected_package_store_test.go @@ -540,11 +540,11 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "specific distro", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Distro: &DistroSpecifier{ + OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, @@ -552,11 +552,11 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "distro major version only (allow multiple)", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Distro: &DistroSpecifier{ + OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", AllowMultiple: true, - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2}, }, @@ -564,11 +564,11 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "distro major version only (default)", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Distro: &DistroSpecifier{ + OSs: []*OSSpecifier{{ Name: "ubuntu", MajorVersion: "20", AllowMultiple: false, - }, + }}, }, wantErr: expectErrIs(t, ErrMultipleOSMatches), }, @@ -576,10 +576,10 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "distro codename", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Distro: &DistroSpecifier{ + OSs: []*OSSpecifier{{ Name: "ubuntu", Codename: "groovy", - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d2}, }, @@ -587,7 +587,7 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "no distro", pkg: pkgFromName(pkg2.Package.Name), options: &GetAffectedPackageOptions{ - Distro: NoDistroSpecified, + OSs: []*OSSpecifier{NoOSSpecified}, }, expected: []AffectedPackageHandle{*pkg2}, }, @@ -595,9 +595,9 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "any distro", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Distro: AnyDistroSpecified, + OSs: []*OSSpecifier{AnyOSSpecified}, }, - expected: []AffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2}, + expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2, *pkg2}, }, { name: "package type", @@ -608,9 +608,9 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "specific CVE", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Vulnerability: &VulnerabilitySpecifier{ + Vulnerabilities: []VulnerabilitySpecifier{{ Name: "CVE-2023-1234", - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, @@ -618,12 +618,12 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "any CVE published after a date", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Vulnerability: &VulnerabilitySpecifier{ + Vulnerabilities: []VulnerabilitySpecifier{{ PublishedAfter: func() *time.Time { now := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC) return &now }(), - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d1, *pkg2d2}, }, @@ -631,12 +631,12 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "any CVE modified after a date", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Vulnerability: &VulnerabilitySpecifier{ + Vulnerabilities: []VulnerabilitySpecifier{{ ModifiedAfter: func() *time.Time { now := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC).Add(time.Hour * 2) return &now }(), - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, @@ -644,9 +644,9 @@ func TestAffectedPackageStore_GetAffectedPackages(t *testing.T) { name: "any rejected CVE", pkg: pkgFromName(pkg2d1.Package.Name), options: &GetAffectedPackageOptions{ - Vulnerability: &VulnerabilitySpecifier{ + Vulnerabilities: []VulnerabilitySpecifier{{ Status: VulnerabilityRejected, - }, + }}, }, expected: []AffectedPackageHandle{*pkg2d1}, }, @@ -720,13 +720,13 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { tests := []struct { name string - distro DistroSpecifier + distro OSSpecifier expected []OperatingSystem expectErr require.ErrorAssertionFunc }{ { name: "specific distro with major and minor version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", @@ -735,7 +735,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "alias resolution with major version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "centos", MajorVersion: "8", }, @@ -743,7 +743,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "alias resolution with major and minor version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "centos", MajorVersion: "8", MinorVersion: "1", @@ -752,7 +752,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "distro with major version only", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "debian", MajorVersion: "10", }, @@ -760,7 +760,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "codename resolution", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "ubuntu", Codename: "focal", }, @@ -768,7 +768,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "codename and version info", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", @@ -778,7 +778,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "conflicting codename and version info", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", @@ -787,7 +787,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "alpine edge version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "alpine", MajorVersion: "3", MinorVersion: "21", @@ -797,14 +797,14 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "arch rolling variant", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "arch", }, expected: []OperatingSystem{*arch}, }, { name: "wolfi rolling variant", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "wolfi", MajorVersion: "20221018", }, @@ -812,7 +812,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "debian by codename for rolling alias", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "debian", MajorVersion: "13", Codename: "trixie", // TODO: what about sid status indication from pretty-name or /etc/debian_version? @@ -821,7 +821,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "debian by codename", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "debian", Codename: "wheezy", }, @@ -829,7 +829,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "debian by major version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "debian", MajorVersion: "7", }, @@ -837,7 +837,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "debian by major.minor version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "debian", MajorVersion: "7", MinorVersion: "2", @@ -846,7 +846,7 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "alpine with major and minor version", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "alpine", MajorVersion: "3", MinorVersion: "18", @@ -855,14 +855,14 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { }, { name: "missing distro name", - distro: DistroSpecifier{ + distro: OSSpecifier{ MajorVersion: "8", }, expectErr: expectErrIs(t, ErrMissingDistroIdentification), }, { name: "nonexistent distro", - distro: DistroSpecifier{ + distro: OSSpecifier{ Name: "madeup", MajorVersion: "99", }, @@ -887,32 +887,32 @@ func TestAffectedPackageStore_ResolveDistro(t *testing.T) { } } -func TestDistroDisplay(t *testing.T) { +func TestDistroSpecifier_String(t *testing.T) { tests := []struct { name string - distro *DistroSpecifier + distro *OSSpecifier expected string }{ { name: "nil distro", - distro: AnyDistroSpecified, + distro: AnyOSSpecified, expected: "any", }, { name: "no distro specified", - distro: NoDistroSpecified, + distro: NoOSSpecified, expected: "none", }, { name: "only name specified", - distro: &DistroSpecifier{ + distro: &OSSpecifier{ Name: "ubuntu", }, expected: "ubuntu", }, { name: "name and major version specified", - distro: &DistroSpecifier{ + distro: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", }, @@ -920,7 +920,7 @@ func TestDistroDisplay(t *testing.T) { }, { name: "name, major, and minor version specified", - distro: &DistroSpecifier{ + distro: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", @@ -929,7 +929,7 @@ func TestDistroDisplay(t *testing.T) { }, { name: "name, major version, and codename specified", - distro: &DistroSpecifier{ + distro: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", Codename: "focal", @@ -938,7 +938,7 @@ func TestDistroDisplay(t *testing.T) { }, { name: "name and codename specified", - distro: &DistroSpecifier{ + distro: &OSSpecifier{ Name: "ubuntu", Codename: "focal", }, @@ -946,7 +946,7 @@ func TestDistroDisplay(t *testing.T) { }, { name: "name, major version, minor version, and codename specified", - distro: &DistroSpecifier{ + distro: &OSSpecifier{ Name: "ubuntu", MajorVersion: "20", MinorVersion: "04", @@ -958,7 +958,7 @@ func TestDistroDisplay(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := distroDisplay(tt.distro) + result := tt.distro.String() require.Equal(t, tt.expected, result) }) } diff --git a/grype/db/v6/blobs.go b/grype/db/v6/blobs.go index 6cea2d8bb88..dbd9eecd168 100644 --- a/grype/db/v6/blobs.go +++ b/grype/db/v6/blobs.go @@ -3,6 +3,7 @@ package v6 import ( "encoding/json" "fmt" + "strings" "time" ) @@ -96,6 +97,14 @@ type CVSSSeverity struct { Score float64 `json:"score"` } +func (c CVSSSeverity) String() string { + vector := c.Vector + if !strings.HasPrefix(strings.ToLower(c.Vector), "cvss:") && c.Version != "" { + vector = fmt.Sprintf("CVSS:%s/%s", c.Version, c.Vector) + } + return fmt.Sprintf("%s (%1.1f)", vector, c.Score) +} + // AffectedPackageBlob represents a package affected by a vulnerability. type AffectedPackageBlob struct { // CVEs is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability. diff --git a/grype/db/v6/models.go b/grype/db/v6/models.go index 3a4dbf30c9e..f427fea9a77 100644 --- a/grype/db/v6/models.go +++ b/grype/db/v6/models.go @@ -187,13 +187,13 @@ type VulnerabilityAlias struct { // name (which might or might not be the product name in the CPE), in which case AffectedCPEHandle should be used. type AffectedPackageHandle struct { ID ID `gorm:"column:id;primaryKey"` - VulnerabilityID ID `gorm:"column:vulnerability_id;not null"` + VulnerabilityID ID `gorm:"column:vulnerability_id;index;not null"` Vulnerability *VulnerabilityHandle `gorm:"foreignKey:VulnerabilityID"` - OperatingSystemID *ID `gorm:"column:operating_system_id"` + OperatingSystemID *ID `gorm:"column:operating_system_id;index"` OperatingSystem *OperatingSystem `gorm:"foreignKey:OperatingSystemID"` - PackageID ID `gorm:"column:package_id"` + PackageID ID `gorm:"column:package_id;index"` Package *Package `gorm:"foreignKey:PackageID"` BlobID ID `gorm:"column:blob_id"` @@ -282,7 +282,7 @@ type OperatingSystem struct { MajorVersion string `gorm:"column:major_version;index:os_idx,unique"` MinorVersion string `gorm:"column:minor_version;index:os_idx,unique"` LabelVersion string `gorm:"column:label_version;index:os_idx,unique"` - Codename string `gorm:"column:codename"` + Codename string `gorm:"column:codename"` // TODO: should this be removed and use label-version instead? } func (os *OperatingSystem) BeforeCreate(tx *gorm.DB) (err error) { diff --git a/grype/db/v6/vulnerability_store.go b/grype/db/v6/vulnerability_store.go index 8b0a74aa923..76f5a172930 100644 --- a/grype/db/v6/vulnerability_store.go +++ b/grype/db/v6/vulnerability_store.go @@ -25,6 +25,8 @@ type GetVulnerabilityOptions struct { Preload bool } +type VulnerabilitySpecifiers []VulnerabilitySpecifier + type VulnerabilitySpecifier struct { // Name of the vulnerability (e.g. CVE-2020-1234) Name string @@ -43,6 +45,9 @@ type VulnerabilitySpecifier struct { // IncludeAliases for the given name or ID in results IncludeAliases bool + + // Providers + Providers []string } func (v *VulnerabilitySpecifier) String() string { @@ -67,7 +72,23 @@ func (v *VulnerabilitySpecifier) String() string { parts = append(parts, fmt.Sprintf("modifiedAfter=%s", v.ModifiedAfter.String())) } - return fmt.Sprintf("VulnerabilitySpecifier(%s)", strings.Join(parts, ", ")) + if v.IncludeAliases { + parts = append(parts, "includeAliases=true") + } + + if len(v.Providers) > 0 { + parts = append(parts, fmt.Sprintf("providers=%s", strings.Join(v.Providers, ","))) + } + + return fmt.Sprintf("vulnerability(%s)", strings.Join(parts, ", ")) +} + +func (s VulnerabilitySpecifiers) String() string { + var parts []string + for _, v := range s { + parts = append(parts, v.String()) + } + return strings.Join(parts, ", ") } func DefaultGetVulnerabilityOptions() *GetVulnerabilityOptions { @@ -168,9 +189,13 @@ func (s *vulnerabilityStore) GetVulnerabilities(vuln *VulnerabilitySpecifier, co var models []VulnerabilityHandle - query, err := handleVulnerabilityOptions(s.db, vuln) - if err != nil { - return nil, err + var err error + query := s.db + if vuln != nil { + query, err = handleVulnerabilityOptions(s.db.Model(&VulnerabilityHandle{}), query, *vuln) + if err != nil { + return nil, err + } } query = s.handlePreload(query, *config) @@ -218,31 +243,50 @@ func (s *vulnerabilityStore) attachBlob(vh *VulnerabilityHandle) error { return nil } -func handleVulnerabilityOptions(query *gorm.DB, config *VulnerabilitySpecifier) (*gorm.DB, error) { - if config.Name != "" { - if config.IncludeAliases { - query = query.Joins("LEFT JOIN vulnerability_aliases ON vulnerability_aliases.name = vulnerability_handles.name") - query = query.Where("vulnerability_handles.name = ? OR vulnerability_aliases.alias = ?", config.Name, config.Name) - } else { - query = query.Where("vulnerability_handles.name = ?", config.Name) - } +func handleVulnerabilityOptions(base, parentQuery *gorm.DB, configs ...VulnerabilitySpecifier) (*gorm.DB, error) { + if len(configs) == 0 { + return parentQuery, nil } - if config.ID != 0 { - query = query.Where("vulnerability_handles.id = ?", config.ID) - } + orConditions := base.Model(&VulnerabilityHandle{}) + var includeAliasJoin bool + for _, config := range configs { + query := base.Model(&VulnerabilityHandle{}) + if config.Name != "" { + if config.IncludeAliases { + includeAliasJoin = true + query = query.Where("vulnerability_handles.name = ? OR vulnerability_aliases.alias = ?", config.Name, config.Name) + } else { + query = query.Where("vulnerability_handles.name = ?", config.Name) + } + } - if config.PublishedAfter != nil { - query = query.Where("vulnerability_handles.published_date > ?", *config.PublishedAfter) - } + if config.ID != 0 { + query = query.Where("vulnerability_handles.id = ?", config.ID) + } + + if config.PublishedAfter != nil { + query = query.Where("vulnerability_handles.published_date > ?", *config.PublishedAfter) + } + + if config.ModifiedAfter != nil { + query = query.Where("vulnerability_handles.modified_date > ?", *config.ModifiedAfter) + } + + if config.Status != "" { + query = query.Where("vulnerability_handles.status = ?", config.Status) + } + + if len(config.Providers) > 0 { + query = query.Where("vulnerability_handles.provider_id IN ?", config.Providers) + } - if config.ModifiedAfter != nil { - query = query.Where("vulnerability_handles.modified_date > ?", *config.ModifiedAfter) + orConditions = orConditions.Or(query) } - if config.Status != "" { - query = query.Where("vulnerability_handles.status = ?", config.Status) + if includeAliasJoin { + parentQuery = parentQuery.Joins("LEFT JOIN vulnerability_aliases ON vulnerability_aliases.name = vulnerability_handles.name") } - return query, nil + return parentQuery.Where(orConditions), nil } diff --git a/grype/db/v6/vulnerability_store_test.go b/grype/db/v6/vulnerability_store_test.go index 73cf933bd47..c6267e0fc9f 100644 --- a/grype/db/v6/vulnerability_store_test.go +++ b/grype/db/v6/vulnerability_store_test.go @@ -321,3 +321,75 @@ func testVulnerabilityHandle() VulnerabilityHandle { }, } } + +func TestVulnerabilityStore_GetVulnerabilities_ByProviders(t *testing.T) { + db := setupTestStore(t).db + bw := newBlobStore(db) + s := newVulnerabilityStore(db, bw) + + provider1 := &Provider{ID: "provider1"} + provider2 := &Provider{ID: "provider2"} + + vuln1 := VulnerabilityHandle{Name: "CVE-1234-5678", BlobID: 1, Provider: provider1} + vuln2 := VulnerabilityHandle{Name: "CVE-2345-6789", BlobID: 2, Provider: provider2} + + err := s.AddVulnerabilities(&vuln1, &vuln2) + require.NoError(t, err) + + results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1"}}, nil) + require.NoError(t, err) + require.Len(t, results, 1) + assert.Equal(t, vuln1.Name, results[0].Name) + assert.Equal(t, vuln1.Provider.ID, results[0].ProviderID) + + results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1", "provider2"}}, nil) + require.NoError(t, err) + require.Len(t, results, 2) + assert.ElementsMatch(t, []string{vuln1.Name, vuln2.Name}, []string{results[0].Name, results[1].Name}) +} + +func TestVulnerabilityStore_GetVulnerabilities_FilterByMultipleFactors(t *testing.T) { + db := setupTestStore(t).db + bw := newBlobStore(db) + s := newVulnerabilityStore(db, bw) + + now := time.Now() + oneDayAgo := now.Add(-24 * time.Hour) + halfDayAgo := now.Add(-12 * time.Hour) + tenDaysAgo := now.Add(-240 * time.Hour) + + provider1 := &Provider{ID: "provider1"} + provider2 := &Provider{ID: "provider2"} + + vuln1 := VulnerabilityHandle{ + Name: "CVE-1234-5678", + BlobID: 1, + Provider: provider1, + PublishedDate: &halfDayAgo, + } + + vuln2 := VulnerabilityHandle{ + Name: "CVE-2345-6789", + BlobID: 2, + Provider: provider2, // filtered out due to provider + PublishedDate: &now, + } + + vuln3 := VulnerabilityHandle{ + Name: "CVE-1234-5678", + BlobID: 3, + Provider: provider1, + PublishedDate: &tenDaysAgo, // filtered out due to date + } + + err := s.AddVulnerabilities(&vuln1, &vuln2, &vuln3) + require.NoError(t, err) + + results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{ + Providers: []string{"provider1"}, // filter by provider... + PublishedAfter: &oneDayAgo, // filter by date published... + }, nil) + require.NoError(t, err) + require.Len(t, results, 1) + assert.Equal(t, vuln1.Name, results[0].Name) +}