Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add db search subcommand #2031

Merged
merged 16 commits into from
Aug 12, 2024
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
Loading