diff --git a/cmd/govulncheck/testdata/usage.ct b/cmd/govulncheck/testdata/usage.ct index 72afbcc8..7306f30f 100644 --- a/cmd/govulncheck/testdata/usage.ct +++ b/cmd/govulncheck/testdata/usage.ct @@ -5,6 +5,8 @@ usage: -dir string directory to use for loading source files + -ignore-file string + ignore vulnerability IDs listed in this file, one ID per line -json output JSON -tags list @@ -24,6 +26,8 @@ usage: -dir string directory to use for loading source files + -ignore-file string + ignore vulnerability IDs listed in this file, one ID per line -json output JSON -tags list diff --git a/internal/govulncheck/handler.go b/internal/govulncheck/handler.go index 7187fb46..fb47e87b 100644 --- a/internal/govulncheck/handler.go +++ b/internal/govulncheck/handler.go @@ -13,6 +13,9 @@ type Handler interface { // Vulnerability adds a vulnerability to be printed to the output. Vulnerability(vuln *Vuln) error + // Ignored adds a vulnerability to be printed as ignored in the output + Ignored(vuln *Vuln) error + // Preamble communicates introductory message to the user. Preamble(preamble *Preamble) error diff --git a/internal/govulncheck/json.go b/internal/govulncheck/json.go index d770d33b..4f29572e 100644 --- a/internal/govulncheck/json.go +++ b/internal/govulncheck/json.go @@ -60,6 +60,11 @@ func (o *jsonHandler) Vulnerability(vuln *Vuln) error { return o.enc.Encode(Message{Vulnerability: vuln}) } +func (o *jsonHandler) Ignored(vuln *Vuln) error { + // @TODO using a pointer here so that 'ignored' field gets skipped in the output. Is that good? + return o.enc.Encode(Message{Vulnerability: vuln, Ignored: new(bool)}) +} + // Preamble does not do anything in JSON mode. func (o *jsonHandler) Preamble(preamble *Preamble) error { return o.enc.Encode(Message{Preamble: preamble}) diff --git a/internal/govulncheck/result.go b/internal/govulncheck/result.go index 349f3f2c..0bb1ca6e 100644 --- a/internal/govulncheck/result.go +++ b/internal/govulncheck/result.go @@ -35,6 +35,7 @@ type Message struct { Preamble *Preamble `json:"preamble,omitempty"` Progress string `json:"progress,omitempty"` Vulnerability *Vuln `json:"vulnerability,omitempty"` + Ignored *bool `json:"ignored,omitempty"` } type Preamble struct { diff --git a/internal/scan/flags.go b/internal/scan/flags.go index 6b3e1efb..8e1369f5 100644 --- a/internal/scan/flags.go +++ b/internal/scan/flags.go @@ -18,6 +18,7 @@ type config struct { patterns []string sourceAnalysis bool db string + ignoreFile string json bool dir string verbose bool @@ -35,6 +36,7 @@ func (c *Cmd) parseFlags() (*config, error) { var tagsFlag buildutil.TagsFlag flags := flag.NewFlagSet("", flag.ContinueOnError) flags.BoolVar(&cfg.json, "json", false, "output JSON") + flags.StringVar(&cfg.ignoreFile, "ignore-file", "", "ignore vulnerability IDs listed in this file, one ID per line") flags.BoolVar(&cfg.verbose, "v", false, "print a full call stack for each vulnerability") flags.BoolVar(&cfg.test, "test", false, "analyze test files. Only valid for source code.") flags.Var(&tagsFlag, "tags", "comma-separated `list` of build tags") diff --git a/internal/scan/run.go b/internal/scan/run.go index af91c584..7d91bb4c 100644 --- a/internal/scan/run.go +++ b/internal/scan/run.go @@ -5,8 +5,10 @@ package scan import ( + "bufio" "context" "io" + "os" "path" "path/filepath" "runtime/debug" @@ -60,23 +62,68 @@ func doGovulncheck(cfg *config, w io.Writer) error { return err } + ignored, err := readIgnoreFile(cfg) + if err != nil { + return err + } + // For each vulnerability, queue it to be written to the output. for _, v := range res.Vulns { - if err := output.Vulnerability(v); err != nil { - return err + if ignored[v.OSV.ID] { + if err := output.Ignored(v); err != nil { + return err + } + } else { + if err := output.Vulnerability(v); err != nil { + return err + } } } if err := output.Flush(); err != nil { return err } - if containsAffectedVulnerabilities(res) { + if containsAffectedVulnerabilities(filterIgnoredVulnerabilities(res.Vulns, ignored)) { return ErrVulnerabilitiesFound } return nil } -func containsAffectedVulnerabilities(r *govulncheck.Result) bool { - for _, v := range r.Vulns { +func readIgnoreFile(cfg *config) (map[string]bool, error) { + if cfg.ignoreFile == "" { + return nil, nil + } + ignored := make(map[string]bool) + file, err := os.Open(cfg.ignoreFile) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + // @TODO Limitation: need to resize scanner's capacity for lines over 64K + for scanner.Scan() { + id := scanner.Text() + ignored[id] = true + } + if err := scanner.Err(); err != nil { + return nil, err + } + return ignored, nil + +} + +func filterIgnoredVulnerabilities(vulns []*govulncheck.Vuln, ignored map[string]bool) (ret []*govulncheck.Vuln) { + for _, v := range vulns { + if ignored[v.OSV.ID] { + continue + } + ret = append(ret, v) + } + return +} + +func containsAffectedVulnerabilities(vulns []*govulncheck.Vuln) bool { + for _, v := range vulns { if IsCalled(v) { return true } diff --git a/internal/scan/text.go b/internal/scan/text.go index 09c427b6..13287aca 100644 --- a/internal/scan/text.go +++ b/internal/scan/text.go @@ -70,6 +70,14 @@ func (o *textHandler) Vulnerability(vuln *govulncheck.Vuln) error { return nil } +func (o *textHandler) Ignored(vuln *govulncheck.Vuln) error { + // @TODO Improve formatting? Probably the user should still see more details than just the ID of the vuln? + fmt.Fprintln(o.w) + fmt.Fprintf(o.w, "Ignoring vulnerability %s", vuln.OSV.ID) + fmt.Fprintln(o.w) + return nil +} + // Preamble writes text output formatted according to govulncheck-intro.tmpl. func (o *textHandler) Preamble(preamble *govulncheck.Preamble) error { p := *preamble