feat: add db search subcommand (#2031)

Signed-off-by: Tomer Seinfeld <tomersein@gmail.com>
This commit is contained in:
GGMU 2024-08-13 00:45:25 +03:00 committed by GitHub
parent 89c4190914
commit e7ceffadc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 174 additions and 30 deletions

View file

@ -31,6 +31,7 @@ func DB(app clio.Application) *cobra.Command {
DBList(app),
DBStatus(app),
DBUpdate(app),
DBSearch(app),
)
return db

View file

@ -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
}

View file

@ -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 {

View file

@ -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",
@ -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",
@ -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:*:*", ""),
},
@ -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:*:*", ""),
},
@ -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:*:*", ""),
},
@ -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:*:*", ""),
},
@ -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{},

View file

@ -15,6 +15,7 @@ type Reference struct {
}
type Vulnerability struct {
PackageName string
Constraint version.Constraint
PackageQualifiers []qualifier.Qualifier
CPEs []cpe.CPE
@ -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),

View file

@ -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",
@ -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",
@ -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",
@ -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:*:*"),
},
@ -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",
@ -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:*:*"),
},
@ -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:*:*"),
},
@ -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",
@ -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:*:*"),
},
@ -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",
@ -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",
@ -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:*:*"),
},