-
Notifications
You must be signed in to change notification settings - Fork 381
Expand file tree
/
Copy pathOsv.kt
More file actions
175 lines (150 loc) · 7.39 KB
/
Osv.kt
File metadata and controls
175 lines (150 loc) · 7.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/*
* Copyright (C) 2022 The ORT Project Copyright Holders <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.osv
import java.time.Instant
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import org.apache.logging.log4j.kotlin.logger
import org.ossreviewtoolkit.clients.osv.OsvServiceWrapper
import org.ossreviewtoolkit.clients.osv.VulnerabilitiesForPackageRequest
import org.ossreviewtoolkit.clients.osv.Vulnerability
import org.ossreviewtoolkit.model.AdvisorDetails
import org.ossreviewtoolkit.model.AdvisorResult
import org.ossreviewtoolkit.model.AdvisorSummary
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.vulnerabilities.VulnerabilityReference
import org.ossreviewtoolkit.plugins.advisors.api.AdviceProvider
import org.ossreviewtoolkit.plugins.advisors.api.AdviceProviderFactory
import org.ossreviewtoolkit.plugins.api.OrtPlugin
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
import org.ossreviewtoolkit.utils.common.collectMessages
import org.ossreviewtoolkit.utils.common.toUri
import org.ossreviewtoolkit.utils.ort.OkHttpClientHelper
/**
* An advice provider that obtains vulnerability information from [Open Source Vulnerabilities](https://osv.dev/).
*/
@OrtPlugin(
id = "OSV",
displayName = "OSV",
summary = "An advisor that retrieves vulnerability information from the Open Source Vulnerabilities database.",
factory = AdviceProviderFactory::class
)
class Osv(
override val descriptor: PluginDescriptor = OsvFactory.descriptor,
config: OsvConfiguration
) : AdviceProvider {
override val details = AdvisorDetails(descriptor.id)
private val service = OsvServiceWrapper(
serverUrl = config.serverUrl,
httpClient = OkHttpClientHelper.buildClient()
)
override suspend fun retrievePackageFindings(packages: Set<Package>): Map<Package, AdvisorResult> {
val startTime = Instant.now()
val vulnerabilityIdsForPackageId = getVulnerabilityIdsForPackages(packages)
val allVulnerabilityIds = vulnerabilityIdsForPackageId.values.flatten().toSet()
val vulnerabilityForId = getVulnerabilitiesForIds(allVulnerabilityIds).associateBy { it.id }
return packages.mapNotNull { pkg ->
vulnerabilityIdsForPackageId[pkg.id]?.let { ids ->
pkg to AdvisorResult(
advisor = details,
summary = AdvisorSummary(startTime = startTime, endTime = Instant.now()),
vulnerabilities = ids.map { vulnerabilityForId.getValue(it).toOrtVulnerability() }
)
}
}.toMap()
}
private fun getVulnerabilityIdsForPackages(packages: Set<Package>): Map<Identifier, List<String>> {
val requests = packages.mapNotNull { pkg ->
when {
pkg.purl.isNotEmpty() -> pkg to VulnerabilitiesForPackageRequest(
pkg = org.ossreviewtoolkit.clients.osv.Package(purl = pkg.purl)
)
// TODO: Consider doing this in more cases, like for the upcoming generic "git" type of PURLs, see
// https://github.com/package-url/purl-spec/issues/780.
pkg.vcsProcessed.revision.isNotEmpty() -> pkg to VulnerabilitiesForPackageRequest(
commit = pkg.vcsProcessed.revision
)
else -> {
logger.warn {
"${pkg.id.toCoordinates()} does not provide any metadata to identify vulnerabilities."
}
null
}
}
}
val result = service.getVulnerabilityIdsForPackages(requests.map { it.second })
val results = mutableListOf<Pair<Identifier, List<String>>>()
result.map { allVulnerabilities ->
// OSV returns vulnerability results in the same order as packages were requested, so use the list index to
// identify to which package a result belongs. This means that also empty results are returned as otherwise
// list indices would not match, so filter these out.
allVulnerabilities.mapIndexedNotNullTo(results) { i, pkgVulnerabilities ->
pkgVulnerabilities.takeUnless { it.isEmpty() }?.let { requests[i].first.id to it }
}
}.onFailure {
logger.error {
"Requesting vulnerability IDs for packages failed: ${it.collectMessages()}"
}
}
return results.toMap()
}
private fun getVulnerabilitiesForIds(ids: Set<String>): List<Vulnerability> {
val result = service.getVulnerabilitiesForIds(ids)
return result.getOrElse {
logger.error {
"Requesting vulnerabilities for IDs failed: ${it.collectMessages()}"
}
emptyList()
}
}
}
private fun Vulnerability.toOrtVulnerability(): org.ossreviewtoolkit.model.vulnerabilities.Vulnerability {
// The ORT and OSV vulnerability data models are different in that ORT uses a severity for each reference (assuming
// that different references could use different severities), whereas OSV manages severities and references on the
// same level, which means it is not possible to identify whether a reference belongs to a specific severity.
// To map between these different model, simply use the "cartesian product" to create an ORT reference for each
// combination of an OSV severity and reference.
val ortReferences = mutableListOf<VulnerabilityReference>()
severity.map {
it.type.name to it.score
}.ifEmpty {
listOf(null to null)
}.forEach { (scoringSystem, vector) ->
references.mapNotNullTo(ortReferences) { reference ->
val url = reference.url.trim().let { if (it.startsWith("://")) "https$it" else it }
url.toUri().onFailure {
logger.debug { "Could not parse reference URL for vulnerability '$id': ${it.collectMessages()}." }
}.map {
// 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 severityRating = (specificSeverity as? JsonPrimitive)?.contentOrNull
// OSV never provides the numeric base score as it can be calculated from the vector string.
VulnerabilityReference(it, scoringSystem, severityRating, null, vector)
}.getOrNull()
}
}
return org.ossreviewtoolkit.model.vulnerabilities.Vulnerability(
id = id,
summary = summary,
description = details,
references = ortReferences
)
}