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

VEX Autodiscovery! #1619

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
74 changes: 8 additions & 66 deletions cmd/grype/cli/commands/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"context"
"errors"
"fmt"
"strings"
Expand Down Expand Up @@ -73,40 +74,19 @@ You can also pipe in Syft JSON directly:
Args: validateRootArgs,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
userInput := ""
if len(args) > 0 {
userInput = args[0]
}
return runGrype(app, opts, userInput)
return runGrype(cmd.Context(), app, opts, userInput)
},
ValidArgsFunction: dockerImageValidArgsFunction,
}, opts)
}

var ignoreNonFixedMatches = []match.IgnoreRule{
{FixState: string(grypeDb.NotFixedState)},
{FixState: string(grypeDb.WontFixState)},
{FixState: string(grypeDb.UnknownFixState)},
}

var ignoreFixedMatches = []match.IgnoreRule{
{FixState: string(grypeDb.FixedState)},
}

var ignoreVEXFixedNotAffected = []match.IgnoreRule{
{VexStatus: string(vex.StatusNotAffected)},
{VexStatus: string(vex.StatusFixed)},
}

var ignoreLinuxKernelHeaders = []match.IgnoreRule{
{Package: match.IgnoreRulePackage{Name: "kernel-headers", UpstreamName: "kernel", Type: string(syftPkg.RpmPkg)}, MatchType: match.ExactIndirectMatch},
{Package: match.IgnoreRulePackage{Name: "linux-headers-.*", UpstreamName: "linux", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},
{Package: match.IgnoreRulePackage{Name: "linux-libc-dev", UpstreamName: "linux", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},
}

//nolint:funlen
func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs error) {
func runGrype(ctx context.Context, app clio.Application, opts *options.Grype, userInput string) (errs error) {
writer, err := format.MakeScanResultWriter(opts.Outputs, opts.File, format.PresentationConfig{
TemplateFilePath: opts.OutputTemplateFile,
ShowSuppressed: opts.ShowSuppressed,
Expand All @@ -122,18 +102,6 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
var s *sbom.SBOM
var pkgContext pkg.Context

if opts.OnlyFixed {
opts.Ignore = append(opts.Ignore, ignoreNonFixedMatches...)
}

if opts.OnlyNotFixed {
opts.Ignore = append(opts.Ignore, ignoreFixedMatches...)
}

if !opts.MatchUpstreamKernelHeaders {
opts.Ignore = append(opts.Ignore, ignoreLinuxKernelHeaders...)
}

for _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) {
switch grypeDb.FixState(ignoreState) {
case grypeDb.UnknownFixState, grypeDb.FixedState, grypeDb.NotFixedState, grypeDb.WontFixState:
Expand Down Expand Up @@ -175,10 +143,6 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
defer dbCloser.Close()
}

if err = applyVexRules(opts); err != nil {
return fmt.Errorf("applying vex rules: %w", err)
}

applyDistroHint(packages, &pkgContext, opts)

vulnMatcher := grype.VulnerabilityMatcher{
Expand All @@ -188,12 +152,13 @@ func runGrype(app clio.Application, opts *options.Grype, userInput string) (errs
FailSeverity: opts.FailOnSeverity(),
Matchers: getMatchers(opts),
VexProcessor: vex.NewProcessor(vex.ProcessorOptions{
Documents: opts.VexDocuments,
IgnoreRules: opts.Ignore,
Documents: opts.VexDocuments,
Autodiscover: opts.VexAutodiscover,
IgnoreRules: opts.Ignore,
}),
}

remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext)
remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(ctx, packages, pkgContext)
if err != nil {
if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
return err
Expand Down Expand Up @@ -350,26 +315,3 @@ func validateRootArgs(cmd *cobra.Command, args []string) error {

return cobra.MaximumNArgs(1)(cmd, args)
}

func applyVexRules(opts *options.Grype) error {
if len(opts.Ignore) == 0 && len(opts.VexDocuments) > 0 {
opts.Ignore = append(opts.Ignore, ignoreVEXFixedNotAffected...)
}

for _, vexStatus := range opts.VexAdd {
switch vexStatus {
case string(vex.StatusAffected):
opts.Ignore = append(
opts.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusAffected)},
)
case string(vex.StatusUnderInvestigation):
opts.Ignore = append(
opts.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusUnderInvestigation)},
)
default:
return fmt.Errorf("invalid VEX status in vex-add setting: %s", vexStatus)
}
}

return nil
}
70 changes: 70 additions & 0 deletions cmd/grype/cli/options/grype.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,36 @@ import (
"fmt"

"github.com/anchore/clio"
grypeDb "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/vex"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/format"
syftPkg "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)

var ignoreNonFixedMatches = []match.IgnoreRule{
{FixState: string(grypeDb.NotFixedState)},
{FixState: string(grypeDb.WontFixState)},
{FixState: string(grypeDb.UnknownFixState)},
}

var ignoreFixedMatches = []match.IgnoreRule{
{FixState: string(grypeDb.FixedState)},
}

var ignoreVEXFixedNotAffected = []match.IgnoreRule{
{VexStatus: string(vex.StatusNotAffected)},
{VexStatus: string(vex.StatusFixed)},
}

var ignoreLinuxKernelHeaders = []match.IgnoreRule{
{Package: match.IgnoreRulePackage{Name: "kernel-headers", UpstreamName: "kernel", Type: string(syftPkg.RpmPkg)}, MatchType: match.ExactIndirectMatch},
{Package: match.IgnoreRulePackage{Name: "linux-headers-.*", UpstreamName: "linux", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},
{Package: match.IgnoreRulePackage{Name: "linux-libc-dev", UpstreamName: "linux", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},
}

type Grype struct {
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, <presenter>=<file> the Presenter hint string to use for report formatting and the output file
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
Expand All @@ -36,6 +60,7 @@ type Grype struct {
VexDocuments []string `yaml:"vex-documents" json:"vex-documents" mapstructure:"vex-documents"`
VexAdd []string `yaml:"vex-add" json:"vex-add" mapstructure:"vex-add"` // GRYPE_VEX_ADD
MatchUpstreamKernelHeaders bool `yaml:"match-upstream-kernel-headers" json:"match-upstream-kernel-headers" mapstructure:"match-upstream-kernel-headers"` // Show matches on kernel-headers packages where the match is on kernel upstream instead of marking them as ignored, default=false
VexAutodiscover bool `yaml:"vex-autodiscover" json:"vex-vex-autodiscover" mapstructure:"vex-autodiscover"` // GRYPE_VEX_ADD
}

var _ interface {
Expand Down Expand Up @@ -136,6 +161,11 @@ func (o *Grype) AddFlags(flags clio.FlagSet) {
"vex", "",
"a list of VEX documents to consider when producing scanning results",
)

flags.BoolVarP(&o.VexAutodiscover,
"vex-autodiscover", "",
"attempt to automatically locate relevant VEX data",
)
}

func (o *Grype) PostLoad() error {
Expand All @@ -145,6 +175,46 @@ func (o *Grype) PostLoad() error {
return fmt.Errorf("bad --fail-on severity value '%s'", o.FailOn)
}
}

if err := o.applyVexRules(); err != nil {
return fmt.Errorf("failed to apply VEX rules: %w", err)
}

if o.OnlyFixed {
o.Ignore = append(o.Ignore, ignoreNonFixedMatches...)
}

if o.OnlyNotFixed {
o.Ignore = append(o.Ignore, ignoreFixedMatches...)
}

if !o.MatchUpstreamKernelHeaders {
o.Ignore = append(o.Ignore, ignoreLinuxKernelHeaders...)
}

return nil
}

func (o *Grype) applyVexRules() error {
if (len(o.Ignore) == 0 && len(o.VexDocuments) > 0) || o.VexAutodiscover {
o.Ignore = append(o.Ignore, ignoreVEXFixedNotAffected...)
}

for _, vexStatus := range o.VexAdd {
switch vexStatus {
case string(vex.StatusAffected):
o.Ignore = append(
o.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusAffected)},
)
case string(vex.StatusUnderInvestigation):
o.Ignore = append(
o.Ignore, match.IgnoreRule{VexStatus: string(vex.StatusUnderInvestigation)},
)
default:
return fmt.Errorf("invalid VEX status in vex-add setting: %s", vexStatus)
}
}

return nil
}

Expand Down
55 changes: 55 additions & 0 deletions cmd/grype/cli/ui/handle_vex_document_discovery_started.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ui

import (
"fmt"

tea "github.com/charmbracelet/bubbletea"
"github.com/dustin/go-humanize"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"

"github.com/anchore/bubbly/bubbles/taskprogress"
"github.com/anchore/grype/grype/event/parsers"
"github.com/anchore/grype/internal/log"
)

type vexDocumentDiscoveryStager struct {
prog progress.StagedProgressable
}

func (s vexDocumentDiscoveryStager) Stage() string {
stage := s.prog.Stage()
if stage == "downloading" {
// note: since validation is baked into the download progress there is no visibility into this stage.
// for that reason we report "validating" on the last byte being downloaded (which tends to be the longest
// since go-downloader is doing this work).
if s.prog.Current() >= s.prog.Size()-1 {
return "validating"
}
// show intermediate progress of the download
return fmt.Sprintf("%s / %s", humanize.Bytes(uint64(s.prog.Current())), humanize.Bytes(uint64(s.prog.Size())))
}
return stage
}

func (m *Handler) handleVexDocumentDiscoveryStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
prog, err := parsers.ParseVexDocumentDiscoveryStarted(e)
if err != nil {
log.WithFields("error", err).Warn("unable to parse event")
return nil, nil
}

tsk := m.newTaskProgress(
taskprogress.Title{
Default: "Search for VEX Documents",
Running: "Searching for VEX Documents",
Success: "Searched for VEX Documents",
},
taskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event
taskprogress.WithStager(vexDocumentDiscoveryStager{prog: prog}),
)

tsk.HideStageOnSuccess = false

return []tea.Model{tsk}, tsk.Init()
}
1 change: 1 addition & 0 deletions cmd/grype/cli/ui/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func New(cfg HandlerConfig) *Handler {
event.UpdateVulnerabilityDatabase: h.handleUpdateVulnerabilityDatabase,
event.VulnerabilityScanningStarted: h.handleVulnerabilityScanningStarted,
event.DatabaseDiffingStarted: h.handleDatabaseDiffStarted,
event.VexDocumentDiscoveryStarted: h.handleVexDocumentDiscoveryStarted,
})

return h
Expand Down