Skip to content

Commit

Permalink
feat(black-duck): Add a mechanism to query vulnerabilities by origin-id
Browse files Browse the repository at this point in the history
By default, the vulnerabilities are queried by the respective purl. This
does not work in all cases, e.g.:

1. Some origins do not (yet) have a purl associated.
2. For some ecosystem, querying by purl doesn't work, even though the
   data sets have a purl.
3. The knowledge base may not contain a data set for the exact package
   but for the same "package" in a different ecosystem. For example,
   query vulnerabilities for the "ubuntu" package instead of for the
   "github" package.

So, allow to set the origin-id via a package label curation, to override
the origin for which the vulnerabilities shall be retrieved.

Note: Since the amount of external namespaces is rather large, test the
querying only for a few ecosystems. This should be fine, because there
is very little namespace specific logic in ORT's code involved.

Signed-off-by: Frank Viernau <x9fviern@zeiss.com>
  • Loading branch information
fviernau committed Jan 23, 2025
1 parent 2a3c692 commit 44c9067
Show file tree
Hide file tree
Showing 7 changed files with 559 additions and 3 deletions.
411 changes: 411 additions & 0 deletions plugins/advisors/black-duck/src/funTest/assets/recorded-responses.json

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions plugins/advisors/black-duck/src/funTest/kotlin/BlackDuckFunTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import java.time.Instant
import org.ossreviewtoolkit.advisor.normalizeVulnerabilityData
import org.ossreviewtoolkit.model.AdvisorResult
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.readValue
import org.ossreviewtoolkit.model.toYaml
import org.ossreviewtoolkit.utils.common.Os
Expand Down Expand Up @@ -80,6 +81,28 @@ class BlackDuckFunTest : WordSpec({
}
}

"return the vulnerabilities for some supported namespaces by origin-id" {
val packages = setOf(
"Github::behdad/harbuzz:2.2.0" to "github:behdad/harfbuzz:2.2.0",
"NuGet::Bunkum:4.0.0" to "nuget:Bunkum/4.0.0",
"Pypi:donfig:0.2.0" to "pypi:donfig/0.2.0"
).mapTo(mutableSetOf()) { (coordinates, originId) ->
Package.EMPTY.copy(
id = Identifier(coordinates),
labels = mapOf(
BlackDuck.PACKAGE_LABEL_BLACK_DUCK_ORIGIN_ID to originId
)
)
}

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>>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.ossreviewtoolkit.plugins.advisors.blackduck

import com.blackduck.integration.bdio.model.externalid.ExternalId
import com.blackduck.integration.blackduck.api.generated.response.ComponentsView
import com.blackduck.integration.blackduck.api.generated.view.OriginView
import com.blackduck.integration.blackduck.api.generated.view.VulnerabilityView
Expand Down Expand Up @@ -60,6 +61,11 @@ internal class ResponseCachingComponentServiceClient(
delegate?.searchKbComponentsByPurl(purl).orEmpty()
}

override fun searchKbComponentsByExternalId(externalId: ExternalId): List<ComponentsView> =
cache.componentsViewsForExternalId.getOrPut(externalId.createExternalId()) {
delegate?.searchKbComponentsByExternalId(externalId).orEmpty()
}

override fun getOriginView(searchResult: ComponentsView): OriginView? =
cache.originViewForComponentsViewKey.getOrPut(searchResult.key) {
delegate?.getOriginView(searchResult)
Expand All @@ -80,6 +86,7 @@ internal class ResponseCachingComponentServiceClient(

private class ResponseCache {
val componentsViewsForPurl = ConcurrentHashMap<String, List<ComponentsView>>()
val componentsViewsForExternalId = ConcurrentHashMap<String, List<ComponentsView>>()
val originViewForComponentsViewKey = ConcurrentHashMap<String, OriginView?>()
val vulnerabilityViewsForOriginViewKey = ConcurrentHashMap<String, List<VulnerabilityView>>()
}
Expand Down
54 changes: 51 additions & 3 deletions plugins/advisors/black-duck/src/main/kotlin/BlackDuck.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.ossreviewtoolkit.model.AdvisorSummary
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Issue
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.Severity
import org.ossreviewtoolkit.model.createAndLogIssue
import org.ossreviewtoolkit.model.vulnerabilities.Cvss2Rating
import org.ossreviewtoolkit.model.vulnerabilities.Vulnerability
Expand All @@ -48,6 +49,11 @@ import org.ossreviewtoolkit.plugins.api.PluginDescriptor
import org.ossreviewtoolkit.utils.common.collectMessages
import org.ossreviewtoolkit.utils.common.enumSetOf

/**
* This advice provider by default retrieves vulnerabilities by the purl corresponding to the package. If a package has
* the label [BlackDuck.PACKAGE_LABEL_BLACK_DUCK_ORIGIN_ID] set, then the vulnerabilities are retrieved by that
* origin-id instead of by the purl.
*/
@OrtPlugin(
displayName = "Black Duck",
description = "An advisor that retrieves vulnerability information from a Black Duck instance.",
Expand All @@ -57,6 +63,14 @@ class BlackDuck(
override val descriptor: PluginDescriptor,
private val blackDuckApi: ComponentServiceClient
) : AdviceProvider {
companion object {
/**
* The key of the package label for specifying the Black Duck origin-id in the form
* "$externalNamespace:$externalId", see also [BlackDuckOriginId.parse].
*/
const val PACKAGE_LABEL_BLACK_DUCK_ORIGIN_ID = "black-duck:origin-id"
}

override val details = AdvisorDetails(descriptor.id, enumSetOf(AdvisorCapability.VULNERABILITIES))

constructor(descriptor: PluginDescriptor, config: BlackDuckConfiguration) : this(
Expand Down Expand Up @@ -99,16 +113,39 @@ class BlackDuck(
}

private fun getOrigins(pkg: Package, issues: MutableList<Issue>): List<OriginView> {
val searchResults = runCatching {
blackDuckApi.searchKbComponentsByPurl(pkg.purl)
val externalId = runCatching {
pkg.blackDuckOriginId?.let { BlackDuckOriginId.parse(it).toExternalId() }
}.getOrElse {
issues += createAndLogIssue(
source = descriptor.displayName,
message = "Requesting origins for purl ${pkg.purl} failed: ${it.collectMessages()}"
message = "Could not parse origin-id '${pkg.blackDuckOriginId}' for '${pkg.id.toCoordinates()}: " +
it.collectMessages()
)
return emptyList()
}

val searchResults = if (externalId != null) {
runCatching {
blackDuckApi.searchKbComponentsByExternalId(externalId)
}.getOrElse {
issues += createAndLogIssue(
source = descriptor.displayName,
message = "Requesting origins for externalId '$externalId' failed: ${it.collectMessages()}"
)
return emptyList()
}
} else {
runCatching {
blackDuckApi.searchKbComponentsByPurl(pkg.purl)
}.getOrElse {
issues += createAndLogIssue(
source = descriptor.displayName,
message = "Requesting origins for purl ${pkg.purl} failed: ${it.collectMessages()}"
)
return emptyList()
}
}

val origins = searchResults.mapNotNull { searchResult ->
runCatching {
blackDuckApi.getOriginView(searchResult)
Expand All @@ -129,6 +166,15 @@ class BlackDuck(
}
}

if (externalId != null && origins.isEmpty()) {
issues += createAndLogIssue(
source = descriptor.displayName,
message = "The origin-id '${pkg.blackDuckOriginId} of package ${pkg.id.toCoordinates()} does not " +
"match any origin.",
severity = Severity.WARNING
)
}

return origins
}

Expand Down Expand Up @@ -195,3 +241,5 @@ private fun Map<Identifier, List<OriginView>>.getSummary(): String =
}
}
}

private val Package.blackDuckOriginId: String? get() = labels[BlackDuck.PACKAGE_LABEL_BLACK_DUCK_ORIGIN_ID]
60 changes: 60 additions & 0 deletions plugins/advisors/black-duck/src/main/kotlin/BlackDuckOriginId.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2025 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.blackduck.integration.bdio.model.Forge
import com.blackduck.integration.bdio.model.externalid.ExternalId

/**
* A unique identifier of a Black Duck Origin, see also
* https://community.blackduck.com/s/article/What-is-an-Origin-and-Origin-ID-in-Blackduck.
* Note: While the term "origin" is still current, the properties `originType` and`originId` haven been deprecated in
* favor of `externalNamespace` and `externalId`.
*/
internal data class BlackDuckOriginId(
/**
* The namespace such as 'maven', 'pypi' or 'github'.
*/
val externalNamespace: String,

/**
* The component's identifier within the external namespace.
*/
val externalId: String
) {
fun toExternalId(): ExternalId {
val forge = requireNotNull(Forge.getKnownForges()[externalNamespace]) {
"Unknown forge for namespace: '$externalNamespace'."
}

return ExternalId.createFromExternalId(forge, externalId, null, null)
}

companion object {
fun parse(coordinates: String): BlackDuckOriginId {
val parts = coordinates.split(':', limit = 2)
require(parts.size == 2) {
"Could not parse originId '$coordinates'. Missing ':' separator ."
}

return BlackDuckOriginId(externalNamespace = parts[0], externalId = parts[1])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.ossreviewtoolkit.plugins.advisors.blackduck

import com.blackduck.integration.bdio.model.externalid.ExternalId
import com.blackduck.integration.blackduck.api.generated.response.ComponentsView
import com.blackduck.integration.blackduck.api.generated.view.OriginView
import com.blackduck.integration.blackduck.api.generated.view.VulnerabilityView
Expand All @@ -29,4 +30,6 @@ interface ComponentServiceClient {
fun getVulnerabilities(originView: OriginView): List<VulnerabilityView>

fun searchKbComponentsByPurl(purl: String): List<ComponentsView>

fun searchKbComponentsByExternalId(externalId: ExternalId): List<ComponentsView>
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.ossreviewtoolkit.plugins.advisors.blackduck

import com.blackduck.integration.bdio.model.externalid.ExternalId
import com.blackduck.integration.blackduck.api.core.BlackDuckPath
import com.blackduck.integration.blackduck.api.core.response.LinkMultipleResponses
import com.blackduck.integration.blackduck.api.generated.discovery.ApiDiscovery
Expand Down Expand Up @@ -80,6 +81,9 @@ internal class ExtendedComponentService(
return blackDuckApiClient.getAllResponses(request)
}

override fun searchKbComponentsByExternalId(externalId: ExternalId): List<ComponentsView> =
getAllSearchResults(externalId)

override fun getComponentView(searchResult: ComponentsView): Optional<ComponentView> {
// The super function accidentally uses the URL to the version view, while it should use the URL to the
// component view. This override fixes that.
Expand Down

0 comments on commit 44c9067

Please sign in to comment.