Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80",
"version": 1,
"metadata": {
"timestamp": "2024-01-15T10:30:00Z",
"component": {
"bom-ref": "test-component-2",
"type": "application",
"name": "test-app-2",
"version": "1.0.0"
}
},
"vulnerabilities": [
{
"id": "CVE-2024-0002",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0002"
},
"ratings": [
{
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator"
},
"score": 6.0,
"severity": "medium",
"method": "CVSSv31"
}
],
"analysis": {
"state": "false_positive",
"detail": "Vulnerability was falsely identified or associated with this component"
},
"affects": [
{
"ref": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2",
"versions": [
{
"version": "1.0.0",
"status": "unaffected"
}
]
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2024-01-15T10:30:00Z",
"component": {
"bom-ref": "test-component",
"type": "application",
"name": "test-app",
"version": "1.0.0"
}
},
"vulnerabilities": [
{
"id": "CVE-2024-0001",
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0001"
},
"ratings": [
{
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator"
},
"score": 7.5,
"severity": "high",
"method": "CVSSv31"
}
],
"analysis": {
"state": "resolved_with_pedigree",
"detail": "Vulnerability has been remediated with evidence provided in component pedigree"
},
"affects": [
{
"ref": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component",
"versions": [
{
"version": "1.0.0",
"status": "affected"
}
]
}
]
}
]
}
115 changes: 115 additions & 0 deletions internal/testing/testdata/testdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ var (
//go:embed exampledata/cyclonedx-vex-no-analysis.json
CycloneDXVEXWithoutAnalysis []byte

//go:embed exampledata/cyclonedx-vex-resolved-with-pedigree.json
CycloneDXVEXResolvedWithPedigree []byte

//go:embed exampledata/cyclonedx-vex-false-positive.json
CycloneDXVEXFalsePositive []byte

//go:embed exampledata/cyclonedx-vex.xml
CyloneDXVEXExampleXML []byte

Expand Down Expand Up @@ -304,6 +310,53 @@ var (
},
},
}
// VexData for resolved_with_pedigree status (maps to VexStatusFixed)
VexDataResolvedWithPedigree = &generated.VexStatementInputSpec{
Status: generated.VexStatusFixed,
VexJustification: generated.VexJustificationNotProvided,
Statement: "",
StatusNotes: "Vulnerability has been remediated with evidence provided in component pedigree",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be missing something but it seems like the status note inclusion may be missing in the parser_cyclonedx change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding correctly, the StatusNotes extraction is already handled in parser_cyclonedx.go lines 582-592. I think the code extracts StatusNotes from vulnerability.Analysis.Detail for analysis states, including resolved_with_pedigree and false_positive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Irene, ah sorry i didn't realize this was in the "Details" field in the test, i thought that was a derivation of the status notes from the ENUM.

Along that line of thinking I am thinking if it would be possible to add status notes here about the enum if "Details" is empty? So that we don't lose that bit of information if it is not already captured within Details?..

i.e. if Details is empty we can include StatusNotes as "CDX state: false_positive", or "CDX state: resolved_with_pedigree" since the bit would be lost otherwise?

wdyt?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good idea! I've added your suggestion to the code.

KnownSince: time.Unix(0, 0).UTC(),
}
// VexData for false_positive status (maps to VexStatusNotAffected)
VexDataFalsePositive = &generated.VexStatementInputSpec{
Status: generated.VexStatusNotAffected,
VexJustification: generated.VexJustificationNotProvided,
Statement: "",
StatusNotes: "Vulnerability was falsely identified or associated with this component",
KnownSince: time.Unix(0, 0).UTC(),
}
// Vulnerability specs for new test cases
VulnSpecResolvedWithPedigree = &generated.VulnerabilityInputSpec{
Type: "cve",
VulnerabilityID: "cve-2024-0001",
}
VulnSpecFalsePositive = &generated.VulnerabilityInputSpec{
Type: "cve",
VulnerabilityID: "cve-2024-0002",
}
// VulnMetadata for resolved_with_pedigree test
CycloneDXResolvedWithPedigreeVulnMetadata = []assembler.VulnMetadataIngest{
{
Vulnerability: VulnSpecResolvedWithPedigree,
VulnMetadata: &generated.VulnerabilityMetadataInputSpec{
ScoreType: generated.VulnerabilityScoreTypeCvssv31,
ScoreValue: 7.5,
Timestamp: time.Unix(0, 0).UTC(),
},
},
}
// VulnMetadata for false_positive test
CycloneDXFalsePositiveVulnMetadata = []assembler.VulnMetadataIngest{
{
Vulnerability: VulnSpecFalsePositive,
VulnMetadata: &generated.VulnerabilityMetadataInputSpec{
ScoreType: generated.VulnerabilityScoreTypeCvssv31,
ScoreValue: 6.0,
Timestamp: time.Unix(0, 0).UTC(),
},
},
}

topLevelPkg, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/ABC")
HasSBOMVexAffected = []assembler.HasSBOMIngest{
Expand All @@ -326,6 +379,68 @@ var (
},
},
}
// HasSBOM for resolved_with_pedigree test
topLevelPkgResolvedWithPedigree, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/test-app@1.0.0")
HasSBOMVexResolvedWithPedigree = []assembler.HasSBOMIngest{
{
Pkg: topLevelPkgResolvedWithPedigree,
HasSBOM: &model.HasSBOMInputSpec{
Uri: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
Algorithm: "sha256",
Digest: "a9e5e5fcc0939b4e9ddf74a5863ff577bef9bbf8086d99a4dafb8154c451b56f",
KnownSince: parseRfc3339("2024-01-15T10:30:00Z"),
},
},
}
// HasSBOM for false_positive test
topLevelPkgFalsePositive, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/test-app-2@1.0.0")
HasSBOMVexFalsePositive = []assembler.HasSBOMIngest{
{
Pkg: topLevelPkgFalsePositive,
HasSBOM: &model.HasSBOMInputSpec{
Uri: "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80",
Algorithm: "sha256",
Digest: "738690dd4acaf82b417072354ee631a20a50453278053b558770c6f65906f11d",
KnownSince: parseRfc3339("2024-01-15T10:30:00Z"),
},
},
}
// Predicates for resolved_with_pedigree test
// The affects ref is "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component"
// The parser splits on "#" and uses "test-component" as pkdIdentifier
// Then creates PURL as pkg:guac/pkg/test-component@1.0.0 using guacCDXPkgPurl
resolvedWithPedigreePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component@1.0.0")
CycloneDXResolvedWithPedigreeVexIngest = []assembler.VexIngest{
{
Pkg: resolvedWithPedigreePkg,
Vulnerability: VulnSpecResolvedWithPedigree,
VexData: VexDataResolvedWithPedigree,
},
}
CycloneDXResolvedWithPedigreePredicates = assembler.IngestPredicates{
HasSBOM: HasSBOMVexResolvedWithPedigree,
VulnMetadata: CycloneDXResolvedWithPedigreeVulnMetadata,
Vex: CycloneDXResolvedWithPedigreeVexIngest,
// Note: No CertifyVuln because status is Fixed (not Affected/UnderInvestigation)
}
// Predicates for false_positive test
// The affects ref is "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2"
// The parser splits on "#" and uses "test-component-2" as pkdIdentifier
// Then creates PURL as pkg:guac/pkg/test-component-2@1.0.0 using guacCDXPkgPurl
falsePositivePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2@1.0.0")
CycloneDXFalsePositiveVexIngest = []assembler.VexIngest{
{
Pkg: falsePositivePkg,
Vulnerability: VulnSpecFalsePositive,
VexData: VexDataFalsePositive,
},
}
CycloneDXFalsePositivePredicates = assembler.IngestPredicates{
HasSBOM: HasSBOMVexFalsePositive,
VulnMetadata: CycloneDXFalsePositiveVulnMetadata,
Vex: CycloneDXFalsePositiveVexIngest,
// Note: No CertifyVuln because status is NotAffected (not Affected/UnderInvestigation)
}

// DSSE/SLSA Testdata

Expand Down
10 changes: 6 additions & 4 deletions pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ var json = jsoniter.ConfigCompatibleWithStandardLibrary
var zeroTime = time.Unix(0, 0).UTC()

var vexStatusMap = map[cdx.ImpactAnalysisState]model.VexStatus{
cdx.IASResolved: model.VexStatusFixed,
cdx.IASExploitable: model.VexStatusAffected,
cdx.IASInTriage: model.VexStatusUnderInvestigation,
cdx.IASNotAffected: model.VexStatusNotAffected,
cdx.IASResolved: model.VexStatusFixed,
cdx.IASExploitable: model.VexStatusAffected,
cdx.IASInTriage: model.VexStatusUnderInvestigation,
cdx.IASNotAffected: model.VexStatusNotAffected,
cdx.IASResolvedWithPedigree: model.VexStatusFixed,
cdx.IASFalsePositive: model.VexStatusNotAffected,
}

var justificationsMap = map[cdx.ImpactAnalysisJustification]model.VexJustification{
Expand Down
18 changes: 18 additions & 0 deletions pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,24 @@ func Test_cyclonedxParser(t *testing.T) {
},
wantPredicates: &testdata.XraySBOMVulnsPredicates,
wantErr: false,
}, {
name: "valid CycloneDX VEX document with resolved_with_pedigree status",
doc: &processor.Document{
Blob: testdata.CycloneDXVEXResolvedWithPedigree,
Format: processor.FormatJSON,
Type: processor.DocumentCycloneDX,
},
wantPredicates: &testdata.CycloneDXResolvedWithPedigreePredicates,
wantErr: false,
}, {
name: "valid CycloneDX VEX document with false_positive status",
doc: &processor.Document{
Blob: testdata.CycloneDXVEXFalsePositive,
Format: processor.FormatJSON,
Type: processor.DocumentCycloneDX,
},
wantPredicates: &testdata.CycloneDXFalsePositivePredicates,
wantErr: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down