262 lines
6.1 KiB
Go
262 lines
6.1 KiB
Go
|
|
// Copyright 2024 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 openvex
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"slices"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"golang.org/x/vuln/internal/govulncheck"
|
||
|
|
"golang.org/x/vuln/internal/osv"
|
||
|
|
)
|
||
|
|
|
||
|
|
type findingLevel int
|
||
|
|
|
||
|
|
const (
|
||
|
|
invalid findingLevel = iota
|
||
|
|
required
|
||
|
|
imported
|
||
|
|
called
|
||
|
|
)
|
||
|
|
|
||
|
|
type handler struct {
|
||
|
|
w io.Writer
|
||
|
|
cfg *govulncheck.Config
|
||
|
|
sbom *govulncheck.SBOM
|
||
|
|
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(cfg *govulncheck.Config) error {
|
||
|
|
h.cfg = cfg
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *handler) Progress(progress *govulncheck.Progress) error {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *handler) SBOM(s *govulncheck.SBOM) error {
|
||
|
|
h.sbom = s
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *handler) OSV(e *osv.Entry) error {
|
||
|
|
h.osvs[e.ID] = e
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// foundAtLevel returns the level at which a specific finding is present in the
|
||
|
|
// scanned product.
|
||
|
|
func foundAtLevel(f *govulncheck.Finding) findingLevel {
|
||
|
|
frame := f.Trace[0]
|
||
|
|
if frame.Function != "" {
|
||
|
|
return called
|
||
|
|
}
|
||
|
|
if frame.Package != "" {
|
||
|
|
return imported
|
||
|
|
}
|
||
|
|
return required
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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 at the same level of precision.
|
||
|
|
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 the vex json to w.
|
||
|
|
// This is needed as vex is not streamed.
|
||
|
|
func (h *handler) Flush() error {
|
||
|
|
doc := toVex(h)
|
||
|
|
out, err := json.MarshalIndent(doc, "", " ")
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
_, err = h.w.Write(out)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func toVex(h *handler) Document {
|
||
|
|
doc := Document{
|
||
|
|
Context: ContextURI,
|
||
|
|
Author: DefaultAuthor,
|
||
|
|
Timestamp: time.Now().UTC(),
|
||
|
|
Version: 1,
|
||
|
|
Tooling: Tooling,
|
||
|
|
Statements: statements(h),
|
||
|
|
}
|
||
|
|
|
||
|
|
id := hashVex(doc)
|
||
|
|
doc.ID = "govulncheck/vex:" + id
|
||
|
|
return doc
|
||
|
|
}
|
||
|
|
|
||
|
|
// Given a slice of findings, returns those findings as a set of subcomponents
|
||
|
|
// that are unique per the vulnerable artifact's PURL.
|
||
|
|
func subcomponentSet(findings []*govulncheck.Finding) []Component {
|
||
|
|
var scs []Component
|
||
|
|
seen := make(map[string]bool)
|
||
|
|
for _, f := range findings {
|
||
|
|
purl := purlFromFinding(f)
|
||
|
|
if !seen[purl] {
|
||
|
|
scs = append(scs, Component{
|
||
|
|
ID: purlFromFinding(f),
|
||
|
|
})
|
||
|
|
seen[purl] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return scs
|
||
|
|
}
|
||
|
|
|
||
|
|
// statements combines all OSVs found by govulncheck and generates the list of
|
||
|
|
// vex statements with the proper affected level and justification to match the
|
||
|
|
// openVex specification.
|
||
|
|
func statements(h *handler) []Statement {
|
||
|
|
var scanLevel findingLevel
|
||
|
|
switch h.cfg.ScanLevel {
|
||
|
|
case govulncheck.ScanLevelModule:
|
||
|
|
scanLevel = required
|
||
|
|
case govulncheck.ScanLevelPackage:
|
||
|
|
scanLevel = imported
|
||
|
|
case govulncheck.ScanLevelSymbol:
|
||
|
|
scanLevel = called
|
||
|
|
}
|
||
|
|
|
||
|
|
var statements []Statement
|
||
|
|
for id, osv := range h.osvs {
|
||
|
|
// if there are no findings emitted for a given OSV that means that
|
||
|
|
// the vulnerable module is not required at a vulnerable version.
|
||
|
|
if len(h.findings[id]) == 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
description := osv.Summary
|
||
|
|
if description == "" {
|
||
|
|
description = osv.Details
|
||
|
|
}
|
||
|
|
|
||
|
|
s := Statement{
|
||
|
|
Vulnerability: Vulnerability{
|
||
|
|
ID: fmt.Sprintf("https://pkg.go.dev/vuln/%s", id),
|
||
|
|
Name: id,
|
||
|
|
Description: description,
|
||
|
|
Aliases: osv.Aliases,
|
||
|
|
},
|
||
|
|
Products: []Product{
|
||
|
|
{
|
||
|
|
Component: Component{ID: DefaultPID},
|
||
|
|
Subcomponents: subcomponentSet(h.findings[id]),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Findings are guaranteed to be at the same level, so we can just check the first element
|
||
|
|
fLevel := foundAtLevel(h.findings[id][0])
|
||
|
|
if fLevel >= scanLevel {
|
||
|
|
s.Status = StatusAffected
|
||
|
|
} else {
|
||
|
|
s.Status = StatusNotAffected
|
||
|
|
s.ImpactStatement = Impact
|
||
|
|
s.Justification = JustificationNotPresent
|
||
|
|
// We only reach this case if running in symbol mode
|
||
|
|
if fLevel == imported {
|
||
|
|
s.Justification = JustificationNotExecuted
|
||
|
|
}
|
||
|
|
}
|
||
|
|
statements = append(statements, s)
|
||
|
|
}
|
||
|
|
|
||
|
|
slices.SortFunc(statements, func(a, b Statement) int {
|
||
|
|
if a.Vulnerability.ID > b.Vulnerability.ID {
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
if a.Vulnerability.ID < b.Vulnerability.ID {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
// this should never happen in practice, since statements are being
|
||
|
|
// populated from a map with the vulnerability IDs as keys
|
||
|
|
return 0
|
||
|
|
})
|
||
|
|
return statements
|
||
|
|
}
|
||
|
|
|
||
|
|
func hashVex(doc Document) string {
|
||
|
|
// json.Marshal should never error here (because of the structure of Document).
|
||
|
|
// If an error does occur, it won't be a jsonerror, but instead a panic
|
||
|
|
d := Document{
|
||
|
|
Context: doc.Context,
|
||
|
|
ID: doc.ID,
|
||
|
|
Author: doc.Author,
|
||
|
|
Version: doc.Version,
|
||
|
|
Tooling: doc.Tooling,
|
||
|
|
Statements: doc.Statements,
|
||
|
|
}
|
||
|
|
out, err := json.Marshal(d)
|
||
|
|
if err != nil {
|
||
|
|
panic(err)
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%x", sha256.Sum256(out))
|
||
|
|
}
|