cmd/govulncheck: summarized call stacks
This CL is another attempt at compact but helpful default output.
For each vuln, pick a single call stack and summarize it.
The vulncheck package sorts the call stacks in a reasonable way, so
we use the first one.
Sample output:
Change-Id: I9a2928a4ce1f3b79f9c6b09e79cd2c53490756b5
---
package: github.com/opencontainers/selinux/go-selinux
your version: v0.0.0-20170621221121-4a2974bf1ee9
fixed version: v1.0.0-rc8.0.20190930145003-cad42f6e0932
sample call stacks:
k8s.io/kubernetes/pkg/util/selinux.SELinuxEnabled calls github.com/opencontainers/selinux/go-selinux.GetEnabled
k8s.io/kubernetes/pkg/util/selinux.SetFileLabel calls github.com/opencontainers/selinux/go-selinux.SetFileLabel
k8s.io/kubernetes/pkg/util/selinux.realSELinuxRunner.Getfilecon calls github.com/opencontainers/selinux/go-selinux.FileLabel
reference: https://pkg.go.dev/vuln/GO-2021-0085
description: AppArmor restrictions may be bypassed due to improper validation
of mount targets, allowing a malicious image to mount volumes
over e.g. /proc.
---
Change-Id: I982228e84dcd71870d67a467fd789547ef24d484
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/395156
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 123de4f..63bc149 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -176,26 +176,26 @@
// All the vulns in vg have the same PkgPath, ModPath and OSV.
// All have a non-zero CallSink.
v0 := vg[0]
-
- current := moduleVersions[v0.ModPath]
- fixed := "v" + latestFixed(v0.OSV.Affected)
- ref := fmt.Sprintf("https://pkg.go.dev/vuln/%s", v0.OSV.ID)
-
- fns := representativeFuncs(vg, topPackages, callStacks)
- // Use first as representative.
- rep := funcName(fns[0])
- var syms string
- if len(fns) == 1 {
- syms = rep
- } else {
- syms = fmt.Sprintf("%s and %d others", rep, len(fns)-1)
- }
line("package:", v0.PkgPath)
- line("your version:", current)
- line("fixed version:", fixed)
- line("symbols:", syms)
- line("reference:", ref)
-
+ line("your version:", moduleVersions[v0.ModPath])
+ line("fixed version:", "v"+latestFixed(v0.OSV.Affected))
+ var summaries []string
+ for _, v := range vg {
+ if css := callStacks[v]; len(css) > 0 {
+ if sum := summarizeCallStack(css[0], topPackages, v.PkgPath); sum != "" {
+ summaries = append(summaries, sum)
+ }
+ }
+ }
+ if len(summaries) > 0 {
+ sort.Strings(summaries)
+ summaries = compact(summaries)
+ line("sample call stacks:", "")
+ for _, s := range summaries {
+ line("", s)
+ }
+ }
+ line("reference:", fmt.Sprintf("https://pkg.go.dev/vuln/%s", v0.OSV.ID))
desc := strings.Split(wrap(v0.OSV.Details, 80-labelWidth), "\n")
for i, l := range desc {
if i == 0 {
@@ -284,29 +284,59 @@
return v
}
-// representativeFuncs collects representative functions for the group
-// of vulns from the the call stacks.
-func representativeFuncs(vg []*vulncheck.Vuln, topPkgs map[string]bool, callStacks map[*vulncheck.Vuln][]vulncheck.CallStack) []*vulncheck.FuncNode {
- // Collect unique top of call stacks.
- fns := map[*vulncheck.FuncNode]bool{}
- for _, v := range vg {
- for _, cs := range callStacks[v] {
- // Find the lowest function in the stack that is in
- // one of the top packages.
- for i := len(cs) - 1; i >= 0; i-- {
- pkg := pkgPath(cs[i].Function)
- if topPkgs[pkg] {
- fns[cs[i].Function] = true
- break
- }
- }
+// summarizeCallStack returns a short description of the call stack.
+// It uses one of two forms, depending on what the lowest function F in topPkgs
+// calls:
+// - If it calls a function V from the vulnerable package, then summarizeCallStack
+// returns "F calls V".
+// - If it calls a function G in some other package, which eventually calls V,
+// it returns "F calls G, which eventually calls V".
+// If it can't find any of these functions, summarizeCallStack returns the empty string.
+func summarizeCallStack(cs vulncheck.CallStack, topPkgs map[string]bool, vulnPkg string) string {
+ // Find the lowest function in the top packages.
+ iTop := lowest(cs, func(e vulncheck.StackEntry) bool {
+ return topPkgs[pkgPath(e.Function)]
+ })
+ if iTop < 0 {
+ return ""
+ }
+ // Find the highest function in the vulnerable package that is below iTop.
+ iVuln := highest(cs[iTop+1:], func(e vulncheck.StackEntry) bool {
+ return pkgPath(e.Function) == vulnPkg
+ })
+ if iVuln < 0 {
+ return ""
+ }
+ iVuln += iTop + 1 // adjust for slice in call to highest.
+ topName := funcName(cs[iTop].Function)
+ vulnName := funcName(cs[iVuln].Function)
+ if iVuln == iTop+1 {
+ return fmt.Sprintf("%s calls %s", topName, vulnName)
+ }
+ return fmt.Sprintf("%s calls %s, which eventually calls %s",
+ topName, funcName(cs[iTop+1].Function), vulnName)
+}
+
+// highest returns the highest (one with the smallest index) entry in the call
+// stack for which f returns true.
+func highest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int {
+ for i := 0; i < len(cs); i++ {
+ if f(cs[i]) {
+ return i
}
}
- var res []*vulncheck.FuncNode
- for fn := range fns {
- res = append(res, fn)
+ return -1
+}
+
+// lowest returns the lowest (one with the largets index) entry in the call
+// stack for which f returns true.
+func lowest(cs vulncheck.CallStack, f func(e vulncheck.StackEntry) bool) int {
+ for i := len(cs) - 1; i >= 0; i-- {
+ if f(cs[i]) {
+ return i
+ }
}
- return res
+ return -1
}
func pkgPath(fn *vulncheck.FuncNode) string {
@@ -324,6 +354,27 @@
return strings.TrimPrefix(fn.String(), "*")
}
+// compact replaces consecutive runs of equal elements with a single copy.
+// This is like the uniq command found on Unix.
+// compact modifies the contents of the slice s; it does not create a new slice.
+//
+// Modified (generics removed) from exp/slices/slices.go.
+func compact(s []string) []string {
+ if len(s) == 0 {
+ return s
+ }
+ i := 1
+ last := s[0]
+ for _, v := range s[1:] {
+ if v != last {
+ s[i] = v
+ i++
+ last = v
+ }
+ }
+ return s[:i]
+}
+
func die(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/main_test.go
index 45b37de..ba70211 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/main_test.go
@@ -8,6 +8,7 @@
package main
import (
+ "strings"
"testing"
"golang.org/x/vuln/osv"
@@ -110,3 +111,48 @@
}
}
}
+
+func TestSummarizeCallStack(t *testing.T) {
+ topPkgs := map[string]bool{"t1": true, "t2": true}
+ vulnPkg := "v"
+
+ for _, test := range []struct {
+ in, want string
+ }{
+ {"a.F", ""},
+ {"t1.F", ""},
+ {"v.V", ""},
+ {
+ "t1.F v.V",
+ "t1.F calls v.V",
+ },
+ {
+ "t1.F t2.G v.V1 v.v2",
+ "t2.G calls v.V1",
+ },
+ {
+ "t1.F x.Y t2.G a.H b.I c.J v.V",
+ "t2.G calls a.H, which eventually calls v.V",
+ },
+ } {
+ in := stringToCallStack(test.in)
+ got := summarizeCallStack(in, topPkgs, vulnPkg)
+ if got != test.want {
+ t.Errorf("%s:\ngot %s\nwant %s", test.in, got, test.want)
+ }
+ }
+}
+
+func stringToCallStack(s string) vulncheck.CallStack {
+ var cs vulncheck.CallStack
+ for _, e := range strings.Fields(s) {
+ parts := strings.Split(e, ".")
+ cs = append(cs, vulncheck.StackEntry{
+ Function: &vulncheck.FuncNode{
+ PkgPath: parts[0],
+ Name: parts[1],
+ },
+ })
+ }
+ return cs
+}