From 1af097e9b93de69c9a3d80ae9cb01f9aca8ce83f Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Fri, 27 Oct 2023 15:03:56 +0300 Subject: [PATCH] feat(reporter): Support the CycloneDX vulnerability extension in Reporter Signed-off-by: George Andrinopoulos --- ...eporter-expected-result-with-findings.json | 17 +++++---- .../cyclonedx-reporter-expected-result.json | 35 ++++++++++++++++--- .../cyclonedx-reporter-expected-result.xml | 30 +++++++++++++--- .../kotlin/CycloneDxReporterFunTest.kt | 17 +++++---- .../src/main/kotlin/CycloneDxReporter.kt | 35 +++++++++++++++++++ reporter/src/testFixtures/kotlin/TestData.kt | 4 ++- 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result-with-findings.json b/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result-with-findings.json index a77dad2464805..f4960f487171c 100644 --- a/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result-with-findings.json +++ b/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result-with-findings.json @@ -79,7 +79,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:concluded-license:1.0" }, { "group": "@ort", @@ -124,7 +125,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:declared-license:1.0" }, { "group": "@ort", @@ -159,7 +161,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:license-file:1.0" }, { "group": "@ort", @@ -204,7 +207,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:license-file-and-additional-licenses:1.0" }, { "group": "@ort", @@ -233,7 +237,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:no-license-file:1.0" } ], "externalReferences": [ @@ -255,4 +260,4 @@ "comment": "Package-URL of the project" } ] -} +} \ No newline at end of file diff --git a/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.json b/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.json index 45bd6e09edb62..295b136bbcb15 100644 --- a/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.json +++ b/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.json @@ -79,7 +79,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:concluded-license:1.0" }, { "group": "@ort", @@ -124,7 +125,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:declared-license:1.0" }, { "group": "@ort", @@ -159,7 +161,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:license-file:1.0" }, { "group": "@ort", @@ -204,7 +207,8 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:license-file-and-additional-licenses:1.0" }, { "group": "@ort", @@ -233,7 +237,28 @@ "url": "https://github.com/oss-review-toolkit/ort" } ], - "type": "library" + "type": "library", + "bom-ref": "NPM:@ort:no-license-file:1.0" + } + ], + "vulnerabilities": [ + { + "id": "CVE-2021-1234", + "ratings": [ + { + "source": { + "url": "https://cves.example.org/cve1" + }, + "severity": "medium" + } + ], + "description": "A vulnerability description", + "detail": "A vulnerability summary", + "affects": [ + { + "ref": "NPM:@ort:declared-license:1.0" + } + ] } ] } \ No newline at end of file diff --git a/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.xml b/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.xml index f026cf3d4d896..f709609760b2d 100644 --- a/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.xml +++ b/plugins/reporters/cyclonedx/src/funTest/assets/cyclonedx-reporter-expected-result.xml @@ -15,7 +15,7 @@ - + @ort concluded-license 1.0 @@ -49,7 +49,7 @@ false https://github.com/oss-review-toolkit/ortdirect - + @ort declared-license 1.0 @@ -72,7 +72,7 @@ false https://github.com/oss-review-toolkit/ortdirect - + @ort license-file 1.0 @@ -91,7 +91,7 @@ false https://github.com/oss-review-toolkit/ortdirect - + @ort license-file-and-additional-licenses 1.0 @@ -114,7 +114,7 @@ false https://github.com/oss-review-toolkit/ortdirect - + @ort no-license-file 1.0 @@ -131,4 +131,24 @@ https://github.com/oss-review-toolkit/ortdirect + + + CVE-2021-1234 + + + + https://cves.example.org/cve1 + + medium + + + A vulnerability description + A vulnerability summary + + + NPM:@ort:declared-license:1.0 + + + + diff --git a/plugins/reporters/cyclonedx/src/funTest/kotlin/CycloneDxReporterFunTest.kt b/plugins/reporters/cyclonedx/src/funTest/kotlin/CycloneDxReporterFunTest.kt index 81645a421d3e4..a45b631443cd9 100644 --- a/plugins/reporters/cyclonedx/src/funTest/kotlin/CycloneDxReporterFunTest.kt +++ b/plugins/reporters/cyclonedx/src/funTest/kotlin/CycloneDxReporterFunTest.kt @@ -34,6 +34,7 @@ import org.cyclonedx.parsers.JsonParser import org.cyclonedx.parsers.XmlParser import org.ossreviewtoolkit.reporter.ORT_RESULT +import org.ossreviewtoolkit.reporter.ORT_RESULT_WITH_VULNERABILITIES import org.ossreviewtoolkit.reporter.ReporterInput import org.ossreviewtoolkit.utils.common.normalizeLineBreaks import org.ossreviewtoolkit.utils.test.getAssetAsString @@ -67,7 +68,8 @@ class CycloneDxReporterFunTest : WordSpec({ val expectedBom = getAssetAsString("cyclonedx-reporter-expected-result.xml") val xmlOptions = optionSingle + mapOf("output.file.formats" to "xml") - val bomFile = CycloneDxReporter().generateReport(ReporterInput(ORT_RESULT), outputDir, xmlOptions).single() + val bomFile = CycloneDxReporter() + .generateReport(ReporterInput(ORT_RESULT_WITH_VULNERABILITIES), outputDir, xmlOptions).single() val actualBom = bomFile.readText().patchCycloneDxResult().normalizeLineBreaks() actualBom shouldBe expectedBom @@ -76,7 +78,8 @@ class CycloneDxReporterFunTest : WordSpec({ "be valid JSON according to schema version $defaultSchemaVersion" { val jsonOptions = optionSingle + mapOf("output.file.formats" to "json") - val bomFile = CycloneDxReporter().generateReport(ReporterInput(ORT_RESULT), outputDir, jsonOptions).single() + val bomFile = CycloneDxReporter() + .generateReport(ReporterInput(ORT_RESULT_WITH_VULNERABILITIES), outputDir, jsonOptions).single() bomFile shouldBe aFile() bomFile shouldNotBe emptyFile() @@ -87,7 +90,8 @@ class CycloneDxReporterFunTest : WordSpec({ val expectedBom = getAssetAsString("cyclonedx-reporter-expected-result.json") val jsonOptions = optionSingle + mapOf("output.file.formats" to "json") - val bomFile = CycloneDxReporter().generateReport(ReporterInput(ORT_RESULT), outputDir, jsonOptions).single() + val bomFile = CycloneDxReporter() + .generateReport(ReporterInput(ORT_RESULT_WITH_VULNERABILITIES), outputDir, jsonOptions).single() val actualBom = bomFile.readText().patchCycloneDxResult() actualBom shouldEqualJson expectedBom @@ -100,7 +104,8 @@ class CycloneDxReporterFunTest : WordSpec({ "create one file per project" { val jsonOptions = optionMulti + mapOf("output.file.formats" to "json") - val bomFiles = CycloneDxReporter().generateReport(ReporterInput(ORT_RESULT), outputDir, jsonOptions) + val bomFiles = CycloneDxReporter() + .generateReport(ReporterInput(ORT_RESULT_WITH_VULNERABILITIES), outputDir, jsonOptions) bomFiles shouldHaveSize 2 } @@ -109,7 +114,7 @@ class CycloneDxReporterFunTest : WordSpec({ val xmlOptions = optionMulti + mapOf("output.file.formats" to "xml") val (bomFileProjectWithFindings, bomFileProjectWithoutFindings) = CycloneDxReporter() - .generateReport(ReporterInput(ORT_RESULT), outputDir, xmlOptions).also { + .generateReport(ReporterInput(ORT_RESULT_WITH_VULNERABILITIES), outputDir, xmlOptions).also { it shouldHaveSize 2 } @@ -132,7 +137,7 @@ class CycloneDxReporterFunTest : WordSpec({ val jsonOptions = optionMulti + mapOf("output.file.formats" to "json") val (bomFileProjectWithFindings, bomFileProjectWithoutFindings) = CycloneDxReporter() - .generateReport(ReporterInput(ORT_RESULT), outputDir, jsonOptions).also { + .generateReport(ReporterInput(ORT_RESULT_WITH_VULNERABILITIES), outputDir, jsonOptions).also { it shouldHaveSize 2 } diff --git a/plugins/reporters/cyclonedx/src/main/kotlin/CycloneDxReporter.kt b/plugins/reporters/cyclonedx/src/main/kotlin/CycloneDxReporter.kt index 276ba37b61259..120df23ddd8e1 100644 --- a/plugins/reporters/cyclonedx/src/main/kotlin/CycloneDxReporter.kt +++ b/plugins/reporters/cyclonedx/src/main/kotlin/CycloneDxReporter.kt @@ -39,11 +39,13 @@ import org.cyclonedx.model.Metadata import org.cyclonedx.model.metadata.ToolInformation import org.ossreviewtoolkit.model.FileFormat +import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.LicenseSource import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.Project import org.ossreviewtoolkit.model.licenses.ResolvedLicenseInfo import org.ossreviewtoolkit.model.utils.toPurl +import org.ossreviewtoolkit.model.vulnerabilities.Vulnerability import org.ossreviewtoolkit.reporter.Reporter import org.ossreviewtoolkit.reporter.ReporterInput import org.ossreviewtoolkit.utils.common.isFalse @@ -202,6 +204,8 @@ class CycloneDxReporter : Reporter { addPackageToBom(input, pkg, bom, dependencyType) } + addVulnerabilitiesToBom(input.ortResult.getVulnerabilities(), bom) + outputFiles += writeBom(bom, schemaVersion, outputDir, REPORT_BASE_FILENAME, outputFileFormats) } else { projects.forEach { project -> @@ -244,6 +248,8 @@ class CycloneDxReporter : Reporter { addPackageToBom(input, pkg, bom, dependencyType) } + addVulnerabilitiesToBom(input.ortResult.getVulnerabilities(), bom) + val reportName = "$REPORT_BASE_FILENAME-${project.id.toPath("-")}" outputFiles += writeBom(bom, schemaVersion, outputDir, reportName, outputFileFormats) } @@ -252,6 +258,34 @@ class CycloneDxReporter : Reporter { return outputFiles } + private fun addVulnerabilitiesToBom(advisorVulnerabilities: Map>, bom: Bom) { + val vulnerabilities = mutableListOf() + advisorVulnerabilities.forEach { + val vulnerabilityBomRef = it.key.toCoordinates() + it.value.forEach { + val vulnerability = org.cyclonedx.model.vulnerability.Vulnerability().apply { + id = it.id + description = it.description + detail = it.summary + ratings = it.references.map { reference -> + org.cyclonedx.model.vulnerability.Vulnerability.Rating().apply { + source = org.cyclonedx.model.vulnerability.Vulnerability.Source() + .apply { url = reference.url.toString() } + severity = org.cyclonedx.model.vulnerability.Vulnerability.Rating.Severity + .fromString(reference.severityRating.lowercase()) + } + } + affects = mutableListOf( + org.cyclonedx.model.vulnerability.Vulnerability.Affect() + .apply { ref = vulnerabilityBomRef } + ) + } + vulnerabilities.add(vulnerability) + } + bom.vulnerabilities = vulnerabilities + } + } + private fun addPackageToBom(input: ReporterInput, pkg: Package, bom: Bom, dependencyType: String) { val resolvedLicenseInfo = input.licenseInfoResolver.resolveLicenseInfo(pkg.id).filterExcluded() .applyChoices(input.ortResult.getPackageLicenseChoices(pkg.id)) @@ -280,6 +314,7 @@ class CycloneDxReporter : Reporter { name = pkg.id.name version = pkg.id.version description = pkg.description + bomRef = pkg.id.toCoordinates() // TODO: Map package-manager-specific OPTIONAL scopes. scope = if (input.ortResult.isExcluded(pkg.id)) { diff --git a/reporter/src/testFixtures/kotlin/TestData.kt b/reporter/src/testFixtures/kotlin/TestData.kt index 15aa19e77382c..284d17c8fc423 100644 --- a/reporter/src/testFixtures/kotlin/TestData.kt +++ b/reporter/src/testFixtures/kotlin/TestData.kt @@ -405,8 +405,10 @@ val ORT_RESULT = OrtResult( val VULNERABILITY = Vulnerability( id = "CVE-2021-1234", + summary = "A vulnerability summary", + description = "A vulnerability description", references = listOf( - VulnerabilityReference(URI("https://cves.example.org/cve1"), "Cvss2", "MEDIUM") + VulnerabilityReference(URI("https://cves.example.org/cve1"), "Cvss2", "6.0") ) )