Skip to content

Commit

Permalink
internal/openvex: populate product subcomponents
Browse files Browse the repository at this point in the history
Populates the "subcomponent" field of a outputted vex statement with the
PURL to the vulnerable dependency.

updates golang/go#68152

Change-Id: I9e7b9a6686744496b3409ee9d4d0f3d70917db45
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/598956
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Zvonimir Pavlinovic <[email protected]>
  • Loading branch information
Maceo Thompson committed Sep 25, 2024
1 parent 2e326d4 commit bd80eaa
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
$ govulncheck -format openvex -mode binary ${common_vuln_binary}
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "govulncheck/vex:b1a12e6f591b29f244e67c80a88d10539c220a04f6ca48d3fe7af2faf0189437",
"@id": "govulncheck/vex:261b597336f7aa5eb53a4a196c354c5afed43fe55658ae3816194192b5268881",
"author": "Unknown Author",
"timestamp": "2024-01-01T00:00:00",
"version": 1,
Expand All @@ -21,7 +21,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
}
]
}
],
"status": "not_affected",
Expand All @@ -40,7 +45,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
}
]
}
],
"status": "affected"
Expand All @@ -57,7 +67,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
}
]
}
],
"status": "not_affected",
Expand All @@ -78,7 +93,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
}
]
}
],
"status": "affected"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
$ govulncheck -C ${moddir}/vuln -format openvex ./...
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "govulncheck/vex:b1a12e6f591b29f244e67c80a88d10539c220a04f6ca48d3fe7af2faf0189437",
"@id": "govulncheck/vex:261b597336f7aa5eb53a4a196c354c5afed43fe55658ae3816194192b5268881",
"author": "Unknown Author",
"timestamp": "2024-01-01T00:00:00",
"version": 1,
Expand All @@ -21,7 +21,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
}
]
}
],
"status": "not_affected",
Expand All @@ -40,7 +45,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
}
]
}
],
"status": "affected"
Expand All @@ -57,7 +67,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
}
]
}
],
"status": "not_affected",
Expand All @@ -78,7 +93,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
},
"products": [
{
"@id": "Unknown Product"
"@id": "Unknown Product",
"subcomponents": [
{
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
}
]
}
],
"status": "affected"
Expand Down
97 changes: 83 additions & 14 deletions internal/openvex/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,22 @@ const (
)

type handler struct {
w io.Writer
cfg *govulncheck.Config
osvs map[string]*osv.Entry
levels map[string]findingLevel
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),
levels: make(map[string]findingLevel),
w: w,
osvs: make(map[string]*osv.Entry),
findings: make(map[string][]*govulncheck.Finding),
}
}

Expand Down Expand Up @@ -67,11 +72,52 @@ func foundAtLevel(f *govulncheck.Finding) findingLevel {
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 {
fLevel := foundAtLevel(f)
if fLevel > h.levels[f.OSV] {
h.levels[f.OSV] = fLevel
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
}

Expand Down Expand Up @@ -102,6 +148,23 @@ func toVex(h *handler) Document {
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.
Expand All @@ -118,13 +181,16 @@ func statements(h *handler) []Statement {

var statements []Statement
for id, osv := range h.osvs {
if _, found := h.levels[id]; !found {
// 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),
Expand All @@ -134,19 +200,22 @@ func statements(h *handler) []Statement {
},
Products: []Product{
{
ID: DefaultPID,
Component: Component{ID: DefaultPID},
Subcomponents: subcomponentSet(h.findings[id]),
},
},
}

if h.levels[id] >= scanLevel {
// 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 h.levels[id] == imported {
if fLevel == imported {
s.Justification = JustificationNotExecuted
}
}
Expand Down
Loading

0 comments on commit bd80eaa

Please sign in to comment.