mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
Implemented new CLI flag: --show-suppressed (#966)
This commit is contained in:
parent
142ebb9a60
commit
0c4a372910
7 changed files with 159 additions and 76 deletions
12
cmd/root.go
12
cmd/root.go
|
@ -122,6 +122,7 @@ func setGlobalCliOptions() {
|
|||
rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func setRootFlags(flags *pflag.FlagSet) {
|
||||
flags.StringP(
|
||||
"scope", "s", source.SquashedScope.String(),
|
||||
|
@ -166,6 +167,11 @@ func setRootFlags(flags *pflag.FlagSet) {
|
|||
"ignore matches for vulnerabilities that are fixed",
|
||||
)
|
||||
|
||||
flags.BoolP(
|
||||
"show-suppressed", "", false,
|
||||
"show suppressed/ignored vulnerabilities in the output (only supported with table output format)",
|
||||
)
|
||||
|
||||
flags.StringArrayP(
|
||||
"exclude", "", nil,
|
||||
"exclude paths from being scanned using a glob expression",
|
||||
|
@ -223,6 +229,10 @@ func bindRootConfigOptions(flags *pflag.FlagSet) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("show-suppressed", flags.Lookup("show-suppressed")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -282,7 +292,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
go func() {
|
||||
defer close(errs)
|
||||
|
||||
presenterConfig, err := presenter.ValidatedConfig(appConfig.Output, appConfig.OutputTemplateFile)
|
||||
presenterConfig, err := presenter.ValidatedConfig(appConfig.Output, appConfig.OutputTemplateFile, appConfig.ShowSuppressed)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
|
|
@ -13,11 +13,12 @@ import (
|
|||
type Config struct {
|
||||
format format
|
||||
templateFilePath string
|
||||
showSuppressed bool
|
||||
}
|
||||
|
||||
// ValidatedConfig returns a new, validated presenter.Config. If a valid Config cannot be created using the given input,
|
||||
// an error is returned.
|
||||
func ValidatedConfig(output, outputTemplateFile string) (Config, error) {
|
||||
func ValidatedConfig(output, outputTemplateFile string, showSuppressed bool) (Config, error) {
|
||||
format := parse(output)
|
||||
|
||||
if format == unknownFormat {
|
||||
|
@ -58,6 +59,7 @@ func ValidatedConfig(output, outputTemplateFile string) (Config, error) {
|
|||
}
|
||||
|
||||
return Config{
|
||||
format: format,
|
||||
format: format,
|
||||
showSuppressed: showSuppressed,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ func TestValidatedConfig(t *testing.T) {
|
|||
cases := []struct {
|
||||
name string
|
||||
outputValue string
|
||||
includeSuppressed bool
|
||||
outputTemplateFileValue string
|
||||
expectedConfig Config
|
||||
assertErrExpectation func(assert.TestingT, error, ...interface{}) bool
|
||||
|
@ -17,6 +18,7 @@ func TestValidatedConfig(t *testing.T) {
|
|||
{
|
||||
"valid template config",
|
||||
"template",
|
||||
false,
|
||||
"./template/test-fixtures/test.valid.template",
|
||||
Config{
|
||||
format: "template",
|
||||
|
@ -27,6 +29,7 @@ func TestValidatedConfig(t *testing.T) {
|
|||
{
|
||||
"template file with non-template format",
|
||||
"json",
|
||||
false,
|
||||
"./some/path/to/a/custom.template",
|
||||
Config{},
|
||||
assert.Error,
|
||||
|
@ -34,6 +37,7 @@ func TestValidatedConfig(t *testing.T) {
|
|||
{
|
||||
"unknown format",
|
||||
"some-made-up-format",
|
||||
false,
|
||||
"",
|
||||
Config{},
|
||||
assert.Error,
|
||||
|
@ -42,9 +46,11 @@ func TestValidatedConfig(t *testing.T) {
|
|||
{
|
||||
"table format",
|
||||
"table",
|
||||
true,
|
||||
"",
|
||||
Config{
|
||||
format: tableFormat,
|
||||
format: tableFormat,
|
||||
showSuppressed: true,
|
||||
},
|
||||
assert.NoError,
|
||||
},
|
||||
|
@ -52,7 +58,7 @@ func TestValidatedConfig(t *testing.T) {
|
|||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualConfig, actualErr := ValidatedConfig(tc.outputValue, tc.outputTemplateFileValue)
|
||||
actualConfig, actualErr := ValidatedConfig(tc.outputValue, tc.outputTemplateFileValue, tc.includeSuppressed)
|
||||
|
||||
assert.Equal(t, tc.expectedConfig, actualConfig)
|
||||
tc.assertErrExpectation(t, actualErr)
|
||||
|
|
|
@ -27,7 +27,10 @@ func GetPresenter(presenterConfig Config, matches match.Matches, ignoredMatches
|
|||
case jsonFormat:
|
||||
return json.NewPresenter(matches, ignoredMatches, packages, context, metadataProvider, appConfig, dbStatus)
|
||||
case tableFormat:
|
||||
return table.NewPresenter(matches, packages, metadataProvider)
|
||||
if presenterConfig.showSuppressed {
|
||||
return table.NewPresenter(matches, packages, metadataProvider, ignoredMatches)
|
||||
}
|
||||
return table.NewPresenter(matches, packages, metadataProvider, nil)
|
||||
case cycloneDXFormat:
|
||||
return cyclonedx.NewPresenter(matches, packages, context.Source, metadataProvider)
|
||||
case embeddedVEXJSON:
|
||||
|
|
|
@ -14,17 +14,23 @@ import (
|
|||
"github.com/anchore/grype/grype/vulnerability"
|
||||
)
|
||||
|
||||
const (
|
||||
appendSuppressed = " (suppressed)"
|
||||
)
|
||||
|
||||
// Presenter is a generic struct for holding fields needed for reporting
|
||||
type Presenter struct {
|
||||
results match.Matches
|
||||
ignoredMatches []match.IgnoredMatch
|
||||
packages []pkg.Package
|
||||
metadataProvider vulnerability.MetadataProvider
|
||||
}
|
||||
|
||||
// NewPresenter is a *Presenter constructor
|
||||
func NewPresenter(results match.Matches, packages []pkg.Package, metadataProvider vulnerability.MetadataProvider) *Presenter {
|
||||
func NewPresenter(results match.Matches, packages []pkg.Package, metadataProvider vulnerability.MetadataProvider, ignoredMatches []match.IgnoredMatch) *Presenter {
|
||||
return &Presenter{
|
||||
results: results,
|
||||
ignoredMatches: ignoredMatches,
|
||||
packages: packages,
|
||||
metadataProvider: metadataProvider,
|
||||
}
|
||||
|
@ -35,27 +41,24 @@ func (pres *Presenter) Present(output io.Writer) error {
|
|||
rows := make([][]string, 0)
|
||||
|
||||
columns := []string{"Name", "Installed", "Fixed-In", "Type", "Vulnerability", "Severity"}
|
||||
// Generate rows for matching vulnerabilities
|
||||
for m := range pres.results.Enumerate() {
|
||||
var severity string
|
||||
row, err := createRow(m, pres.metadataProvider, "")
|
||||
|
||||
metadata, err := pres.metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
|
||||
return err
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
if metadata != nil {
|
||||
severity = metadata.Severity
|
||||
// Generate rows for suppressed vulnerabilities
|
||||
for _, m := range pres.ignoredMatches {
|
||||
row, err := createRow(m.Match, pres.metadataProvider, appendSuppressed)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fixVersion := strings.Join(m.Vulnerability.Fix.Versions, ", ")
|
||||
switch m.Vulnerability.Fix.State {
|
||||
case grypeDb.WontFixState:
|
||||
fixVersion = "(won't fix)"
|
||||
case grypeDb.UnknownFixState:
|
||||
fixVersion = ""
|
||||
}
|
||||
|
||||
rows = append(rows, []string{m.Package.Name, m.Package.Version, fixVersion, string(m.Package.Type), m.Vulnerability.ID, severity})
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
|
@ -113,3 +116,26 @@ func removeDuplicateRows(items [][]string) [][]string {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func createRow(m match.Match, metadataProvider vulnerability.MetadataProvider, severitySuffix string) ([]string, error) {
|
||||
var severity string
|
||||
|
||||
metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
|
||||
}
|
||||
|
||||
if metadata != nil {
|
||||
severity = metadata.Severity + severitySuffix
|
||||
}
|
||||
|
||||
fixVersion := strings.Join(m.Vulnerability.Fix.Versions, ", ")
|
||||
switch m.Vulnerability.Fix.State {
|
||||
case grypeDb.WontFixState:
|
||||
fixVersion = "(won't fix)"
|
||||
case grypeDb.UnknownFixState:
|
||||
fixVersion = ""
|
||||
}
|
||||
|
||||
return []string{m.Package.Name, m.Package.Version, fixVersion, string(m.Package.Type), m.Vulnerability.ID, severity}, nil
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
|
@ -18,69 +19,103 @@ import (
|
|||
|
||||
var update = flag.Bool("update", false, "update the *.golden files for table presenters")
|
||||
|
||||
var pkg1 = pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: syftPkg.DebPkg,
|
||||
}
|
||||
|
||||
var pkg2 = pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: syftPkg.DebPkg,
|
||||
}
|
||||
|
||||
var match1 = match.Match{
|
||||
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-1999-0001",
|
||||
Namespace: "source-1",
|
||||
},
|
||||
Package: pkg1,
|
||||
Details: []match.Detail{
|
||||
{
|
||||
Type: match.ExactDirectMatch,
|
||||
Matcher: match.DpkgMatcher,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var match2 = match.Match{
|
||||
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-1999-0002",
|
||||
Namespace: "source-2",
|
||||
Fix: vulnerability.Fix{
|
||||
Versions: []string{
|
||||
"the-next-version",
|
||||
},
|
||||
},
|
||||
},
|
||||
Package: pkg2,
|
||||
Details: []match.Detail{
|
||||
{
|
||||
Type: match.ExactIndirectMatch,
|
||||
Matcher: match.DpkgMatcher,
|
||||
SearchedBy: map[string]interface{}{
|
||||
"some": "key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestCreateRow(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
match match.Match
|
||||
severitySuffix string
|
||||
expectedErr error
|
||||
expectedRow []string
|
||||
}{
|
||||
{
|
||||
name: "create row for vulnerability",
|
||||
match: match1,
|
||||
severitySuffix: "",
|
||||
expectedErr: nil,
|
||||
expectedRow: []string{match1.Package.Name, match1.Package.Version, "", string(match1.Package.Type), match1.Vulnerability.ID, "Low"},
|
||||
},
|
||||
{
|
||||
name: "create row for suppressed vulnerability",
|
||||
match: match1,
|
||||
severitySuffix: appendSuppressed,
|
||||
expectedErr: nil,
|
||||
expectedRow: []string{match1.Package.Name, match1.Package.Version, "", string(match1.Package.Type), match1.Vulnerability.ID, "Low (suppressed)"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
row, err := createRow(testCase.match, models.NewMetadataMock(), testCase.severitySuffix)
|
||||
|
||||
assert.Equal(t, testCase.expectedErr, err)
|
||||
assert.Equal(t, testCase.expectedRow, row)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTablePresenter(t *testing.T) {
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
var pkg1 = pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: syftPkg.DebPkg,
|
||||
}
|
||||
|
||||
var pkg2 = pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: syftPkg.DebPkg,
|
||||
}
|
||||
|
||||
var match1 = match.Match{
|
||||
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-1999-0001",
|
||||
Namespace: "source-1",
|
||||
},
|
||||
Package: pkg1,
|
||||
Details: []match.Detail{
|
||||
{
|
||||
Type: match.ExactDirectMatch,
|
||||
Matcher: match.DpkgMatcher,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var match2 = match.Match{
|
||||
|
||||
Vulnerability: vulnerability.Vulnerability{
|
||||
ID: "CVE-1999-0002",
|
||||
Namespace: "source-2",
|
||||
Fix: vulnerability.Fix{
|
||||
Versions: []string{
|
||||
"the-next-version",
|
||||
},
|
||||
},
|
||||
},
|
||||
Package: pkg2,
|
||||
Details: []match.Detail{
|
||||
{
|
||||
Type: match.ExactIndirectMatch,
|
||||
Matcher: match.DpkgMatcher,
|
||||
SearchedBy: map[string]interface{}{
|
||||
"some": "key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
matches := match.NewMatches()
|
||||
|
||||
matches.Add(match1, match2)
|
||||
|
||||
packages := []pkg.Package{pkg1, pkg2}
|
||||
|
||||
pres := NewPresenter(matches, packages, models.NewMetadataMock())
|
||||
pres := NewPresenter(matches, packages, models.NewMetadataMock(), []match.IgnoredMatch{})
|
||||
|
||||
// TODO: add a constructor for a match.Match when the data is better shaped
|
||||
|
||||
|
@ -113,7 +148,7 @@ func TestEmptyTablePresenter(t *testing.T) {
|
|||
|
||||
matches := match.NewMatches()
|
||||
|
||||
pres := NewPresenter(matches, []pkg.Package{}, models.NewMetadataMock())
|
||||
pres := NewPresenter(matches, []pkg.Package{}, models.NewMetadataMock(), []match.IgnoredMatch{})
|
||||
|
||||
// run presenter
|
||||
err := pres.Present(&buffer)
|
||||
|
|
|
@ -53,6 +53,7 @@ type Application struct {
|
|||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"`
|
||||
Attestation Attestation `yaml:"attestation" json:"attestation" mapstructure:"attestation"`
|
||||
ShowSuppressed bool `yaml:"show-suppressed" json:"show-suppressed" mapstructure:"show-suppressed"`
|
||||
}
|
||||
|
||||
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
|
||||
|
|
Loading…
Reference in a new issue