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 4, 2024
1 parent 37245b0 commit 08505f6
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 12 deletions.
159 changes: 150 additions & 9 deletions cmd/grype/cli/commands/db_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import (
"fmt"
"io"
"strings"
"time"

"github.com/araddon/dateparse"
"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/grype"
v6 "github.com/anchore/grype/grype/db/v6"
"github.com/anchore/grype/grype/db/v6/distribution"
Expand All @@ -20,14 +23,50 @@ import (
)

type dbQueryOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
Output string `yaml:"output" json:"output" mapstructure:"output"`

PublishedAfter string `yaml:"published-after" json:"published-after" mapstructure:"published-after"`
publishedAfter *time.Time

ModifiedAfter string `yaml:"modified-after" json:"modified-after" mapstructure:"modified-after"`
modifiedAfter *time.Time

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])")
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)")
}

func (c *dbQueryOptions) PostLoad() error {
handleTimeOption := func(val string, flag string) (*time.Time, error) {
if val == "" {
return nil, nil
}
parsed, err := dateparse.ParseIn(val, time.UTC)
if err != nil {
return nil, fmt.Errorf("invalid date format for %s=%q: %w", flag, val, err)
}
return &parsed, nil
}

if c.PublishedAfter != "" && c.ModifiedAfter != "" {
return fmt.Errorf("only one of --published-after or --modified-after can be set")
}

var err error
if c.publishedAfter, err = handleTimeOption(c.PublishedAfter, "published-after"); err != nil {
return err
}
if c.modifiedAfter, err = handleTimeOption(c.ModifiedAfter, "modified-after"); err != nil {
return err
}

return nil
}

func DBSearch(app clio.Application) *cobra.Command {
Expand All @@ -39,7 +78,7 @@ func DBSearch(app clio.Application) *cobra.Command {
return app.SetupCommand(&cobra.Command{
Use: "search [vulnerability_id]",
Short: "get information on a vulnerability from the db",
Args: cobra.ExactArgs(1),
Args: cobra.ArbitraryArgs,
RunE: func(_ *cobra.Command, args []string) (err error) {
id := args[0]
return runDBSearch(*opts, id)
Expand Down Expand Up @@ -69,26 +108,128 @@ func newDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
if err != nil {
return fmt.Errorf("unable to get providers: %w", err)
}
// TODO: refactor this in terms of search function pattern described in #2132 (in other words, the store should not be directly accessed here)

vh, err := reader.GetVulnerabilities(&v6.VulnerabilitySpecifier{Name: vulnerabilityID}, &v6.GetVulnerabilityOptions{
Preload: true,
affectedPkgs, err := reader.GetAffectedPackages(nil, &v6.GetAffectedPackageOptions{
PreloadOS: true,
PreloadPackage: true,
PreloadPackageCPEs: false,
PreloadVulnerability: true,
PreloadBlob: true,
Distro: nil,
Vulnerability: &v6.VulnerabilitySpecifier{
Name: vulnerabilityID,
PublishedAfter: opts.publishedAfter,
ModifiedAfter: opts.modifiedAfter,
},
})
if err != nil {
return fmt.Errorf("unable to get vulnerability: %w", err)
return fmt.Errorf("unable to get affected packages: %w", err)
}

if len(vh) == 0 {
return fmt.Errorf("vulnerability doesn't exist in the DB: %s", vulnerabilityID)
affectedCPEs, err := reader.GetAffectedCPEs(nil, &v6.GetAffectedCPEOptions{
PreloadCPE: true,
PreloadVulnerability: true,
PreloadBlob: true,
Vulnerability: &v6.VulnerabilitySpecifier{
Name: vulnerabilityID,
PublishedAfter: opts.publishedAfter,
ModifiedAfter: opts.modifiedAfter,
},
})
if err != nil {
return fmt.Errorf("unable to get affected cpes: %w", err)
}

rows := dbsearch.NewRows(affectedPkgs, affectedCPEs)

if len(rows) == 0 {
return fmt.Errorf("no packages affected by the given vulnerability ID: %s", vulnerabilityID)
}

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

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

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

table := tablewriter.NewWriter(output)
columns := []string{"ID", "Package", "Ecosystem", "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)

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 renderTableRows(structuredRows []dbsearch.Row) [][]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)
}

// TODO: we need to implement the functions that inflate models to the grype vulnerability.Vulnerability struct
panic("not implemented")
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
}

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

func legacyDBSearch(opts dbQueryOptions, vulnerabilityID string) error {
if opts.modifiedAfter != nil || opts.publishedAfter != nil {
return fmt.Errorf("date filtering is only available for v6+ schemas")
}

log.Debug("loading DB")
str, status, err := grype.LoadVulnerabilityDB(opts.DB.ToLegacyCuratorConfig(), opts.DB.AutoUpdate)
err = validateDBLoad(err, status)
Expand Down
127 changes: 127 additions & 0 deletions cmd/grype/cli/commands/internal/dbsearch/row.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package dbsearch

import (
"encoding/json"
"time"

v6 "github.com/anchore/grype/grype/db/v6"
)

type Row struct {
Vulnerability vulnerability `json:"vulnerability"`
OS *OS `json:"os,omitempty"`
Package *Package `json:"package,omitempty"`
CPE *v6.Cpe `json:"cpe,omitempty"`
Detail v6.AffectedPackageBlob `json:"detail"`
}

type vulnerability struct {
v6.VulnerabilityBlob `json:",inline"`
Provider string `json:"provider"`
Status string `json:"status"`
PublishedDate *time.Time `json:"published_date"`
ModifiedDate *time.Time `json:"modified_date"`
WithdrawnDate *time.Time `json:"withdrawn_date"`
}

func (r Row) MarshalJSON() ([]byte, error) {
var cpe string
if r.CPE != nil {
cpe = r.CPE.String()
}
return json.Marshal(&struct {
Vulnerability vulnerability `json:"vulnerability"`
OS *OS `json:"os,omitempty"`
Package *Package `json:"package,omitempty"`
CPE string `json:"cpe,omitempty"`
Detail v6.AffectedPackageBlob `json:"detail"`
}{
Vulnerability: r.Vulnerability,
OS: r.OS,
Package: r.Package,
CPE: cpe,
Detail: r.Detail,
})
}

type Package struct {
Name string `json:"name"`
Ecosystem string `json:"ecosystem"`
}

type OS struct {
Family string `json:"family"`
Version string `json:"version"`
}

func NewRows(affectedPkgs []v6.AffectedPackageHandle, affectedCPEs []v6.AffectedCPEHandle) []Row {
var rows []Row
for _, pkg := range affectedPkgs {
var detail v6.AffectedPackageBlob
if pkg.BlobValue != nil {
detail = *pkg.BlobValue
}
rows = append(rows, Row{
Vulnerability: toVulnerability(pkg.Vulnerability),
OS: toOS(pkg.OperatingSystem),
Package: toPackage(pkg.Package),
Detail: detail,
})
}

for _, ac := range affectedCPEs {
var detail v6.AffectedPackageBlob
if ac.BlobValue != nil {
detail = *ac.BlobValue
}
rows = append(rows, Row{
Vulnerability: toVulnerability(ac.Vulnerability),
CPE: ac.CPE,
Detail: detail,
})
}
return rows
}

func toVulnerability(vuln *v6.VulnerabilityHandle) vulnerability {
if vuln == nil {
return vulnerability{}
}
var blob v6.VulnerabilityBlob
if vuln.BlobValue != nil {
blob = *vuln.BlobValue
}
return vulnerability{
VulnerabilityBlob: blob,
Provider: vuln.Provider.ID,
Status: vuln.Status,
PublishedDate: vuln.PublishedDate,
ModifiedDate: vuln.ModifiedDate,
WithdrawnDate: vuln.WithdrawnDate,
}
}

func toPackage(pkg *v6.Package) *Package {
if pkg == nil {
return nil
}
return &Package{
Name: pkg.Name,
Ecosystem: pkg.Type,
}
}

func toOS(os *v6.OperatingSystem) *OS {
if os == nil {
return nil
}
version := os.VersionNumber()
if version == "" {
version = os.Version()
}

return &OS{
Family: os.Name,
Version: version,
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/anchore/stereoscope v0.0.9
github.com/anchore/syft v1.17.0
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/charmbracelet/bubbletea v1.2.4
github.com/charmbracelet/lipgloss v1.0.0
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 h1:
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg=
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M=
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
Expand Down Expand Up @@ -746,6 +748,7 @@ github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZ
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
Expand Down Expand Up @@ -868,6 +871,7 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
Expand Down Expand Up @@ -896,6 +900,7 @@ github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtC
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ=
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
Expand Down
4 changes: 2 additions & 2 deletions grype/db/v6/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ type AffectedRange struct {
// Fix conveys availability of a fix for a vulnerability.
type Fix struct {
// Version is the version number of the fix.
Version string `json:"version"`
Version string `json:"version,omitempty"`

// State represents the status of the fix (e.g., "fixed", "unaffected").
State FixStatus `json:"state"`
Expand All @@ -153,7 +153,7 @@ type FixDetail struct {
// AffectedVersion defines the versioning format and constraints.
type AffectedVersion struct {
// Type specifies the versioning system used (e.g., "semver", "rpm").
Type string `json:"type"`
Type string `json:"type,omitempty"`

// Constraint defines the version range constraint for affected versions.
Constraint string `json:"constraint"`
Expand Down
Loading

0 comments on commit 08505f6

Please sign in to comment.