Skip to content

Commit 02b8977

Browse files
Korbinian Singhammersschuberth
Korbinian Singhammer
authored andcommitted
advisor: Redesign the Advisor class
Let vulnerabilty providers, e.g. NexusIq, implement the VulnerabilityProvider class. Move responsibility to create providers to the Advisor class, which can then call the provider. This is a preparation to let the Advisor handle multiple providers. Signed-off-by: Korbinian Singhammer <[email protected]>
1 parent 5578f81 commit 02b8977

File tree

7 files changed

+71
-106
lines changed

7 files changed

+71
-106
lines changed

advisor/src/main/kotlin/Advisor.kt

+6-45
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,22 @@ import java.util.ServiceLoader
2626

2727
import kotlinx.coroutines.runBlocking
2828

29-
import org.ossreviewtoolkit.model.AdvisorDetails
3029
import org.ossreviewtoolkit.model.AdvisorRecord
31-
import org.ossreviewtoolkit.model.AdvisorResult
3230
import org.ossreviewtoolkit.model.AdvisorRun
33-
import org.ossreviewtoolkit.model.AdvisorSummary
3431
import org.ossreviewtoolkit.model.OrtResult
35-
import org.ossreviewtoolkit.model.Package
3632
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
37-
import org.ossreviewtoolkit.model.createAndLogIssue
3833
import org.ossreviewtoolkit.model.readValue
3934
import org.ossreviewtoolkit.utils.Environment
40-
import org.ossreviewtoolkit.utils.collectMessagesAsString
41-
import org.ossreviewtoolkit.utils.showStackTrace
4235

4336
/**
44-
* The class to retrieve security advisories.
37+
* The class to manage [VulnerabilityProvider]s that retrieve security advisories.
4538
*/
46-
abstract class Advisor(val advisorName: String, protected val config: AdvisorConfiguration) {
39+
class Advisor(private val providerFactory: VulnerabilityProviderFactory, private val config: AdvisorConfiguration) {
4740
companion object {
4841
private val LOADER = ServiceLoader.load(VulnerabilityProviderFactory::class.java)!!
4942

5043
/**
51-
* The list of all available advisors in the classpath.
44+
* The list of all available [VulnerabilityProvider]s in the classpath.
5245
*/
5346
val ALL by lazy { LOADER.iterator().asSequence().toList() }
5447
}
@@ -69,8 +62,10 @@ abstract class Advisor(val advisorName: String, protected val config: AdvisorCon
6962
"The provided ORT result file '${ortResultFile.canonicalPath}' does not contain an analyzer result."
7063
}
7164

65+
val provider = providerFactory.create(config)
66+
7267
val packages = ortResult.getPackages(skipExcluded).map { it.pkg }
73-
val results = runBlocking { retrievePackageVulnerabilities(packages) }
68+
val results = runBlocking { provider.retrievePackageVulnerabilities(packages) }
7469
.mapKeysTo(sortedMapOf()) { (pkg, _) -> pkg.id }
7570

7671
val advisorRecord = AdvisorRecord(results)
@@ -80,38 +75,4 @@ abstract class Advisor(val advisorName: String, protected val config: AdvisorCon
8075
val advisorRun = AdvisorRun(startTime, endTime, Environment(), config, advisorRecord)
8176
return ortResult.copy(advisor = advisorRun)
8277
}
83-
84-
protected abstract suspend fun retrievePackageVulnerabilities(
85-
packages: List<Package>
86-
): Map<Package, List<AdvisorResult>>
87-
88-
protected fun createFailedResults(
89-
startTime: Instant,
90-
packages: List<Package>,
91-
t: Throwable
92-
): Map<Package, List<AdvisorResult>> {
93-
val endTime = Instant.now()
94-
95-
t.showStackTrace()
96-
97-
val failedResults = listOf(
98-
AdvisorResult(
99-
vulnerabilities = emptyList(),
100-
advisor = AdvisorDetails(advisorName),
101-
summary = AdvisorSummary(
102-
startTime = startTime,
103-
endTime = endTime,
104-
issues = listOf(
105-
createAndLogIssue(
106-
source = advisorName,
107-
message = "Failed to retrieve security vulnerabilities from $advisorName: " +
108-
t.collectMessagesAsString()
109-
)
110-
)
111-
)
112-
)
113-
)
114-
115-
return packages.associateWith { failedResults }
116-
}
11778
}

advisor/src/main/kotlin/VulnerabilityProvider.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ abstract class VulnerabilityProvider(val providerName: String) {
4040
* that associates each package with a list of [AdvisorResult]s. Needs to be implemented
4141
* by child classes.
4242
*/
43-
protected abstract suspend fun retrievePackageVulnerabilities(
43+
abstract suspend fun retrievePackageVulnerabilities(
4444
packages: List<Package>
4545
): Map<Package, List<AdvisorResult>>
4646

advisor/src/main/kotlin/VulnerabilityProviderFactory.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,32 @@ import org.ossreviewtoolkit.utils.ortConfigDirectory
3131
*/
3232
interface VulnerabilityProviderFactory {
3333
/**
34-
* The name to use to refer to the advisor.
34+
* The name to use to refer to the provider.
3535
*/
36-
val advisorName: String
36+
val providerName: String
3737

3838
/**
39-
* Create an [Advisor] using the specified [config].
39+
* Create a [VulnerabilityProvider] using the specified [config].
4040
*/
41-
fun create(config: AdvisorConfiguration): Advisor
41+
fun create(config: AdvisorConfiguration): VulnerabilityProvider
4242
}
4343

4444
/**
45-
* A generic factory class for an [Advisor].
45+
* A generic factory class for a [VulnerabilityProvider].
4646
*/
47-
abstract class AbstractVulnerabilityProviderFactory<out T : Advisor>(
48-
override val advisorName: String
47+
abstract class AbstractVulnerabilityProviderFactory<out T : VulnerabilityProvider>(
48+
override val providerName: String
4949
) : VulnerabilityProviderFactory {
5050
abstract override fun create(config: AdvisorConfiguration): T
5151

5252
protected fun <T> getProviderConfiguration(config: AdvisorConfiguration, select: (AdvisorConfiguration) -> T?): T =
5353
select(config) ?: throw IllegalArgumentException(
54-
"No $advisorName advisor configuration found in ${ortConfigDirectory.resolve(ORT_CONFIG_FILENAME)}"
54+
"No configuration for $providerName found in ${ortConfigDirectory.resolve(ORT_CONFIG_FILENAME)}"
5555
)
5656

5757
/**
58-
* Return the advisor's name here to allow Clikt to display something meaningful when listing the scanners
58+
* Return the provider's name here to allow Clikt to display something meaningful when listing the scanners
5959
* which are enabled by default via their factories.
6060
*/
61-
override fun toString() = advisorName
61+
override fun toString() = providerName
6262
}

advisor/src/main/kotlin/advisors/NexusIq.kt

+6-14
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,20 @@ import java.net.URI
2424
import java.time.Instant
2525

2626
import org.ossreviewtoolkit.advisor.AbstractVulnerabilityProviderFactory
27-
import org.ossreviewtoolkit.advisor.Advisor
27+
import org.ossreviewtoolkit.advisor.VulnerabilityProvider
2828
import org.ossreviewtoolkit.clients.nexusiq.NexusIqService
2929
import org.ossreviewtoolkit.model.AdvisorDetails
3030
import org.ossreviewtoolkit.model.AdvisorResult
3131
import org.ossreviewtoolkit.model.AdvisorSummary
3232
import org.ossreviewtoolkit.model.Package
3333
import org.ossreviewtoolkit.model.Vulnerability
3434
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
35+
import org.ossreviewtoolkit.model.config.NexusIqConfiguration
3536
import org.ossreviewtoolkit.model.utils.PurlType
3637
import org.ossreviewtoolkit.model.utils.getPurlType
3738
import org.ossreviewtoolkit.model.utils.toPurl
38-
import org.ossreviewtoolkit.utils.ORT_CONFIG_FILENAME
3939
import org.ossreviewtoolkit.utils.OkHttpClientHelper
4040
import org.ossreviewtoolkit.utils.log
41-
import org.ossreviewtoolkit.utils.ortConfigDirectory
4241

4342
import retrofit2.HttpException
4443

@@ -50,19 +49,12 @@ private const val REQUEST_CHUNK_SIZE = 100
5049
/**
5150
* A wrapper for [Nexus IQ Server](https://help.sonatype.com/iqserver) security vulnerability data.
5251
*/
53-
class NexusIq(
54-
name: String,
55-
config: AdvisorConfiguration
56-
) : Advisor(name, config) {
52+
class NexusIq(name: String, private val nexusIqConfig: NexusIqConfiguration) : VulnerabilityProvider(name) {
5753
class Factory : AbstractVulnerabilityProviderFactory<NexusIq>("NexusIQ") {
58-
override fun create(config: AdvisorConfiguration) = NexusIq(advisorName, config)
54+
override fun create(config: AdvisorConfiguration) =
55+
NexusIq(providerName, getProviderConfiguration(config) { it.nexusIq })
5956
}
6057

61-
private val nexusIqConfig = config.nexusIq
62-
?: throw IllegalArgumentException(
63-
"No $advisorName advisor configuration found in ${ortConfigDirectory.resolve(ORT_CONFIG_FILENAME)}"
64-
)
65-
6658
private val service by lazy {
6759
NexusIqService.create(
6860
nexusIqConfig.serverUrl,
@@ -108,7 +100,7 @@ class NexusIq(
108100
pkg to listOf(
109101
AdvisorResult(
110102
details.securityData.securityIssues.map { it.toVulnerability() },
111-
AdvisorDetails(advisorName),
103+
AdvisorDetails(providerName),
112104
AdvisorSummary(startTime, endTime)
113105
)
114106
)

advisor/src/main/kotlin/advisors/VulnerableCode.kt

+10-11
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,29 @@ import kotlinx.coroutines.awaitAll
2828
import kotlinx.coroutines.coroutineScope
2929

3030
import org.ossreviewtoolkit.advisor.AbstractVulnerabilityProviderFactory
31-
import org.ossreviewtoolkit.advisor.Advisor
31+
import org.ossreviewtoolkit.advisor.VulnerabilityProvider
3232
import org.ossreviewtoolkit.clients.vulnerablecode.VulnerableCodeService
3333
import org.ossreviewtoolkit.model.AdvisorDetails
3434
import org.ossreviewtoolkit.model.AdvisorResult
3535
import org.ossreviewtoolkit.model.AdvisorSummary
3636
import org.ossreviewtoolkit.model.Package
3737
import org.ossreviewtoolkit.model.Vulnerability
3838
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
39+
import org.ossreviewtoolkit.model.config.VulnerableCodeConfiguration
3940
import org.ossreviewtoolkit.model.utils.toPurl
40-
import org.ossreviewtoolkit.utils.ORT_CONFIG_FILENAME
4141
import org.ossreviewtoolkit.utils.OkHttpClientHelper
42-
import org.ossreviewtoolkit.utils.ortConfigDirectory
4342

4443
/**
45-
* An [Advisor] implementation that obtains security vulnerability information from a
44+
* A [VulnerabilityProvider] implementation that obtains security vulnerability information from a
4645
* [VulnerableCode][https://github.com/nexB/vulnerablecode] instance.
4746
*/
48-
class VulnerableCode(name: String, config: AdvisorConfiguration) : Advisor(name, config) {
47+
class VulnerableCode(
48+
name: String,
49+
private val vulnerableCodeConfiguration: VulnerableCodeConfiguration
50+
) : VulnerabilityProvider(name) {
4951
class Factory : AbstractVulnerabilityProviderFactory<VulnerableCode>("VulnerableCode") {
50-
override fun create(config: AdvisorConfiguration) = VulnerableCode(advisorName, config)
52+
override fun create(config: AdvisorConfiguration) =
53+
VulnerableCode(providerName, getProviderConfiguration(config) { it.vulnerableCode })
5154
}
5255

5356
companion object {
@@ -73,10 +76,6 @@ class VulnerableCode(name: String, config: AdvisorConfiguration) : Advisor(name,
7376
}
7477

7578
private val service by lazy {
76-
val vulnerableCodeConfiguration = config.vulnerableCode
77-
?: throw IllegalArgumentException(
78-
"No $advisorName advisor configuration found in ${ortConfigDirectory.resolve(ORT_CONFIG_FILENAME)}"
79-
)
8079
VulnerableCodeService.create(vulnerableCodeConfiguration.serverUrl, OkHttpClientHelper.buildClient())
8180
}
8281

@@ -103,7 +102,7 @@ class VulnerableCode(name: String, config: AdvisorConfiguration) : Advisor(name,
103102
pkg to listOf(
104103
AdvisorResult(
105104
vulnerabilities,
106-
AdvisorDetails(advisorName),
105+
AdvisorDetails(providerName),
107106
AdvisorSummary(startTime, endTime)
108107
)
109108
)

advisor/src/test/kotlin/advisors/VulnerableCodeTest.kt

+28-17
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,21 @@ import io.kotest.matchers.collections.beEmpty
3636
import io.kotest.matchers.collections.containExactly
3737
import io.kotest.matchers.collections.containExactlyInAnyOrder
3838
import io.kotest.matchers.collections.shouldHaveSize
39-
import io.kotest.matchers.nulls.shouldNotBeNull
39+
import io.kotest.matchers.maps.shouldNotBeEmpty
4040
import io.kotest.matchers.should
4141
import io.kotest.matchers.shouldBe
4242

4343
import java.io.File
4444
import java.net.URI
4545

46+
import kotlinx.coroutines.runBlocking
47+
4648
import org.ossreviewtoolkit.model.Identifier
49+
import org.ossreviewtoolkit.model.OrtResult
4750
import org.ossreviewtoolkit.model.Severity
4851
import org.ossreviewtoolkit.model.Vulnerability
49-
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
5052
import org.ossreviewtoolkit.model.config.VulnerableCodeConfiguration
53+
import org.ossreviewtoolkit.model.readValue
5154
import org.ossreviewtoolkit.model.utils.toPurl
5255
import org.ossreviewtoolkit.utils.test.shouldNotBeNull
5356

@@ -71,7 +74,7 @@ class VulnerableCodeTest : WordSpec({
7174
wiremock.resetAll()
7275
}
7376

74-
"VulnerabilityCode" should {
77+
"VulnerableCode" should {
7578
"return vulnerability information" {
7679
stubFor(
7780
post(urlPathEqualTo("/api/packages/bulk_search"))
@@ -87,10 +90,12 @@ class VulnerableCodeTest : WordSpec({
8790
stubVulnerability("v2", "CVE-2009-1382", 11.0f)
8891
stubVulnerability("v3", "CVE-2019-CoV19", 77.0f)
8992

90-
val advisor = createAdvisor(wiremock)
91-
val result = advisor.retrieveVulnerabilityInformation(resultFile()).advisor?.results?.advisorResults
93+
val vulnerableCode = createVulnerableCode(wiremock)
94+
val packagesToAdvise = resultFile().readValue<OrtResult>().getPackages(false).map { it.pkg }
95+
96+
val result = vulnerableCode.retrievePackageVulnerabilities(packagesToAdvise).mapKeys { it.key.id }
9297

93-
result.shouldNotBeNull()
98+
result.shouldNotBeEmpty()
9499
result.keys should containExactlyInAnyOrder(idLang, idStruts)
95100

96101
val langResults = result.getValue(idLang)
@@ -141,8 +146,10 @@ class VulnerableCodeTest : WordSpec({
141146
)
142147
)
143148

144-
val advisor = createAdvisor(wiremock)
145-
advisor.retrieveVulnerabilityInformation(resultFile()).advisor?.results?.advisorResults shouldNotBeNull {
149+
val vulnerableCode = createVulnerableCode(wiremock)
150+
val packagesToAdvise = resultFile().readValue<OrtResult>().getPackages(false).map { it.pkg }
151+
152+
vulnerableCode.retrievePackageVulnerabilities(packagesToAdvise).mapKeys { it.key.id } shouldNotBeNull {
146153
val strutsResults = getValue(idStruts)
147154
val expStrutsVulnerabilities = listOf(
148155
Vulnerability(
@@ -211,7 +218,7 @@ private val idHamcrest = Identifier("Maven:org.hamcrest:hamcrest-core:1.3")
211218
private val packageIdentifiers = listOf(idJUnit, idLang, idText, idStruts, idHamcrest)
212219

213220
/**
214-
* The list of packages referenced by the test result. These packages should be requested by the advisor.
221+
* The list of packages referenced by the test result. These packages should be requested by the vulnerability provider.
215222
*/
216223
private val packages = packageIdentifiers.map { it.toPurl() }
217224

@@ -227,12 +234,16 @@ private val packagesRequestJson = generateListRequest(packages, "packages")
227234
private val vulnerabilityDetailsTemplate = File(TEST_FILES_ROOT).resolve(VULNERABILITY_TEMPLATE).readText()
228235

229236
/**
230-
* Run a test with the VulnerabilityCode advisor against the given [test server][wiremock] and expect the
237+
* Run a test with the VulnerabilityCode provider against the given [test server][wiremock] and expect the
231238
* operation to fail. In this case, for all packages a result with an error issue should have been created.
232239
*/
233240
private fun expectErrorResult(wiremock: WireMockServer) {
234-
val advisor = createAdvisor(wiremock)
235-
val result = advisor.retrieveVulnerabilityInformation(resultFile()).advisor?.results?.advisorResults
241+
val vulnerableCode = createVulnerableCode(wiremock)
242+
val packagesToAdvise = resultFile().readValue<OrtResult>().getPackages(false).map { it.pkg }
243+
244+
val result = runBlocking {
245+
vulnerableCode.retrievePackageVulnerabilities(packagesToAdvise).mapKeys { it.key.id }
246+
}
236247

237248
result shouldNotBeNull {
238249
keys should containExactly(packageIdentifiers)
@@ -250,17 +261,17 @@ private fun expectErrorResult(wiremock: WireMockServer) {
250261
}
251262

252263
/**
253-
* Create a configuration for the [VulnerableCode] advisor that points to the local [wireMockServer].
264+
* Create a configuration for the [VulnerableCode] vulnerability provider that points to the local [wireMockServer].
254265
*/
255-
private fun createConfig(wireMockServer: WireMockServer): AdvisorConfiguration {
266+
private fun createConfig(wireMockServer: WireMockServer): VulnerableCodeConfiguration {
256267
val url = "http://localhost:${wireMockServer.port()}"
257-
return AdvisorConfiguration(vulnerableCode = VulnerableCodeConfiguration(url))
268+
return VulnerableCodeConfiguration(url)
258269
}
259270

260271
/**
261-
* Create a test advisor instance that communicates with the local [wireMockServer].
272+
* Create a test instance of [VulnerableCode] that communicates with the local [wireMockServer].
262273
*/
263-
private fun createAdvisor(wireMockServer: WireMockServer): VulnerableCode =
274+
private fun createVulnerableCode(wireMockServer: WireMockServer): VulnerableCode =
264275
VulnerableCode(ADVISOR_NAME, createConfig(wireMockServer))
265276

266277
/**

0 commit comments

Comments
 (0)