cmd/govulncheck: add unaffected information
Modules that contain vulnerabilities that are not called are now printed
at the end of a default output. This information makes it clear to the
user that we didn't miss a vulnerability, and rather are actively
informing them that these vulnerabilities are irrelevant to them.
Change-Id: I162a976eba6554ffbefca77178e27e3332db871b
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/409814
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Julie Qiu <julieqiu@google.com>
Run-TryBot: Julie Qiu <julieqiu@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 74c3fc6..4279018 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -90,9 +90,10 @@
`)
}
var (
- r *vulncheck.Result
- pkgs []*vulncheck.Package
- ctx = context.Background()
+ r *vulncheck.Result
+ pkgs []*vulncheck.Package
+ unaffectedMods map[string][]string
+ ctx = context.Background()
)
if len(patterns) == 1 && isFile(patterns[0]) {
f, err := os.Open(patterns[0])
@@ -117,6 +118,7 @@
if err != nil {
die("govulncheck: %v", err)
}
+ unaffectedMods = filterUnaffected(r.Vulns)
r.Vulns = filterCalled(r)
}
@@ -130,7 +132,7 @@
die("writing HTML: %v", err)
}
} else {
- writeText(r, ci)
+ writeText(r, ci, unaffectedMods)
}
}
exitCode := 0
@@ -152,6 +154,38 @@
return vulns
}
+// filterUnaffected returns vulnerabilities where no symbols are called,
+// grouped by module.
+func filterUnaffected(vulns []*vulncheck.Vuln) map[string][]string {
+ // It is possible that the same vuln.OSV.ID has vuln.CallSink != 0
+ // for one symbol, but vuln.CallSink == 0 for a different one, so
+ // we need to filter out ones that have been called.
+ called := map[string]bool{}
+ for _, vuln := range vulns {
+ if vuln.CallSink != 0 {
+ called[vuln.OSV.ID] = true
+ }
+ }
+
+ modToIDs := map[string]map[string]bool{}
+ for _, vuln := range vulns {
+ if !called[vuln.OSV.ID] {
+ if _, ok := modToIDs[vuln.ModPath]; !ok {
+ modToIDs[vuln.ModPath] = map[string]bool{}
+ }
+ modToIDs[vuln.ModPath][vuln.OSV.ID] = true
+ }
+ }
+ output := map[string][]string{}
+ for m, idSet := range modToIDs {
+ for id := range idSet {
+ output[m] = append(output[m], id)
+ }
+ sort.Strings(output[m])
+ }
+ return output
+}
+
func writeJSON(r *vulncheck.Result) {
b, err := json.MarshalIndent(r, "", "\t")
if err != nil {
@@ -167,7 +201,9 @@
fmt.Printf("%-*s%s\n", labelWidth, label, text)
}
-func writeText(r *vulncheck.Result, ci *govulncheck.CallInfo) {
+const lineLength = 55
+
+func writeText(r *vulncheck.Result, ci *govulncheck.CallInfo, unaffectedMods map[string][]string) {
uniqueVulns := map[string]bool{}
for _, v := range r.Vulns {
uniqueVulns[v.OSV.ID] = true
@@ -181,7 +217,7 @@
default:
fmt.Printf("Found %d known vulnerabilities.\n", len(uniqueVulns))
}
- fmt.Println(strings.Repeat("-", 55))
+ fmt.Println(strings.Repeat("-", lineLength))
fmt.Println()
for _, vg := range ci.VulnGroups {
@@ -207,6 +243,18 @@
}
fmt.Println()
}
+ if len(unaffectedMods) > 0 {
+ fmt.Println()
+ fmt.Println(strings.Repeat("-", lineLength))
+ fmt.Println()
+ fmt.Println(wrap("These vulnerabilities exist in required modules, but no vulnerable symbols are used. No action is required. For more information, visit https://pkg.go.dev/vuln.", 80-labelWidth))
+ fmt.Println()
+ for m, ids := range unaffectedMods {
+ fmt.Printf("%s (%s)", m, strings.Join(ids, ", "))
+ }
+ fmt.Println()
+ }
+ fmt.Println()
}
func writeCallStacksDefault(vg []*vulncheck.Vuln, ci *govulncheck.CallInfo) {