Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions advisor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {

implementation(projects.utils.ortUtils)

implementation(libs.aeSecurity)
implementation(libs.kotlinx.coroutines)

testImplementation(libs.mockk)
Expand Down
46 changes: 44 additions & 2 deletions advisor/src/main/kotlin/Advisor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ import kotlinx.coroutines.withContext

import org.apache.logging.log4j.kotlin.logger

import org.metaeffekt.core.security.cvss.CvssVector

import org.ossreviewtoolkit.model.AdvisorResult
import org.ossreviewtoolkit.model.AdvisorRun
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.OrtResult
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
import org.ossreviewtoolkit.model.vulnerabilities.Vulnerability
import org.ossreviewtoolkit.model.vulnerabilities.VulnerabilityReference
import org.ossreviewtoolkit.plugins.api.PluginConfig
import org.ossreviewtoolkit.utils.ort.Environment

Expand Down Expand Up @@ -98,9 +102,13 @@ class Advisor(
providerResults
}
}.forEach { providerResults ->
// Merge results from different providers into a single map keyed by the package ID. The original
// provider is still maintained as part of the AdvisorResult's AdvisorDetails.
providerResults.await().forEach { (pkg, advisorResults) ->
results.merge(pkg.id, listOf(advisorResults)) { oldResults, newResults ->
oldResults + newResults
val normalizedResults = advisorResults.normalizeVulnerabilityData()

results.merge(pkg.id, listOf(normalizedResults)) { existingResults, additionalResults ->
existingResults + additionalResults
}
}
}
Expand All @@ -111,3 +119,37 @@ class Advisor(
AdvisorRun(startTime, endTime, Environment(), config, results)
}
}

fun AdvisorResult.normalizeVulnerabilityData(): AdvisorResult =
copy(vulnerabilities = vulnerabilities.normalizeVulnerabilityData())

fun List<Vulnerability>.normalizeVulnerabilityData(): List<Vulnerability> =
map { vulnerability ->
val normalizedReferences = vulnerability.references.map { reference ->
reference
.run {
// Treat "MODERATE" as an alias for "MEDIUM" independently of the scoring system.
if (severity == "MODERATE") copy(severity = "MEDIUM") else this
}
.run {
// Reconstruct the base score from the vector if possible.
if (score == null && vector != null) {
val score = CvssVector.parseVector(vector)?.baseScore?.toFloat()
copy(score = score)
} else {
this
}
}
.run {
// Reconstruct the severity from the scoring system and score if possible.
if (severity == null && scoringSystem != null && score != null) {
val severity = VulnerabilityReference.getQualitativeRating(scoringSystem, score)?.name
copy(severity = severity)
} else {
this
}
}
}

vulnerability.copy(references = normalizedReferences)
}
1 change: 1 addition & 0 deletions advisor/src/test/kotlin/AdvisorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,5 @@ private fun mockkAdviceProvider(): AdviceProvider =
private fun mockkAdvisorResult(): AdvisorResult =
mockk<AdvisorResult>().apply {
every { vulnerabilities } returns emptyList()
every { copy(vulnerabilities = any()) } returns this
}
1 change: 0 additions & 1 deletion plugins/advisors/osv/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ dependencies {
implementation(projects.utils.commonUtils)
implementation(projects.utils.ortUtils)

implementation(libs.aeSecurity)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@
"references" : [ {
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2020-7764",
"scoring_system" : "CVSS_V3",
"severity" : "MODERATE",
"severity" : "MEDIUM",
"score" : 5.9,
"vector" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
}, {
"url" : "https://github.com/delvedor/find-my-way/commit/ab408354690e6b9cf3c4724befb3b3fa4bb90aac",
"scoring_system" : "CVSS_V3",
"severity" : "MODERATE",
"severity" : "MEDIUM",
"score" : 5.9,
"vector" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
}, {
"url" : "https://snyk.io/vuln/SNYK-JS-FINDMYWAY-1038269",
"scoring_system" : "CVSS_V3",
"severity" : "MODERATE",
"severity" : "MEDIUM",
"score" : 5.9,
"vector" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
}, {
"url" : "https://www.npmjs.com/package/find-my-way",
"scoring_system" : "CVSS_V3",
"severity" : "MODERATE",
"severity" : "MEDIUM",
"score" : 5.9,
"vector" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
} ]
Expand Down
3 changes: 2 additions & 1 deletion plugins/advisors/osv/src/funTest/kotlin/OsvFunTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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.Package
Expand Down Expand Up @@ -94,7 +95,7 @@ private fun createOsv(): Osv = OsvFactory.create()

private fun Map<Identifier, AdvisorResult>.patchTimes(): Map<Identifier, AdvisorResult> =
mapValues { (_, advisorResult) ->
advisorResult.copy(
advisorResult.normalizeVulnerabilityData().copy(
summary = advisorResult.summary.copy(
startTime = Instant.EPOCH,
endTime = Instant.EPOCH
Expand Down
15 changes: 3 additions & 12 deletions plugins/advisors/osv/src/main/kotlin/Osv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import kotlinx.serialization.json.contentOrNull

import org.apache.logging.log4j.kotlin.logger

import org.metaeffekt.core.security.cvss.CvssVector

import org.ossreviewtoolkit.advisor.AdviceProvider
import org.ossreviewtoolkit.advisor.AdviceProviderFactory
import org.ossreviewtoolkit.clients.osv.OsvServiceWrapper
Expand Down Expand Up @@ -157,7 +155,7 @@ private fun Vulnerability.toOrtVulnerability(): org.ossreviewtoolkit.model.vulne
it.type.name to it.score
}.ifEmpty {
listOf(null to null)
}.forEach { (scoringSystem, severity) ->
}.forEach { (scoringSystem, vector) ->
references.mapNotNullTo(ortReferences) { reference ->
val url = reference.url.trim().let { if (it.startsWith("://")) "https$it" else it }

Expand All @@ -167,17 +165,10 @@ private fun Vulnerability.toOrtVulnerability(): org.ossreviewtoolkit.model.vulne
// Use the 'severity' property of the unspecified 'databaseSpecific' object.
// See also https://github.com/google/osv.dev/issues/484.
val specificSeverity = databaseSpecific?.get("severity")

val baseScore = runCatching {
CvssVector.parseVector(severity)?.baseScore?.toFloat()
}.onFailure {
logger.debug { "Unable to parse CVSS vector '$severity': ${it.collectMessages()}." }
}.getOrNull()

val severityRating = (specificSeverity as? JsonPrimitive)?.contentOrNull
?: VulnerabilityReference.getQualitativeRating(scoringSystem, baseScore)?.name

VulnerabilityReference(it, scoringSystem, severityRating, baseScore, severity)
// OSV never provides the numeric base score as it can be calculated from the vector string.
VulnerabilityReference(it, scoringSystem, severityRating, null, vector)
}.getOrNull()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe

import org.ossreviewtoolkit.advisor.normalizeVulnerabilityData
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.utils.toPurl
Expand All @@ -37,10 +38,10 @@ class VulnerableCodeFunTest : WordSpec({
val id = Identifier("Go::github.com/quic-go/quic-go:0.40.0")
val pkg = Package.EMPTY.copy(id, purl = id.toPurl())

val findings = vc.retrievePackageFindings(setOf(pkg))
val results = vc.retrievePackageFindings(setOf(pkg)).values.map { it.normalizeVulnerabilityData() }

findings.values.flatMap { it.summary.issues } should beEmpty()
with(findings.values.flatMap { it.vulnerabilities }.associateBy { it.id }) {
results.flatMap { it.summary.issues } should beEmpty()
with(results.flatMap { it.vulnerabilities }.associateBy { it.id }) {
keys shouldContainAll setOf(
"CVE-2023-49295"
)
Expand All @@ -63,10 +64,10 @@ class VulnerableCodeFunTest : WordSpec({
val id = Identifier("Maven:com.google.guava:guava:19.0")
val pkg = Package.EMPTY.copy(id, purl = id.toPurl())

val findings = vc.retrievePackageFindings(setOf(pkg))
val results = vc.retrievePackageFindings(setOf(pkg)).values.map { it.normalizeVulnerabilityData() }

findings.values.flatMap { it.summary.issues } should beEmpty()
with(findings.values.flatMap { it.vulnerabilities }.associateBy { it.id }) {
results.flatMap { it.summary.issues } should beEmpty()
with(results.flatMap { it.vulnerabilities }.associateBy { it.id }) {
keys shouldContainAll setOf(
"CVE-2018-10237",
"CVE-2020-8908",
Expand All @@ -89,10 +90,10 @@ class VulnerableCodeFunTest : WordSpec({
val id = Identifier("Maven:org.apache.commons:commons-compress:1.23.0")
val pkg = Package.EMPTY.copy(id, purl = id.toPurl())

val findings = vc.retrievePackageFindings(setOf(pkg))
val results = vc.retrievePackageFindings(setOf(pkg)).values.map { it.normalizeVulnerabilityData() }

findings.values.flatMap { it.summary.issues } should beEmpty()
with(findings.values.flatMap { it.vulnerabilities }.associateBy { it.id }) {
results.flatMap { it.summary.issues } should beEmpty()
with(results.flatMap { it.vulnerabilities }.associateBy { it.id }) {
keys shouldContainAll setOf(
"CVE-2023-42503"
)
Expand All @@ -115,10 +116,10 @@ class VulnerableCodeFunTest : WordSpec({
val id = Identifier("NPM::elliptic:6.5.7")
val pkg = Package.EMPTY.copy(id, purl = id.toPurl())

val findings = vc.retrievePackageFindings(setOf(pkg))
val results = vc.retrievePackageFindings(setOf(pkg)).values.map { it.normalizeVulnerabilityData() }

findings.values.flatMap { it.summary.issues } should beEmpty()
with(findings.values.flatMap { it.vulnerabilities }.associateBy { it.id }) {
results.flatMap { it.summary.issues } should beEmpty()
with(results.flatMap { it.vulnerabilities }.associateBy { it.id }) {
keys shouldContainAll setOf(
"CVE-2024-48948"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,14 @@ class VulnerableCode(override val descriptor: PluginDescriptor, config: Vulnerab

if (scores.isEmpty()) return listOf(VulnerabilityReference(sourceUri, null, null, null, null))

return scores.map { (scoringSystem, scoringElements, value) ->
val score = value.toFloatOrNull()

val severity = if (score != null) {
VulnerabilityReference.getQualitativeRating(scoringSystem, score)?.name
} else {
// VulnerableCode returns "MODERATE" instead of "MEDIUM" in case of the "cvssv3.1_qr" scoring
// system, see https://github.com/aboutcode-org/vulnerablecode/issues/1186.
value.takeUnless { it == "MODERATE" && scoringSystem == "cvssv3.1_qr" } ?: "MEDIUM"
}
return scores.map {
// In VulnerableCode's data model, a Score class's value is either a numeric score or a severity string.
val score = it.value.toFloatOrNull()
val severity = it.value.takeUnless { score != null }

val vector = it.scoringElements?.ifEmpty { null }

VulnerabilityReference(sourceUri, scoringSystem, severity, score, scoringElements?.ifEmpty { null })
VulnerabilityReference(sourceUri, it.scoringSystem, severity, score, vector)
}
}.onFailure {
issues += createAndLogIssue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import io.kotest.matchers.shouldBe
import java.io.File
import java.net.URI

import org.ossreviewtoolkit.advisor.normalizeVulnerabilityData
import org.ossreviewtoolkit.model.AdvisorCapability
import org.ossreviewtoolkit.model.AdvisorDetails
import org.ossreviewtoolkit.model.Identifier
Expand Down Expand Up @@ -111,7 +112,7 @@ class VulnerableCodeTest : WordSpec({
)
)
)
langResult.vulnerabilities should containExactly(expLangVulnerability)
langResult.vulnerabilities.normalizeVulnerabilityData() should containExactly(expLangVulnerability)

val strutsResult = result.getValue(idStruts)
strutsResult.advisor shouldBe vulnerableCode.details
Expand Down Expand Up @@ -149,7 +150,8 @@ class VulnerableCodeTest : WordSpec({
)
)
)
strutsResult.vulnerabilities should containExactlyInAnyOrder(expStrutsVulnerabilities)
strutsResult.vulnerabilities.normalizeVulnerabilityData() should
containExactlyInAnyOrder(expStrutsVulnerabilities)
}

"extract the CVE ID from an alias" {
Expand Down Expand Up @@ -178,7 +180,8 @@ class VulnerableCodeTest : WordSpec({
)
)
)
result.getValue(idJUnit).vulnerabilities should containExactly(expJunitVulnerability)
result.getValue(idJUnit).vulnerabilities.normalizeVulnerabilityData() should
containExactly(expJunitVulnerability)
}

"extract other official identifiers from aliases" {
Expand Down Expand Up @@ -214,7 +217,8 @@ class VulnerableCodeTest : WordSpec({
)
)
)
result.getValue(idLog4j).vulnerabilities should containExactlyInAnyOrder(expLog4jVulnerabilities)
result.getValue(idLog4j).vulnerabilities.normalizeVulnerabilityData() should
containExactlyInAnyOrder(expLog4jVulnerabilities)
}

"handle a failure response from the server" {
Expand Down
Loading