mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
add --fail-on threshold support (#156)
* add --fail-on threshold support Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * rename fail-on support functions and variables Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove UK spelling of canceled Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
0397206376
commit
f0f8f4bf02
9 changed files with 298 additions and 26 deletions
2
Makefile
2
Makefile
|
@ -14,7 +14,7 @@ RESET := $(shell tput -T linux sgr0)
|
|||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 60
|
||||
COVERAGE_THRESHOLD := 55
|
||||
|
||||
## Build variables
|
||||
DISTDIR=./dist
|
||||
|
|
57
cmd/root.go
57
cmd/root.go
|
@ -2,6 +2,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
@ -10,7 +11,9 @@ import (
|
|||
|
||||
"github.com/anchore/grype/grype"
|
||||
"github.com/anchore/grype/grype/event"
|
||||
"github.com/anchore/grype/grype/grypeerr"
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
"github.com/anchore/grype/grype/result"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/bus"
|
||||
|
@ -68,13 +71,18 @@ var rootCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
var grypeErr grypeerr.ExpectedErr
|
||||
if errors.As(err, &grypeErr) {
|
||||
fmt.Fprintln(os.Stderr, format.Red.Format(grypeErr.Error()))
|
||||
} else {
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
|
||||
dockerImageRepoTags, err := ListLocalDockerImages(toComplete)
|
||||
dockerImageRepoTags, err := listLocalDockerImages(toComplete)
|
||||
if err != nil {
|
||||
// Indicates that an error occurred and completions should be ignored
|
||||
return []string{"completion failed"}, cobra.ShellCompDirectiveError
|
||||
|
@ -112,9 +120,19 @@ func init() {
|
|||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rootCmd.Flags().StringP(
|
||||
"fail-on", "f", "",
|
||||
fmt.Sprintf("set the return code to 1 if a vulnerability is found with a severity >= the given severity, options=%v", vulnerability.AllSeverities),
|
||||
)
|
||||
if err := viper.BindPFlag("fail-on-severity", rootCmd.Flags().Lookup("fail-on")); err != nil {
|
||||
fmt.Printf("unable to bind flag '%s': %+v", "fail-on", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func startWorker(userInput string) <-chan error {
|
||||
// nolint:funlen
|
||||
func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-chan error {
|
||||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
|
@ -169,6 +187,13 @@ func startWorker(userInput string) <-chan error {
|
|||
|
||||
matches := grype.FindVulnerabilitiesForCatalog(provider, *theDistro, catalog)
|
||||
|
||||
// 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, results, metadataProvider) {
|
||||
errs <- grypeerr.ErrAboveSeverityThreshold
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.VulnerabilityScanningFinished,
|
||||
Value: presenter.GetPresenter(appConfig.PresenterOpt, matches, catalog, *theScope, metadataProvider),
|
||||
|
@ -179,12 +204,34 @@ func startWorker(userInput string) <-chan error {
|
|||
|
||||
func runDefaultCmd(_ *cobra.Command, args []string) error {
|
||||
userInput := args[0]
|
||||
errs := startWorker(userInput)
|
||||
errs := startWorker(userInput, appConfig.FailOnSeverity)
|
||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
||||
return ux(errs, eventSubscription)
|
||||
}
|
||||
|
||||
func ListLocalDockerImages(prefix string) ([]string, error) {
|
||||
// hitSeverityThreshold indicates if there are any severities >= to the max allowable severity (which is optional)
|
||||
func hitSeverityThreshold(thresholdSeverity *vulnerability.Severity, results result.Result, metadataProvider vulnerability.MetadataProvider) bool {
|
||||
if thresholdSeverity != nil {
|
||||
var maxDiscoveredSeverity vulnerability.Severity
|
||||
for m := range results.Enumerate() {
|
||||
metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.RecordSource)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
severity := vulnerability.ParseSeverity(metadata.Severity)
|
||||
if severity > maxDiscoveredSeverity {
|
||||
maxDiscoveredSeverity = severity
|
||||
}
|
||||
}
|
||||
|
||||
if maxDiscoveredSeverity >= *thresholdSeverity {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func listLocalDockerImages(prefix string) ([]string, error) {
|
||||
var repoTags = make([]string, 0)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
|
|
107
cmd/root_test.go
Normal file
107
cmd/root_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/result"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
import v1 "github.com/anchore/grype-db/pkg/db/v1"
|
||||
|
||||
type mockMetadataStore struct {
|
||||
data map[string]map[string]*v1.VulnerabilityMetadata
|
||||
}
|
||||
|
||||
func newMockStore() *mockMetadataStore {
|
||||
d := mockMetadataStore{
|
||||
data: make(map[string]map[string]*v1.VulnerabilityMetadata),
|
||||
}
|
||||
d.stub()
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *mockMetadataStore) stub() {
|
||||
d.data["CVE-2014-fake-1"] = map[string]*v1.VulnerabilityMetadata{
|
||||
"source-1": {
|
||||
Severity: "medium",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *mockMetadataStore) GetVulnerabilityMetadata(id, recordSource string) (*v1.VulnerabilityMetadata, error) {
|
||||
return d.data[id][recordSource], nil
|
||||
}
|
||||
|
||||
func TestAboveAllowableSeverity(t *testing.T) {
|
||||
thePkg := &pkg.Package{
|
||||
Name: "the-package",
|
||||
Version: "v0.1",
|
||||
FoundBy: "nothing",
|
||||
Type: pkg.RpmPkg,
|
||||
}
|
||||
|
||||
matches := result.NewResult()
|
||||
matches.Add(thePkg, match.Match{
|
||||
Type: match.ExactDirectMatch,
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-2014-fake-1",
|
||||
RecordSource: "source-1",
|
||||
},
|
||||
Package: thePkg,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
failOnSeverity string
|
||||
matches result.Result
|
||||
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 := vulnerability.NewMetadataStoreProvider(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
6
grype/grypeerr/errors.go
Normal file
6
grype/grypeerr/errors.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package grypeerr
|
||||
|
||||
var (
|
||||
// ErrAboveSeverityThreshold indicates when a vulnerability severity is discovered that is above the given --fail-on severity value
|
||||
ErrAboveSeverityThreshold = NewExpectedErr("discovered vulnerabilities at or above the severity threshold")
|
||||
)
|
22
grype/grypeerr/expected_error.go
Normal file
22
grype/grypeerr/expected_error.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package grypeerr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ExpectedErr represents a class of expected errors that grype may produce.
|
||||
type ExpectedErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// New generates a new ExpectedErr.
|
||||
func NewExpectedErr(msgFormat string, args ...interface{}) ExpectedErr {
|
||||
return ExpectedErr{
|
||||
Err: fmt.Errorf(msgFormat, args...),
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns a string representing the underlying error condition.
|
||||
func (e ExpectedErr) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
56
grype/vulnerability/severity.go
Normal file
56
grype/vulnerability/severity.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package vulnerability
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
UnknownSeverity Severity = iota
|
||||
NegligibleSeverity
|
||||
LowSeverity
|
||||
MediumSeverity
|
||||
HighSeverity
|
||||
CriticalSeverity
|
||||
)
|
||||
|
||||
var matcherTypeStr = []string{
|
||||
"UnknownSeverity",
|
||||
"negligible",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"critical",
|
||||
}
|
||||
|
||||
var AllSeverities = []Severity{
|
||||
NegligibleSeverity,
|
||||
LowSeverity,
|
||||
MediumSeverity,
|
||||
HighSeverity,
|
||||
CriticalSeverity,
|
||||
}
|
||||
|
||||
type Severity int
|
||||
|
||||
func (f Severity) String() string {
|
||||
if int(f) >= len(matcherTypeStr) || f < 0 {
|
||||
return matcherTypeStr[0]
|
||||
}
|
||||
|
||||
return matcherTypeStr[f]
|
||||
}
|
||||
|
||||
func ParseSeverity(severity string) Severity {
|
||||
switch strings.ToLower(severity) {
|
||||
case NegligibleSeverity.String():
|
||||
return NegligibleSeverity
|
||||
case LowSeverity.String():
|
||||
return LowSeverity
|
||||
case MediumSeverity.String():
|
||||
return MediumSeverity
|
||||
case HighSeverity.String():
|
||||
return HighSeverity
|
||||
case CriticalSeverity.String():
|
||||
return CriticalSeverity
|
||||
default:
|
||||
return UnknownSeverity
|
||||
}
|
||||
}
|
|
@ -5,15 +5,14 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/anchore/grype/grype/db"
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/syft/syft/scope"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
@ -34,6 +33,8 @@ type Application struct {
|
|||
Db Database `mapstructure:"db"`
|
||||
Dev Development `mapstructure:"dev"`
|
||||
CheckForAppUpdate bool `mapstructure:"check-for-app-update"`
|
||||
FailOn string `mapstructure:"fail-on-severity"`
|
||||
FailOnSeverity *vulnerability.Severity
|
||||
}
|
||||
|
||||
type Logging struct {
|
||||
|
@ -143,6 +144,15 @@ func (cfg *Application) Build() error {
|
|||
}
|
||||
}
|
||||
|
||||
// set the fail-on option
|
||||
if cfg.FailOn != "" {
|
||||
failOnSeverity := vulnerability.ParseSeverity(cfg.FailOn)
|
||||
if failOnSeverity == vulnerability.UnknownSeverity {
|
||||
return fmt.Errorf("bad --fail-on severity value '%s'", cfg.FailOn)
|
||||
}
|
||||
cfg.FailOnSeverity = &failOnSeverity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,13 @@ package etui
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
grypeEvent "github.com/anchore/grype/grype/event"
|
||||
"github.com/anchore/grype/grype/grypeerr"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/logger"
|
||||
"github.com/anchore/grype/internal/ui/common"
|
||||
|
@ -73,16 +75,25 @@ func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscr
|
|||
ctx := context.Background()
|
||||
grypeUIHandler := grypeUI.NewHandler()
|
||||
|
||||
eventLoop:
|
||||
var errResult error
|
||||
for {
|
||||
select {
|
||||
case err := <-workerErrs:
|
||||
case err, ok := <-workerErrs:
|
||||
if err != nil {
|
||||
if errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
|
||||
errResult = err
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
// worker completed
|
||||
workerErrs = nil
|
||||
}
|
||||
case e, ok := <-events:
|
||||
if !ok {
|
||||
break eventLoop
|
||||
// event bus closed
|
||||
events = nil
|
||||
}
|
||||
switch {
|
||||
case grypeUIHandler.RespondsTo(e):
|
||||
|
@ -112,15 +123,15 @@ eventLoop:
|
|||
}
|
||||
|
||||
// this is the last expected event
|
||||
break eventLoop
|
||||
events = nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
log.Errorf("cancelled (%+v)", err)
|
||||
}
|
||||
break eventLoop
|
||||
return grypeerr.NewExpectedErr("canceled: %w", ctx.Err())
|
||||
}
|
||||
if events == nil && workerErrs == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return errResult
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
grypeEvent "github.com/anchore/grype/grype/event"
|
||||
"github.com/anchore/grype/grype/grypeerr"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/ui/common"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
@ -9,17 +12,25 @@ import (
|
|||
|
||||
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||
events := subscription.Events()
|
||||
eventLoop:
|
||||
var errResult error
|
||||
for {
|
||||
select {
|
||||
case err := <-workerErrs:
|
||||
case err, ok := <-workerErrs:
|
||||
if err != nil {
|
||||
if errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {
|
||||
errResult = err
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
// worker completed
|
||||
workerErrs = nil
|
||||
}
|
||||
case e, ok := <-events:
|
||||
if !ok {
|
||||
// event bus closed...
|
||||
break eventLoop
|
||||
// event bus closed
|
||||
events = nil
|
||||
}
|
||||
|
||||
// ignore all events except for the final event
|
||||
|
@ -30,10 +41,12 @@ eventLoop:
|
|||
}
|
||||
|
||||
// this is the last expected event
|
||||
break eventLoop
|
||||
events = nil
|
||||
}
|
||||
}
|
||||
if events == nil && workerErrs == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return errResult
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue