Skip to content

Commit

Permalink
Advisor: Add Sonatype Nexus IQ as a security advisor
Browse files Browse the repository at this point in the history
Add Sonatype Nexus IQ Server [1] as a security advisor module.

[1] https://help.sonatype.com/iqserver

Resolves oss-review-toolkit#3268.

Signed-off-by: Marcel Bochtler <[email protected]>
  • Loading branch information
MarcelBochtler authored and sschuberth committed Nov 13, 2020
1 parent 6a4a90e commit e311617
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 7 deletions.
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,13 @@ use):
Version Control System (VCS) or other means are used to retrieve the source code.
* [_Scanner_](#scanner) - uses configured source code scanners to detect license / copyright findings, abstracting
the type of scanner.
* [_Advisor_](#advisor) - retrieves security advisories for used dependencies from configured vulnerability data
services.
* [_Evaluator_](#evaluator) - evaluates license / copyright findings against customizable policy rules and license
classifications.
* [_Reporter_](#reporter) - presents results in various formats such as visual reports, Open Source notices or
Bill-Of-Materials (BOMs) to easily identify dependencies, licenses, copyrights or policy rule violations.

The following tools are [planned](https://github.com/oss-review-toolkit/ort/projects/1) but not yet available:

* _Advisor_ - retrieves security advisories based on the Analyzer result.

# Installation

## From binaries
Expand Down Expand Up @@ -540,6 +538,31 @@ ort {
}
```

<a name="advisor">&nbsp;</a>

[![Advisor](./logos/advisor.png)](./advisor/src/main/kotlin)

The _advisor_ retrieves security advisories from configured services. It requires the analyzer result as an input.

### Configuration

The advisor needs to be configured in the ORT configuration file:

```hocon
ort {
advisor {
nexusiq {
serverUrl = "https://nexusiq.ossreviewtoolkit.org"
username = myUser
password = myPassword
}
}
}
```

Currently [Nexus IQ Server](https://help.sonatype.com/iqserver) (`-a NexusIQ`) is the only supported security data
provider.

<a name="evaluator">&nbsp;</a>

[![Evaluator](./logos/evaluator.png)](./evaluator/src/main/kotlin)
Expand Down
1 change: 1 addition & 0 deletions advisor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ plugins {
}

dependencies {
api(project(":clients:nexus-iq"))
api(project(":model"))

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
Expand Down
175 changes: 175 additions & 0 deletions advisor/src/main/kotlin/advisors/NexusIq.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright (C) 2020 Bosch.IO GmbH
*
* 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
*
* http://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.advisor.advisors

import java.io.IOException
import java.net.URL
import java.time.Instant
import java.util.concurrent.Executors

import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext

import org.ossreviewtoolkit.advisor.AbstractAdvisorFactory
import org.ossreviewtoolkit.advisor.Advisor
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.Vulnerability
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
import org.ossreviewtoolkit.model.config.NexusIqConfiguration
import org.ossreviewtoolkit.model.createAndLogIssue
import org.ossreviewtoolkit.nexusiq.NexusIqService
import org.ossreviewtoolkit.utils.NamedThreadFactory
import org.ossreviewtoolkit.utils.OkHttpClientHelper
import org.ossreviewtoolkit.utils.collectMessagesAsString
import org.ossreviewtoolkit.utils.log
import org.ossreviewtoolkit.utils.showStackTrace

import retrofit2.Call

/**
* A wrapper for [Nexus IQ Server](https://help.sonatype.com/iqserver) security vulnerability data.
*/
class NexusIq(
name: String,
config: AdvisorConfiguration
) : Advisor(name, config) {
class Factory : AbstractAdvisorFactory<NexusIq>("NexusIQ") {
override fun create(config: AdvisorConfiguration) = NexusIq(advisorName, config)
}

data class NexusIqVulnerability(
override val id: String,
override val severity: Float,

// URL to http://mitre.org for more information about the CVE.
val url: URL
) : Vulnerability

private val nexusIqConfig = config as NexusIqConfiguration

override suspend fun retrievePackageVulnerabilities(packages: List<Package>): Map<Package, List<AdvisorResult>> {
val service = NexusIqService.create(
nexusIqConfig.serverUrl,
nexusIqConfig.username,
nexusIqConfig.password,
OkHttpClientHelper.buildClient()
)

val advisorDispatcher =
Executors.newSingleThreadExecutor(NamedThreadFactory(advisorName)).asCoroutineDispatcher()

return coroutineScope {
val startTime = Instant.now()

val components = packages.map { pkg ->
val packageUrl = buildString {
append(pkg.purl)
val purlType = pkg.id.getPurlType()
if (purlType == Identifier.PurlType.MAVEN) append("?type=jar")
if (purlType == Identifier.PurlType.PYPI) append("?extension=tar.gz")
}

NexusIqService.Component(packageUrl)
}

try {
val componentDetails =
execute(withContext(advisorDispatcher) {
service.getComponentDetails(NexusIqService.ComponentsWrapper(components))
}).componentDetails
.associateBy { it.component.packageUrl.substringBefore("?") }

val endTime = Instant.now()

packages.mapNotNullTo(mutableListOf()) { pkg ->
componentDetails[pkg.id.toPurl()]?.takeUnless {
it.securityData.securityIssues.isEmpty()
}?.let { details ->
pkg to listOf(
AdvisorResult(
details.securityData.securityIssues.mapToVulnerabilities(),
AdvisorDetails(advisorName),
AdvisorSummary(startTime, endTime)
)
)
}
}.toMap()
} catch (e: IOException) {
e.showStackTrace()

val now = Instant.now()
packages.associateWith {
listOf(
AdvisorResult(
vulnerability = emptyList(),
advisor = AdvisorDetails(advisorName),
summary = AdvisorSummary(
startTime = now,
endTime = now,
issues = listOf(
createAndLogIssue(
source = advisorName,
message = "Failed to retrieve security vulnerabilities from $advisorName: " +
e.collectMessagesAsString()
)
)
)
)
)
}
}
}
}

private fun Collection<NexusIqService.SecurityIssue>.mapToVulnerabilities() =
map {
NexusIqVulnerability(
it.reference,
it.severity,
it.url
)
}

/**
* Execute an HTTP request specified by the given [call]. The response status is checked. If everything went
* well, the marshalled body of the request is returned; otherwise, the function throws an exception.
*/
private fun <T> execute(call: Call<T>): T {
val request = "${call.request().method} on ${call.request().url}"
log.debug { "Executing HTTP $request." }

val response = call.execute()
log.debug { "HTTP response is (status ${response.code()}): ${response.message()}" }

val body = response.body()
if (!response.isSuccessful || body == null) {
throw IOException(
"Failed HTTP $request with status ${response.code()} and error: ${response.errorBody()?.string()}."
)
}

return body
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.ossreviewtoolkit.advisor.advisors.NexusIq$Factory
7 changes: 4 additions & 3 deletions cli/src/main/kotlin/commands/AdvisorCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.github.ajalt.clikt.parameters.types.file

import org.ossreviewtoolkit.GlobalOptions
import org.ossreviewtoolkit.advisor.Advisor
import org.ossreviewtoolkit.advisor.advisors.NexusIq
import org.ossreviewtoolkit.model.FileFormat
import org.ossreviewtoolkit.model.config.AdvisorConfiguration
import org.ossreviewtoolkit.model.config.NexusIqConfiguration
Expand Down Expand Up @@ -84,7 +85,7 @@ class AdvisorCommand : CliktCommand(name = "advise", help = "Run vulnerability d
).convert { advisorName ->
Advisor.ALL.find { it.advisorName.equals(advisorName, ignoreCase = true) }
?: throw BadParameterValue("Advisor '$advisorName' is not one of ${Advisor.ALL}")
}
}.default(NexusIq.Factory())

private val skipExcluded by option(
"--skip-excluded",
Expand All @@ -99,7 +100,7 @@ class AdvisorCommand : CliktCommand(name = "advise", help = "Run vulnerability d
)
}

val advisor = advisorFactory?.create(config) ?: throw IllegalArgumentException("No advisor specified.")
val advisor = advisorFactory.create(config)
println("Using advisor '${advisor.advisorName}'.")

return advisor
Expand All @@ -118,7 +119,7 @@ class AdvisorCommand : CliktCommand(name = "advise", help = "Run vulnerability d
}

val config = globalOptionsForSubcommands.config
val advisorConfig = config.advisor?.get(advisorFactory?.advisorName?.toLowerCase())
val advisorConfig = config.advisor?.get(advisorFactory.advisorName.toLowerCase())
val advisor = configureAdvisor(advisorConfig)

val ortResult = advisor.retrieveVulnerabilityInformation(input, skipExcluded).mergeLabels(labels)
Expand Down

0 comments on commit e311617

Please sign in to comment.