cmd/govulncheck: HTML output
If the -html flag is provided, print HTML to standard out.
The HTML is similar to the default text output, except
full call stacks are available by opening detail elements.
Change-Id: I7ea9bfeb6f9cd43b66e8a659c2b61a6b2e5a95a3
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/395554
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/govulncheck/html.go b/cmd/govulncheck/html.go
new file mode 100644
index 0000000..70a4105
--- /dev/null
+++ b/cmd/govulncheck/html.go
@@ -0,0 +1,112 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.18
+// +build go1.18
+
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "io"
+
+ "golang.org/x/vuln/vulncheck"
+)
+
+func html(w io.Writer, r *vulncheck.Result, callStacks map[*vulncheck.Vuln][]vulncheck.CallStack, moduleVersions map[string]string, topPackages map[string]bool, vulnGroups [][]*vulncheck.Vuln) error {
+ tmpl, err := template.New("").Funcs(template.FuncMap{
+ "funcName": funcName,
+ }).Parse(templateSource)
+ if err != nil {
+ return err
+ }
+
+ type vuln struct {
+ PkgPath string
+ CurrentVersion string
+ FixedVersion string
+ Reference string
+ Details string
+ }
+
+ type callstack struct {
+ Summary string
+ Stack vulncheck.CallStack
+ }
+
+ type callstacks struct {
+ ID string // osv.Entry ID
+ Stacks []callstack
+ }
+
+ data := struct {
+ Vulns []vuln
+ CallStacks []callstacks
+ }{}
+
+ for _, vg := range vulnGroups {
+ v0 := vg[0]
+ data.Vulns = append(data.Vulns, vuln{
+ PkgPath: v0.PkgPath,
+ CurrentVersion: moduleVersions[v0.ModPath],
+ FixedVersion: "v" + latestFixed(v0.OSV.Affected),
+ Reference: fmt.Sprintf("https://pkg.go.dev/vuln/%s", v0.OSV.ID),
+ Details: v0.OSV.Details,
+ })
+ // Keep first call stack for each vuln.
+ stacks := callstacks{ID: v0.OSV.ID}
+ for _, v := range vg {
+ if css := callStacks[v]; len(css) > 0 {
+ stacks.Stacks = append(stacks.Stacks, callstack{
+ Summary: summarizeCallStack(css[0], topPackages, v.PkgPath),
+ Stack: css[0],
+ })
+ }
+ }
+ data.CallStacks = append(data.CallStacks, stacks)
+ }
+ return tmpl.Execute(w, data)
+}
+
+var templateSource = `
+<!DOCTYPE html>
+<html lang="en">
+<meta charset="utf-8">
+<title>govulncheck Results</title>
+
+<body>
+ {{with .Vulns}}
+ <h2>Vulnerabilities</h2>
+ <table>
+ <tr><th>Package</th><th>Your Version</th><th>Fixed Version</th><th>Reference</th><th>Details</th><tr>
+ {{range .Vulns}}
+ <tr>
+ <td>{[.PkgPath}}</td>
+ <td>{{.CurrentVersion}}</td>
+ <td>{{.FixedVersion}}</td>
+ <td>{{.Reference}}</td>
+ <td>{{.Details}}</td>
+ </tr>
+ {{end}}
+ </table>
+
+ <h2>Call Stacks</h2>
+ {{range .CallStacks}}
+ <h3>.ID</h3>
+ {{range .Stacks}}
+ <details>
+ <summary>{{.Summary}}</summary>
+ {{range .Stack}}
+ <p>{{.Function | funcName}}</p>
+ {{end}}
+ </details>
+ {{end}}
+ {{end}}
+ {{else}}
+ No vulnerabilities found.
+ {{end}}
+</body>
+</html>
+`
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 63bc149..05327d8 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -40,6 +40,7 @@
jsonFlag = flag.Bool("json", false, "")
verboseFlag = flag.Bool("v", false, "")
testsFlag = flag.Bool("tests", false, "")
+ htmlFlag = flag.Bool("html", false, "")
)
const usage = `govulncheck: identify known vulnerabilities by call graph traversal.
@@ -52,11 +53,13 @@
Flags:
- -json Print vulnerability findings in JSON format.
+ -json Print vulnerability findings in JSON format.
- -tags Comma-separated list of build tags.
+ -html Generate HTML with the vulnerability findings.
- -tests Boolean flag indicating if test files should be analyzed too.
+ -tags Comma-separated list of build tags.
+
+ -tests Boolean flag indicating if test files should be analyzed too.
govulncheck can be used with either one or more package patterns (i.e. golang.org/x/crypto/...
or ./...) or with a single path to a Go binary. In the latter case module and symbol
@@ -135,7 +138,20 @@
if *jsonFlag {
writeJSON(r)
} else {
- writeText(r, pkgs, moduleVersions)
+ callStacks := vulncheck.CallStacks(r)
+ // Create set of top-level packages, used to find representative symbols
+ topPackages := map[string]bool{}
+ for _, p := range pkgs {
+ topPackages[p.PkgPath] = true
+ }
+ vulnGroups := groupByIDAndPackage(r.Vulns)
+ if *htmlFlag {
+ if err := html(os.Stdout, r, callStacks, moduleVersions, topPackages, vulnGroups); err != nil {
+ die("writing HTML: %v", err)
+ }
+ } else {
+ writeText(r, callStacks, moduleVersions, topPackages, vulnGroups)
+ }
}
exitCode := 0
// Following go vet, fail with 3 if there are findings (in this case, vulns).
@@ -154,24 +170,11 @@
fmt.Println()
}
-func writeText(r *vulncheck.Result, pkgs []*packages.Package, moduleVersions map[string]string) {
- if len(r.Vulns) == 0 {
- return
- }
- callStacks := vulncheck.CallStacks(r)
-
+func writeText(r *vulncheck.Result, callStacks map[*vulncheck.Vuln][]vulncheck.CallStack, moduleVersions map[string]string, topPackages map[string]bool, vulnGroups [][]*vulncheck.Vuln) {
const labelWidth = 16
line := func(label, text string) {
fmt.Printf("%-*s%s\n", labelWidth, label, text)
}
-
- // Create set of top-level packages, used to find
- // representative symbols
- topPackages := map[string]bool{}
- for _, p := range pkgs {
- topPackages[p.PkgPath] = true
- }
- vulnGroups := groupByIDAndPackage(r.Vulns)
for _, vg := range vulnGroups {
// All the vulns in vg have the same PkgPath, ModPath and OSV.
// All have a non-zero CallSink.