diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json b/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json new file mode 100644 index 0000000000..2a35b67e5e --- /dev/null +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json @@ -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" + } + ] + } + ] + } + ] +} diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json b/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json new file mode 100644 index 0000000000..d5420774b7 --- /dev/null +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json @@ -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" + } + ] + } + ] + } + ] +} diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index e8ee92cac1..ae86676d10 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -120,6 +120,18 @@ 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-resolved-with-pedigree-no-detail.json + CycloneDXVEXResolvedWithPedigreeNoDetail []byte + + //go:embed exampledata/cyclonedx-vex-false-positive-no-detail.json + CycloneDXVEXFalsePositiveNoDetail []byte + //go:embed exampledata/cyclonedx-vex.xml CyloneDXVEXExampleXML []byte @@ -304,6 +316,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", + 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{ @@ -326,6 +385,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 diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index cdd61dd4a2..2bd8ca2c29 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -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{ @@ -577,6 +579,8 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { vd.KnownSince = publishedTime vd.Statement = vulnerability.Description + // Extract StatusNotes from analysis detail field. + // This applies to all analysis states including resolved_with_pedigree and false_positive. if vulnerability.Analysis.Detail != "" { vd.StatusNotes = vulnerability.Analysis.Detail } else if vulnerability.Analysis.Response != nil { @@ -585,8 +589,12 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { response = append(response, string(res)) } vd.StatusNotes = strings.Join(response, ",") - } else { + } else if vulnerability.Detail != "" { vd.StatusNotes = vulnerability.Detail + } else { + // If all detail fields are empty, preserve the CDX state enum information + // to avoid losing this metadata (e.g., "CDX state: resolved_with_pedigree") + vd.StatusNotes = fmt.Sprintf("CDX state: %s", string(vulnerability.Analysis.State)) } } else { vd = model.VexStatementInputSpec{ diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go index 48e64b590a..5f607fe0b0 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx_test.go @@ -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) {