Skip to content

Commit 60ef7c9

Browse files
committed
feat(advisor)!: Rework VulnerabilityReference semantics
Previously, the semantics of the `severity` property were too loaded as the string could have also represented a numeric value. Introduce a dedicated `score` for the latter and clarify that `severity` from now on is supposed to exclusively contain a semantic rating like "LOW" or "HIGH". Additionally, start to maintain the individual metrics as usually encoded into a vector string. This allows for more sophisticated visualizations e.g. via [1]. Adjust tests accordingly and make some smaller improvements along the way. [1]: https://github.com/org-metaeffekt/metaeffekt-universal-cvss-calculator Signed-off-by: Sebastian Schuberth <sebastian@doubleopen.org>
1 parent d1fa1f2 commit 60ef7c9

File tree

26 files changed

+215
-177
lines changed

26 files changed

+215
-177
lines changed

cli/src/funTest/assets/semver4j-ort-result.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,8 @@ advisor:
394394
references:
395395
- url: "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15250"
396396
scoring_system: "CVSS2"
397-
severity: "5.5"
398-
severity_rating: "MEDIUM"
397+
severity: "MEDIUM"
398+
score: 5.5
399399
evaluator: null
400400
resolved_configuration:
401401
package_curations:

evaluator/src/main/kotlin/PackageRule.kt

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import org.ossreviewtoolkit.model.config.Excludes
2929
import org.ossreviewtoolkit.model.licenses.LicenseView
3030
import org.ossreviewtoolkit.model.licenses.ResolvedLicense
3131
import org.ossreviewtoolkit.model.licenses.ResolvedLicenseInfo
32+
import org.ossreviewtoolkit.model.vulnerabilities.Cvss2Rating
33+
import org.ossreviewtoolkit.model.vulnerabilities.Cvss3Rating
34+
import org.ossreviewtoolkit.model.vulnerabilities.Cvss4Rating
3235
import org.ossreviewtoolkit.model.vulnerabilities.Vulnerability
3336
import org.ossreviewtoolkit.model.vulnerabilities.VulnerabilityReference
3437
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
@@ -83,21 +86,40 @@ open class PackageRule(
8386

8487
/**
8588
* A [RuleMatcher] that checks whether any vulnerability for the [package][pkg] has a
86-
* [reference][Vulnerability.references] with a [severity][VulnerabilityReference.severity] that equals or is
87-
* greater than [threshold] according to the [scoringSystem] and the belonging [severityComparator].
89+
* [reference][Vulnerability.references] with a [score][VulnerabilityReference.score] that equals or is
90+
* greater than [scoreThreshold] according to the [scoringSystem]. If the reference provides no score but a
91+
* [severity][VulnerabilityReference.severity], the threshold is mapped to a qualitative rating for comparison.
8892
*/
89-
fun hasVulnerability(threshold: String, scoringSystem: String, severityComparator: (String, String) -> Boolean) =
93+
fun hasVulnerability(scoreThreshold: Float, scoringSystem: String) =
9094
object : RuleMatcher {
91-
override val description = "hasVulnerability($threshold, $scoringSystem)"
95+
override val description = "hasVulnerability($scoreThreshold, $scoringSystem)"
9296

9397
override fun matches(): Boolean {
9498
val run = ruleSet.ortResult.advisor ?: return false
95-
return run.getVulnerabilities(pkg.metadata.id).asSequence()
99+
val matchingSystems = run.getVulnerabilities(pkg.metadata.id).asSequence()
96100
.filter { !ruleSet.resolutionProvider.isResolved(it) }
97101
.flatMap { it.references }
98102
.filter { it.scoringSystem == scoringSystem }
103+
104+
val scores = matchingSystems.mapNotNull { it.score }
105+
if (scores.any()) return scores.any { it >= scoreThreshold }
106+
107+
// Fall back to a more coarse comparison of qualitative severity ratings if no scores are available.
108+
val severityThreshold = VulnerabilityReference.getQualitativeRating(scoringSystem, scoreThreshold)
109+
?: return false
110+
111+
val severities = matchingSystems
99112
.mapNotNull { it.severity }
100-
.any { severityComparator(it, threshold) }
113+
.mapNotNull {
114+
when (scoringSystem.uppercase()) {
115+
in Cvss2Rating.NAMES -> enumValueOf<Cvss2Rating>(it)
116+
in Cvss3Rating.NAMES -> enumValueOf<Cvss3Rating>(it)
117+
in Cvss4Rating.NAMES -> enumValueOf<Cvss4Rating>(it)
118+
else -> null
119+
}
120+
}
121+
122+
return severities.any { it.ordinal >= severityThreshold.ordinal }
101123
}
102124
}
103125

evaluator/src/test/kotlin/PackageRuleTest.kt

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -215,45 +215,28 @@ class PackageRuleTest : WordSpec() {
215215

216216
"return true if a severity of a vulnerability is higher than the threshold" {
217217
val rule = createPackageRule(packageWithVulnerabilities)
218-
val matcher = rule.hasVulnerability("8.9", "CVSS3") { value, threshold ->
219-
value.toFloat() >= threshold.toFloat()
220-
}
218+
val matcher = rule.hasVulnerability(8.9f, "CVSS3")
221219

222220
matcher.matches() shouldBe true
223221
}
224222

225223
"return false if a severity of a vulnerability is lower than the threshold" {
226224
val rule = createPackageRule(packageWithVulnerabilities)
227-
val matcher = rule.hasVulnerability("9.1", "CVSS3") { value, threshold ->
228-
value.toFloat() >= threshold.toFloat()
229-
}
225+
val matcher = rule.hasVulnerability(9.1f, "CVSS3")
230226

231227
matcher.matches() shouldBe false
232228
}
233229

234230
"return true if a severity of a vulnerability is the same as the threshold" {
235231
val rule = createPackageRule(packageWithVulnerabilities)
236-
val matcher = rule.hasVulnerability("9.0", "CVSS3") { value, threshold ->
237-
value.toFloat() >= threshold.toFloat()
238-
}
239-
240-
matcher.matches() shouldBe true
241-
}
242-
243-
"return true if a severity of a vulnerability is the same as the threshold without decimals" {
244-
val rule = createPackageRule(packageWithVulnerabilities)
245-
val matcher = rule.hasVulnerability("9", "CVSS3") { value, threshold ->
246-
value.toFloat() >= threshold.toFloat()
247-
}
232+
val matcher = rule.hasVulnerability(9.0f, "CVSS3")
248233

249234
matcher.matches() shouldBe true
250235
}
251236

252237
"return false if no vulnerability is found for the scoringSystem" {
253238
val rule = createPackageRule(packageWithVulnerabilities)
254-
val matcher = rule.hasVulnerability("10.0", "fake-scoring-system") { value, threshold ->
255-
value.toFloat() >= threshold.toFloat()
256-
}
239+
val matcher = rule.hasVulnerability(10.0f, "fake-scoring-system")
257240

258241
matcher.matches() shouldBe false
259242
}

evaluator/src/test/kotlin/TestData.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ val ortResult = OrtResult(
232232
VulnerabilityReference(
233233
url = URI("https://oss-review-toolkit.org"),
234234
scoringSystem = "CVSS3",
235-
severity = "9.0"
235+
severity = "CRITICAL",
236+
score = 9.0f,
237+
vector = null
236238
)
237239
)
238240
),
@@ -242,7 +244,9 @@ val ortResult = OrtResult(
242244
VulnerabilityReference(
243245
url = URI("https://oss-review-toolkit.org"),
244246
scoringSystem = "CVSS3",
245-
severity = "2.0"
247+
severity = "LOW",
248+
score = 2.0f,
249+
vector = null
246250
)
247251
)
248252
)

examples/example.rules.kts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,20 +214,18 @@ fun RuleSet.vulnerabilityInPackageRule() = packageRule("VULNERABILITY_IN_PACKAGE
214214
}
215215

216216
fun RuleSet.highSeverityVulnerabilityInPackageRule() = packageRule("HIGH_SEVERITY_VULNERABILITY_IN_PACKAGE") {
217-
val maxAcceptedSeverity = "5.0"
217+
val scoreThreshold = 5.0f
218218
val scoringSystem = "CVSS2"
219219

220220
require {
221221
-isExcluded()
222-
+hasVulnerability(maxAcceptedSeverity, scoringSystem) { value, threshold ->
223-
value.toFloat() >= threshold.toFloat()
224-
}
222+
+hasVulnerability(scoreThreshold, scoringSystem)
225223
}
226224

227225
issue(
228226
Severity.ERROR,
229227
"The package ${pkg.metadata.id.toCoordinates()} has a vulnerability with $scoringSystem severity > " +
230-
maxAcceptedSeverity,
228+
"${scoreThreshold}",
231229
howToFixDefault()
232230
)
233231
}

model/src/main/kotlin/vulnerabilities/VulnerabilityReference.kt

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,37 @@ import java.net.URI
3232
* with a list of references; each reference points to the source of the information and has some detailed information
3333
* provided by this source.
3434
*/
35-
@JsonIgnoreProperties(value = ["severity_rating"], allowGetters = true)
35+
@JsonIgnoreProperties(value = ["severity_rating"])
3636
data class VulnerabilityReference(
3737
/**
3838
* The URI pointing to details of the belonging vulnerability.
3939
*/
4040
val url: URI,
4141

4242
/**
43-
* The name of the scoring system, if any, to interpret the [severity] by.
43+
* The name of the scoring system, if any, as reported by the advice provider.
4444
*/
4545
val scoringSystem: String?,
4646

4747
/**
48-
* The severity assigned by this reference to the belonging vulnerability. This is string meaning depends on the
49-
* [scoringSystem]. It could be a number, but also a semantic expression like "LOW" or "HIGH". If this is `null`,
50-
* it means that this reference does not contain any information about the severity.
48+
* The severity, if any, this reference assigns to the belonging vulnerability. This string is supposed to be a
49+
* qualitative rating like "LOW" or "HIGH".
5150
*/
52-
val severity: String?
51+
val severity: String?,
52+
53+
/**
54+
* The (base) score, if any, this reference assigns to the belonging vulnerability. The meaning of this number
55+
* depends on the [scoringSystem].
56+
*/
57+
val score: Float?,
58+
59+
/**
60+
* The full CVSS vector, if any, this reference assigns to the belonging vulnerability. Note that while the vector
61+
* usually contains the [scoringSystem], that is not the case for e.g. CVSS version 2.
62+
*/
63+
val vector: String?
5364
) {
5465
companion object {
55-
private val CVSS3_SEVERITIES = Cvss3Rating.entries.map { it.name }
56-
5766
/**
5867
* Return a qualitative rating that is determined based on the given [scoringSystem] and [score].
5968
*/
@@ -64,18 +73,5 @@ data class VulnerabilityReference(
6473
in Cvss4Rating.NAMES -> score?.let { Cvss4Rating.fromScore(it) }
6574
else -> null
6675
}
67-
68-
/**
69-
* Return a human-readable string that is determined based on the given [scoringSystem] and [severity].
70-
*/
71-
fun getSeverityString(scoringSystem: String?, severity: String?): String =
72-
severity?.toFloatOrNull()?.let { getQualitativeRating(scoringSystem, it)?.name }
73-
?: severity?.uppercase()?.takeIf { it in CVSS3_SEVERITIES }
74-
?: "UNKNOWN"
7576
}
76-
77-
/**
78-
* Return a human-readable severity rating string.
79-
*/
80-
val severityRating: String by lazy { getSeverityString(scoringSystem, severity) }
8177
}

model/src/test/kotlin/AdvisorResultTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,13 @@ private fun createVulnerability(
289289
id: String,
290290
uriPrefix: String = "http://cve.mitre.org/cgi-bin/cvename.cgi?name=",
291291
scoringSystem: String = "cvssv3.1_qr",
292-
severity: String = "MODERATE"
292+
severity: String = "MEDIUM",
293+
score: Float = 5.0f
293294
): Vulnerability =
294295
Vulnerability(
295296
id = id,
296297
references = listOf(
297-
VulnerabilityReference(URI("$uriPrefix$id"), scoringSystem, severity)
298+
VulnerabilityReference(URI("$uriPrefix$id"), scoringSystem, severity, score, null)
298299
)
299300
)
300301

model/src/test/kotlin/vulnerabilities/VulnerabilityReferenceTest.kt

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,35 @@
2020
package org.ossreviewtoolkit.model.vulnerabilities
2121

2222
import io.kotest.core.spec.style.StringSpec
23+
import io.kotest.matchers.nulls.beNull
24+
import io.kotest.matchers.should
2325
import io.kotest.matchers.shouldBe
2426

2527
class VulnerabilityReferenceTest : StringSpec({
26-
"The severity string should be correct for a given CVSS 2 score" {
27-
VulnerabilityReference.getSeverityString("CVSS2", "1.0") shouldBe "LOW"
28-
VulnerabilityReference.getSeverityString("CVSSV2", "5.0") shouldBe "MEDIUM"
29-
VulnerabilityReference.getSeverityString("CVSS:2.0", "8.0") shouldBe "HIGH"
28+
"The severity rating should be correct for a given CVSS 2 score" {
29+
VulnerabilityReference.getQualitativeRating("CVSS2", 1.0f) shouldBe Cvss2Rating.LOW
30+
VulnerabilityReference.getQualitativeRating("CVSSV2", 5.0f) shouldBe Cvss2Rating.MEDIUM
31+
VulnerabilityReference.getQualitativeRating("CVSS:2.0", 8.0f) shouldBe Cvss2Rating.HIGH
3032
}
3133

32-
"The severity string should be correct for a given CVSS 3 score" {
33-
VulnerabilityReference.getSeverityString("CVSS3", "0.0") shouldBe "NONE"
34-
VulnerabilityReference.getSeverityString("CVSSV3", "1.0") shouldBe "LOW"
35-
VulnerabilityReference.getSeverityString("CVSS:3.0", "5.0") shouldBe "MEDIUM"
36-
VulnerabilityReference.getSeverityString("CVSS:3.1", "8.0") shouldBe "HIGH"
37-
VulnerabilityReference.getSeverityString("CVSS3", "9.0") shouldBe "CRITICAL"
34+
"The severity rating should be correct for a given CVSS 3 score" {
35+
VulnerabilityReference.getQualitativeRating("CVSS3", 0.0f) shouldBe Cvss3Rating.NONE
36+
VulnerabilityReference.getQualitativeRating("CVSSV3", 1.0f) shouldBe Cvss3Rating.LOW
37+
VulnerabilityReference.getQualitativeRating("CVSS:3.0", 5.0f) shouldBe Cvss3Rating.MEDIUM
38+
VulnerabilityReference.getQualitativeRating("CVSS:3.1", 8.0f) shouldBe Cvss3Rating.HIGH
39+
VulnerabilityReference.getQualitativeRating("CVSS3", 9.0f) shouldBe Cvss3Rating.CRITICAL
3840
}
3941

40-
"The severity string should be correct for a given CVSS 4 score" {
41-
VulnerabilityReference.getSeverityString("CVSS4", "0.0") shouldBe "NONE"
42-
VulnerabilityReference.getSeverityString("CVSSV4", "1.0") shouldBe "LOW"
43-
VulnerabilityReference.getSeverityString("CVSS:4.0", "5.0") shouldBe "MEDIUM"
44-
VulnerabilityReference.getSeverityString("CVSS4", "8.0") shouldBe "HIGH"
45-
VulnerabilityReference.getSeverityString("CVSS4", "9.0") shouldBe "CRITICAL"
42+
"The severity rating should be correct for a given CVSS 4 score" {
43+
VulnerabilityReference.getQualitativeRating("CVSS4", 0.0f) shouldBe Cvss4Rating.NONE
44+
VulnerabilityReference.getQualitativeRating("CVSSV4", 1.0f) shouldBe Cvss4Rating.LOW
45+
VulnerabilityReference.getQualitativeRating("CVSS:4.0", 5.0f) shouldBe Cvss4Rating.MEDIUM
46+
VulnerabilityReference.getQualitativeRating("CVSS4", 8.0f) shouldBe Cvss4Rating.HIGH
47+
VulnerabilityReference.getQualitativeRating("CVSS4", 9.0f) shouldBe Cvss4Rating.CRITICAL
4648
}
4749

48-
"The severity string should be correct for a given qualitative rating from an unknown scoring system" {
49-
VulnerabilityReference.getSeverityString("", "NONE") shouldBe "NONE"
50-
VulnerabilityReference.getSeverityString("", "LOW") shouldBe "LOW"
51-
VulnerabilityReference.getSeverityString("", "MEDIUM") shouldBe "MEDIUM"
52-
VulnerabilityReference.getSeverityString("", "HIGH") shouldBe "HIGH"
53-
VulnerabilityReference.getSeverityString("", "CRITICAL") shouldBe "CRITICAL"
50+
"The severity rating should be null if either the system or score is null" {
51+
VulnerabilityReference.getQualitativeRating(null, 8.0f) should beNull()
52+
VulnerabilityReference.getQualitativeRating("CVSS4", null) should beNull()
5453
}
5554
})

plugins/advisors/nexus-iq/src/main/kotlin/NexusIq.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class NexusIq(override val descriptor: PluginDescriptor, private val config: Nex
157157
val references = mutableListOf<VulnerabilityReference>()
158158

159159
val browseUrl = URI("${config.browseUrl}/assets/index.html#/vulnerabilities/$reference")
160-
val nexusIqReference = VulnerabilityReference(browseUrl, scoringSystem(), severity.toString())
160+
val nexusIqReference = VulnerabilityReference(browseUrl, scoringSystem(), threatCategory, severity, null)
161161

162162
references += nexusIqReference
163163
url.takeIf { it != browseUrl }?.let { references += nexusIqReference.copy(url = it) }

plugins/advisors/oss-index/src/main/kotlin/OssIndex.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.ossreviewtoolkit.model.AdvisorSummary
3636
import org.ossreviewtoolkit.model.Issue
3737
import org.ossreviewtoolkit.model.Package
3838
import org.ossreviewtoolkit.model.config.PluginConfiguration
39+
import org.ossreviewtoolkit.model.vulnerabilities.Cvss2Rating
3940
import org.ossreviewtoolkit.model.vulnerabilities.Vulnerability
4041
import org.ossreviewtoolkit.model.vulnerabilities.VulnerabilityReference
4142
import org.ossreviewtoolkit.plugins.api.OrtPlugin
@@ -136,11 +137,11 @@ class OssIndex(override val descriptor: PluginDescriptor, config: OssIndexConfig
136137
* [OssIndexService.Vulnerability].
137138
*/
138139
private fun OssIndexService.Vulnerability.toVulnerability(): Vulnerability {
139-
val reference = VulnerabilityReference(
140-
url = URI(reference),
141-
scoringSystem = cvssVector?.substringBefore('/'),
142-
severity = cvssScore.toString()
143-
)
140+
// Only CVSS version 2 vectors do not contain the "CVSS:" label and version prefix.
141+
val scoringSystem = cvssVector?.substringBefore('/', Cvss2Rating.NAMES.first())
142+
143+
val severity = VulnerabilityReference.getQualitativeRating(scoringSystem, cvssScore)?.name
144+
val reference = VulnerabilityReference(URI(reference), scoringSystem, severity, cvssScore, cvssVector)
144145

145146
val references = mutableListOf(reference)
146147
externalReferences?.mapTo(references) { reference.copy(url = URI(it)) }

0 commit comments

Comments
 (0)