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

feat!: added adapter config to TlcAdapter #18

Merged
merged 12 commits into from
Sep 4, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,17 @@

package de.fraunhofer.iem.kpiCalculator.adapter

import de.fraunhofer.iem.kpiCalculator.adapter.tools.tlc.TechLagResult
import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpi

/***
* Adapter that transforms a list of elements into a list of raw KPI values.
* @param T The type of the element that shall be transformed.
*/
interface KpiAdapter<T> {
/**
* Transforms a specified list of elements into a list of raw KPI values.
* In the case of failure, the result may contain elements indicating the status of the failure.
* @param data The elements that shall be transformed
* @return The transformed elements.
*/
fun transformDataToKpi(data: Collection<T>): Collection<AdapterResult>
/**
* Transforms an elements into a list of raw KPI values.
* In the case of failure, the result indicates the status of the failures.
* @param data The element that shall be transformed
* @return The transformed elements.
*/
fun transformDataToKpi(data: T): Collection<AdapterResult> = transformDataToKpi(listOf(data))
}

enum class ErrorType { DATA_VALIDATION_ERROR }

sealed class AdapterResult {
data class Success(val rawValueKpi: RawValueKpi) : AdapterResult()
sealed class Success(val rawValueKpi: RawValueKpi) : AdapterResult() {
class Kpi(rawValueKpi: RawValueKpi) : Success(rawValueKpi)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want these to be data classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but it was complicated.. :D
the following code is not allowed:
data class Kpi(override val rawValueKpi: RawValueKpi) : Success(rawValueKpi)
as we may not have override modifiers in primary constructors. We also must only have val / var in primary constructors. So we can't do the pattern we do with a class class Kpi(rawValueKpi: RawValueKpi) : Success(rawValueKpi) and "hand down" the rawValueKpi to its superclass.

class KpiTechLag(rawValueKpi: RawValueKpi, val techLag: TechLagResult.Success) : Success(rawValueKpi)
}

data class Error(val type: ErrorType) : AdapterResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,17 @@ package de.fraunhofer.iem.kpiCalculator.adapter.kpis.cve

import de.fraunhofer.iem.kpiCalculator.adapter.AdapterResult
import de.fraunhofer.iem.kpiCalculator.adapter.ErrorType
import de.fraunhofer.iem.kpiCalculator.adapter.KpiAdapter
import de.fraunhofer.iem.kpiCalculator.model.adapter.vulnerability.VulnerabilityDto
import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId
import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpi

object CveAdapter : KpiAdapter<VulnerabilityDto> {
object CveAdapter {

override fun transformDataToKpi(data: Collection<VulnerabilityDto>): Collection<AdapterResult> {
fun transformDataToKpi(data: Collection<VulnerabilityDto>): Collection<AdapterResult> {
return data
.map {
return@map if (isValid(it)) {
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.VULNERABILITY_SCORE,
score = (it.severity * 10).toInt()
Expand All @@ -36,9 +35,9 @@ object CveAdapter : KpiAdapter<VulnerabilityDto> {

private fun isValid(data: VulnerabilityDto): Boolean {
return (
data.severity in 0.0..10.0 &&
data.packageName.isNotBlank() &&
data.cveIdentifier.isNotBlank()
)
data.severity in 0.0..10.0 &&
data.packageName.isNotBlank() &&
data.cveIdentifier.isNotBlank()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ package de.fraunhofer.iem.kpiCalculator.adapter.kpis.vcs

import de.fraunhofer.iem.kpiCalculator.adapter.AdapterResult
import de.fraunhofer.iem.kpiCalculator.adapter.ErrorType
import de.fraunhofer.iem.kpiCalculator.adapter.KpiAdapter
import de.fraunhofer.iem.kpiCalculator.model.adapter.vcs.RepositoryDetailsDto
import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId
import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpi

object VcsAdapter : KpiAdapter<RepositoryDetailsDto> {
object VcsAdapter {

override fun transformDataToKpi(data: Collection<RepositoryDetailsDto>): Collection<AdapterResult> {
fun transformDataToKpi(data: Collection<RepositoryDetailsDto>): Collection<AdapterResult> {

if (data.size != 1) {
return listOf(
Expand All @@ -29,19 +28,19 @@ object VcsAdapter : KpiAdapter<RepositoryDetailsDto> {
//XXX: we need to decide about error handling in adapters
val repoDetailsDto = data.first()
return listOf(
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.NUMBER_OF_COMMITS,
score = repoDetailsDto.numberOfCommits
)
),
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.NUMBER_OF_SIGNED_COMMITS,
score = repoDetailsDto.numberOfSignedCommits
)
),
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.IS_DEFAULT_BRANCH_PROTECTED,
score = if (repoDetailsDto.isDefaultBranchProtected) 100 else 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,51 @@
package de.fraunhofer.iem.kpiCalculator.adapter.tools.occmd

import de.fraunhofer.iem.kpiCalculator.adapter.AdapterResult
import de.fraunhofer.iem.kpiCalculator.adapter.KpiAdapter
import de.fraunhofer.iem.kpiCalculator.model.adapter.occmd.Checks
import de.fraunhofer.iem.kpiCalculator.model.adapter.occmd.OccmdDto
import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId
import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpi

object OccmdAdapter : KpiAdapter<OccmdDto> {
object OccmdAdapter {

override fun transformDataToKpi(data: Collection<OccmdDto>): Collection<AdapterResult> {
fun transformDataToKpi(data: Collection<OccmdDto>): Collection<AdapterResult> {

return data.mapNotNull {
return@mapNotNull when (Checks.fromString(it.check)) {
Checks.CheckedInBinaries ->
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.CHECKED_IN_BINARIES,
score = (it.score * 100).toInt()
)
)

Checks.SastUsageBasic ->
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.SAST_USAGE,
score = (it.score * 100).toInt()
)
)

Checks.Secrets ->
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.SECRETS,
score = (it.score * 100).toInt()
)
)

Checks.CommentsInCode ->
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.COMMENTS_IN_CODE,
score = (it.score * 100).toInt()
)
)

Checks.DocumentationInfrastructure ->
AdapterResult.Success(
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.DOCUMENTATION_INFRASTRUCTURE,
score = (it.score * 100).toInt()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,41 @@ package de.fraunhofer.iem.kpiCalculator.adapter.tools.tlc

import de.fraunhofer.iem.kpiCalculator.adapter.AdapterResult
import de.fraunhofer.iem.kpiCalculator.adapter.ErrorType
import de.fraunhofer.iem.kpiCalculator.adapter.KpiAdapter
import de.fraunhofer.iem.kpiCalculator.adapter.tools.tlc.model.Project
import de.fraunhofer.iem.kpiCalculator.adapter.tools.tlc.model.Version
import de.fraunhofer.iem.kpiCalculator.adapter.tools.tlc.util.TechLagHelper.getTechLagForGraph
import de.fraunhofer.iem.kpiCalculator.model.adapter.tlc.TlcConfig
import de.fraunhofer.iem.kpiCalculator.model.adapter.tlc.TlcDefaultConfig
import de.fraunhofer.iem.kpiCalculator.model.adapter.tlc.TlcDto
import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId
import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.InputStream

sealed class TechLagResult {
data class Success(val libyear: Long) : TechLagResult()
data class Empty(val reason: String) : TechLagResult()
}

object TlcAdapter : KpiAdapter<TlcDto> {
override fun transformDataToKpi(data: Collection<TlcDto>): Collection<AdapterResult> {
object TlcAdapter {

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

@OptIn(ExperimentalSerializationApi::class)
fun dtoFromJson(jsonData: InputStream): TlcDto {
return jsonParser.decodeFromStream<TlcDto>(jsonData)
}

fun transformDataToKpi(
data: Collection<TlcDto>,
config: TlcConfig = TlcDefaultConfig.get()
): Collection<AdapterResult> {

return data.flatMap { tlcDto ->
tlcDto.projectDtos.flatMap {
val project = Project.from(it)
Expand All @@ -40,7 +60,7 @@ object TlcAdapter : KpiAdapter<TlcDto> {

if (techLag is TechLagResult.Success) {

val libyearScore = getLibyearScore(techLag.libyear)
val libyearScore = getLibyearScore(techLag.libyear, config)

val rawValueKpi = if (
isProductionScope(ecosystem = project.ecosystem, scope = scope)
Expand All @@ -56,7 +76,10 @@ object TlcAdapter : KpiAdapter<TlcDto> {
)
}

return@map AdapterResult.Success(rawValueKpi)
return@map AdapterResult.Success.KpiTechLag(
rawValueKpi = rawValueKpi,
techLag = techLag
)
}

return@map AdapterResult.Error(ErrorType.DATA_VALIDATION_ERROR)
Expand All @@ -65,16 +88,21 @@ object TlcAdapter : KpiAdapter<TlcDto> {
}
}

private fun getLibyearScore(libyear: Long): Int {
// NB: these values need to be sanity checked
// TODO: libyear thresholds should live in a config file
return when (libyear) {
in Int.MIN_VALUE..30 -> 100
in 31..90 -> 75
in 91..180 -> 50
in 181..360 -> 25
else -> 0
private fun getLibyearScore(libyear: Long, config: TlcConfig): Int {

if (libyear < 0L) {
return 100
}

val sortedThresholds = config.thresholds.sortedBy { it.range.from }

sortedThresholds.forEach { threshold ->
if (libyear > threshold.range.from && libyear < threshold.range.to) {
return threshold.score
}
}

return 0
}

private fun isProductionScope(scope: String, ecosystem: String): Boolean {
Expand All @@ -83,5 +111,4 @@ object TlcAdapter : KpiAdapter<TlcDto> {
else -> true
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ internal object TechLagHelper {
}

private fun getTechLagForNode(node: Node, artifacts: List<Artifact>, targetUpdateType: Version): TechLagResult {

val childSum = node.children.sumOf {
val techLag = getTechLagForNode(it, artifacts, targetUpdateType)
if (techLag is TechLagResult.Success) {
techLag.libyear
} else {
0
}
}

val artifact = artifacts[node.artifactIdx]

val targetVersion = getTargetVersion(
Expand All @@ -61,6 +71,6 @@ internal object TechLagHelper {
newestVersion = targetVersion.releaseDate
)

return TechLagResult.Success(libyear = diffInDays)
return TechLagResult.Success(libyear = diffInDays + childSum)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
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
Expand All @@ -20,7 +19,7 @@ import kotlinx.serialization.json.*
import java.io.InputStream
import kotlin.math.max

object TrivyAdapter : KpiAdapter<TrivyDto> {
object TrivyAdapter {

private val logger = KotlinLogging.logger {}

Expand All @@ -29,28 +28,28 @@ object TrivyAdapter : KpiAdapter<TrivyDto> {
explicitNulls = false
}

override fun transformDataToKpi(data: Collection<TrivyDto>): Collection<AdapterResult> {
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)
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
val schemaVersion = json["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 {
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 })
Expand All @@ -69,7 +68,7 @@ object TrivyAdapter : KpiAdapter<TrivyDto> {
* 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> {
private fun createVulnerabilitiesDto(vulnerabilities: Collection<TrivyVulnerabilityDto>): Collection<VulnerabilityDto> {
return vulnerabilities
.mapNotNull {
if (it.CVSS == null) {
Expand All @@ -87,7 +86,7 @@ object TrivyAdapter : KpiAdapter<TrivyDto> {
}
}

private fun getHighestCvssScore(scores: Collection<CVSSData>) : Double {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ class CveAdapterTest {
val adapter = CveAdapter
// valid input
val validKpi = adapter.transformDataToKpi(
VulnerabilityDto(
cveIdentifier = "not blank",
packageName = "not blank",
severity = 0.1
listOf(
VulnerabilityDto(
cveIdentifier = "not blank",
packageName = "not blank",
severity = 0.1
)
)
)
when (val kpi = validKpi.first()) {
Expand Down
Loading