From 9839d30b12d9a9db5826bddf58ef90c07b3e85d2 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 18 Nov 2024 08:24:27 -0500 Subject: [PATCH 1/6] adjust enumerations Signed-off-by: Alex Goodman --- grype/db/v6/enumerations.go | 24 +++++++++++++++++++++--- grype/db/v6/enumerations_test.go | 1 + 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/grype/db/v6/enumerations.go b/grype/db/v6/enumerations.go index 1f2ad4c6a14..c5364ac234b 100644 --- a/grype/db/v6/enumerations.go +++ b/grype/db/v6/enumerations.go @@ -7,7 +7,7 @@ import "strings" type VulnerabilityStatus string const ( - UnknownVulnerabilityStatus VulnerabilityStatus = "?" + UnknownVulnerabilityStatus VulnerabilityStatus = "" // VulnerabilityActive means that the information from the vulnerability record is actionable VulnerabilityActive VulnerabilityStatus = "active" // empty also means active @@ -26,7 +26,7 @@ const ( type SeverityScheme string const ( - UnknownSeverityScheme SeverityScheme = "?" + UnknownSeverityScheme SeverityScheme = "" // SeveritySchemeCVSS is the Common Vulnerability Scoring System severity scheme SeveritySchemeCVSS SeverityScheme = "CVSS" @@ -34,6 +34,9 @@ const ( // SeveritySchemeHML is a string severity scheme (High, Medium, Low) SeveritySchemeHML SeverityScheme = "HML" + // SeveritySchemeCHML is a string severity scheme (Critical, High, Medium, Low) + SeveritySchemeCHML SeverityScheme = "CHML" + // SeveritySchemeCHMLN is a string severity scheme (Critical, High, Medium, Low, Negligible) SeveritySchemeCHMLN SeverityScheme = "CHMLN" ) @@ -42,7 +45,7 @@ const ( type FixStatus string const ( - UnknownFixStatus FixStatus = "?" + UnknownFixStatus FixStatus = "" // FixedStatus affirms the package is affected and a fix is available FixedStatus FixStatus = "fixed" @@ -57,6 +60,11 @@ const ( NotAffectedFixStatus FixStatus = "not-affected" ) +const ( + // AdvisoryReferenceTag is a reference to a vulnerability advisory + AdvisoryReferenceTag string = "advisory" +) + func ParseVulnerabilityStatus(s string) VulnerabilityStatus { switch strings.TrimSpace(strings.ToLower(s)) { case string(VulnerabilityActive), "": @@ -78,6 +86,8 @@ func ParseSeverityScheme(s string) SeverityScheme { return SeveritySchemeCVSS case strings.ToLower(string(SeveritySchemeHML)): return SeveritySchemeHML + case strings.ToLower(string(SeveritySchemeCHML)): + return SeveritySchemeCHML case strings.ToLower(string(SeveritySchemeCHMLN)): return SeveritySchemeCHMLN default: @@ -100,6 +110,14 @@ func ParseFixStatus(s string) FixStatus { } } +func NormalizeReferenceTags(tags []string) []string { + var normalized []string + for _, tag := range tags { + normalized = append(normalized, replaceAny(strings.ToLower(strings.TrimSpace(tag)), "-", " ", "_")) + } + return normalized +} + func replaceAny(input string, newStr string, searchFor ...string) string { for _, s := range searchFor { input = strings.ReplaceAll(input, s, newStr) diff --git a/grype/db/v6/enumerations_test.go b/grype/db/v6/enumerations_test.go index 974129126c2..304db01051e 100644 --- a/grype/db/v6/enumerations_test.go +++ b/grype/db/v6/enumerations_test.go @@ -35,6 +35,7 @@ func TestParseSeverityScheme(t *testing.T) { }{ {"CVSS scheme", "Cvss", SeveritySchemeCVSS}, {"HML scheme", "H-M-l", SeveritySchemeHML}, + {"CHML scheme", "ChmL", SeveritySchemeCHML}, {"CHMLN scheme", "CHmLN", SeveritySchemeCHMLN}, {"Unknown scheme", "unknown", UnknownSeverityScheme}, } From a16151b439f87b8d3d5260b7c477897c3a8c924d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 20 Nov 2024 11:17:33 -0500 Subject: [PATCH 2/6] simplify distribution artifacts and plumb to import Signed-off-by: Alex Goodman --- cmd/grype/cli/commands/db_check.go | 2 +- cmd/grype/cli/commands/db_delete.go | 2 +- cmd/grype/cli/commands/db_diff.go | 4 +- cmd/grype/cli/commands/db_import.go | 39 +++++++++++- cmd/grype/cli/commands/db_list.go | 2 +- cmd/grype/cli/commands/db_providers.go | 2 +- cmd/grype/cli/commands/db_search.go | 2 +- cmd/grype/cli/commands/db_status.go | 2 +- cmd/grype/cli/commands/db_update.go | 2 +- cmd/grype/cli/commands/root.go | 2 +- cmd/grype/cli/options/database.go | 27 ++++++++- grype/db/v6/description.go | 75 ++++++++---------------- grype/db/v6/description_test.go | 2 +- grype/db/v6/distribution/latest.go | 60 ++++++++++++++----- grype/db/v6/installation/curator.go | 41 +++++++++---- grype/db/v6/installation/curator_test.go | 2 +- grype/db/v6/provider_store.go | 13 ++++ grype/db/v6/store.go | 8 +-- 18 files changed, 183 insertions(+), 104 deletions(-) diff --git a/cmd/grype/cli/commands/db_check.go b/cmd/grype/cli/commands/db_check.go index c4e6fd107a0..0f5fedd856c 100644 --- a/cmd/grype/cli/commands/db_check.go +++ b/cmd/grype/cli/commands/db_check.go @@ -34,7 +34,7 @@ func DBCheck(app clio.Application) *cobra.Command { } func runDBCheck(opts options.Database) error { - dbCurator, err := distribution.NewCurator(opts.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig()) if err != nil { return err } diff --git a/cmd/grype/cli/commands/db_delete.go b/cmd/grype/cli/commands/db_delete.go index df57fe0e8f7..0bb4b6553e4 100644 --- a/cmd/grype/cli/commands/db_delete.go +++ b/cmd/grype/cli/commands/db_delete.go @@ -25,7 +25,7 @@ func DBDelete(app clio.Application) *cobra.Command { } func runDBDelete(opts options.Database) error { - dbCurator, err := distribution.NewCurator(opts.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig()) if err != nil { return err } diff --git a/cmd/grype/cli/commands/db_diff.go b/cmd/grype/cli/commands/db_diff.go index 0072a4544aa..ab9b7f31bdc 100644 --- a/cmd/grype/cli/commands/db_diff.go +++ b/cmd/grype/cli/commands/db_diff.go @@ -65,7 +65,7 @@ func DBDiff(app clio.Application) *cobra.Command { } func runDBDiff(opts *dbDiffOptions, base string, target string) (errs error) { - d, err := differ.NewDiffer(opts.DB.ToCuratorConfig()) + d, err := differ.NewDiffer(opts.DB.ToLegacyCuratorConfig()) if err != nil { return err } @@ -104,7 +104,7 @@ func runDBDiff(opts *dbDiffOptions, base string, target string) (errs error) { } func getDefaultURLs(opts options.Database) (baseURL string, targetURL string, err error) { - dbCurator, err := distribution.NewCurator(opts.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig()) if err != nil { return "", "", err } diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go index ce1d8236472..43e15cdc886 100644 --- a/cmd/grype/cli/commands/db_import.go +++ b/cmd/grype/cli/commands/db_import.go @@ -2,12 +2,16 @@ package commands import ( "fmt" + "github.com/anchore/grype/grype/db/v6/distribution" + "github.com/anchore/grype/grype/db/v6/installation" + "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" - "github.com/anchore/grype/grype/db/legacy/distribution" + legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution" "github.com/anchore/grype/internal" ) @@ -27,14 +31,43 @@ func DBImport(app clio.Application) *cobra.Command { } func runDBImport(opts options.Database, dbArchivePath string) error { - dbCurator, err := distribution.NewCurator(opts.ToCuratorConfig()) + // TODO: tui update? better logging? + + if isLegacy(dbArchivePath) { + return legacyDBImport(opts, dbArchivePath) + } + return importDB(opts, dbArchivePath) +} + +func importDB(opts options.Database, dbArchivePath string) error { + client, err := distribution.NewClient(opts.ToClientConfig()) + if err != nil { + return fmt.Errorf("unable to create distribution client: %w", err) + } + c, err := installation.NewCurator(opts.ToCuratorConfig(), client) + if err != nil { + return fmt.Errorf("unable to create curator: %w", err) + } + + if err := c.Import(dbArchivePath); err != nil { + return fmt.Errorf("unable to import vulnerability database: %w", err) + } + return stderrPrintLnf("Vulnerability database imported") +} + +func legacyDBImport(opts options.Database, dbArchivePath string) error { + dbCurator, err := legacyDistribution.NewCurator(opts.ToLegacyCuratorConfig()) if err != nil { return err } if err := dbCurator.ImportFrom(dbArchivePath); err != nil { - return fmt.Errorf("unable to import vulnerability database: %+v", err) + return fmt.Errorf("unable to import vulnerability database: %w", err) } return stderrPrintLnf("Vulnerability database imported") } + +func isLegacy(path string) bool { + return !strings.Contains(filepath.Base(path), "vulnerability-db_v6") +} diff --git a/cmd/grype/cli/commands/db_list.go b/cmd/grype/cli/commands/db_list.go index 30a1cfd7bec..16a77e5d43e 100644 --- a/cmd/grype/cli/commands/db_list.go +++ b/cmd/grype/cli/commands/db_list.go @@ -40,7 +40,7 @@ func DBList(app clio.Application) *cobra.Command { } func runDBList(opts *dbListOptions) error { - dbCurator, err := distribution.NewCurator(opts.DB.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(opts.DB.ToLegacyCuratorConfig()) if err != nil { return err } diff --git a/cmd/grype/cli/commands/db_providers.go b/cmd/grype/cli/commands/db_providers.go index d25de084582..92410502d1f 100644 --- a/cmd/grype/cli/commands/db_providers.go +++ b/cmd/grype/cli/commands/db_providers.go @@ -83,7 +83,7 @@ func runDBProviders(opts *dbProvidersOptions, app clio.Application) error { } func getMetadataFileLocation(app clio.Application) (*string, error) { - dbCurator, err := distribution.NewCurator(dbOptionsDefault(app.ID()).DB.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(dbOptionsDefault(app.ID()).DB.ToLegacyCuratorConfig()) if err != nil { return nil, err } diff --git a/cmd/grype/cli/commands/db_search.go b/cmd/grype/cli/commands/db_search.go index 841d216b735..adcbc64abc7 100644 --- a/cmd/grype/cli/commands/db_search.go +++ b/cmd/grype/cli/commands/db_search.go @@ -46,7 +46,7 @@ func DBSearch(app clio.Application) *cobra.Command { func runDBSearch(opts *dbQueryOptions, vulnerabilityID string) error { log.Debug("loading DB") - str, status, dbCloser, err := grype.LoadVulnerabilityDB(opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate) + str, status, dbCloser, err := grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate) err = validateDBLoad(err, status) if err != nil { return err diff --git a/cmd/grype/cli/commands/db_status.go b/cmd/grype/cli/commands/db_status.go index 67c3f59ec9f..04834b27709 100644 --- a/cmd/grype/cli/commands/db_status.go +++ b/cmd/grype/cli/commands/db_status.go @@ -25,7 +25,7 @@ func DBStatus(app clio.Application) *cobra.Command { } func runDBStatus(opts options.Database) error { - dbCurator, err := distribution.NewCurator(opts.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig()) if err != nil { return err } diff --git a/cmd/grype/cli/commands/db_update.go b/cmd/grype/cli/commands/db_update.go index 9c055396d0c..d9515b945fb 100644 --- a/cmd/grype/cli/commands/db_update.go +++ b/cmd/grype/cli/commands/db_update.go @@ -31,7 +31,7 @@ func DBUpdate(app clio.Application) *cobra.Command { } func runDBUpdate(opts options.Database) error { - dbCurator, err := distribution.NewCurator(opts.ToCuratorConfig()) + dbCurator, err := distribution.NewCurator(opts.ToLegacyCuratorConfig()) if err != nil { return err } diff --git a/cmd/grype/cli/commands/root.go b/cmd/grype/cli/commands/root.go index c0c350dbc58..e95942bcbd9 100644 --- a/cmd/grype/cli/commands/root.go +++ b/cmd/grype/cli/commands/root.go @@ -154,7 +154,7 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs }, func() (err error) { log.Debug("loading DB") - str, status, dbCloser, err = grype.LoadVulnerabilityDB(opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate) + str, status, dbCloser, err = grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate) return validateDBLoad(err, status) }, func() (err error) { diff --git a/cmd/grype/cli/options/database.go b/cmd/grype/cli/options/database.go index 30b2f6aa49e..525868e40e4 100644 --- a/cmd/grype/cli/options/database.go +++ b/cmd/grype/cli/options/database.go @@ -1,13 +1,15 @@ package options import ( + "github.com/anchore/grype/grype/db/v6/distribution" + "github.com/anchore/grype/grype/db/v6/installation" "path" "time" "github.com/adrg/xdg" "github.com/anchore/clio" - "github.com/anchore/grype/grype/db/legacy/distribution" + legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution" "github.com/anchore/grype/internal" ) @@ -53,8 +55,29 @@ func DefaultDatabase(id clio.Identification) Database { } } -func (cfg Database) ToCuratorConfig() distribution.Config { +func (cfg Database) ToClientConfig() distribution.Config { return distribution.Config{ + ID: cfg.ID, + LatestURL: cfg.UpdateURL, + CACert: cfg.CACert, + RequireUpdateCheck: cfg.RequireUpdateCheck, + CheckTimeout: cfg.UpdateAvailableTimeout, // TODO: is this right? + UpdateTimeout: cfg.UpdateDownloadTimeout, + } +} + +func (cfg Database) ToCuratorConfig() installation.Config { + return installation.Config{ + DBRootDir: cfg.Dir, + ValidateAge: cfg.ValidateAge, + ValidateChecksum: cfg.ValidateByHashOnStart, + MaxAllowedBuiltAge: cfg.MaxAllowedBuiltAge, + UpdateCheckMaxFrequency: cfg.MaxUpdateCheckFrequency, + } +} + +func (cfg Database) ToLegacyCuratorConfig() legacyDistribution.Config { + return legacyDistribution.Config{ ID: cfg.ID, DBRootDir: cfg.Dir, ListingURL: cfg.UpdateURL, diff --git a/grype/db/v6/description.go b/grype/db/v6/description.go index a7100c11e95..9b8c57bdb53 100644 --- a/grype/db/v6/description.go +++ b/grype/db/v6/description.go @@ -2,6 +2,7 @@ package v6 import ( "bytes" + "crypto/sha256" "errors" "fmt" "io" @@ -25,9 +26,6 @@ type Description struct { // Built is the timestamp the database was built Built Time `json:"built"` - - // Checksum is the self-describing digest of the database file - Checksum string `json:"checksum"` } type Time struct { @@ -58,9 +56,7 @@ func (t Time) String() string { return t.Time.UTC().Round(time.Second).Format(time.RFC3339) } -func ReadDescription(dir string) (*Description, error) { - dbFilePath := filepath.Join(dir, VulnerabilityDBFileName) - +func ReadDescription(dbFilePath string) (*Description, error) { // check if exists if _, err := os.Stat(dbFilePath); err != nil { if errors.Is(err, os.ErrNotExist) { @@ -69,20 +65,23 @@ func ReadDescription(dir string) (*Description, error) { return nil, fmt.Errorf("failed to access database file: %w", err) } - desc, err := newPartialDescriptionFromDB(dbFilePath) + // access the DB to get the built time and schema version + r, err := NewReader(Config{ + DBDirPath: filepath.Dir(dbFilePath), + }) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read DB description: %w", err) } - // read checksums file value - checksum, err := ReadDBChecksum(dir) + meta, err := r.GetDBMetadata() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read DB metadata: %w", err) } - desc.Checksum = checksum - - return desc, nil + return &Description{ + SchemaVersion: schemaver.New(meta.Model, meta.Revision, meta.Addition), + Built: Time{Time: *meta.BuildTimestamp}, + }, nil } func ReadDBChecksum(dir string) (string, error) { @@ -103,23 +102,7 @@ func ReadDBChecksum(dir string) (string, error) { return string(checksums), nil } -func CalculateDescription(dbFilePath string) (*Description, error) { - desc, err := newPartialDescriptionFromDB(dbFilePath) - if err != nil { - return nil, err - } - - namedDigest, err := CalculateDigest(dbFilePath) - if err != nil { - return nil, err - } - - desc.Checksum = namedDigest - - return desc, nil -} - -func CalculateDigest(dbFilePath string) (string, error) { +func CalculateDBDigest(dbFilePath string) (string, error) { digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, xxhash.New64()) if err != nil { return "", fmt.Errorf("failed to calculate checksum for DB file: %w", err) @@ -127,39 +110,27 @@ func CalculateDigest(dbFilePath string) (string, error) { return fmt.Sprintf("xxh64:%s", digest), nil } -func newPartialDescriptionFromDB(dbFilePath string) (*Description, error) { - // access the DB to get the built time and schema version - r, err := NewReader(Config{ - DBDirPath: filepath.Dir(dbFilePath), - }) +func CalculateArchiveDigest(dbFilePath string) (string, error) { + digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, sha256.New()) if err != nil { - return nil, fmt.Errorf("failed to read DB description: %w", err) + return "", fmt.Errorf("failed to calculate checksum for DB archive file: %w", err) } - - meta, err := r.GetDBMetadata() - if err != nil { - return nil, fmt.Errorf("failed to read DB metadata: %w", err) - } - - return &Description{ - SchemaVersion: schemaver.New(meta.Model, meta.Revision, meta.Addition), - Built: Time{Time: *meta.BuildTimestamp}, - }, nil + return fmt.Sprintf("sha256:%s", digest), nil } func (m Description) String() string { - return fmt.Sprintf("DB(version=%s built=%s checksum=%s)", m.SchemaVersion, m.Built, m.Checksum) + return fmt.Sprintf("DB(version=%s built=%s)", m.SchemaVersion, m.Built) } -func WriteChecksums(writer io.Writer, m Description) error { - if m.Checksum == "" { +func WriteChecksums(writer io.Writer, value string) error { + if value == "" { return fmt.Errorf("checksum is required") } - if !strings.HasPrefix(m.Checksum, "xxh64:") { + if !strings.HasPrefix(value, "xxh64:") { return fmt.Errorf("checksum missing algorithm prefix") } - _, err := writer.Write([]byte(m.Checksum)) + _, err := writer.Write([]byte(value)) return err } diff --git a/grype/db/v6/description_test.go b/grype/db/v6/description_test.go index e4c2847975b..2987eadee8c 100644 --- a/grype/db/v6/description_test.go +++ b/grype/db/v6/description_test.go @@ -316,7 +316,7 @@ func TestCalculateDigest(t *testing.T) { require.NoError(t, err) } - digest, err := CalculateDigest(filePath) + digest, err := CalculateDBDigest(filePath) if tt.expectedErr != "" { require.ErrorContains(t, err, tt.expectedErr) diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go index 52ae13d7d60..e0b4b545349 100644 --- a/grype/db/v6/distribution/latest.go +++ b/grype/db/v6/distribution/latest.go @@ -3,29 +3,28 @@ package distribution import ( "encoding/json" "fmt" + "github.com/mholt/archiver/v3" "io" + "os" + "path/filepath" "sort" - "github.com/anchore/grype/grype/db/internal/schemaver" db "github.com/anchore/grype/grype/db/v6" ) const LatestFileName = "latest.json" type LatestDocument struct { - // SchemaVersion is the version of the DB schema - SchemaVersion schemaver.SchemaVer `json:"schemaVersion"` - // Status indicates if the database is actively being maintained and distributed Status Status `json:"status"` // Archive is the most recent database that has been built and distributed, additionally annotated with provider-level information - Archive Archive `json:"archive"` + Archive `json:",inline"` } type Archive struct { // Description contains details about the database contained within the distribution archive - Description db.Description `json:"database"` + db.Description `json:",inline"` // Path is the path to a DB archive relative to the listing file hosted location. // Note: this is NOT the absolute URL to download the database. @@ -46,9 +45,8 @@ func NewLatestDocument(entries ...Archive) *LatestDocument { }) return &LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), - Archive: entries[0], - Status: LifecycleStatus, + Archive: entries[0], + Status: LifecycleStatus, } } @@ -67,6 +65,43 @@ func NewLatestFromReader(reader io.Reader) (*LatestDocument, error) { return &l, nil } +func NewArchive(path string) (*Archive, error) { + tmpDir, err := os.MkdirTemp("", "grype-db-archive") + if err != nil { + return nil, fmt.Errorf("unable to create temp dir for grype-db archive: %w", err) + } + + if err = archiver.Unarchive(path, tmpDir); err != nil { + return nil, fmt.Errorf("unable to extract archive: %w", err) + } + + cfg := db.Config{ + DBDirPath: tmpDir, + } + + desc, err := db.ReadDescription(cfg.DBFilePath()) + if err != nil { + return nil, fmt.Errorf("failed to calculate description: %w", err) + } + + if desc == nil { + return nil, fmt.Errorf("unable to describe the database") + } + + // calculate the sh256sum of the archive + checksum, err := db.CalculateArchiveDigest(cfg.DBFilePath()) + if err != nil { + return nil, fmt.Errorf("failed to calculate archive checksum: %w", err) + } + + return &Archive{ + Description: *desc, + // this is not the path on disk, this is the path relative to the latest.json file when hosted + Path: filepath.Base(path), + Checksum: checksum, + }, nil +} + func (l LatestDocument) Write(writer io.Writer) error { if l.SchemaVersion == "" { return fmt.Errorf("missing schema version") @@ -88,13 +123,6 @@ func (l LatestDocument) Write(writer io.Writer) error { return fmt.Errorf("missing built time") } - if l.Archive.Description.Checksum == "" { - return fmt.Errorf("missing database checksum") - } - - // we don't need to store duplicate information from the archive section in the doc - l.Archive.Description.SchemaVersion = "" - contents, err := json.MarshalIndent(&l, "", " ") if err != nil { return fmt.Errorf("failed to encode listing file: %w", err) diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go index 807a0034049..a329e9d46f3 100644 --- a/grype/db/v6/installation/curator.go +++ b/grype/db/v6/installation/curator.go @@ -2,6 +2,7 @@ package installation import ( "fmt" + "github.com/hashicorp/go-multierror" "os" "path" "path/filepath" @@ -89,7 +90,7 @@ func (c curator) Reader() (db.Reader, error) { func (c curator) Status() db.Status { dbDir := c.config.DBDirectoryPath() - d, err := db.ReadDescription(dbDir) + d, err := db.ReadDescription(c.config.DBFilePath()) if err != nil { return db.Status{ Err: err, @@ -101,12 +102,22 @@ func (c curator) Status() db.Status { } } + var persistentErr error + digest, persistentErr := db.CalculateDBDigest(c.config.DBFilePath()) + + if valiadateErr := c.validate(); valiadateErr != nil { + if persistentErr != nil { + persistentErr = multierror.Append(persistentErr, valiadateErr) + } + persistentErr = valiadateErr + } + return db.Status{ Built: db.Time{Time: d.Built.Time}, SchemaVersion: d.SchemaVersion.String(), Location: dbDir, - Checksum: d.Checksum, - Err: c.validate(), + Checksum: digest, + Err: persistentErr, } } @@ -257,7 +268,7 @@ func (c curator) setLastSuccessfulUpdateCheck() { // validate checks the current database to ensure file integrity and if it can be used by this version of the application. func (c curator) validate() error { - metadata, err := c.validateIntegrity(c.config.DBDirectoryPath()) + metadata, err := c.validateIntegrity(c.config.DBFilePath()) if err != nil { return err } @@ -301,13 +312,13 @@ func (c curator) activate(dbDirPath string, mon monitor) error { // overwrite the checksums file with the new checksums dbFilePath := filepath.Join(dbDirPath, db.VulnerabilityDBFileName) - desc, err := db.CalculateDescription(dbFilePath) + digest, err := db.CalculateDBDigest(dbFilePath) if err != nil { return err } checksumsFilePath := filepath.Join(dbDirPath, db.ChecksumFileName) - if err := afero.WriteFile(c.fs, checksumsFilePath, []byte(desc.Checksum), 0644); err != nil { + if err := afero.WriteFile(c.fs, checksumsFilePath, []byte(digest), 0644); err != nil { return err } @@ -327,24 +338,28 @@ func (c curator) activate(dbDirPath string, mon monitor) error { return os.Rename(dbDirPath, dbDir) } -func (c curator) validateIntegrity(dbDirPath string) (*db.Description, error) { +func (c curator) validateIntegrity(dbFilePath string) (*db.Description, error) { // check that the disk checksum still matches the db payload - metadata, err := db.ReadDescription(dbDirPath) + metadata, err := db.ReadDescription(dbFilePath) if err != nil { - return nil, fmt.Errorf("failed to parse database metadata (%s): %w", dbDirPath, err) + return nil, fmt.Errorf("failed to parse database metadata (%s): %w", dbFilePath, err) } if metadata == nil { - return nil, fmt.Errorf("database metadata not found: %s", dbDirPath) + return nil, fmt.Errorf("database not found: %s", dbFilePath) + } + + digest, err := db.CalculateDBDigest(c.config.DBFilePath()) + if err != nil { + return nil, err } if c.config.ValidateChecksum { - dbPath := path.Join(dbDirPath, db.VulnerabilityDBFileName) - valid, actualHash, err := file.ValidateByHash(c.fs, dbPath, metadata.Checksum) + valid, actualHash, err := file.ValidateByHash(c.fs, dbFilePath, digest) if err != nil { return nil, err } if !valid { - return nil, fmt.Errorf("bad db checksum (%s): %q vs %q", dbPath, metadata.Checksum, actualHash) + return nil, fmt.Errorf("bad db checksum (%s): %q vs %q", dbFilePath, digest, actualHash) } } diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go index 61794d41d6e..55d77fee568 100644 --- a/grype/db/v6/installation/curator_test.go +++ b/grype/db/v6/installation/curator_test.go @@ -139,7 +139,7 @@ func writeTestDescriptionToDB(t *testing.T, dir string, desc db.Description) str require.NoError(t, d.Exec("VACUUM").Error) - digest, err := db.CalculateDigest(c.DBFilePath()) + digest, err := db.CalculateDBDigest(c.DBFilePath()) require.NoError(t, err) // write the checksums file diff --git a/grype/db/v6/provider_store.go b/grype/db/v6/provider_store.go index 4fafc0382ed..66f5d33ea40 100644 --- a/grype/db/v6/provider_store.go +++ b/grype/db/v6/provider_store.go @@ -15,6 +15,7 @@ type ProviderStoreWriter interface { type ProviderStoreReader interface { GetProvider(name string) (*Provider, error) + AllProviders() ([]Provider, error) } type providerStore struct { @@ -64,3 +65,15 @@ func (s *providerStore) GetProvider(name string) (*Provider, error) { return &provider, nil } + +func (s *providerStore) AllProviders() ([]Provider, error) { + log.Trace("fetching all provider records") + + var providers []Provider + result := s.db.Find(&providers) + if result.Error != nil { + return nil, fmt.Errorf("failed to fetch all providers: %w", result.Error) + } + + return providers, nil +} diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go index 120b69b45b0..38acfa9a820 100644 --- a/grype/db/v6/store.go +++ b/grype/db/v6/store.go @@ -62,19 +62,15 @@ func (s *store) Close() error { return fmt.Errorf("failed to vacuum: %w", err) } - desc, err := CalculateDescription(filepath.Join(s.config.DBDirPath, VulnerabilityDBFileName)) + digest, err := CalculateDBDigest(filepath.Join(s.config.DBDirPath, VulnerabilityDBFileName)) if err != nil { return fmt.Errorf("failed to create description from dir: %w", err) } - if desc == nil { - return fmt.Errorf("unable to describe the database") - } - fh, err := os.OpenFile(filepath.Join(s.config.DBDirPath, ChecksumFileName), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open description file: %w", err) } - return WriteChecksums(fh, *desc) + return WriteChecksums(fh, digest) } From 8b50e3117a31f2fba978ed4de9048c29f6d91993 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 21 Nov 2024 10:51:13 -0500 Subject: [PATCH 3/6] make assigner plural Signed-off-by: Alex Goodman --- grype/db/v6/blobs.go | 4 ++-- grype/db/v6/vulnerability_store_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grype/db/v6/blobs.go b/grype/db/v6/blobs.go index a166171cc36..9dd2eae91e0 100644 --- a/grype/db/v6/blobs.go +++ b/grype/db/v6/blobs.go @@ -14,8 +14,8 @@ type VulnerabilityBlob struct { // ProviderName of the Vunnel provider (or sub processor responsible for data records from a single specific source, e.g. "ubuntu") ProviderName string `json:"provider"` - // Assigner is a list of names, email, or organizations who submitted the vulnerability - Assigner []string `json:"assigner,omitempty"` + // Assigners is a list of names, email, or organizations who submitted the vulnerability + Assigners []string `json:"assigner,omitempty"` // Status conveys the actionability of the current record Status VulnerabilityStatus `json:"status"` diff --git a/grype/db/v6/vulnerability_store_test.go b/grype/db/v6/vulnerability_store_test.go index d8bad4d6c1d..2ca944b42ef 100644 --- a/grype/db/v6/vulnerability_store_test.go +++ b/grype/db/v6/vulnerability_store_test.go @@ -110,7 +110,7 @@ func testVulnerabilityHandle() *VulnerabilityHandle { BlobValue: &VulnerabilityBlob{ ID: "CVE-8765-4321", ProviderName: "provider!", - Assigner: []string{"assigner!"}, + Assigners: []string{"assigner!"}, Status: "status!", Description: "description!", PublishedDate: &now, From ba4a2b41b64f5783ad64e8b54dcc1b657efa9603 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 21 Nov 2024 13:09:11 -0500 Subject: [PATCH 4/6] fix tests Signed-off-by: Alex Goodman --- Taskfile.yaml | 3 +- cmd/grype/cli/commands/db_import.go | 4 +- cmd/grype/cli/options/database.go | 6 +- grype/db/v6/description_test.go | 116 +++-------------------- grype/db/v6/distribution/client_test.go | 27 ++---- grype/db/v6/distribution/latest.go | 18 +++- grype/db/v6/distribution/latest_test.go | 64 ++++++------- grype/db/v6/installation/curator.go | 11 ++- grype/db/v6/installation/curator_test.go | 15 ++- grype/db/v6/internal/db.go | 2 +- 10 files changed, 81 insertions(+), 185 deletions(-) diff --git a/Taskfile.yaml b/Taskfile.yaml index 2cfc9deeccc..b4221933ab4 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -57,7 +57,8 @@ tasks: - task: check-licenses - task: lint - task: validate-cyclonedx-schema - - task: validate-grype-db-schema +# TODO: while developing v6, we need to disable this check (since v5 and v6 are imported in the same codebase) +# - task: validate-grype-db-schema test: desc: Run all levels of test diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go index 43e15cdc886..762b2b1575b 100644 --- a/cmd/grype/cli/commands/db_import.go +++ b/cmd/grype/cli/commands/db_import.go @@ -2,8 +2,6 @@ package commands import ( "fmt" - "github.com/anchore/grype/grype/db/v6/distribution" - "github.com/anchore/grype/grype/db/v6/installation" "path/filepath" "strings" @@ -12,6 +10,8 @@ import ( "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/options" legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution" + "github.com/anchore/grype/grype/db/v6/distribution" + "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal" ) diff --git a/cmd/grype/cli/options/database.go b/cmd/grype/cli/options/database.go index 525868e40e4..df6b10c436e 100644 --- a/cmd/grype/cli/options/database.go +++ b/cmd/grype/cli/options/database.go @@ -1,8 +1,6 @@ package options import ( - "github.com/anchore/grype/grype/db/v6/distribution" - "github.com/anchore/grype/grype/db/v6/installation" "path" "time" @@ -10,6 +8,8 @@ import ( "github.com/anchore/clio" legacyDistribution "github.com/anchore/grype/grype/db/legacy/distribution" + "github.com/anchore/grype/grype/db/v6/distribution" + "github.com/anchore/grype/grype/db/v6/installation" "github.com/anchore/grype/internal" ) @@ -61,7 +61,7 @@ func (cfg Database) ToClientConfig() distribution.Config { LatestURL: cfg.UpdateURL, CACert: cfg.CACert, RequireUpdateCheck: cfg.RequireUpdateCheck, - CheckTimeout: cfg.UpdateAvailableTimeout, // TODO: is this right? + CheckTimeout: cfg.UpdateAvailableTimeout, UpdateTimeout: cfg.UpdateDownloadTimeout, } } diff --git a/grype/db/v6/description_test.go b/grype/db/v6/description_test.go index 2987eadee8c..6fca037ed3e 100644 --- a/grype/db/v6/description_test.go +++ b/grype/db/v6/description_test.go @@ -2,8 +2,6 @@ package v6 import ( "encoding/json" - "fmt" - "io" "os" "path" "path/filepath" @@ -11,17 +9,15 @@ import ( "testing" "time" - "github.com/OneOfOne/xxhash" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/db/internal/schemaver" ) -func TestNewDatabaseDescriptionFromDir(t *testing.T) { +func TestReadDescription(t *testing.T) { tempDir := t.TempDir() - // make a test DB s, err := NewWriter(Config{DBDirPath: tempDir}) require.NoError(t, err) require.NoError(t, s.SetDBMetadata()) @@ -29,26 +25,15 @@ func TestNewDatabaseDescriptionFromDir(t *testing.T) { require.NoError(t, err) require.NoError(t, s.Close()) - // get the xxhash of the db file - hasher := xxhash.New64() dbFilePath := path.Join(tempDir, VulnerabilityDBFileName) - f, err := os.Open(dbFilePath) - require.NoError(t, err) - _, err = io.Copy(hasher, f) - require.NoError(t, err) - require.NoError(t, f.Close()) - expectedHash := fmt.Sprintf("xxh64:%x", hasher.Sum(nil)) - // run the test subject - description, err := CalculateDescription(dbFilePath) + description, err := ReadDescription(dbFilePath) require.NoError(t, err) require.NotNil(t, description) - // did it work? assert.Equal(t, Description{ SchemaVersion: schemaver.New(expected.Model, expected.Revision, expected.Addition), Built: Time{*expected.BuildTimestamp}, - Checksum: expectedHash, }, *description) } @@ -131,30 +116,23 @@ func TestTime_JSONUnmarshalling(t *testing.T) { func TestWriteChecksums(t *testing.T) { cases := []struct { - name string - description Description - expected string - wantErr require.ErrorAssertionFunc + name string + digest string + expected string + wantErr require.ErrorAssertionFunc }{ { - name: "go case", - description: Description{ - SchemaVersion: "1.0.0", - Built: Time{Time: time.Date(2023, 9, 26, 12, 2, 3, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", - }, + name: "go case", + digest: "xxh64:dummychecksum", expected: "xxh64:dummychecksum", }, { - name: "empty checksum", - description: Description{}, - wantErr: require.Error, + name: "empty checksum", + wantErr: require.Error, }, { - name: "missing prefix", - description: Description{ - Checksum: "dummychecksum", - }, + name: "missing prefix", + digest: "dummychecksum", wantErr: require.Error, }, } @@ -165,7 +143,7 @@ func TestWriteChecksums(t *testing.T) { tc.wantErr = require.NoError } sb := strings.Builder{} - err := WriteChecksums(&sb, tc.description) + err := WriteChecksums(&sb, tc.digest) tc.wantErr(t, err) if err == nil { assert.Equal(t, tc.expected, sb.String()) @@ -174,74 +152,6 @@ func TestWriteChecksums(t *testing.T) { } } -func TestReadDescriptionAndCalculateDescription(t *testing.T) { - tests := []struct { - name string - setupFiles func(t testing.TB, dir string) error - expectedErr string - }{ - { - name: "database file missing", - setupFiles: func(t testing.TB, dir string) error { - return nil - }, - expectedErr: "database does not exist", - }, - { - name: "checksum file missing", - setupFiles: func(t testing.TB, dir string) error { - s := setupTestStore(t, dir) - require.NoError(t, s.SetDBMetadata()) - // since we don't close, there is no checksums - return nil - }, - expectedErr: "failed to read checksums file", - }, - { - name: "checksum file empty", - setupFiles: func(t testing.TB, dir string) error { - s := setupTestStore(t, dir) - require.NoError(t, s.SetDBMetadata()) - require.NoError(t, s.Close()) - // truncate the checksums file - require.NoError(t, os.Truncate(filepath.Join(dir, ChecksumFileName), 0)) - return nil - }, - expectedErr: "checksums file is empty", - }, - { - name: "valid database", - setupFiles: func(t testing.TB, dir string) error { - s := setupTestStore(t, dir) - require.NoError(t, s.SetDBMetadata()) - require.NoError(t, s.Close()) - return nil - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dir := t.TempDir() - err := tt.setupFiles(t, dir) - require.NoError(t, err) - - desc, err := ReadDescription(dir) - - if tt.expectedErr != "" { - require.ErrorContains(t, err, tt.expectedErr) - require.Nil(t, desc) - } else { - require.NoError(t, err) - require.NotNil(t, desc) - calcDesc, err := CalculateDescription(filepath.Join(dir, VulnerabilityDBFileName)) - require.NoError(t, err) - assert.Equal(t, calcDesc, desc) - } - }) - } -} - func TestReadDBChecksum(t *testing.T) { tests := []struct { name string diff --git a/grype/db/v6/distribution/client_test.go b/grype/db/v6/distribution/client_test.go index f79a8cbc1c6..d96fb44d81b 100644 --- a/grype/db/v6/distribution/client_test.go +++ b/grype/db/v6/distribution/client_test.go @@ -27,13 +27,11 @@ func TestClient_LatestFromURL(t *testing.T) { name: "go case", setupServer: func() *httptest.Server { doc := LatestDocument{ - SchemaVersion: "1.0.0", - Status: "active", + Status: "active", Archive: Archive{ Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive", Checksum: "checksum123", @@ -49,13 +47,11 @@ func TestClient_LatestFromURL(t *testing.T) { })) }, expectedDoc: &LatestDocument{ - SchemaVersion: "1.0.0", - Status: "active", + Status: "active", Archive: Archive{ Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive", Checksum: "checksum123", @@ -193,13 +189,11 @@ func TestClient_IsUpdateAvailable(t *testing.T) { { name: "update available", candidate: &LatestDocument{ - SchemaVersion: "1.0.0", - Status: StatusActive, + Status: StatusActive, Archive: Archive{ Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", @@ -209,7 +203,6 @@ func TestClient_IsUpdateAvailable(t *testing.T) { Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", @@ -218,13 +211,11 @@ func TestClient_IsUpdateAvailable(t *testing.T) { { name: "no update available", candidate: &LatestDocument{ - SchemaVersion: "1.0.0", - Status: "active", + Status: "active", Archive: Archive{ Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", @@ -240,13 +231,11 @@ func TestClient_IsUpdateAvailable(t *testing.T) { { name: "candidate deprecated", candidate: &LatestDocument{ - SchemaVersion: "1.0.0", - Status: StatusDeprecated, + Status: StatusDeprecated, Archive: Archive{ Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", @@ -256,7 +245,6 @@ func TestClient_IsUpdateAvailable(t *testing.T) { Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", @@ -266,13 +254,11 @@ func TestClient_IsUpdateAvailable(t *testing.T) { { name: "candidate end of life", candidate: &LatestDocument{ - SchemaVersion: "1.0.0", - Status: StatusEndOfLife, + Status: StatusEndOfLife, Archive: Archive{ Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", @@ -282,7 +268,6 @@ func TestClient_IsUpdateAvailable(t *testing.T) { Description: db.Description{ SchemaVersion: "1.0.0", Built: db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)}, - Checksum: "xxh64:dummychecksum", }, Path: "path/to/archive.tar.gz", Checksum: "checksum123", diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go index e0b4b545349..b8e432913e2 100644 --- a/grype/db/v6/distribution/latest.go +++ b/grype/db/v6/distribution/latest.go @@ -3,12 +3,13 @@ package distribution import ( "encoding/json" "fmt" - "github.com/mholt/archiver/v3" "io" "os" "path/filepath" "sort" + "github.com/mholt/archiver/v3" + db "github.com/anchore/grype/grype/db/v6" ) @@ -35,17 +36,24 @@ type Archive struct { } func NewLatestDocument(entries ...Archive) *LatestDocument { - if len(entries) == 0 { + var validEntries []Archive + for _, entry := range entries { + if modelPart, ok := entry.SchemaVersion.ModelPart(); ok && modelPart == db.ModelVersion { + validEntries = append(validEntries, entry) + } + } + + if len(validEntries) == 0 { return nil } // sort from most recent to the least recent - sort.SliceStable(entries, func(i, j int) bool { - return entries[i].Description.Built.After(entries[j].Description.Built.Time) + sort.SliceStable(validEntries, func(i, j int) bool { + return validEntries[i].Description.Built.After(entries[j].Description.Built.Time) }) return &LatestDocument{ - Archive: entries[0], + Archive: validEntries[0], Status: LifecycleStatus, } } diff --git a/grype/db/v6/distribution/latest_test.go b/grype/db/v6/distribution/latest_test.go index b13210e48c6..e4d7e1c56ef 100644 --- a/grype/db/v6/distribution/latest_test.go +++ b/grype/db/v6/distribution/latest_test.go @@ -17,12 +17,14 @@ func TestNewLatestDocument(t *testing.T) { t.Run("valid entries", func(t *testing.T) { archive1 := Archive{ Description: db.Description{ - Built: db.Time{Time: time.Now()}, + SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), + Built: db.Time{Time: time.Now()}, }, } archive2 := Archive{ Description: db.Description{ - Built: db.Time{Time: time.Now().Add(-1 * time.Hour)}, + SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), + Built: db.Time{Time: time.Now().Add(-1 * time.Hour)}, }, } @@ -34,6 +36,28 @@ func TestNewLatestDocument(t *testing.T) { require.Equal(t, actual, db.ModelVersion) }) + t.Run("filter entries", func(t *testing.T) { + archive1 := Archive{ + Description: db.Description{ + SchemaVersion: schemaver.New(5, db.Revision, db.Addition), // old! + Built: db.Time{Time: time.Now()}, + }, + } + archive2 := Archive{ + Description: db.Description{ + SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), + Built: db.Time{Time: time.Now().Add(-1 * time.Hour)}, + }, + } + + latestDoc := NewLatestDocument(archive1, archive2) + require.NotNil(t, latestDoc) + require.Equal(t, latestDoc.Archive, archive2) // most recent archive with valid version + actual, ok := latestDoc.SchemaVersion.ModelPart() + require.True(t, ok) + require.Equal(t, actual, db.ModelVersion) + }) + t.Run("no entries", func(t *testing.T) { latestDoc := NewLatestDocument() require.Nil(t, latestDoc) @@ -43,7 +67,6 @@ func TestNewLatestDocument(t *testing.T) { func TestNewLatestFromReader(t *testing.T) { t.Run("valid JSON", func(t *testing.T) { latestDoc := LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Archive: Archive{ Description: db.Description{ Built: db.Time{Time: time.Now().Truncate(time.Second).UTC()}, @@ -87,15 +110,13 @@ func TestLatestDocument_Write(t *testing.T) { { name: "valid document", latestDoc: LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Archive: Archive{ Description: db.Description{ Built: now, - Checksum: "xxh64:validchecksum", SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", - Checksum: "xxh64:validchecksum", + Checksum: "sha256:validchecksum", }, // note: status not supplied, should assume to be active }, @@ -104,11 +125,9 @@ func TestLatestDocument_Write(t *testing.T) { { name: "explicit status", latestDoc: LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Archive: Archive{ Description: db.Description{ Built: now, - Checksum: "xxh64:validchecksum", SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", @@ -123,9 +142,7 @@ func TestLatestDocument_Write(t *testing.T) { latestDoc: LatestDocument{ Archive: Archive{ Description: db.Description{ - Built: now, - Checksum: "xxh64:validchecksum", - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), + Built: now, }, Path: "valid/path/to/archive", Checksum: "xxh64:validchecksum", @@ -137,11 +154,9 @@ func TestLatestDocument_Write(t *testing.T) { { name: "missing archive path", latestDoc: LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Archive: Archive{ Description: db.Description{ Built: now, - Checksum: "xxh64:validchecksum", SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "", // this! @@ -154,11 +169,9 @@ func TestLatestDocument_Write(t *testing.T) { { name: "missing archive checksum", latestDoc: LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Archive: Archive{ Description: db.Description{ Built: now, - Checksum: "xxh64:validchecksum", SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", @@ -171,11 +184,9 @@ func TestLatestDocument_Write(t *testing.T) { { name: "missing built time", latestDoc: LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Archive: Archive{ Description: db.Description{ Built: db.Time{}, // this! - Checksum: "xxh64:validchecksum", SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), }, Path: "valid/path/to/archive", @@ -185,23 +196,6 @@ func TestLatestDocument_Write(t *testing.T) { }, expectedError: errContains("missing built time"), }, - { - name: "missing database checksum", - latestDoc: LatestDocument{ - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), - Archive: Archive{ - Description: db.Description{ - Built: now, - Checksum: "", // this! - SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), - }, - Path: "valid/path/to/archive", - Checksum: "xxh64:validchecksum", - }, - Status: "active", - }, - expectedError: errContains("missing database checksum"), - }, } for _, tt := range tests { @@ -219,10 +213,8 @@ func TestLatestDocument_Write(t *testing.T) { var result LatestDocument assert.NoError(t, json.Unmarshal(buf.Bytes(), &result)) assert.Equal(t, tt.latestDoc.SchemaVersion, result.SchemaVersion, "schema version mismatch") - assert.Empty(t, result.Archive.Description.SchemaVersion, "nested schema version should be empty") assert.Equal(t, tt.latestDoc.Archive.Checksum, result.Archive.Checksum, "archive checksum mismatch") assert.Equal(t, tt.latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time, "built time mismatch") - assert.Equal(t, tt.latestDoc.Archive.Description.Checksum, result.Archive.Description.Checksum, "database checksum mismatch") assert.Equal(t, tt.latestDoc.Archive.Path, result.Archive.Path, "path mismatch") if tt.latestDoc.Status == "" { assert.Equal(t, StatusActive, result.Status, "status mismatch") diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go index a329e9d46f3..05b66210d84 100644 --- a/grype/db/v6/installation/curator.go +++ b/grype/db/v6/installation/curator.go @@ -2,7 +2,6 @@ package installation import ( "fmt" - "github.com/hashicorp/go-multierror" "os" "path" "path/filepath" @@ -11,6 +10,7 @@ import ( "github.com/adrg/xdg" "github.com/hako/durafmt" + "github.com/hashicorp/go-multierror" "github.com/mholt/archiver/v3" "github.com/spf13/afero" "github.com/wagoodman/go-partybus" @@ -105,11 +105,12 @@ func (c curator) Status() db.Status { var persistentErr error digest, persistentErr := db.CalculateDBDigest(c.config.DBFilePath()) - if valiadateErr := c.validate(); valiadateErr != nil { + if validateErr := c.validate(); validateErr != nil { if persistentErr != nil { - persistentErr = multierror.Append(persistentErr, valiadateErr) + persistentErr = multierror.Append(persistentErr, validateErr) + } else { + persistentErr = validateErr } - persistentErr = valiadateErr } return db.Status{ @@ -348,7 +349,7 @@ func (c curator) validateIntegrity(dbFilePath string) (*db.Description, error) { return nil, fmt.Errorf("database not found: %s", dbFilePath) } - digest, err := db.CalculateDBDigest(c.config.DBFilePath()) + digest, err := db.ReadDBChecksum(filepath.Dir(dbFilePath)) if err != nil { return nil, err } diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go index 55d77fee568..549e2455107 100644 --- a/grype/db/v6/installation/curator_test.go +++ b/grype/db/v6/installation/curator_test.go @@ -81,12 +81,13 @@ func setupCuratorForUpdate(t *testing.T, opts ...setupOption) curator { oldDescription := db.Description{ SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition), Built: db.Time{Time: time.Now().Add(-48 * time.Hour)}, - Checksum: writeTestDB(t, c.fs, dbDir), } + writeTestDB(t, c.fs, dbDir) newDescription := oldDescription newDescription.Built = db.Time{Time: time.Now()} - newDescription.Checksum = writeTestDB(t, c.fs, stageDir) + + writeTestDB(t, c.fs, stageDir) writeTestDescriptionToDB(t, dbDir, oldDescription) writeTestDescriptionToDB(t, stageDir, newDescription) @@ -487,9 +488,8 @@ func TestCurator_ValidateIntegrity(t *testing.T) { t.Run("valid metadata with correct checksum", func(t *testing.T) { c := newCurator(t) - dbDir := c.config.DBDirectoryPath() - result, err := c.validateIntegrity(dbDir) + result, err := c.validateIntegrity(c.config.DBFilePath()) require.NoError(t, err) require.NotNil(t, result) }) @@ -505,7 +505,7 @@ func TestCurator_ValidateIntegrity(t *testing.T) { c := newCurator(t) dbDir := c.config.DBDirectoryPath() require.NoError(t, os.Remove(filepath.Join(dbDir, db.ChecksumFileName))) - _, err := c.validateIntegrity(dbDir) + _, err := c.validateIntegrity(c.config.DBFilePath()) require.ErrorContains(t, err, "no such file or directory") }) @@ -515,13 +515,12 @@ func TestCurator_ValidateIntegrity(t *testing.T) { writeTestChecksumsFile(t, c.fs, dbDir, "xxh64:invalidchecksum") - _, err := c.validateIntegrity(dbDir) + _, err := c.validateIntegrity(c.config.DBFilePath()) require.ErrorContains(t, err, "bad db checksum") }) t.Run("unsupported database version", func(t *testing.T) { c := newCurator(t) - dbDir := c.config.DBDirectoryPath() oldDescription := db.Description{ SchemaVersion: schemaver.New(db.ModelVersion-1, 0, 0), @@ -530,7 +529,7 @@ func TestCurator_ValidateIntegrity(t *testing.T) { writeTestDescriptionToDB(t, c.config.DBDirectoryPath(), oldDescription) - _, err := c.validateIntegrity(dbDir) + _, err := c.validateIntegrity(c.config.DBFilePath()) require.ErrorContains(t, err, "unsupported database version") }) } diff --git a/grype/db/v6/internal/db.go b/grype/db/v6/internal/db.go index c58b816a846..8f288d37ff1 100644 --- a/grype/db/v6/internal/db.go +++ b/grype/db/v6/internal/db.go @@ -14,7 +14,7 @@ func NewDB(dbFilePath string, models []any, truncate bool) (*gorm.DB, error) { return nil, err } - if len(models) > 0 { + if len(models) > 0 && truncate { if err := db.AutoMigrate(models...); err != nil { return nil, fmt.Errorf("unable to create tables: %w", err) } From b097c1e004259f380772bf225830021ea7e3e585 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 26 Nov 2024 11:59:29 -0500 Subject: [PATCH 5/6] incorporate review comments Signed-off-by: Alex Goodman --- cmd/grype/cli/commands/db_import.go | 11 +++-- grype/db/v6/distribution/latest.go | 37 ++++----------- grype/db/v6/distribution/latest_test.go | 53 +++++++++++++++++++++ grype/db/v6/installation/curator.go | 60 +++++++++++------------- grype/db/v6/installation/curator_test.go | 11 +++-- grype/db/v6/internal/db.go | 3 ++ grype/db/v6/provider_store.go | 5 ++ grype/db/v6/store.go | 1 + 8 files changed, 109 insertions(+), 72 deletions(-) diff --git a/cmd/grype/cli/commands/db_import.go b/cmd/grype/cli/commands/db_import.go index 762b2b1575b..52820b0709f 100644 --- a/cmd/grype/cli/commands/db_import.go +++ b/cmd/grype/cli/commands/db_import.go @@ -33,10 +33,11 @@ func DBImport(app clio.Application) *cobra.Command { func runDBImport(opts options.Database, dbArchivePath string) error { // TODO: tui update? better logging? - if isLegacy(dbArchivePath) { - return legacyDBImport(opts, dbArchivePath) + // TODO: we will only support v6 after development is complete + if isV6DB(dbArchivePath) { + return importDB(opts, dbArchivePath) } - return importDB(opts, dbArchivePath) + return legacyDBImport(opts, dbArchivePath) } func importDB(opts options.Database, dbArchivePath string) error { @@ -68,6 +69,6 @@ func legacyDBImport(opts options.Database, dbArchivePath string) error { return stderrPrintLnf("Vulnerability database imported") } -func isLegacy(path string) bool { - return !strings.Contains(filepath.Base(path), "vulnerability-db_v6") +func isV6DB(path string) bool { + return strings.Contains(filepath.Base(path), "vulnerability-db_v6") } diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go index b8e432913e2..ca3127cd116 100644 --- a/grype/db/v6/distribution/latest.go +++ b/grype/db/v6/distribution/latest.go @@ -4,12 +4,11 @@ import ( "encoding/json" "fmt" "io" - "os" "path/filepath" "sort" + "time" - "github.com/mholt/archiver/v3" - + "github.com/anchore/grype/grype/db/internal/schemaver" db "github.com/anchore/grype/grype/db/v6" ) @@ -73,37 +72,17 @@ func NewLatestFromReader(reader io.Reader) (*LatestDocument, error) { return &l, nil } -func NewArchive(path string) (*Archive, error) { - tmpDir, err := os.MkdirTemp("", "grype-db-archive") - if err != nil { - return nil, fmt.Errorf("unable to create temp dir for grype-db archive: %w", err) - } - - if err = archiver.Unarchive(path, tmpDir); err != nil { - return nil, fmt.Errorf("unable to extract archive: %w", err) - } - - cfg := db.Config{ - DBDirPath: tmpDir, - } - - desc, err := db.ReadDescription(cfg.DBFilePath()) - if err != nil { - return nil, fmt.Errorf("failed to calculate description: %w", err) - } - - if desc == nil { - return nil, fmt.Errorf("unable to describe the database") - } - - // calculate the sh256sum of the archive - checksum, err := db.CalculateArchiveDigest(cfg.DBFilePath()) +func NewArchive(path string, t time.Time, model, revision, addition int) (*Archive, error) { + checksum, err := db.CalculateArchiveDigest(path) if err != nil { return nil, fmt.Errorf("failed to calculate archive checksum: %w", err) } return &Archive{ - Description: *desc, + Description: db.Description{ + SchemaVersion: schemaver.New(model, revision, addition), + Built: db.Time{Time: t}, + }, // this is not the path on disk, this is the path relative to the latest.json file when hosted Path: filepath.Base(path), Checksum: checksum, diff --git a/grype/db/v6/distribution/latest_test.go b/grype/db/v6/distribution/latest_test.go index e4d7e1c56ef..c37ffd026c7 100644 --- a/grype/db/v6/distribution/latest_test.go +++ b/grype/db/v6/distribution/latest_test.go @@ -3,9 +3,12 @@ package distribution import ( "bytes" "encoding/json" + "os" + "path/filepath" "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -224,3 +227,53 @@ func TestLatestDocument_Write(t *testing.T) { }) } } + +func TestNewArchive(t *testing.T) { + tests := []struct { + name string + contents string + time time.Time + model int + revision int + addition int + expectErr require.ErrorAssertionFunc + expected *Archive + }{ + { + name: "valid input", + contents: "test archive content", + time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC), + model: 1, + revision: 0, + addition: 5, + expectErr: require.NoError, + expected: &Archive{ + Description: db.Description{ + SchemaVersion: schemaver.New(1, 0, 5), + Built: db.Time{Time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC)}, + }, + Path: "archive.tar.gz", + Checksum: "sha256:2a11c11d2c3803697c458a1f5f03c2b73235c101f93c88193cc8810003c40d87", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := t.TempDir() + tempFile, err := os.Create(filepath.Join(d, tt.expected.Path)) + require.NoError(t, err) + _, err = tempFile.WriteString(tt.contents) + require.NoError(t, err) + + archive, err := NewArchive(tempFile.Name(), tt.time, tt.model, tt.revision, tt.addition) + tt.expectErr(t, err) + if err != nil { + return + } + if diff := cmp.Diff(tt.expected, archive); diff != "" { + t.Errorf("unexpected archive (-want +got):\n%s", diff) + } + }) + } +} diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go index 05b66210d84..f0b43fa9436 100644 --- a/grype/db/v6/installation/curator.go +++ b/grype/db/v6/installation/curator.go @@ -10,7 +10,6 @@ import ( "github.com/adrg/xdg" "github.com/hako/durafmt" - "github.com/hashicorp/go-multierror" "github.com/mholt/archiver/v3" "github.com/spf13/afero" "github.com/wagoodman/go-partybus" @@ -84,7 +83,9 @@ func (c curator) Reader() (db.Reader, error) { return nil, err } - return s, c.validate() + _, err = c.validate(c.config.ValidateChecksum) + + return s, err } func (c curator) Status() db.Status { @@ -102,23 +103,15 @@ func (c curator) Status() db.Status { } } - var persistentErr error - digest, persistentErr := db.CalculateDBDigest(c.config.DBFilePath()) - - if validateErr := c.validate(); validateErr != nil { - if persistentErr != nil { - persistentErr = multierror.Append(persistentErr, validateErr) - } else { - persistentErr = validateErr - } - } + // override the checksum validation setting to ensure the checksum is always validated + digest, validateErr := c.validate(true) return db.Status{ Built: db.Time{Time: d.Built.Time}, SchemaVersion: d.SchemaVersion.String(), Location: dbDir, Checksum: digest, - Err: persistentErr, + Err: validateErr, } } @@ -268,13 +261,13 @@ func (c curator) setLastSuccessfulUpdateCheck() { } // validate checks the current database to ensure file integrity and if it can be used by this version of the application. -func (c curator) validate() error { - metadata, err := c.validateIntegrity(c.config.DBFilePath()) +func (c curator) validate(validateChecksum bool) (string, error) { + metadata, digest, err := c.validateIntegrity(c.config.DBFilePath(), validateChecksum) if err != nil { - return err + return "", err } - return c.ensureNotStale(metadata) + return digest, c.ensureNotStale(metadata) } // Import takes a DB archive file and imports it into the final DB location. @@ -305,7 +298,7 @@ func (c curator) Import(dbArchivePath string) error { return nil } -// activate swaps over the downloaded db to the application directory +// activate swaps over the downloaded db to the application directory, calculates the checksum, and records the checksums to a file. func (c curator) activate(dbDirPath string, mon monitor) error { defer mon.importProgress.SetCompleted() @@ -339,37 +332,38 @@ func (c curator) activate(dbDirPath string, mon monitor) error { return os.Rename(dbDirPath, dbDir) } -func (c curator) validateIntegrity(dbFilePath string) (*db.Description, error) { +func (c curator) validateIntegrity(dbFilePath string, validateChecksum bool) (*db.Description, string, error) { // check that the disk checksum still matches the db payload metadata, err := db.ReadDescription(dbFilePath) if err != nil { - return nil, fmt.Errorf("failed to parse database metadata (%s): %w", dbFilePath, err) + return nil, "", fmt.Errorf("failed to parse database metadata (%s): %w", dbFilePath, err) } if metadata == nil { - return nil, fmt.Errorf("database not found: %s", dbFilePath) + return nil, "", fmt.Errorf("database not found: %s", dbFilePath) } - digest, err := db.ReadDBChecksum(filepath.Dir(dbFilePath)) - if err != nil { - return nil, err + gotModel, ok := metadata.SchemaVersion.ModelPart() + if !ok || gotModel != db.ModelVersion { + return nil, "", fmt.Errorf("unsupported database version: have=%d want=%d", gotModel, db.ModelVersion) } - if c.config.ValidateChecksum { + var digest string + if validateChecksum { + digest, err = db.ReadDBChecksum(filepath.Dir(dbFilePath)) + if err != nil { + return nil, "", err + } + valid, actualHash, err := file.ValidateByHash(c.fs, dbFilePath, digest) if err != nil { - return nil, err + return nil, "", err } if !valid { - return nil, fmt.Errorf("bad db checksum (%s): %q vs %q", dbFilePath, digest, actualHash) + return nil, "", fmt.Errorf("bad db checksum (%s): %q vs %q", dbFilePath, digest, actualHash) } } - gotModel, ok := metadata.SchemaVersion.ModelPart() - if !ok || gotModel != db.ModelVersion { - return nil, fmt.Errorf("unsupported database version: have=%d want=%d", gotModel, db.ModelVersion) - } - - return metadata, nil + return metadata, digest, nil } // ensureNotStale ensures the vulnerability database has not passed diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go index 549e2455107..1b2ba2f7ee3 100644 --- a/grype/db/v6/installation/curator_test.go +++ b/grype/db/v6/installation/curator_test.go @@ -489,15 +489,16 @@ func TestCurator_ValidateIntegrity(t *testing.T) { t.Run("valid metadata with correct checksum", func(t *testing.T) { c := newCurator(t) - result, err := c.validateIntegrity(c.config.DBFilePath()) + result, digest, err := c.validateIntegrity(c.config.DBFilePath(), true) require.NoError(t, err) require.NotNil(t, result) + require.NotEmpty(t, digest) }) t.Run("db does not exist", func(t *testing.T) { c := newCurator(t) - _, err := c.validateIntegrity("non/existent/path") + _, _, err := c.validateIntegrity("non/existent/path", true) require.ErrorContains(t, err, "database does not exist") }) @@ -505,7 +506,7 @@ func TestCurator_ValidateIntegrity(t *testing.T) { c := newCurator(t) dbDir := c.config.DBDirectoryPath() require.NoError(t, os.Remove(filepath.Join(dbDir, db.ChecksumFileName))) - _, err := c.validateIntegrity(c.config.DBFilePath()) + _, _, err := c.validateIntegrity(c.config.DBFilePath(), true) require.ErrorContains(t, err, "no such file or directory") }) @@ -515,7 +516,7 @@ func TestCurator_ValidateIntegrity(t *testing.T) { writeTestChecksumsFile(t, c.fs, dbDir, "xxh64:invalidchecksum") - _, err := c.validateIntegrity(c.config.DBFilePath()) + _, _, err := c.validateIntegrity(c.config.DBFilePath(), true) require.ErrorContains(t, err, "bad db checksum") }) @@ -529,7 +530,7 @@ func TestCurator_ValidateIntegrity(t *testing.T) { writeTestDescriptionToDB(t, c.config.DBDirectoryPath(), oldDescription) - _, err := c.validateIntegrity(c.config.DBFilePath()) + _, _, err := c.validateIntegrity(c.config.DBFilePath(), true) require.ErrorContains(t, err, "unsupported database version") }) } diff --git a/grype/db/v6/internal/db.go b/grype/db/v6/internal/db.go index 8f288d37ff1..ba0368c4757 100644 --- a/grype/db/v6/internal/db.go +++ b/grype/db/v6/internal/db.go @@ -8,6 +8,7 @@ import ( "github.com/anchore/grype/grype/db/internal/gormadapter" ) +// NewDB creates a new empty DB for writing or opens an existing one for reading from the given path. func NewDB(dbFilePath string, models []any, truncate bool) (*gorm.DB, error) { db, err := gormadapter.Open(dbFilePath, gormadapter.WithTruncate(truncate)) if err != nil { @@ -15,6 +16,8 @@ func NewDB(dbFilePath string, models []any, truncate bool) (*gorm.DB, error) { } if len(models) > 0 && truncate { + // note: never migrate if this is for reading only (if we are truncating). Migrating will change the contents + // of the DB so any checksums verifications will fail even though this is logically a no-op. if err := db.AutoMigrate(models...); err != nil { return nil, fmt.Errorf("unable to create tables: %w", err) } diff --git a/grype/db/v6/provider_store.go b/grype/db/v6/provider_store.go index 66f5d33ea40..35257a5aeae 100644 --- a/grype/db/v6/provider_store.go +++ b/grype/db/v6/provider_store.go @@ -3,6 +3,7 @@ package v6 import ( "errors" "fmt" + "sort" "gorm.io/gorm" @@ -75,5 +76,9 @@ func (s *providerStore) AllProviders() ([]Provider, error) { return nil, fmt.Errorf("failed to fetch all providers: %w", result.Error) } + sort.Slice(providers, func(i, j int) bool { + return providers[i].ID < providers[j].ID + }) + return providers, nil } diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go index 38acfa9a820..c0ab552395f 100644 --- a/grype/db/v6/store.go +++ b/grype/db/v6/store.go @@ -47,6 +47,7 @@ func newStore(cfg Config, write bool) (*store, error) { }, nil } +// Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, it does nothing. func (s *store) Close() error { log.Debug("closing store") if !s.write { From de1008de96fd2bd760e63bd9dd1e0510e248f4ba Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 26 Nov 2024 12:47:46 -0500 Subject: [PATCH 6/6] remove advisory reference tag Signed-off-by: Alex Goodman --- grype/db/v6/enumerations.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/grype/db/v6/enumerations.go b/grype/db/v6/enumerations.go index c5364ac234b..88bd828a3e8 100644 --- a/grype/db/v6/enumerations.go +++ b/grype/db/v6/enumerations.go @@ -60,11 +60,6 @@ const ( NotAffectedFixStatus FixStatus = "not-affected" ) -const ( - // AdvisoryReferenceTag is a reference to a vulnerability advisory - AdvisoryReferenceTag string = "advisory" -) - func ParseVulnerabilityStatus(s string) VulnerabilityStatus { switch strings.TrimSpace(strings.ToLower(s)) { case string(VulnerabilityActive), "":