Implemented new CLI flag: --show-suppressed (#966)

This commit is contained in:
vimalpatel19 2022-11-01 13:02:26 -05:00 committed by GitHub
parent 142ebb9a60
commit 0c4a372910
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 159 additions and 76 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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