Skip to content

Commit bd80eaa

Browse files
author
Maceo Thompson
committed
internal/openvex: populate product subcomponents
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]>
1 parent 2e326d4 commit bd80eaa

File tree

7 files changed

+367
-47
lines changed

7 files changed

+367
-47
lines changed

cmd/govulncheck/testdata/common/testfiles/binary-call/binary_vex.ct

+25-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
$ govulncheck -format openvex -mode binary ${common_vuln_binary}
44
{
55
"@context": "https://openvex.dev/ns/v0.2.0",
6-
"@id": "govulncheck/vex:b1a12e6f591b29f244e67c80a88d10539c220a04f6ca48d3fe7af2faf0189437",
6+
"@id": "govulncheck/vex:261b597336f7aa5eb53a4a196c354c5afed43fe55658ae3816194192b5268881",
77
"author": "Unknown Author",
88
"timestamp": "2024-01-01T00:00:00",
99
"version": 1,
@@ -21,7 +21,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
2121
},
2222
"products": [
2323
{
24-
"@id": "Unknown Product"
24+
"@id": "Unknown Product",
25+
"subcomponents": [
26+
{
27+
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
28+
}
29+
]
2530
}
2631
],
2732
"status": "not_affected",
@@ -40,7 +45,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
4045
},
4146
"products": [
4247
{
43-
"@id": "Unknown Product"
48+
"@id": "Unknown Product",
49+
"subcomponents": [
50+
{
51+
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
52+
}
53+
]
4454
}
4555
],
4656
"status": "affected"
@@ -57,7 +67,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
5767
},
5868
"products": [
5969
{
60-
"@id": "Unknown Product"
70+
"@id": "Unknown Product",
71+
"subcomponents": [
72+
{
73+
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
74+
}
75+
]
6176
}
6277
],
6378
"status": "not_affected",
@@ -78,7 +93,12 @@ $ govulncheck -format openvex -mode binary ${common_vuln_binary}
7893
},
7994
"products": [
8095
{
81-
"@id": "Unknown Product"
96+
"@id": "Unknown Product",
97+
"subcomponents": [
98+
{
99+
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
100+
}
101+
]
82102
}
83103
],
84104
"status": "affected"

cmd/govulncheck/testdata/common/testfiles/source-call/source_call_vex.ct

+25-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
$ govulncheck -C ${moddir}/vuln -format openvex ./...
44
{
55
"@context": "https://openvex.dev/ns/v0.2.0",
6-
"@id": "govulncheck/vex:b1a12e6f591b29f244e67c80a88d10539c220a04f6ca48d3fe7af2faf0189437",
6+
"@id": "govulncheck/vex:261b597336f7aa5eb53a4a196c354c5afed43fe55658ae3816194192b5268881",
77
"author": "Unknown Author",
88
"timestamp": "2024-01-01T00:00:00",
99
"version": 1,
@@ -21,7 +21,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
2121
},
2222
"products": [
2323
{
24-
"@id": "Unknown Product"
24+
"@id": "Unknown Product",
25+
"subcomponents": [
26+
{
27+
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
28+
}
29+
]
2530
}
2631
],
2732
"status": "not_affected",
@@ -40,7 +45,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
4045
},
4146
"products": [
4247
{
43-
"@id": "Unknown Product"
48+
"@id": "Unknown Product",
49+
"subcomponents": [
50+
{
51+
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
52+
}
53+
]
4454
}
4555
],
4656
"status": "affected"
@@ -57,7 +67,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
5767
},
5868
"products": [
5969
{
60-
"@id": "Unknown Product"
70+
"@id": "Unknown Product",
71+
"subcomponents": [
72+
{
73+
"@id": "pkg:golang/golang.org%2Fx%[email protected]"
74+
}
75+
]
6176
}
6277
],
6378
"status": "not_affected",
@@ -78,7 +93,12 @@ $ govulncheck -C ${moddir}/vuln -format openvex ./...
7893
},
7994
"products": [
8095
{
81-
"@id": "Unknown Product"
96+
"@id": "Unknown Product",
97+
"subcomponents": [
98+
{
99+
"@id": "pkg:golang/github.com%2Ftidwall%[email protected]"
100+
}
101+
]
82102
}
83103
],
84104
"status": "affected"

internal/openvex/handler.go

+83-14
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,22 @@ const (
2626
)
2727

2828
type handler struct {
29-
w io.Writer
30-
cfg *govulncheck.Config
31-
osvs map[string]*osv.Entry
32-
levels map[string]findingLevel
29+
w io.Writer
30+
cfg *govulncheck.Config
31+
osvs map[string]*osv.Entry
32+
// findings contains same-level findings for an
33+
// OSV at the most precise level of granularity
34+
// available. This means, for instance, that if
35+
// an osv is indeed called, then all findings for
36+
// the osv will have call stack info.
37+
findings map[string][]*govulncheck.Finding
3338
}
3439

3540
func NewHandler(w io.Writer) *handler {
3641
return &handler{
37-
w: w,
38-
osvs: make(map[string]*osv.Entry),
39-
levels: make(map[string]findingLevel),
42+
w: w,
43+
osvs: make(map[string]*osv.Entry),
44+
findings: make(map[string][]*govulncheck.Finding),
4045
}
4146
}
4247

@@ -67,11 +72,52 @@ func foundAtLevel(f *govulncheck.Finding) findingLevel {
6772
return required
6873
}
6974

75+
// moreSpecific favors a call finding over a non-call
76+
// finding and a package finding over a module finding.
77+
func moreSpecific(f1, f2 *govulncheck.Finding) int {
78+
if len(f1.Trace) > 1 && len(f2.Trace) > 1 {
79+
// Both are call stack findings.
80+
return 0
81+
}
82+
if len(f1.Trace) > 1 {
83+
return -1
84+
}
85+
if len(f2.Trace) > 1 {
86+
return 1
87+
}
88+
89+
fr1, fr2 := f1.Trace[0], f2.Trace[0]
90+
if fr1.Function != "" && fr2.Function == "" {
91+
return -1
92+
}
93+
if fr1.Function == "" && fr2.Function != "" {
94+
return 1
95+
}
96+
if fr1.Package != "" && fr2.Package == "" {
97+
return -1
98+
}
99+
if fr1.Package == "" && fr2.Package != "" {
100+
return -1
101+
}
102+
return 0 // findings always have module info
103+
}
104+
70105
func (h *handler) Finding(f *govulncheck.Finding) error {
71-
fLevel := foundAtLevel(f)
72-
if fLevel > h.levels[f.OSV] {
73-
h.levels[f.OSV] = fLevel
106+
fs := h.findings[f.OSV]
107+
if len(fs) == 0 {
108+
fs = []*govulncheck.Finding{f}
109+
} else {
110+
if ms := moreSpecific(f, fs[0]); ms == -1 {
111+
// The new finding is more specific, so we need
112+
// to erase existing findings and add the new one.
113+
fs = []*govulncheck.Finding{f}
114+
} else if ms == 0 {
115+
// The new finding is at the same level of precision.
116+
fs = append(fs, f)
117+
}
118+
// Otherwise, the new finding is at a less precise level.
74119
}
120+
h.findings[f.OSV] = fs
75121
return nil
76122
}
77123

@@ -102,6 +148,23 @@ func toVex(h *handler) Document {
102148
return doc
103149
}
104150

151+
// Given a slice of findings, returns those findings as a set of subcomponents
152+
// that are unique per the vulnerable artifact's PURL.
153+
func subcomponentSet(findings []*govulncheck.Finding) []Component {
154+
var scs []Component
155+
seen := make(map[string]bool)
156+
for _, f := range findings {
157+
purl := purlFromFinding(f)
158+
if !seen[purl] {
159+
scs = append(scs, Component{
160+
ID: purlFromFinding(f),
161+
})
162+
seen[purl] = true
163+
}
164+
}
165+
return scs
166+
}
167+
105168
// statements combines all OSVs found by govulncheck and generates the list of
106169
// vex statements with the proper affected level and justification to match the
107170
// openVex specification.
@@ -118,13 +181,16 @@ func statements(h *handler) []Statement {
118181

119182
var statements []Statement
120183
for id, osv := range h.osvs {
121-
if _, found := h.levels[id]; !found {
184+
// if there are no findings emitted for a given OSV that means that
185+
// the vulnerable module is not required at a vulnerable version.
186+
if len(h.findings[id]) == 0 {
122187
continue
123188
}
124189
description := osv.Summary
125190
if description == "" {
126191
description = osv.Details
127192
}
193+
128194
s := Statement{
129195
Vulnerability: Vulnerability{
130196
ID: fmt.Sprintf("https://pkg.go.dev/vuln/%s", id),
@@ -134,19 +200,22 @@ func statements(h *handler) []Statement {
134200
},
135201
Products: []Product{
136202
{
137-
ID: DefaultPID,
203+
Component: Component{ID: DefaultPID},
204+
Subcomponents: subcomponentSet(h.findings[id]),
138205
},
139206
},
140207
}
141208

142-
if h.levels[id] >= scanLevel {
209+
// Findings are guaranteed to be at the same level, so we can just check the first element
210+
fLevel := foundAtLevel(h.findings[id][0])
211+
if fLevel >= scanLevel {
143212
s.Status = StatusAffected
144213
} else {
145214
s.Status = StatusNotAffected
146215
s.ImpactStatement = Impact
147216
s.Justification = JustificationNotPresent
148217
// We only reach this case if running in symbol mode
149-
if h.levels[id] == imported {
218+
if fLevel == imported {
150219
s.Justification = JustificationNotExecuted
151220
}
152221
}

0 commit comments

Comments
 (0)