Skip to content

Commit

Permalink
feat: add db search subcommand (#2031)
Browse files Browse the repository at this point in the history
Signed-off-by: Tomer Seinfeld <[email protected]>
  • Loading branch information
tomersein authored Aug 12, 2024
1 parent 89c4190 commit e7ceffa
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 30 deletions.
1 change: 1 addition & 0 deletions cmd/grype/cli/commands/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func DB(app clio.Application) *cobra.Command {
DBList(app),
DBStatus(app),
DBUpdate(app),
DBSearch(app),
)

return db
Expand Down
116 changes: 116 additions & 0 deletions cmd/grype/cli/commands/db_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package commands

import (
"encoding/json"
"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"
)

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

func DBSearch(app clio.Application) *cobra.Command {
opts := &dbQueryOptions{
Output: "table",
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)
},
}, opts)
}

func runDBSearch(opts *dbQueryOptions, vulnerabilityID string) error {
log.Debug("loading DB")
str, status, dbCloser, err := grype.LoadVulnerabilityDB(opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate)
err = validateDBLoad(err, status)
if err != nil {
return err
}
if dbCloser != nil {
defer dbCloser.Close()
}

vulnerabilities, err := str.Get(vulnerabilityID, "")
if err != nil {
return err
}

sb := &strings.Builder{}
if len(vulnerabilities) == 0 {
return fmt.Errorf("vulnerability doesn't exist in the DB: %s", vulnerabilityID)
}

err = present(opts.Output, vulnerabilities, sb)
bus.Report(sb.String())

return err
}

func present(outputFormat string, vulnerabilities []vulnerability.Vulnerability, output io.Writer) error {
if vulnerabilities == nil {
return nil
}

switch outputFormat {
case "table":
rows := [][]string{}
for _, v := range vulnerabilities {
rows = append(rows, []string{v.ID, v.PackageName, v.Namespace, v.Constraint.String()})
}

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)

table.AppendBulk(rows)
table.Render()
case "json":
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
}
8 changes: 7 additions & 1 deletion grype/db/v5/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ func (s *store) GetVulnerabilityNamespaces() ([]string, error) {
func (s *store) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) {
var models []model.VulnerabilityModel

result := s.db.Where("namespace = ? AND id = ?", namespace, id).Find(&models)
query := s.db.Where("id = ?", id)

if namespace != "" {
query = query.Where("namespace = ?", namespace)
}

result := query.Find(&models)

var vulnerabilities = make([]v5.Vulnerability, len(models))
for idx, m := range models {
Expand Down
23 changes: 15 additions & 8 deletions grype/db/vulnerability_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func Test_GetByDistro(t *testing.T) {

expected := []vulnerability.Vulnerability{
{
PackageName: "neutron",
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
Expand All @@ -41,6 +42,7 @@ func Test_GetByDistro(t *testing.T) {
Advisories: []vulnerability.Advisory{},
},
{
PackageName: "neutron",
Constraint: version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
ID: "CVE-2013-fake-2",
Namespace: "debian:distro:debian:8",
Expand Down Expand Up @@ -87,8 +89,9 @@ func Test_GetByCPE(t *testing.T) {
cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", ""),
expected: []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
},
Expand All @@ -103,8 +106,9 @@ func Test_GetByCPE(t *testing.T) {
cpe: cpe.Must("cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*", ""),
expected: []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
},
Expand All @@ -119,8 +123,9 @@ func Test_GetByCPE(t *testing.T) {
cpe: cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*", ""),
expected: []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""),
},
Expand All @@ -129,8 +134,9 @@ func Test_GetByCPE(t *testing.T) {
Advisories: []vulnerability.Advisory{},
},
{
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
ID: "CVE-2014-fake-4",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
},
Expand Down Expand Up @@ -183,6 +189,7 @@ func Test_Get(t *testing.T) {
expected := []vulnerability.Vulnerability{
{
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
PackageName: "neutron",
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
PackageQualifiers: []qualifier.Qualifier{},
Expand Down
2 changes: 2 additions & 0 deletions grype/vulnerability/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Reference struct {
}

type Vulnerability struct {
PackageName string
Constraint version.Constraint
PackageQualifiers []qualifier.Qualifier
CPEs []cpe.CPE
Expand Down Expand Up @@ -55,6 +56,7 @@ func NewVulnerability(vuln grypeDB.Vulnerability) (*Vulnerability, error) {
}

return &Vulnerability{
PackageName: vuln.PackageName,
Constraint: constraint,
ID: vuln.ID,
CPEs: make([]cpe.CPE, 0),
Expand Down
54 changes: 33 additions & 21 deletions grype/vulnerability_matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
PackageName: "neutron",
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
Expand Down Expand Up @@ -444,6 +445,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
PackageName: "neutron",
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
Expand Down Expand Up @@ -524,6 +526,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
PackageName: "neutron",
Constraint: version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
ID: "CVE-2014-fake-1",
Namespace: "debian:distro:debian:8",
Expand Down Expand Up @@ -572,9 +575,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
Expand Down Expand Up @@ -609,9 +613,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "CVE-2014-fake-3",
Expand Down Expand Up @@ -666,9 +671,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
Expand Down Expand Up @@ -752,9 +758,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
Expand Down Expand Up @@ -793,6 +800,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
{
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
Expand Down Expand Up @@ -861,9 +869,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
{
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
Expand Down Expand Up @@ -911,6 +920,7 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
Expand Down Expand Up @@ -969,9 +979,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
wantMatches: match.NewMatches(
match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "GHSA-2014-fake-3",
Namespace: "github:language:ruby",
RelatedVulnerabilities: []vulnerability.Reference{
{
ID: "CVE-2014-fake-3",
Expand Down Expand Up @@ -1010,9 +1021,10 @@ func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
},
Match: match.Match{
Vulnerability: vulnerability.Vulnerability{
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
PackageName: "activerecord",
Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
ID: "CVE-2014-fake-3",
Namespace: "nvd:cpe",
CPEs: []cpe.CPE{
mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
},
Expand Down

0 comments on commit e7ceffa

Please sign in to comment.