Skip to content

Commit c715000

Browse files
authored
fix: Missing VEX status mappings for resolved_with_pedigree and false_positive (#2813)
* Add missing VEX status mappings for resolved_with_pedigree and false_positive Signed-off-by: Irena Liu <irena.liu@verkada.com> * Add unit tests for resolved_with_pedigree and false_positive VEX statuses Signed-off-by: Irena Liu <irena.liu@verkada.com> * set statusnote if all detail fields are empty Signed-off-by: Irena Liu <irena.liu@verkada.com> --------- Signed-off-by: Irena Liu <irena.liu@verkada.com>
1 parent 9ead9e8 commit c715000

File tree

5 files changed

+358
-5
lines changed

5 files changed

+358
-5
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.6",
4+
"serialNumber": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2024-01-15T10:30:00Z",
8+
"component": {
9+
"bom-ref": "test-component-2",
10+
"type": "application",
11+
"name": "test-app-2",
12+
"version": "1.0.0"
13+
}
14+
},
15+
"vulnerabilities": [
16+
{
17+
"id": "CVE-2024-0002",
18+
"source": {
19+
"name": "NVD",
20+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0002"
21+
},
22+
"ratings": [
23+
{
24+
"source": {
25+
"name": "NVD",
26+
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator"
27+
},
28+
"score": 6.0,
29+
"severity": "medium",
30+
"method": "CVSSv31"
31+
}
32+
],
33+
"analysis": {
34+
"state": "false_positive",
35+
"detail": "Vulnerability was falsely identified or associated with this component"
36+
},
37+
"affects": [
38+
{
39+
"ref": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2",
40+
"versions": [
41+
{
42+
"version": "1.0.0",
43+
"status": "unaffected"
44+
}
45+
]
46+
}
47+
]
48+
},
49+
{
50+
"id": "CVE-2024-0004",
51+
"source": {
52+
"name": "NVD",
53+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0004"
54+
},
55+
"ratings": [
56+
{
57+
"source": {
58+
"name": "NVD",
59+
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator"
60+
},
61+
"score": 6.0,
62+
"severity": "medium",
63+
"method": "CVSSv31"
64+
}
65+
],
66+
"analysis": {
67+
"state": "false_positive"
68+
},
69+
"affects": [
70+
{
71+
"ref": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2-no-detail",
72+
"versions": [
73+
{
74+
"version": "1.0.0",
75+
"status": "unaffected"
76+
}
77+
]
78+
}
79+
]
80+
}
81+
]
82+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.6",
4+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2024-01-15T10:30:00Z",
8+
"component": {
9+
"bom-ref": "test-component",
10+
"type": "application",
11+
"name": "test-app",
12+
"version": "1.0.0"
13+
}
14+
},
15+
"vulnerabilities": [
16+
{
17+
"id": "CVE-2024-0001",
18+
"source": {
19+
"name": "NVD",
20+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0001"
21+
},
22+
"ratings": [
23+
{
24+
"source": {
25+
"name": "NVD",
26+
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator"
27+
},
28+
"score": 7.5,
29+
"severity": "high",
30+
"method": "CVSSv31"
31+
}
32+
],
33+
"analysis": {
34+
"state": "resolved_with_pedigree",
35+
"detail": "Vulnerability has been remediated with evidence provided in component pedigree"
36+
},
37+
"affects": [
38+
{
39+
"ref": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component",
40+
"versions": [
41+
{
42+
"version": "1.0.0",
43+
"status": "affected"
44+
}
45+
]
46+
}
47+
]
48+
},
49+
{
50+
"id": "CVE-2024-0003",
51+
"source": {
52+
"name": "NVD",
53+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0003"
54+
},
55+
"ratings": [
56+
{
57+
"source": {
58+
"name": "NVD",
59+
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator"
60+
},
61+
"score": 7.5,
62+
"severity": "high",
63+
"method": "CVSSv31"
64+
}
65+
],
66+
"analysis": {
67+
"state": "resolved_with_pedigree"
68+
},
69+
"affects": [
70+
{
71+
"ref": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component-no-detail",
72+
"versions": [
73+
{
74+
"version": "1.0.0",
75+
"status": "affected"
76+
}
77+
]
78+
}
79+
]
80+
}
81+
]
82+
}

internal/testing/testdata/testdata.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ var (
120120
//go:embed exampledata/cyclonedx-vex-no-analysis.json
121121
CycloneDXVEXWithoutAnalysis []byte
122122

123+
//go:embed exampledata/cyclonedx-vex-resolved-with-pedigree.json
124+
CycloneDXVEXResolvedWithPedigree []byte
125+
126+
//go:embed exampledata/cyclonedx-vex-false-positive.json
127+
CycloneDXVEXFalsePositive []byte
128+
123129
//go:embed exampledata/cyclonedx-vex.xml
124130
CyloneDXVEXExampleXML []byte
125131

@@ -304,6 +310,93 @@ var (
304310
},
305311
},
306312
}
313+
// VexData for resolved_with_pedigree status (maps to VexStatusFixed)
314+
VexDataResolvedWithPedigree = &generated.VexStatementInputSpec{
315+
Status: generated.VexStatusFixed,
316+
VexJustification: generated.VexJustificationNotProvided,
317+
Statement: "",
318+
StatusNotes: "Vulnerability has been remediated with evidence provided in component pedigree",
319+
KnownSince: time.Unix(0, 0).UTC(),
320+
}
321+
// VexData for false_positive status (maps to VexStatusNotAffected)
322+
VexDataFalsePositive = &generated.VexStatementInputSpec{
323+
Status: generated.VexStatusNotAffected,
324+
VexJustification: generated.VexJustificationNotProvided,
325+
Statement: "",
326+
StatusNotes: "Vulnerability was falsely identified or associated with this component",
327+
KnownSince: time.Unix(0, 0).UTC(),
328+
}
329+
// VexData for resolved_with_pedigree status without detail (maps to VexStatusFixed)
330+
VexDataResolvedWithPedigreeNoDetail = &generated.VexStatementInputSpec{
331+
Status: generated.VexStatusFixed,
332+
VexJustification: generated.VexJustificationNotProvided,
333+
Statement: "",
334+
StatusNotes: "CDX state: resolved_with_pedigree",
335+
KnownSince: time.Unix(0, 0).UTC(),
336+
}
337+
// VexData for false_positive status without detail (maps to VexStatusNotAffected)
338+
VexDataFalsePositiveNoDetail = &generated.VexStatementInputSpec{
339+
Status: generated.VexStatusNotAffected,
340+
VexJustification: generated.VexJustificationNotProvided,
341+
Statement: "",
342+
StatusNotes: "CDX state: false_positive",
343+
KnownSince: time.Unix(0, 0).UTC(),
344+
}
345+
VulnSpecResolvedWithPedigree = &generated.VulnerabilityInputSpec{
346+
Type: "cve",
347+
VulnerabilityID: "cve-2024-0001",
348+
}
349+
VulnSpecFalsePositive = &generated.VulnerabilityInputSpec{
350+
Type: "cve",
351+
VulnerabilityID: "cve-2024-0002",
352+
}
353+
// Vulnerability specs for no-detail test cases
354+
VulnSpecResolvedWithPedigreeNoDetail = &generated.VulnerabilityInputSpec{
355+
Type: "cve",
356+
VulnerabilityID: "cve-2024-0003",
357+
}
358+
VulnSpecFalsePositiveNoDetail = &generated.VulnerabilityInputSpec{
359+
Type: "cve",
360+
VulnerabilityID: "cve-2024-0004",
361+
}
362+
// VulnMetadata for resolved_with_pedigree test
363+
CycloneDXResolvedWithPedigreeVulnMetadata = []assembler.VulnMetadataIngest{
364+
{
365+
Vulnerability: VulnSpecResolvedWithPedigree,
366+
VulnMetadata: &generated.VulnerabilityMetadataInputSpec{
367+
ScoreType: generated.VulnerabilityScoreTypeCvssv31,
368+
ScoreValue: 7.5,
369+
Timestamp: time.Unix(0, 0).UTC(),
370+
},
371+
},
372+
{
373+
Vulnerability: VulnSpecResolvedWithPedigreeNoDetail,
374+
VulnMetadata: &generated.VulnerabilityMetadataInputSpec{
375+
ScoreType: generated.VulnerabilityScoreTypeCvssv31,
376+
ScoreValue: 7.5,
377+
Timestamp: time.Unix(0, 0).UTC(),
378+
},
379+
},
380+
}
381+
// VulnMetadata for false_positive test
382+
CycloneDXFalsePositiveVulnMetadata = []assembler.VulnMetadataIngest{
383+
{
384+
Vulnerability: VulnSpecFalsePositive,
385+
VulnMetadata: &generated.VulnerabilityMetadataInputSpec{
386+
ScoreType: generated.VulnerabilityScoreTypeCvssv31,
387+
ScoreValue: 6.0,
388+
Timestamp: time.Unix(0, 0).UTC(),
389+
},
390+
},
391+
{
392+
Vulnerability: VulnSpecFalsePositiveNoDetail,
393+
VulnMetadata: &generated.VulnerabilityMetadataInputSpec{
394+
ScoreType: generated.VulnerabilityScoreTypeCvssv31,
395+
ScoreValue: 6.0,
396+
Timestamp: time.Unix(0, 0).UTC(),
397+
},
398+
},
399+
}
307400

308401
topLevelPkg, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/ABC")
309402
HasSBOMVexAffected = []assembler.HasSBOMIngest{
@@ -326,6 +419,73 @@ var (
326419
},
327420
},
328421
}
422+
// HasSBOM for resolved_with_pedigree test
423+
topLevelPkgResolvedWithPedigree, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/test-app@1.0.0")
424+
HasSBOMVexResolvedWithPedigree = []assembler.HasSBOMIngest{
425+
{
426+
Pkg: topLevelPkgResolvedWithPedigree,
427+
HasSBOM: &model.HasSBOMInputSpec{
428+
Uri: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
429+
Algorithm: "sha256",
430+
Digest: "32981b0c4f87df9243c0e9b8a9600f2e19aae0c0cb76122edfe4a54ef59b9d48",
431+
KnownSince: parseRfc3339("2024-01-15T10:30:00Z"),
432+
},
433+
},
434+
}
435+
// HasSBOM for false_positive test
436+
topLevelPkgFalsePositive, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/test-app-2@1.0.0")
437+
HasSBOMVexFalsePositive = []assembler.HasSBOMIngest{
438+
{
439+
Pkg: topLevelPkgFalsePositive,
440+
HasSBOM: &model.HasSBOMInputSpec{
441+
Uri: "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80",
442+
Algorithm: "sha256",
443+
Digest: "0731373583749ae046d0992e9b417d4b2960f75d7a979c72fd0b7a258566d520",
444+
KnownSince: parseRfc3339("2024-01-15T10:30:00Z"),
445+
},
446+
},
447+
}
448+
// Predicates for resolved_with_pedigree test
449+
resolvedWithPedigreePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component@1.0.0")
450+
resolvedWithPedigreeNoDetailPkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-no-detail@1.0.0")
451+
CycloneDXResolvedWithPedigreeVexIngest = []assembler.VexIngest{
452+
{
453+
Pkg: resolvedWithPedigreePkg,
454+
Vulnerability: VulnSpecResolvedWithPedigree,
455+
VexData: VexDataResolvedWithPedigree,
456+
},
457+
{
458+
Pkg: resolvedWithPedigreeNoDetailPkg,
459+
Vulnerability: VulnSpecResolvedWithPedigreeNoDetail,
460+
VexData: VexDataResolvedWithPedigreeNoDetail,
461+
},
462+
}
463+
CycloneDXResolvedWithPedigreePredicates = assembler.IngestPredicates{
464+
HasSBOM: HasSBOMVexResolvedWithPedigree,
465+
VulnMetadata: CycloneDXResolvedWithPedigreeVulnMetadata,
466+
Vex: CycloneDXResolvedWithPedigreeVexIngest,
467+
// Note: No CertifyVuln because status is Fixed (not Affected/UnderInvestigation)
468+
}
469+
// Predicates for false_positive test
470+
falsePositivePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2@1.0.0")
471+
falsePositiveNoDetailPkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2-no-detail@1.0.0")
472+
CycloneDXFalsePositiveVexIngest = []assembler.VexIngest{
473+
{
474+
Pkg: falsePositivePkg,
475+
Vulnerability: VulnSpecFalsePositive,
476+
VexData: VexDataFalsePositive,
477+
},
478+
{
479+
Pkg: falsePositiveNoDetailPkg,
480+
Vulnerability: VulnSpecFalsePositiveNoDetail,
481+
VexData: VexDataFalsePositiveNoDetail,
482+
},
483+
}
484+
CycloneDXFalsePositivePredicates = assembler.IngestPredicates{
485+
HasSBOM: HasSBOMVexFalsePositive,
486+
VulnMetadata: CycloneDXFalsePositiveVulnMetadata,
487+
Vex: CycloneDXFalsePositiveVexIngest,
488+
}
329489

330490
// DSSE/SLSA Testdata
331491

pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ var json = jsoniter.ConfigCompatibleWithStandardLibrary
4040
var zeroTime = time.Unix(0, 0).UTC()
4141

4242
var vexStatusMap = map[cdx.ImpactAnalysisState]model.VexStatus{
43-
cdx.IASResolved: model.VexStatusFixed,
44-
cdx.IASExploitable: model.VexStatusAffected,
45-
cdx.IASInTriage: model.VexStatusUnderInvestigation,
46-
cdx.IASNotAffected: model.VexStatusNotAffected,
43+
cdx.IASResolved: model.VexStatusFixed,
44+
cdx.IASExploitable: model.VexStatusAffected,
45+
cdx.IASInTriage: model.VexStatusUnderInvestigation,
46+
cdx.IASNotAffected: model.VexStatusNotAffected,
47+
cdx.IASResolvedWithPedigree: model.VexStatusFixed,
48+
cdx.IASFalsePositive: model.VexStatusNotAffected,
4749
}
4850

4951
var justificationsMap = map[cdx.ImpactAnalysisJustification]model.VexJustification{
@@ -577,6 +579,8 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error {
577579
vd.KnownSince = publishedTime
578580
vd.Statement = vulnerability.Description
579581

582+
// Extract StatusNotes from analysis detail field.
583+
// This applies to all analysis states including resolved_with_pedigree and false_positive.
580584
if vulnerability.Analysis.Detail != "" {
581585
vd.StatusNotes = vulnerability.Analysis.Detail
582586
} else if vulnerability.Analysis.Response != nil {
@@ -585,7 +589,14 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error {
585589
response = append(response, string(res))
586590
}
587591
vd.StatusNotes = strings.Join(response, ",")
588-
} else {
592+
} else if vulnerability.Analysis.State != cdx.IASResolved &&
593+
vulnerability.Analysis.State != cdx.IASExploitable &&
594+
vulnerability.Analysis.State != cdx.IASInTriage &&
595+
vulnerability.Analysis.State != cdx.IASNotAffected {
596+
// Only preserve the CDX state enum information if it's not one of the original 4 states, since those provide additional context beyond what's already captured in vd.Status
597+
// Original states: IASResolved, IASExploitable, IASInTriage, IASNotAffected
598+
vd.StatusNotes = fmt.Sprintf("CDX state: %s", string(vulnerability.Analysis.State))
599+
} else if vulnerability.Detail != "" {
589600
vd.StatusNotes = vulnerability.Detail
590601
}
591602
} else {

0 commit comments

Comments
 (0)