The handler keeps track of the most precise findings for a
vulnerability.

Updates golang/go#61347

Change-Id: I8fe8183826f152d8d51d9e5b3117cd192012fdba
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/549775
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Maceo Thompson <maceothompson@google.com>
This commit is contained in:
Zvonimir Pavlinovic 2023-12-14 18:22:32 +00:00
Родитель 334df71c24
Коммит b51a8ac362
4 изменённых файлов: 418 добавлений и 7 удалений

103
internal/sarif/handler.go Normal file
Просмотреть файл

@ -0,0 +1,103 @@
// Copyright 2023 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.
package sarif
import (
"io"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/osv"
)
// handler for sarif output.
type handler struct {
w io.Writer
cfg *govulncheck.Config
osvs map[string]*osv.Entry
// findings contains same-level findings for an
// OSV at the most precise level of granularity
// available. This means, for instance, that if
// an osv is indeed called, then all findings for
// the osv will have call stack info.
findings map[string][]*govulncheck.Finding
}
func NewHandler(w io.Writer) *handler {
return &handler{
w: w,
osvs: make(map[string]*osv.Entry),
findings: make(map[string][]*govulncheck.Finding),
}
}
func (h *handler) Config(c *govulncheck.Config) error {
h.cfg = c
return nil
}
func (h *handler) Progress(p *govulncheck.Progress) error {
return nil // not needed by sarif
}
func (h *handler) OSV(e *osv.Entry) error {
h.osvs[e.ID] = e
return nil
}
// moreSpecific favors a call finding over a non-call
// finding and a package finding over a module finding.
func moreSpecific(f1, f2 *govulncheck.Finding) int {
if len(f1.Trace) > 1 && len(f2.Trace) > 1 {
// Both are call stack findings.
return 0
}
if len(f1.Trace) > 1 {
return -1
}
if len(f2.Trace) > 1 {
return 1
}
fr1, fr2 := f1.Trace[0], f2.Trace[0]
if fr1.Function != "" && fr2.Function == "" {
return -1
}
if fr1.Function == "" && fr2.Function != "" {
return 1
}
if fr1.Package != "" && fr2.Package == "" {
return -1
}
if fr1.Package == "" && fr2.Package != "" {
return -1
}
return 0 // findings always have module info
}
func (h *handler) Finding(f *govulncheck.Finding) error {
fs := h.findings[f.OSV]
if len(fs) == 0 {
fs = []*govulncheck.Finding{f}
} else {
if ms := moreSpecific(f, fs[0]); ms == -1 {
// The new finding is more specific, so we need
// to erase existing findings and add the new one.
fs = []*govulncheck.Finding{f}
} else if ms == 0 {
// The new finding is equal to an existing one and
// because of the invariant on h.findings, it is
// also equal to all existing ones.
fs = append(fs, f)
}
// Otherwise, the new finding is at a less precise level.
}
h.findings[f.OSV] = fs
return nil
}
// Flush is used to print out to w the sarif json output.
// This is needed as sarif is not streamed.
func (h *handler) Flush() error {
return nil
}

Просмотреть файл

@ -0,0 +1,305 @@
// Copyright 2023 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.
package sarif
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/vuln/internal/govulncheck"
)
func level(f *govulncheck.Finding) string {
fr := f.Trace[0]
if fr.Function != "" {
return "symbol"
}
if fr.Package != "" {
return "package"
}
return "module"
}
func TestHandlerSymbol(t *testing.T) {
fs := `
{
"finding": {
"osv": "GO-2021-0054",
"trace": [
{
"module": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0265",
"trace": [
{
"module": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2020-0015",
"trace": [
{
"module": "golang.org/x/text"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0054",
"trace": [
{
"module": "github.com/tidwall/gjson",
"package": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0265",
"trace": [
{
"module": "github.com/tidwall/gjson",
"package": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0265",
"trace": [
{
"module": "github.com/tidwall/gjson",
"package": "github.com/tidwall/gjson",
"function": "Get",
"receiver": "Result"
},
{
"module": "golang.org/vuln",
"package": "golang.org/vuln",
"function": "main"
}
]
}
}`
h := NewHandler(nil)
if err := govulncheck.HandleJSON(strings.NewReader(fs), h); err != nil {
t.Fatal(err)
}
want := map[string]string{
"GO-2021-0265": "symbol",
"GO-2021-0054": "package",
"GO-2020-0015": "module",
}
got := make(map[string]string)
for osv, fs := range h.findings {
got[osv] = level(fs[0])
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("(-want;got+): %s", diff)
}
}
func TestHandlerPackage(t *testing.T) {
fs := `
{
"finding": {
"osv": "GO-2021-0054",
"trace": [
{
"module": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0265",
"trace": [
{
"module": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2020-0015",
"trace": [
{
"module": "golang.org/x/text"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0054",
"trace": [
{
"module": "github.com/tidwall/gjson",
"package": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0265",
"trace": [
{
"module": "github.com/tidwall/gjson",
"package": "github.com/tidwall/gjson"
}
]
}
}`
h := NewHandler(nil)
if err := govulncheck.HandleJSON(strings.NewReader(fs), h); err != nil {
t.Fatal(err)
}
want := map[string]string{
"GO-2021-0265": "package",
"GO-2021-0054": "package",
"GO-2020-0015": "module",
}
got := make(map[string]string)
for osv, fs := range h.findings {
got[osv] = level(fs[0])
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("(-want;got+): %s", diff)
}
}
func TestHandlerModule(t *testing.T) {
fs := `
{
"finding": {
"osv": "GO-2021-0054",
"trace": [
{
"module": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2021-0265",
"trace": [
{
"module": "github.com/tidwall/gjson"
}
]
}
}
{
"finding": {
"osv": "GO-2020-0015",
"trace": [
{
"module": "golang.org/x/text"
}
]
}
}`
h := NewHandler(nil)
if err := govulncheck.HandleJSON(strings.NewReader(fs), h); err != nil {
t.Fatal(err)
}
want := map[string]string{
"GO-2021-0265": "module",
"GO-2021-0054": "module",
"GO-2020-0015": "module",
}
got := make(map[string]string)
for osv, fs := range h.findings {
got[osv] = level(fs[0])
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("(-want;got+): %s", diff)
}
}
func TestMoreSpecific(t *testing.T) {
frame := func(m, p, f string) *govulncheck.Frame {
return &govulncheck.Frame{
Module: m,
Package: p,
Function: f,
}
}
for _, tc := range []struct {
name string
want int
trace1 []*govulncheck.Frame
trace2 []*govulncheck.Frame
}{
{"sym-vs-sym", 0,
[]*govulncheck.Frame{
frame("m1", "p1", "v1"), frame("m1", "p1", "f2")},
[]*govulncheck.Frame{
frame("m1", "p1", "v2"), frame("m1", "p1", "f1"), frame("m2", "p2", "f2")},
},
{"sym-vs-pkg", -1,
[]*govulncheck.Frame{
frame("m1", "p1", "v1"), frame("m1", "p1", "f2")},
[]*govulncheck.Frame{
frame("m1", "p1", "")},
},
{"pkg-vs-sym", 1,
[]*govulncheck.Frame{
frame("m1", "p1", "")},
[]*govulncheck.Frame{
frame("m1", "p1", "v1"), frame("m2", "p2", "v2")},
},
{"pkg-vs-mod", -1,
[]*govulncheck.Frame{
frame("m1", "p1", "")},
[]*govulncheck.Frame{
frame("m1", "", "")},
},
{"mod-vs-sym", 1,
[]*govulncheck.Frame{
frame("m1", "", "")},
[]*govulncheck.Frame{
frame("m1", "p1", "v2"), frame("m1", "p1", "f1")},
},
{"mod-vs-mod", 0,
[]*govulncheck.Frame{
frame("m1", "", "")},
[]*govulncheck.Frame{
frame("m2", "", "")},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
f1 := &govulncheck.Finding{Trace: tc.trace1}
f2 := &govulncheck.Finding{Trace: tc.trace2}
if got := moreSpecific(f1, f2); got != tc.want {
t.Errorf("want %d; got %d", tc.want, got)
}
})
}
}

Просмотреть файл

@ -17,6 +17,7 @@ import (
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/sarif"
)
// RunGovulncheck performs main govulncheck functionality and exits the
@ -38,6 +39,8 @@ func RunGovulncheck(ctx context.Context, env []string, r io.Reader, stdout io.Wr
switch cfg.format {
case formatJSON:
handler = govulncheck.NewJSONHandler(stdout)
case formatSarif:
handler = sarif.NewHandler(stdout)
default:
th := NewTextHandler(stdout)
th.Show(cfg.show)
@ -130,3 +133,10 @@ func scannerVersion(cfg *config, bi *debug.BuildInfo) {
}
cfg.ScannerVersion = buf.String()
}
func Flush(h govulncheck.Handler) error {
if th, ok := h.(interface{ Flush() error }); ok {
return th.Flush()
}
return nil
}

Просмотреть файл

@ -76,13 +76,6 @@ func (h *TextHandler) Show(show []string) {
}
}
func Flush(h govulncheck.Handler) error {
if th, ok := h.(interface{ Flush() error }); ok {
return th.Flush()
}
return nil
}
func (h *TextHandler) Flush() error {
if len(h.findings) == 0 {
h.print(noVulnsMessage + "\n")