зеркало из https://github.com/golang/vuln.git
internal/sarif: add handler
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:
Родитель
334df71c24
Коммит
b51a8ac362
|
@ -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")
|
||||
|
|
Загрузка…
Ссылка в новой задаче