Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement trivy adapter #15

Merged
merged 5 commits into from
Aug 29, 2024
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
5 changes: 5 additions & 0 deletions adapter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@

plugins {
id("spha-kotlin-conventions")
alias(libs.plugins.serialization)
janniclas marked this conversation as resolved.
Show resolved Hide resolved
}

group = "de.fraunhofer.iem.kpiCalculator"

dependencies {
implementation(project(":model"))
implementation(libs.kotlin.serialization.json)

testImplementation(libs.test.junit5.params)
testImplementation(libs.test.mockk)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2024 Fraunhofer IEM. All rights reserved.
*
* Licensed under the MIT license. See LICENSE file in the project root for details.
*
* SPDX-License-Identifier: MIT
* License-Filename: LICENSE
*/

package de.fraunhofer.iem.kpiCalculator.adapter.tools.trivy

import de.fraunhofer.iem.kpiCalculator.adapter.AdapterResult
import de.fraunhofer.iem.kpiCalculator.adapter.KpiAdapter
import de.fraunhofer.iem.kpiCalculator.adapter.kpis.cve.CveAdapter
import de.fraunhofer.iem.kpiCalculator.model.adapter.trivy.*
import de.fraunhofer.iem.kpiCalculator.model.adapter.vulnerability.VulnerabilityDto
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.*
import java.io.InputStream
import kotlin.math.max

object TrivyAdapter : KpiAdapter<TrivyDto> {
janniclas marked this conversation as resolved.
Show resolved Hide resolved

private val logger = KotlinLogging.logger {}

private val jsonParser = Json {
ignoreUnknownKeys = true
explicitNulls = false
}

override fun transformDataToKpi(data: Collection<TrivyDto>): Collection<AdapterResult> {
return CveAdapter.transformDataToKpi(data.flatMap { it.Vulnerabilities })
}

@OptIn(ExperimentalSerializationApi::class)
fun dtoFromJson(jsonData: InputStream): TrivyDto {
val json = Json.decodeFromStream<JsonElement>(jsonData)

if (json is JsonArray)
return parseV1(json)
else if (json !is JsonObject)
throw UnsupportedOperationException("The provided Trivy result is not supported.")

val schemaVersion = json.get("SchemaVersion")?.jsonPrimitive?.intOrNull

if (schemaVersion == 2)
return parseV2(json)

throw UnsupportedOperationException("Trivy results for schema version '$schemaVersion' are currently not supported.")
}

private fun parseV1(json: JsonArray) : TrivyDto {
logger.info { "Processing Trivy result from version 0.19.0 or earlier." }
val v1dto = jsonParser.decodeFromJsonElement<List<TrivyDtoV1>>(json)
val vulnerabilities = createVulnerabilitiesDto(v1dto.flatMap { it.Vulnerabilities })
return TrivyDto(vulnerabilities)
}

private fun parseV2(json: JsonObject): TrivyDto {
logger.info { "Processing Trivy result of SchemaVersion: 2" }
val v2dto = jsonParser.decodeFromJsonElement<TrivyDtoV2>(json)
val vulnerabilities = createVulnerabilitiesDto(v2dto.Results.flatMap { it.Vulnerabilities })
return TrivyDto(vulnerabilities)
}

/***
* Transforms a collection of Trivy-specific vulnerabilities into the generalized vulnerability format.
* Trivy allows to annotate multiple CVSS scores to a vulnerability entry (e.g, CVSS2 or CVSS3 or even vendor specific).
* This transformation always selects the highest available score for each vulnerability.
*/
private fun createVulnerabilitiesDto(vulnerabilities: Collection<TrivyVulnerabilityDto>) : Collection<VulnerabilityDto> {
return vulnerabilities
.mapNotNull {
if (it.CVSS == null) {
logger.debug { "Reported vulnerability '${it.VulnerabilityID}' does not have a score. Skipping!" }
return@mapNotNull null
}

val cvssData = it.CVSS!!.values.map {
jsonParser.decodeFromJsonElement<CVSSData>(it)
}

val score = getHighestCvssScore(cvssData)
logger.trace { "Selected CVSS score $score for vulnerability '${it.VulnerabilityID}'" }
VulnerabilityDto(it.VulnerabilityID, it.PkgID, score)
}
}

private fun getHighestCvssScore(scores: Collection<CVSSData>) : Double {
// NB: If no value was coded we simply return 0.0 (no vulnerability)
// In practice this should never happen
var v2Score = 0.0
var v3Score = 0.0

for (data in scores) {
if (data.V2Score != null)
v2Score = max(v2Score, data.V2Score!!)

if (data.V3Score != null)
v3Score = max(v3Score, data.V3Score!!)
}

return max(v2Score, v3Score)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package de.fraunhofer.iem.kpiCalculator.adapter.tools.trivy

import de.fraunhofer.iem.kpiCalculator.adapter.kpis.cve.CveAdapter
import de.fraunhofer.iem.kpiCalculator.model.adapter.trivy.TrivyDto
import de.fraunhofer.iem.kpiCalculator.model.adapter.vulnerability.VulnerabilityDto
import io.mockk.mockkObject
import io.mockk.verify
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.nio.file.Files
import kotlin.io.path.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class TrivyAdapterTest {

@ParameterizedTest
@ValueSource(strings = [
"{}", // No schema
"{\"SchemaVersion\": 3}" // Not supported schema
])
fun testInvalidJson(input: String) {
input.byteInputStream().use {
assertThrows<UnsupportedOperationException> { TrivyAdapter.dtoFromJson(it) }
}
}

@ParameterizedTest
@ValueSource(strings = [
"[]",
"{\"SchemaVersion\": 2}"
])
fun testEmptyDto(input: String){
input.byteInputStream().use {
val dto = TrivyAdapter.dtoFromJson(it)
assertEquals(0, dto.Vulnerabilities.count())
}
}

@Test
fun testResult2Dto(){
Files.newInputStream(Path("src/test/resources/trivy-result-v2.json")).use {
val dto = assertDoesNotThrow { TrivyAdapter.dtoFromJson(it) }
assertEquals(1, dto.Vulnerabilities.count())

val vuln = dto.Vulnerabilities.first()
assertEquals("CVE-2011-3374", vuln.cveIdentifier)
assertEquals("[email protected]", vuln.packageName)
assertEquals(4.3, vuln.severity)

}
}

@Test
fun testResult1Dto(){
Files.newInputStream(Path("src/test/resources/trivy-result-v1.json")).use {
val dto = assertDoesNotThrow { TrivyAdapter.dtoFromJson(it) }
assertEquals(2, dto.Vulnerabilities.count())

assertTrue { dto.Vulnerabilities.all { it.cveIdentifier == "CVE-2005-2541" } }
assertEquals("[email protected]+dfsg-1.2", dto.Vulnerabilities.first().packageName)
assertEquals(10.0, dto.Vulnerabilities.first().severity)
}
}

@Test
fun testDto2Kpi_VerifyCveAdapterGetsCalled() {
mockkObject(CveAdapter)
val vulns = listOf(
VulnerabilityDto("CVE-1", "A", 1.0),
VulnerabilityDto("CVE-2", "B", 2.0),
VulnerabilityDto("CVE-3", "C", 1.3),
)
TrivyAdapter.transformDataToKpi(listOf(TrivyDto(vulns)))
verify { CveAdapter.transformDataToKpi(vulns) }
}
}
162 changes: 162 additions & 0 deletions adapter/src/test/resources/trivy-result-v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
[
{
"Target": "alpine 3.12",
"Type": "alpine",
"Vulnerabilities": [
{
"VulnerabilityID": "TEMP-0517018-A83CE6",
"PkgID": "[email protected]",
"PkgName": "sysvinit-utils",
"PkgIdentifier": {
"PURL": "pkg:deb/debian/[email protected]?arch=amd64\u0026distro=debian-12.2",
"UID": "a95815274a3d74d8"
},
"InstalledVersion": "3.06-4",
"Status": "affected",
"Layer": {
"DiffID": "sha256:7cea17427f83f6c4706c74f94fb6d7925b06ea9a0701234f1a9d43f6af11432a"
},
"SeveritySource": "debian",
"PrimaryURL": "https://security-tracker.debian.org/tracker/TEMP-0517018-A83CE6",
"DataSource": {
"ID": "debian",
"Name": "Debian Security Tracker",
"URL": "https://salsa.debian.org/security-tracker-team/security-tracker"
},
"Title": "[sysvinit: no-root option in expert installer exposes locally exploitable security flaw]",
"Severity": "LOW",
"VendorSeverity": {
"debian": 1
}
},
{
"VulnerabilityID": "CVE-2005-2541",
"PkgID": "[email protected]+dfsg-1.2",
"PkgName": "tar",
"PkgIdentifier": {
"PURL": "pkg:deb/debian/[email protected]%2Bdfsg-1.2?arch=amd64\u0026distro=debian-12.2",
"UID": "efe23db0e46e9a72"
},
"InstalledVersion": "1.34+dfsg-1.2",
"Status": "affected",
"Layer": {
"DiffID": "sha256:7cea17427f83f6c4706c74f94fb6d7925b06ea9a0701234f1a9d43f6af11432a"
},
"SeveritySource": "debian",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2005-2541",
"DataSource": {
"ID": "debian",
"Name": "Debian Security Tracker",
"URL": "https://salsa.debian.org/security-tracker-team/security-tracker"
},
"Title": "tar: does not properly warn the user when extracting setuid or setgid files",
"Description": "Tar 1.15.1 does not properly warn the user when extracting setuid or setgid files, which may allow local users or remote attackers to gain privileges.",
"Severity": "LOW",
"VendorSeverity": {
"debian": 1,
"nvd": 3,
"redhat": 2
},
"CVSS": {
"nvd": {
"V2Vector": "AV:N/AC:L/Au:N/C:C/I:C/A:C",
"V2Score": 10
},
"redhat": {
"V3Vector": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
"V3Score": 7
}
},
"References": [
"http://marc.info/?l=bugtraq\u0026m=112327628230258\u0026w=2",
"https://access.redhat.com/security/cve/CVE-2005-2541",
"https://lists.apache.org/thread.html/rc713534b10f9daeee2e0990239fa407e2118e4aa9e88a7041177497c%40%3Cissues.guacamole.apache.org%3E",
"https://nvd.nist.gov/vuln/detail/CVE-2005-2541",
"https://www.cve.org/CVERecord?id=CVE-2005-2541"
],
"PublishedDate": "2005-08-10T04:00:00Z",
"LastModifiedDate": "2023-11-07T01:57:39.453Z"
}
]
},
{
"Target": "app/jenkins.jar",
"Type": "java",
"Vulnerabilities": [
{
"VulnerabilityID": "TEMP-0517018-A83CE6",
"PkgID": "[email protected]",
"PkgName": "sysvinit-utils",
"PkgIdentifier": {
"PURL": "pkg:deb/debian/[email protected]?arch=amd64\u0026distro=debian-12.2",
"UID": "a95815274a3d74d8"
},
"InstalledVersion": "3.06-4",
"Status": "affected",
"Layer": {
"DiffID": "sha256:7cea17427f83f6c4706c74f94fb6d7925b06ea9a0701234f1a9d43f6af11432a"
},
"SeveritySource": "debian",
"PrimaryURL": "https://security-tracker.debian.org/tracker/TEMP-0517018-A83CE6",
"DataSource": {
"ID": "debian",
"Name": "Debian Security Tracker",
"URL": "https://salsa.debian.org/security-tracker-team/security-tracker"
},
"Title": "[sysvinit: no-root option in expert installer exposes locally exploitable security flaw]",
"Severity": "LOW",
"VendorSeverity": {
"debian": 1
}
},
{
"VulnerabilityID": "CVE-2005-2541",
"PkgID": "[email protected]+dfsg-1.2",
"PkgName": "tar",
"PkgIdentifier": {
"PURL": "pkg:deb/debian/[email protected]%2Bdfsg-1.2?arch=amd64\u0026distro=debian-12.2",
"UID": "efe23db0e46e9a72"
},
"InstalledVersion": "1.34+dfsg-1.2",
"Status": "affected",
"Layer": {
"DiffID": "sha256:7cea17427f83f6c4706c74f94fb6d7925b06ea9a0701234f1a9d43f6af11432a"
},
"SeveritySource": "debian",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2005-2541",
"DataSource": {
"ID": "debian",
"Name": "Debian Security Tracker",
"URL": "https://salsa.debian.org/security-tracker-team/security-tracker"
},
"Title": "tar: does not properly warn the user when extracting setuid or setgid files",
"Description": "Tar 1.15.1 does not properly warn the user when extracting setuid or setgid files, which may allow local users or remote attackers to gain privileges.",
"Severity": "LOW",
"VendorSeverity": {
"debian": 1,
"nvd": 3,
"redhat": 2
},
"CVSS": {
"nvd": {
"V2Vector": "AV:N/AC:L/Au:N/C:C/I:C/A:C",
"V2Score": 10
},
"redhat": {
"V3Vector": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
"V3Score": 7
}
},
"References": [
"http://marc.info/?l=bugtraq\u0026m=112327628230258\u0026w=2",
"https://access.redhat.com/security/cve/CVE-2005-2541",
"https://lists.apache.org/thread.html/rc713534b10f9daeee2e0990239fa407e2118e4aa9e88a7041177497c%40%3Cissues.guacamole.apache.org%3E",
"https://nvd.nist.gov/vuln/detail/CVE-2005-2541",
"https://www.cve.org/CVERecord?id=CVE-2005-2541"
],
"PublishedDate": "2005-08-10T04:00:00Z",
"LastModifiedDate": "2023-11-07T01:57:39.453Z"
}
]
}
]
Loading