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

Optionally orient results by CVE #1020

Merged
merged 4 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 79 additions & 71 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"errors"
"fmt"
"os"
"strings"
Expand All @@ -21,6 +22,7 @@ import (
"github.com/anchore/grype/grype/matcher"
"github.com/anchore/grype/grype/matcher/dotnet"
"github.com/anchore/grype/grype/matcher/golang"
"github.com/anchore/grype/grype/matcher/java"
"github.com/anchore/grype/grype/matcher/javascript"
"github.com/anchore/grype/grype/matcher/python"
"github.com/anchore/grype/grype/matcher/ruby"
Expand Down Expand Up @@ -167,6 +169,11 @@ func setRootFlags(flags *pflag.FlagSet) {
"ignore matches for vulnerabilities that are fixed",
)

flags.BoolP(
"by-cve", "", false,
Copy link
Contributor

@kzantow kzantow Nov 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: by-cve to me, at least, is not the most clear... maybe something like --cve-first, --orient-by-cve, or --group-by-cve?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I can see how a user could conflate it. I also agree none of the current options standout as a clear winner. I like that --by-cve does not show GHSA. That feels right. The inverse --by-ghsa would not show CVE.

I also ran it locally and it feels right for now, but might be biased since it was the first one I read.

I say ship, and if er end up hating it we can change up the ergonomics if it still feels bad.

Keith also had a good point of eventually making this the default.

"orient results by CVE instead of the original vulnerability ID when possible",
)

flags.BoolP(
"show-suppressed", "", false,
"show suppressed/ignored vulnerabilities in the output (only supported with table output format)",
Expand Down Expand Up @@ -229,6 +236,10 @@ func bindRootConfigOptions(flags *pflag.FlagSet) error {
return err
}

if err := viper.BindPFlag("by-cve", flags.Lookup("by-cve")); err != nil {
return err
}

if err := viper.BindPFlag("show-suppressed", flags.Lookup("show-suppressed")); err != nil {
return err
}
Expand Down Expand Up @@ -298,28 +309,13 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
return
}

if appConfig.CheckForAppUpdate {
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)

bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("No new %s update available", internal.ApplicationName)
}
}
checkForAppUpdate()

var store *store.Store
var str *store.Store
var status *db.Status
var dbCloser *db.Closer
var packages []pkg.Package
var context pkg.Context
var pkgContext pkg.Context
var wg = &sync.WaitGroup{}
var loadedDB, gatheredPackages bool

Expand All @@ -328,7 +324,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
go func() {
defer wg.Done()
log.Debug("loading DB")
store, status, dbCloser, err = grype.LoadVulnerabilityDB(appConfig.DB.ToCuratorConfig(), appConfig.DB.AutoUpdate)
str, status, dbCloser, err = grype.LoadVulnerabilityDB(appConfig.DB.ToCuratorConfig(), appConfig.DB.AutoUpdate)
if err = validateDBLoad(err, status); err != nil {
errs <- err
return
Expand All @@ -339,7 +335,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
go func() {
defer wg.Done()
log.Debugf("gathering packages")
packages, context, err = pkg.Provide(userInput, getProviderConfig())
packages, pkgContext, err = pkg.Provide(userInput, getProviderConfig())
if err != nil {
errs <- fmt.Errorf("failed to catalog: %w", err)
return
Expand All @@ -364,35 +360,27 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
appConfig.Ignore = append(appConfig.Ignore, ignoreFixedMatches...)
}

applyDistroHint(packages, &context, appConfig)

matchers := matcher.NewDefaultMatchers(matcher.Config{
Java: appConfig.ExternalSources.ToJavaMatcherConfig(appConfig.Match.Java),
Ruby: ruby.MatcherConfig(appConfig.Match.Ruby),
Python: python.MatcherConfig(appConfig.Match.Python),
Dotnet: dotnet.MatcherConfig(appConfig.Match.Dotnet),
Javascript: javascript.MatcherConfig(appConfig.Match.Javascript),
Golang: golang.MatcherConfig(appConfig.Match.Golang),
Stock: stock.MatcherConfig(appConfig.Match.Stock),
})

allMatches := grype.FindVulnerabilitiesForPackage(*store, context.Distro, matchers, packages)
remainingMatches, ignoredMatches := match.ApplyIgnoreRules(allMatches, appConfig.Ignore)
applyDistroHint(packages, &pkgContext, appConfig)

if count := len(ignoredMatches); count > 0 {
log.Infof("ignoring %d matches due to user-provided ignore rules", count)
vulnMatcher := grype.VulnerabilityMatcher{
Store: *str,
IgnoreRules: appConfig.Ignore,
NormalizeByCVE: appConfig.ByCVE,
FailSeverity: failOnSeverity,
Matchers: getMatchers(),
}

// determine if there are any severities >= to the max allowable severity (which is optional).
// note: until the shared file lock in sqlittle is fixed the sqlite DB cannot be access concurrently,
// implying that the fail-on-severity check must be done before sending the presenter object.
if hitSeverityThreshold(failOnSeverity, remainingMatches, store) {
errs <- grypeerr.ErrAboveSeverityThreshold
remainingMatches, ignoredMatches, err := vulnMatcher.FindMatches(packages, pkgContext)
if err != nil {
errs <- err
if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
return
}
}

bus.Publish(partybus.Event{
Type: event.VulnerabilityScanningFinished,
Value: presenter.GetPresenter(presenterConfig, remainingMatches, ignoredMatches, packages, context, store, appConfig, status),
Value: presenter.GetPresenter(presenterConfig, *remainingMatches, ignoredMatches, packages, pkgContext, str, appConfig, status),
})
}()
return errs
Expand Down Expand Up @@ -434,15 +422,57 @@ func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, appConfig *config
}
}

func checkForAppUpdate() {
if !appConfig.CheckForAppUpdate {
return
}

isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)

bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}

func getMatchers() []matcher.Matcher {
return matcher.NewDefaultMatchers(
matcher.Config{
Java: java.MatcherConfig{
ExternalSearchConfig: appConfig.ExternalSources.ToJavaMatcherConfig(),
UseCPEs: appConfig.Match.Java.UseCPEs,
},
Ruby: ruby.MatcherConfig(appConfig.Match.Ruby),
Python: python.MatcherConfig(appConfig.Match.Python),
Dotnet: dotnet.MatcherConfig(appConfig.Match.Dotnet),
Javascript: javascript.MatcherConfig(appConfig.Match.Javascript),
Golang: golang.MatcherConfig(appConfig.Match.Golang),
Stock: stock.MatcherConfig(appConfig.Match.Stock),
},
)
}

func getProviderConfig() pkg.ProviderConfig {
return pkg.ProviderConfig{
RegistryOptions: appConfig.Registry.ToOptions(),
Exclusions: appConfig.Exclusions,
CatalogingOptions: appConfig.Search.ToConfig(),
GenerateMissingCPEs: appConfig.GenerateMissingCPEs,
Platform: appConfig.Platform,
AttestationPublicKey: appConfig.Attestation.PublicKey,
AttestationIgnoreVerification: appConfig.Attestation.SkipVerification,
SyftProviderConfig: pkg.SyftProviderConfig{
RegistryOptions: appConfig.Registry.ToOptions(),
Exclusions: appConfig.Exclusions,
CatalogingOptions: appConfig.Search.ToConfig(),
Platform: appConfig.Platform,
AttestationPublicKey: appConfig.Attestation.PublicKey,
AttestationIgnoreVerification: appConfig.Attestation.SkipVerification,
},
SynthesisConfig: pkg.SynthesisConfig{
GenerateMissingCPEs: appConfig.GenerateMissingCPEs,
},
}
}

Expand Down Expand Up @@ -476,25 +506,3 @@ func validateRootArgs(cmd *cobra.Command, args []string) error {

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

// hitSeverityThreshold indicates if there are any severities >= to the max allowable severity (which is optional)
func hitSeverityThreshold(thresholdSeverity *vulnerability.Severity, matches match.Matches, metadataProvider vulnerability.MetadataProvider) bool {
if thresholdSeverity != nil {
var maxDiscoveredSeverity vulnerability.Severity
for m := range matches.Enumerate() {
metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
if err != nil {
continue
}
severity := vulnerability.ParseSeverity(metadata.Severity)
if severity > maxDiscoveredSeverity {
maxDiscoveredSeverity = severity
}
}

if maxDiscoveredSeverity >= *thresholdSeverity {
return true
}
}
return false
}
110 changes: 0 additions & 110 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,122 +3,12 @@ package cmd
import (
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"

"github.com/anchore/grype/grype/db"
grypeDB "github.com/anchore/grype/grype/db/v5"
"github.com/anchore/grype/grype/match"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/grype/internal/config"
syftPkg "github.com/anchore/syft/syft/pkg"
)

type mockMetadataStore struct {
data map[string]map[string]*grypeDB.VulnerabilityMetadata
}

func newMockStore() *mockMetadataStore {
d := mockMetadataStore{
data: make(map[string]map[string]*grypeDB.VulnerabilityMetadata),
}
d.stub()
return &d
}

func (d *mockMetadataStore) stub() {
d.data["CVE-2014-fake-1"] = map[string]*grypeDB.VulnerabilityMetadata{
"source-1": {
Severity: "medium",
},
}
}

func (d *mockMetadataStore) GetVulnerabilityMetadata(id, recordSource string) (*grypeDB.VulnerabilityMetadata, error) {
return d.data[id][recordSource], nil
}

func (d *mockMetadataStore) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) {
return nil, nil
}

func TestAboveAllowableSeverity(t *testing.T) {
thePkg := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "the-package",
Version: "v0.1",
Type: syftPkg.RpmPkg,
}

matches := match.NewMatches()
matches.Add(match.Match{
Vulnerability: vulnerability.Vulnerability{
ID: "CVE-2014-fake-1",
Namespace: "source-1",
},
Package: thePkg,
Details: match.Details{
{
Type: match.ExactDirectMatch,
},
},
})

tests := []struct {
name string
failOnSeverity string
matches match.Matches
expectedResult bool
}{
{
name: "no-severity-set",
failOnSeverity: "",
matches: matches,
expectedResult: false,
},
{
name: "below-threshold",
failOnSeverity: "high",
matches: matches,
expectedResult: false,
},
{
name: "at-threshold",
failOnSeverity: "medium",
matches: matches,
expectedResult: true,
},
{
name: "above-threshold",
failOnSeverity: "low",
matches: matches,
expectedResult: true,
},
}

metadataProvider := db.NewVulnerabilityMetadataProvider(newMockStore())

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var failOnSeverity *vulnerability.Severity
if test.failOnSeverity != "" {
sev := vulnerability.ParseSeverity(test.failOnSeverity)
if sev == vulnerability.UnknownSeverity {
t.Fatalf("could not parse severity")
}
failOnSeverity = &sev
}

actual := hitSeverityThreshold(failOnSeverity, test.matches, metadataProvider)

if test.expectedResult != actual {
t.Errorf("expected: %v got : %v", test.expectedResult, actual)
}
})
}
}

func Test_applyDistroHint(t *testing.T) {
ctx := pkg.Context{}
cfg := config.Application{}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ require (
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 // indirect
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2323,8 +2323,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
Loading