Skip to content

Commit

Permalink
feat(advisor): Add BlackDuck as advisor
Browse files Browse the repository at this point in the history
Signed-off-by: Frank Viernau <x9fviern@zeiss.com>
  • Loading branch information
fviernau committed Dec 20, 2024
1 parent 958c08c commit 3eda163
Show file tree
Hide file tree
Showing 12 changed files with 6,184 additions and 0 deletions.
9 changes: 9 additions & 0 deletions buildSrc/src/main/kotlin/ort-base-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ repositories {
includeGroup("org.gradle")
}
}

maven {
// com.blackducksoftware.bdio:bdio2
url = uri("https://sig-repo.synopsys.com/bds-bdio-release")
}

maven { // com.blackducksoftware.magpie:magpie
url = uri("https://repo.blackduck.com/bds-integrations-release")
}
}

tasks.withType<Jar>().configureEach {
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ versionsPlugin = "0.51.0"
aeSecurity = "0.132.0"
asciidoctorj = "3.0.0"
asciidoctorjPdf = "2.3.19"
blackduckCommon = "66.2.19"
blackduckCommonApi = "2023.10.0.4"
clikt = "5.0.2"
commonsCompress = "1.27.1"
cyclonedx = "10.0.0"
Expand Down Expand Up @@ -90,6 +92,8 @@ aeSecurity = { module = "org.metaeffekt.core:ae-security", version.ref = "aeSecu
asciidoctorj = { module = "org.asciidoctor:asciidoctorj", version.ref = "asciidoctorj" }
asciidoctorj-pdf = { module = "org.asciidoctor:asciidoctorj-pdf", version.ref = "asciidoctorjPdf" }
awsS3 = { module = "software.amazon.awssdk:s3", version.ref = "s3" }
blackduck-common = { module = "com.synopsys.integration:blackduck-common", version.ref = "blackduckCommon" }
blackduck-common-api = { module = "com.synopsys.integration:blackduck-common-api", version.ref = "blackduckCommonApi" }
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
commonsCompress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" }
cyclonedx = { module = "org.cyclonedx:cyclonedx-core-java", version.ref = "cyclonedx" }
Expand Down
44 changes: 44 additions & 0 deletions plugins/advisors/black-duck/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2020 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

plugins {
// Apply precompiled plugins.
id("ort-plugin-conventions")

// Apply third-party plugins.
alias(libs.plugins.kotlinSerialization)
}

dependencies {
api(projects.advisor)
api(projects.model)

implementation(projects.utils.ortUtils)

implementation(libs.blackduck.common)
implementation(libs.blackduck.common.api)
implementation(libs.bundles.ks3)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)

implementation(projects.utils.commonUtils)
implementation(projects.utils.ortUtils)

ksp(projects.advisor)
}
5,521 changes: 5,521 additions & 0 deletions plugins/advisors/black-duck/src/funTest/assets/response-cache.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
Crate::sys-info:0.7.0:
advisor:
name: "BlackDuck"
capabilities:
- "VULNERABILITIES"
summary:
start_time: "1970-01-01T00:00:00Z"
end_time: "1970-01-01T00:00:00Z"
vulnerabilities:
- id: "CVE-2020-36434"
description: "An issue was discovered in the sys-info crate before 0.8.0 for Rust.\
\ sys_info::disk_info calls can trigger a double free."
references:
- url: "https://BLACK_DUCK_SERVER_HOST/api/vulnerabilities/CVE-2020-36434"
scoring_system: "CVSS:3.1"
severity: "CRITICAL"
score: 9.8
vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
- url: "https://BLACK_DUCK_SERVER_HOST/api/cwes/CWE-415"
scoring_system: "CVSS:3.1"
severity: "CRITICAL"
score: 9.8
vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
- url: "https://BLACK_DUCK_SERVER_HOST/api/vulnerabilities/BDSA-2020-4804"
scoring_system: "CVSS:3.1"
severity: "CRITICAL"
score: 9.8
vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
111 changes: 111 additions & 0 deletions plugins/advisors/black-duck/src/funTest/kotlin/BlackDuckFunTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.plugins.advisors.blackduck

import io.kotest.core.spec.style.WordSpec
import io.kotest.inspectors.forAll
import io.kotest.matchers.collections.beEmpty
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNot

import java.time.Instant

import org.ossreviewtoolkit.advisor.normalizeVulnerabilityData
import org.ossreviewtoolkit.model.AdvisorResult
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.readValue
import org.ossreviewtoolkit.model.toYaml
import org.ossreviewtoolkit.utils.test.getAssetFile
import org.ossreviewtoolkit.utils.test.identifierToPackage

class BlackDuckFunTest : WordSpec({
/**
* To run the test against a real instance, and / or to re-record the 'response-cache.json':
* 1. Define the below environment variables: BLACK_DUCK_SERVER_URL and BLACK_DUCK_API_TOKEN
* 2. Delete 'response-cache.json'
* 3. Run the functional test.
*/
val serverUrl = runCatching { System.getenv("BLACK_DUCK_SERVER_URL") }.getOrNull()
val apiToken = runCatching { System.getenv("BLACK_DUCK_API_TOKEN") }.getOrNull()
val componentServiceClient = ResponseCachingComponentServiceClient(
cacheFile = getAssetFile("response-cache.json"),
serverUrl = serverUrl,
apiToken = apiToken
)

val blackDuck = BlackDuck(BlackDuckFactory.descriptor, componentServiceClient)

afterEach { componentServiceClient.flush() }

"retrievePackageFindings()" should {
"return the vulnerabilities for the supported ecosystems" {
val packages = setOf(
// TODO: Add hackage / pod
"Crate::sys-info:0.7.0",
"Gem::rack:2.0.4",
"Maven:com.jfinal:jfinal:1.4",
"NPM::rebber:1.0.0",
"NuGet::Bunkum:4.0.0",
"Pub::http:0.13.1",
"PyPI::django:3.2"
).mapTo(mutableSetOf()) {
identifierToPackage(it)
}

val packageFindings = blackDuck.retrievePackageFindings(packages).mapKeys { it.key.id.toCoordinates() }

packageFindings.keys shouldContainExactlyInAnyOrder packages.map { it.id.toCoordinates() }
packageFindings.keys.forAll { id ->
packageFindings.getValue(id).vulnerabilities shouldNot beEmpty()
}
}

"return the expected result for the given package(s)" {
val expectedResult = getAssetFile("retrieve-package-findings-expected-result.yml")
.readValue<Map<Identifier, AdvisorResult>>()
val packages = setOf(
// Package using CVSS 3.1 vector:
"Crate::sys-info:0.7.0"
// Todo: Add a package using CVSS 2 vector:
).mapTo(mutableSetOf()) {
identifierToPackage(it)
}

val packageFindings = blackDuck.retrievePackageFindings(packages).mapKeys { it.key.id }

packageFindings.patchTimes().toYaml().patchServerUrl(serverUrl) shouldBe
expectedResult.patchTimes().toYaml()
}
}
})

private fun Map<Identifier, AdvisorResult>.patchTimes(): Map<Identifier, AdvisorResult> =
mapValues { (_, advisorResult) ->
advisorResult.normalizeVulnerabilityData().copy(
summary = advisorResult.summary.copy(
startTime = Instant.EPOCH,
endTime = Instant.EPOCH
)
)
}

internal fun String.patchServerUrl(serverUrl: String?) =
serverUrl?.let { replace(it, "https://BLACK_DUCK_SERVER_HOST") } ?: this
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.plugins.advisors.blackduck

import com.google.gson.GsonBuilder

import com.synopsys.integration.blackduck.api.generated.response.ComponentsView
import com.synopsys.integration.blackduck.api.generated.view.OriginView
import com.synopsys.integration.blackduck.api.generated.view.VulnerabilityView

import java.io.File
import java.util.concurrent.ConcurrentHashMap

/**
* This ComponentServiceClient uses a file backed cache for the responses.
* So, if the cache contains all responses for a particular test, an instance of this class can be used as a fake
* ComponentServiceClient.
*/
internal class ResponseCachingComponentServiceClient(
private val cacheFile: File,
private val serverUrl: String?,
apiToken: String?
) : ComponentServiceClient {
// The BlackDuck library uses GSON to serialize its POJOs. So use GSON too because this is the simplest option.
private val gson = GsonBuilder().setPrettyPrinting().create()

private val cache = if (cacheFile.isFile) {
gson.fromJson(cacheFile.readText(), ResponseCache::class.java)
} else {
ResponseCache()
}

private val delegate = if (serverUrl != null && apiToken != null) {
ExtendedComponentService.create(serverUrl, apiToken)
} else {
null
}

override fun searchKbComponentsByPurl(purl: String): List<ComponentsView> =
cache.componentsViewsForPurl.getOrPut(purl) {
delegate?.searchKbComponentsByPurl(purl).orEmpty()
}

override fun getOriginView(searchResult: ComponentsView): OriginView? =
cache.originViewForOriginUuid.getOrPut(searchResult.originId) {
delegate?.getOriginView(searchResult)
}

override fun getVulnerabilities(originView: OriginView): List<VulnerabilityView> =
cache.vulnerabilitiesForOriginUuid.getOrPut(originView.originId) {
delegate?.getVulnerabilities(originView).orEmpty()
}

fun flush() {
if (delegate != null) {
val json = gson.toJson(cache).patchServerUrl(serverUrl)
cacheFile.writeText(json)
}
}
}

private class ResponseCache {
val componentsViewsForPurl = ConcurrentHashMap<String, List<ComponentsView>>()
val originViewForOriginUuid = ConcurrentHashMap<String, OriginView?>()
val vulnerabilitiesForOriginUuid = ConcurrentHashMap<String, List<VulnerabilityView>>()
}
Loading

0 comments on commit 3eda163

Please sign in to comment.