// Command vulncheck runs govulncheck over the module and fails on each // vulnerability that the code calls, the required check "Vulnerabilities" // of docs/agentic-workflow.md, with the levels of docs/risk-analysis.md. // // With -base it fails only on those the branch brings: called at HEAD, // not called at the merge base with that ref. So a vulnerability found // anew in the standard library of main stops no unrelated branch; the // nightly build checks main in full. // // go run ./tools/vulncheck # main, in full // go run ./tools/vulncheck -base origin/main # a branch, only its own // // It is a development tool, not part of the werkdock binary. package main import ( "bufio" "bytes" "encoding/json" "errors" "flag" "fmt" "io" "os" "os/exec" "sort" "strings" ) // The Go toolchain of the module downloads it on first use; it runs on a // newer Go than the module's, but checks the standard library of the // module's own toolchain. const govulncheck = "golang.org/x/vuln/cmd/govulncheck@v1.8.0" const exceptionsFile = "tools/vulnerability-exceptions.txt" // vuln is one OSV entry as far as the verdict needs it. // Calls holds each vulnerable symbol that the code calls, as // "module@version package.function", so that a branch that calls another // symbol of the same advisory, or moves to another affected version, is // not mistaken for its merge base. type vuln struct { ID string Summary string Fixed string Calls map[string]bool } func main() { base := flag.String("base", "", "ref whose merge base with HEAD is the baseline; empty checks in full") flag.Parse() if err := run(*base, os.Stdout); err != nil { fmt.Fprintln(os.Stderr, "vulncheck:", err) os.Exit(1) } } func run(base string, out io.Writer) error { exceptions, err := readExceptions(exceptionsFile) if err != nil { return err } head, err := scan(".") if err != nil { return err } var baseline map[string]vuln if base != "" { if baseline, err = scanMergeBase(base); err != nil { return err } } failing := verdict(head, baseline, exceptions) for _, v := range failing { fmt.Fprintf(out, "%s: %s; fixed in %s; https://pkg.go.dev/vuln/%s\n", v.ID, v.Summary, v.Fixed, v.ID) } if len(failing) > 0 { return fmt.Errorf("%d called vulnerabilities; fix them, or add an exception with its reason to %s", len(failing), exceptionsFile) } fmt.Fprintln(out, "vulncheck: no called vulnerability") return nil } // verdict returns the called vulnerabilities of head without an exception, // and with a baseline only those with a call that the baseline lacks, // sorted by id. func verdict(head, baseline map[string]vuln, exceptions map[string]string) []vuln { var failing []vuln for id, v := range head { if len(v.Calls) == 0 || exceptions[id] != "" { continue } if baseline != nil && !bringsCall(v, baseline[id]) { continue } failing = append(failing, v) } sort.Slice(failing, func(i, j int) bool { return failing[i].ID < failing[j].ID }) return failing } func bringsCall(head, base vuln) bool { for call := range head.Calls { if !base.Calls[call] { return true } } return false } func scan(dir string) (map[string]vuln, error) { cmd := exec.Command("go", "run", govulncheck, "-format", "json", "./...") cmd.Dir = dir cmd.Stderr = os.Stderr stdout, err := cmd.Output() if err != nil { return nil, fmt.Errorf("govulncheck in %s: %w", dir, err) } return parse(bytes.NewReader(stdout)) } // scanMergeBase scans an export of the merge base, which writes nothing into the repository. func scanMergeBase(ref string) (map[string]vuln, error) { mergeBase, err := exec.Command("git", "merge-base", "HEAD", ref).Output() if err != nil { return nil, fmt.Errorf("merge base with %s: %w", ref, err) } dir, err := os.MkdirTemp("", "vulncheck-") if err != nil { return nil, err } defer os.RemoveAll(dir) export := exec.Command("sh", "-c", `git archive "$1" | tar -x -C "$2"`, "sh", strings.TrimSpace(string(mergeBase)), dir) if out, err := export.CombinedOutput(); err != nil { return nil, fmt.Errorf("export of the merge base: %w: %s", err, out) } return scan(dir) } // parse reads govulncheck's JSON stream: one "osv" object per entry, and // "finding" objects whose first trace frame names a function when the // code calls the vulnerable symbol. func parse(r io.Reader) (map[string]vuln, error) { type frame struct { Module string `json:"module"` Version string `json:"version"` Package string `json:"package"` Function string `json:"function"` } var msg struct { OSV *struct { ID string `json:"id"` Summary string `json:"summary"` } `json:"osv"` Finding *struct { OSV string `json:"osv"` FixedVersion string `json:"fixed_version"` Trace []frame `json:"trace"` } `json:"finding"` } vulns := map[string]vuln{} dec := json.NewDecoder(r) for { msg.OSV, msg.Finding = nil, nil if err := dec.Decode(&msg); errors.Is(err, io.EOF) { return vulns, nil } else if err != nil { return nil, fmt.Errorf("govulncheck output: %w", err) } if o := msg.OSV; o != nil { v := vulns[o.ID] v.ID, v.Summary = o.ID, o.Summary vulns[o.ID] = v } if f := msg.Finding; f != nil { v := vulns[f.OSV] v.ID = f.OSV if f.FixedVersion != "" { v.Fixed = f.FixedVersion } if len(f.Trace) > 0 && f.Trace[0].Function != "" { t := f.Trace[0] if v.Calls == nil { v.Calls = map[string]bool{} } v.Calls[t.Module+"@"+t.Version+" "+t.Package+"."+t.Function] = true } vulns[f.OSV] = v } } } // readExceptions reads "ID reason" lines; '#' starts a comment. // An exception without a reason is an error, since the reason is what the // human approves. func readExceptions(path string) (map[string]string, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() return parseExceptions(f) } func parseExceptions(r io.Reader) (map[string]string, error) { exceptions := map[string]string{} sc := bufio.NewScanner(r) for n := 1; sc.Scan(); n++ { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } id, reason, _ := strings.Cut(line, " ") if strings.TrimSpace(reason) == "" { return nil, fmt.Errorf("%s:%d: exception %s without a reason", exceptionsFile, n, id) } exceptions[id] = strings.TrimSpace(reason) } return exceptions, sc.Err() }