From 9c40131f22354d8b62840d06dfde264d2a0c4497 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 17 Dec 2024 10:46:15 -0500 Subject: [PATCH] preserve v5 search command Signed-off-by: Alex Goodman --- cmd/grype/cli/commands/db_search.go | 114 +++++++++++++++++- cmd/grype/cli/commands/db_search_pkg.go | 80 +----------- cmd/grype/cli/commands/db_search_vuln.go | 12 +- .../cli/options/database_search_format.go | 3 +- cmd/grype/cli/options/database_search_os.go | 3 +- .../cli/options/database_search_packages.go | 2 +- .../database_search_vulnerabilities.go | 8 +- 7 files changed, 128 insertions(+), 94 deletions(-) diff --git a/cmd/grype/cli/commands/db_search.go b/cmd/grype/cli/commands/db_search.go index d7526eb7d7b..944745e2d56 100644 --- a/cmd/grype/cli/commands/db_search.go +++ b/cmd/grype/cli/commands/db_search.go @@ -1,15 +1,63 @@ package commands import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "github.com/anchore/clio" + "github.com/anchore/grype/grype" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" ) +var _ clio.FlagAdder = (*dbQueryOptions)(nil) + +type dbQueryOptions struct { + Output string `yaml:"output" json:"output" mapstructure:"output"` + DBOptions `yaml:",inline" mapstructure:",squash"` +} + +func (c *dbQueryOptions) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&c.Output, "output", "o", "format to display results (available=[table, json])") +} + func DBSearch(app clio.Application) *cobra.Command { + opts := &dbQueryOptions{ + Output: tableOutputFormat, + DBOptions: *dbOptionsDefault(app.ID()), + } + cmd := &cobra.Command{ - Use: "search", - Short: "search the DB for vulnerabilities or affected packages", + // this is here to support v5 functionality today but will be removed when v6 is the default DB version + Use: "search ID...", + Short: "get information on vulnerabilities from the db", + //Use: "search", + //Short: "search the DB for vulnerabilities or affected packages", + PreRunE: disableUI(app), + RunE: func(cmd *cobra.Command, args []string) (err error) { + if opts.Experimental.DBv6 { + if len(args) > 0 { + // looks like the user attempted to use the search command as if it's v5 -- let them know about the new commands instead + return errors.New("this command is only supported for schema DB v5, please use `grype db search pkg` or `grype db search vuln` for schema DB v6+") + } + // running without args should only show help, not as a runtime error + return cmd.Usage() + } + + // this is v5, do arg handling here. Why not do this earlier in the struct Args field? When v6 functionality is + // enabled we want this command to show usage and exit, so we need to do this check later in processing (here). + if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { + return err + } + return legacyDBSearchPackages(*opts, args) + }, } cmd.AddCommand( @@ -17,5 +65,65 @@ func DBSearch(app clio.Application) *cobra.Command { DBSearchVulnerabilities(app), ) - return cmd + return app.SetupCommand(cmd, opts) +} + +func legacyDBSearchPackages(opts dbQueryOptions, vulnerabilityIDs []string) error { + log.Debug("loading DB") + str, status, err := grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate) + err = validateDBLoad(err, status) + if err != nil { + return err + } + defer log.CloseAndLogError(str, status.Location) + + var vulnerabilities []vulnerability.Vulnerability + for _, vulnerabilityID := range vulnerabilityIDs { + vulns, err := str.Get(vulnerabilityID, "") + if err != nil { + return fmt.Errorf("unable to get vulnerability %q: %w", vulnerabilityID, err) + } + vulnerabilities = append(vulnerabilities, vulns...) + } + + if len(vulnerabilities) == 0 { + return errors.New("no affected packages found") + } + + sb := &strings.Builder{} + err = presentLegacyDBSearchPackages(opts.Output, vulnerabilities, sb) + bus.Report(sb.String()) + + return err +} + +func presentLegacyDBSearchPackages(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error { + if vulnerabilities == nil { + return nil + } + + switch outputFormat { + case tableOutputFormat: + rows := [][]string{} + for _, v := range vulnerabilities { + rows = append(rows, []string{v.ID, v.PackageName, v.Namespace, v.Constraint.String()}) + } + + table := tablewriter.NewWriter(output) + commonTableWriterOptions(table) + + table.SetHeader([]string{"ID", "Package Name", "Namespace", "Version Constraint"}) + table.AppendBulk(rows) + table.Render() + case jsonOutputFormat: + enc := json.NewEncoder(output) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(vulnerabilities); err != nil { + return fmt.Errorf("failed to encode diff information: %+v", err) + } + default: + return fmt.Errorf("unsupported output format: %s", outputFormat) + } + return nil } diff --git a/cmd/grype/cli/commands/db_search_pkg.go b/cmd/grype/cli/commands/db_search_pkg.go index 7ed6cf5d812..0f1c6b18b6a 100644 --- a/cmd/grype/cli/commands/db_search_pkg.go +++ b/cmd/grype/cli/commands/db_search_pkg.go @@ -13,12 +13,9 @@ import ( "github.com/anchore/clio" "github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch" "github.com/anchore/grype/cmd/grype/cli/options" - "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/db/v6/distribution" "github.com/anchore/grype/grype/db/v6/installation" - "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/grype/internal/bus" - "github.com/anchore/grype/internal/log" ) type dbSearchPackageOptions struct { @@ -48,25 +45,21 @@ func DBSearchPackages(app clio.Application) *cobra.Command { return app.SetupCommand(&cobra.Command{ Use: "pkg PURL|CPE|NAME...", Aliases: []string{"package", "packages", "pkgs"}, - Short: "Search for packages affected by vulnerabilities within the db", + Short: "Search for packages affected by vulnerabilities within the db (supports DB schema v6+ only)", Args: func(_ *cobra.Command, args []string) error { opts.Package.Names = args return nil }, RunE: func(_ *cobra.Command, _ []string) (err error) { + if !opts.Experimental.DBv6 { + return errors.New("this command only supports the v6+ database schemas") + } return runDBSearchPackages(*opts) }, }, opts) } func runDBSearchPackages(opts dbSearchPackageOptions) error { - if opts.Experimental.DBv6 { - return newDBSearchPackages(opts) - } - return legacyDBSearchPackages(opts) -} - -func newDBSearchPackages(opts dbSearchPackageOptions) error { client, err := distribution.NewClient(opts.DB.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) @@ -161,68 +154,3 @@ func renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackageTa } return rows } - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// all legacy processing below //////////////////////////////////////////////////////////////////////////////////////// - -func legacyDBSearchPackages(opts dbSearchPackageOptions) error { - vulnerabilityIDs := opts.Vulnerability.VulnerabilityIDs - - log.Debug("loading DB") - str, status, err := grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate) - err = validateDBLoad(err, status) - if err != nil { - return err - } - defer log.CloseAndLogError(str, status.Location) - - var vulnerabilities []vulnerability.Vulnerability - for _, vulnerabilityID := range vulnerabilityIDs { - vulns, err := str.Get(vulnerabilityID, "") - if err != nil { - return fmt.Errorf("unable to get vulnerability %q: %w", vulnerabilityID, err) - } - vulnerabilities = append(vulnerabilities, vulns...) - } - - if len(vulnerabilities) == 0 { - return errors.New("no affected packages found") - } - - sb := &strings.Builder{} - err = presentLegacyDBSearchPackages(opts.Format.Output, vulnerabilities, sb) - bus.Report(sb.String()) - - return err -} - -func presentLegacyDBSearchPackages(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error { - if vulnerabilities == nil { - return nil - } - - switch outputFormat { - case tableOutputFormat: - rows := [][]string{} - for _, v := range vulnerabilities { - rows = append(rows, []string{v.ID, v.PackageName, v.Namespace, v.Constraint.String()}) - } - - table := tablewriter.NewWriter(output) - commonTableWriterOptions(table) - - table.SetHeader([]string{"ID", "Package Name", "Namespace", "Version Constraint"}) - table.AppendBulk(rows) - table.Render() - case jsonOutputFormat: - enc := json.NewEncoder(output) - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - if err := enc.Encode(vulnerabilities); err != nil { - return fmt.Errorf("failed to encode diff information: %+v", err) - } - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - return nil -} diff --git a/cmd/grype/cli/commands/db_search_vuln.go b/cmd/grype/cli/commands/db_search_vuln.go index 6b29a0109e9..f6c2d9e8a5a 100644 --- a/cmd/grype/cli/commands/db_search_vuln.go +++ b/cmd/grype/cli/commands/db_search_vuln.go @@ -47,7 +47,7 @@ func DBSearchVulnerabilities(app clio.Application) *cobra.Command { return app.SetupCommand(&cobra.Command{ Use: "vuln ID...", Aliases: []string{"vulnerability", "vulnerabilities", "vulns"}, - Short: "Search for vulnerabilities within the DB", + Short: "Search for vulnerabilities within the DB (supports DB schema v6+ only)", Args: func(_ *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("must specify at least one vulnerability ID") @@ -56,19 +56,15 @@ func DBSearchVulnerabilities(app clio.Application) *cobra.Command { return nil }, RunE: func(_ *cobra.Command, _ []string) (err error) { + if !opts.Experimental.DBv6 { + return errors.New("this command only supports the v6+ database schemas") + } return runDBSearchVulnerabilities(*opts) }, }, opts) } func runDBSearchVulnerabilities(opts dbSearchVulnerabilityOptions) error { - if opts.Experimental.DBv6 { - return runNewDBSearchVulnerabilities(opts) - } - return errors.New("this command only supports the v6+ database schemas") -} - -func runNewDBSearchVulnerabilities(opts dbSearchVulnerabilityOptions) error { client, err := distribution.NewClient(opts.DB.ToClientConfig()) if err != nil { return fmt.Errorf("unable to create distribution client: %w", err) diff --git a/cmd/grype/cli/options/database_search_format.go b/cmd/grype/cli/options/database_search_format.go index ae309ce710f..fd9921f20b3 100644 --- a/cmd/grype/cli/options/database_search_format.go +++ b/cmd/grype/cli/options/database_search_format.go @@ -15,7 +15,8 @@ type DBSearchFormat struct { } func (c *DBSearchFormat) AddFlags(flags clio.FlagSet) { - flags.StringVarP(&c.Output, "output", "o", "format to display results (available=[table, json])") + available := strings.Join(c.Allowable, ", ") + flags.StringVarP(&c.Output, "output", "o", fmt.Sprintf("format to display results (available=[%s])", available)) } func (c *DBSearchFormat) PostLoad() error { diff --git a/cmd/grype/cli/options/database_search_os.go b/cmd/grype/cli/options/database_search_os.go index fff02a641c2..d1dc18e6bea 100644 --- a/cmd/grype/cli/options/database_search_os.go +++ b/cmd/grype/cli/options/database_search_os.go @@ -15,7 +15,8 @@ type DBSearchOSs struct { } func (o *DBSearchOSs) AddFlags(flags clio.FlagSet) { - flags.StringArrayVarP(&o.OSs, "distro", "", "refine to results with the given operating system (format: 'name', 'name@version', 'name@maj.min', 'name@codename') (for v6+ schemas only)") // consistent with grype flag today + // consistent with grype --distro flag today + flags.StringArrayVarP(&o.OSs, "distro", "", "refine to results with the given operating system (format: 'name', 'name@version', 'name@maj.min', 'name@codename')") } func (o *DBSearchOSs) PostLoad() error { diff --git a/cmd/grype/cli/options/database_search_packages.go b/cmd/grype/cli/options/database_search_packages.go index 08a1bb18a6d..84d52487669 100644 --- a/cmd/grype/cli/options/database_search_packages.go +++ b/cmd/grype/cli/options/database_search_packages.go @@ -19,7 +19,7 @@ type DBSearchPackages struct { } func (o *DBSearchPackages) AddFlags(flags clio.FlagSet) { - flags.StringVarP(&o.Ecosystem, "ecosystem", "", "ecosystem of the package to search within (for v6+ schemas only)") + flags.StringVarP(&o.Ecosystem, "ecosystem", "", "ecosystem of the package to search within") } func (o *DBSearchPackages) PostLoad() error { diff --git a/cmd/grype/cli/options/database_search_vulnerabilities.go b/cmd/grype/cli/options/database_search_vulnerabilities.go index f280caec80e..c097f91ecd5 100644 --- a/cmd/grype/cli/options/database_search_vulnerabilities.go +++ b/cmd/grype/cli/options/database_search_vulnerabilities.go @@ -25,12 +25,12 @@ type DBSearchVulnerabilities struct { func (c *DBSearchVulnerabilities) AddFlags(flags clio.FlagSet) { if c.UseVulnIDFlag { - flags.StringArrayVarP(&c.VulnerabilityIDs, "vuln", "", "only show results for the given vulnerability ID (for v6+ schemas only)") + flags.StringArrayVarP(&c.VulnerabilityIDs, "vuln", "", "only show results for the given vulnerability ID") } flags.BoolVarP(&c.IncludeAliases, "include-aliases", "", "search for vulnerability aliases (for v6+ schemas only)") - flags.StringVarP(&c.PublishedAfter, "published-after", "", "only show vulnerabilities originally published after the given date (format: YYYY-MM-DD) (for v6+ schemas only)") - flags.StringVarP(&c.ModifiedAfter, "modified-after", "", "only show vulnerabilities originally published or modified since the given date (format: YYYY-MM-DD) (for v6+ schemas only)") - flags.StringArrayVarP(&c.Providers, "provider", "", "only show vulnerabilities from the given provider (for v6+ schemas only)") + flags.StringVarP(&c.PublishedAfter, "published-after", "", "only show vulnerabilities originally published after the given date (format: YYYY-MM-DD)") + flags.StringVarP(&c.ModifiedAfter, "modified-after", "", "only show vulnerabilities originally published or modified since the given date (format: YYYY-MM-DD)") + flags.StringArrayVarP(&c.Providers, "provider", "", "only show vulnerabilities from the given provider") } func (c *DBSearchVulnerabilities) PostLoad() error {