Skip to content

Commit

Permalink
add v6 search command
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman committed Dec 17, 2024
1 parent 224be59 commit 4c79df6
Show file tree
Hide file tree
Showing 16 changed files with 1,541 additions and 76 deletions.
117 changes: 44 additions & 73 deletions cmd/grype/cli/commands/db_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
Expand All @@ -11,21 +12,18 @@ import (

"github.com/anchore/clio"
"github.com/anchore/grype/grype"
v6 "github.com/anchore/grype/grype/db/v6"
"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"
)

var _ clio.FlagAdder = (*dbQueryOptions)(nil)

type dbQueryOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
DBOptions `yaml:",inline" mapstructure:",squash"`
}

var _ clio.FlagAdder = (*dbQueryOptions)(nil)

func (c *dbQueryOptions) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&c.Output, "output", "o", "format to display results (available=[table, json])")
}
Expand All @@ -36,59 +34,41 @@ func DBSearch(app clio.Application) *cobra.Command {
DBOptions: *dbOptionsDefault(app.ID()),
}

return app.SetupCommand(&cobra.Command{
Use: "search [vulnerability_id]",
Short: "get information on a vulnerability from the db",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) (err error) {
id := args[0]
return runDBSearch(*opts, id)
cmd := &cobra.Command{
// 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)
},
}, opts)
}

func runDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
if opts.Experimental.DBv6 {
return newDBSearch(opts, vulnerabilityID)
}
return legacyDBSearch(opts, vulnerabilityID)
}

func newDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
client, err := distribution.NewClient(opts.DB.ToClientConfig())
if err != nil {
return fmt.Errorf("unable to create distribution client: %w", err)
}

c, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
if err != nil {
return fmt.Errorf("unable to create curator: %w", err)
}

reader, err := c.Reader()
if err != nil {
return fmt.Errorf("unable to get providers: %w", err)
}

vh, err := reader.GetVulnerabilities(&v6.VulnerabilitySpecifier{Name: vulnerabilityID}, &v6.GetVulnerabilityOptions{
Preload: true,
})
if err != nil {
return fmt.Errorf("unable to get vulnerability: %w", err)
}

if len(vh) == 0 {
return fmt.Errorf("vulnerability doesn't exist in the DB: %s", vulnerabilityID)
}
cmd.AddCommand(
DBSearchPackages(app),
DBSearchVulnerabilities(app),
)

// TODO: we need to implement the functions that inflate models to the grype vulnerability.Vulnerability struct
panic("not implemented")
return app.SetupCommand(cmd, opts)
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// all legacy processing below ////////////////////////////////////////////////////////////////////////////////////////

func legacyDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
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)
Expand All @@ -97,23 +77,27 @@ func legacyDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
}
defer log.CloseAndLogError(str, status.Location)

vulnerabilities, err := str.Get(vulnerabilityID, "")
if err != nil {
return err
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 fmt.Errorf("vulnerability doesn't exist in the DB: %s", vulnerabilityID)
return errors.New("no affected packages found")
}

sb := &strings.Builder{}
err = presentLegacy(opts.Output, vulnerabilities, sb)
err = presentLegacyDBSearchPackages(opts.Output, vulnerabilities, sb)
bus.Report(sb.String())

return err
}

func presentLegacy(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error {
func presentLegacyDBSearchPackages(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error {
if vulnerabilities == nil {
return nil
}
Expand All @@ -126,22 +110,9 @@ func presentLegacy(outputFormat string, vulnerabilities []vulnerability.Vulnerab
}

table := tablewriter.NewWriter(output)
columns := []string{"ID", "Package Name", "Namespace", "Version Constraint"}

table.SetHeader(columns)
table.SetAutoWrapText(false)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)

table.SetHeaderLine(false)
table.SetBorder(false)
table.SetAutoFormatHeaders(true)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetTablePadding(" ")
table.SetNoWhiteSpace(true)
commonTableWriterOptions(table)

table.SetHeader([]string{"ID", "Package Name", "Namespace", "Version Constraint"})
table.AppendBulk(rows)
table.Render()
case jsonOutputFormat:
Expand Down
156 changes: 156 additions & 0 deletions cmd/grype/cli/commands/db_search_pkg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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/cmd/grype/cli/commands/internal/dbsearch"
"github.com/anchore/grype/cmd/grype/cli/options"
"github.com/anchore/grype/grype/db/v6/distribution"
"github.com/anchore/grype/grype/db/v6/installation"
"github.com/anchore/grype/internal/bus"
)

type dbSearchPackageOptions struct {
Format options.DBSearchFormat `yaml:",inline" mapstructure:",squash"`
Vulnerability options.DBSearchVulnerabilities `yaml:",inline" mapstructure:",squash"`
Package options.DBSearchPackages `yaml:",inline" mapstructure:",squash"`
OS options.DBSearchOSs `yaml:",inline" mapstructure:",squash"`

DBOptions `yaml:",inline" mapstructure:",squash"`
}

func DBSearchPackages(app clio.Application) *cobra.Command {
opts := &dbSearchPackageOptions{
Format: options.DBSearchFormat{
Output: tableOutputFormat,
Allowable: []string{
tableOutputFormat,
jsonOutputFormat,
},
},
Vulnerability: options.DBSearchVulnerabilities{
UseVulnIDFlag: true,
},
DBOptions: *dbOptionsDefault(app.ID()),
}

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 (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 {
client, err := distribution.NewClient(opts.DB.ToClientConfig())
if err != nil {
return fmt.Errorf("unable to create distribution client: %w", err)
}

curator, err := installation.NewCurator(opts.DB.ToCuratorConfig(), client)
if err != nil {
return fmt.Errorf("unable to create curator: %w", err)
}

reader, err := curator.Reader()
if err != nil {
return fmt.Errorf("unable to get providers: %w", err)
}

if err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil {
return err
}

rows, err := dbsearch.AffectedPackages(reader, dbsearch.AffectedPackagesOptions{
Vulnerability: opts.Vulnerability.Specs,
Package: opts.Package.PkgSpecs,
CPE: opts.Package.CPESpecs,
OS: opts.OS.Specs,
})
if err != nil {
return err
}

if len(rows) == 0 {
return errors.New("no affected packages found")
}

sb := &strings.Builder{}
err = presentDBSearchPackages(opts.Format.Output, rows, sb)
bus.Report(sb.String())
return err
}

func presentDBSearchPackages(outputFormat string, structuredRows []dbsearch.AffectedPackageTableRow, output io.Writer) error {
if len(structuredRows) == 0 {
// TODO: show a message that no results were found?
return nil
}

switch outputFormat {
case tableOutputFormat:
rows := renderDBSearchPackagesTableRows(structuredRows)

table := tablewriter.NewWriter(output)
commonTableWriterOptions(table)

table.SetHeader([]string{"ID", "Package", "Ecosystem", "Namespace", "Version Constraint"})
table.AppendBulk(rows)
table.Render()
case jsonOutputFormat:
enc := json.NewEncoder(output)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(structuredRows); err != nil {
return fmt.Errorf("failed to encode diff information: %+v", err)
}
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return nil
}

func renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackageTableRow) [][]string {
var rows [][]string
for _, rr := range structuredRows {
var pkgOrCPE, ecosystem string
if rr.Package != nil {
pkgOrCPE = rr.Package.Name
ecosystem = rr.Package.Ecosystem
} else if rr.CPE != nil {
pkgOrCPE = rr.CPE.String()
ecosystem = rr.CPE.TargetSoftware
}

namespace := rr.Vulnerability.Provider
if rr.OS != nil {
namespace = fmt.Sprintf("%s:%s", rr.OS.Family, rr.OS.Version)
}

var ranges []string
for _, ra := range rr.Detail.Ranges {
ranges = append(ranges, ra.Version.Constraint)
}
rangeStr := strings.Join(ranges, " || ")
rows = append(rows, []string{rr.Vulnerability.ID, pkgOrCPE, ecosystem, namespace, rangeStr})
}
return rows
}
Loading

0 comments on commit 4c79df6

Please sign in to comment.