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

Feature/max min strategy #31

Merged
merged 13 commits into from
Oct 30, 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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ object CveAdapter {
AdapterResult.Success.Kpi(
RawValueKpi(
kind = KpiId.VULNERABILITY_SCORE,
score = (it.severity * 10).toInt(),
score = 100 - (it.severity * 10).toInt(),
)
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ object TrivyAdapter {
val cvssData = it.cvss!!.values.map { jsonParser.decodeFromJsonElement<CVSSData>(it) }

val score = getHighestCvssScore(cvssData)
val packageID = "${it.pkgName}@${it.installedVersion}"
logger.trace { "Selected CVSS score $score for vulnerability '${it.vulnerabilityID}'" }
VulnerabilityDto(it.vulnerabilityID, it.pkgID, score)
VulnerabilityDto(it.vulnerabilityID, packageID, score)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class TrivyAdapterTest {
fun testResult2Dto() {
Files.newInputStream(Path("src/test/resources/trivy-result-v2.json")).use {
val dto = assertDoesNotThrow { TrivyAdapter.dtoFromJson(it) }
assertEquals(1, dto.vulnerabilities.count())
assertEquals(2, dto.vulnerabilities.count())

val vuln = dto.vulnerabilities.first()
assertEquals("CVE-2011-3374", vuln.cveIdentifier)
Expand Down
55 changes: 55 additions & 0 deletions adapter/src/test/resources/trivy-result-v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,61 @@
}
}
]
},
{
"Target": "Python",
"Class": "lang-pkgs",
"Type": "python-pkg",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2024-22190",
"PkgName": "GitPython",
"PkgPath": "home/redacted/.local/lib/python3.11/site-packages/GitPython-3.1.37.dist-info/METADATA",
"PkgIdentifier": {
"PURL": "pkg:pypi/[email protected]",
"UID": "5591a0dc57c78b2"
},
"InstalledVersion": "3.1.37",
"FixedVersion": "3.1.41",
"Status": "fixed",
"Layer": {
"Digest": "sha256:13b0acb9b68e8a74b8e6152932c5bd6c6968e13fa32feba83cc2310346a9b7f9",
"DiffID": "sha256:feea6321b4864eb2cb16188d1619323db7c8738adaca6982c8005da9fe227961"
},
"SeveritySource": "ghsa",
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-22190",
"DataSource": {
"ID": "ghsa",
"Name": "GitHub Security Advisory pip",
"URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip"
},
"Title": "Untrusted search path under some conditions on Windows allows arbitrary code execution",
"Description": "GitPython is a python library used to interact with Git repositories. There is an incomplete fix for CVE-2023-40590. On Windows, GitPython uses an untrusted search path if it uses a shell to run `git`, as well as when it runs `bash.exe` to interpret hooks. If either of those features are used on Windows, a malicious `git.exe` or `bash.exe` may be run from an untrusted repository. This issue has been patched in version 3.1.41.",
"Severity": "HIGH",
"CweIDs": [
"CWE-426"
],
"VendorSeverity": {
"ghsa": 3,
"nvd": 3
},
"CVSS": {
"ghsa": {
"V3Vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"V3Score": 7.8
},
"nvd": {
"V3Vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"V3Score": 7.8
}
},
"References": [
"https://github.com/gitpython-developers/GitPython"
],
"PublishedDate": "2024-01-11T02:15:48.25Z",
"LastModifiedDate": "2024-01-18T13:48:07.553Z"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@ import de.fraunhofer.iem.spha.core.hierarchy.KpiHierarchyEdge
import de.fraunhofer.iem.spha.model.kpi.KpiStrategyId
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiNode
import io.github.oshai.kotlinlogging.KotlinLogging

internal fun getKpiCalculationStrategy(strategyId: KpiStrategyId): KpiCalculationStrategy {
return when (strategyId) {
KpiStrategyId.RAW_VALUE_STRATEGY -> RawValueKpiCalculationStrategy
KpiStrategyId.RATIO_STRATEGY -> RatioKPICalculationStrategy

KpiStrategyId.AGGREGATION_STRATEGY -> AggregationKPICalculationStrategy
KpiStrategyId.WEIGHTED_RATIO_STRATEGY -> WeightedRatioKPICalculationStrategy

KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY -> WeightedAverageKPICalculationStrategy

KpiStrategyId.MAXIMUM_STRATEGY -> MaximumKPICalculationStrategy

KpiStrategyId.WEIGHTED_MAXIMUM_STRATEGY -> MaximumKPICalculationStrategy
KpiStrategyId.MINIMUM_STRATEGY -> MaximumKPICalculationStrategy

KpiStrategyId.WEIGHTED_MAXIMUM_STRATEGY -> WeightedMaximumKPICalculationStrategy

KpiStrategyId.WEIGHTED_MINIMUM_STRATEGY -> WeightedMinimumKPICalculationStrategy
}
}

Expand Down Expand Up @@ -60,6 +66,8 @@ internal interface KpiCalculationStrategy {
fun isValid(node: KpiNode, strict: Boolean = false): Boolean
}

private val logger = KotlinLogging.logger {}

internal abstract class BaseKpiCalculationStrategy : KpiCalculationStrategy {

abstract val kpiStrategyId: KpiStrategyId
Expand All @@ -82,7 +90,7 @@ internal abstract class BaseKpiCalculationStrategy : KpiCalculationStrategy {

updateEdgeWeights(edges = hierarchyEdges, strict)

val result = internalCalculateKpi(hierarchyEdges)
val result = getResultInValidRange(internalCalculateKpi(hierarchyEdges))

if (
hierarchyEdges.any { it.to.result !is KpiCalculationResult.Success } &&
Expand Down Expand Up @@ -149,4 +157,44 @@ internal abstract class BaseKpiCalculationStrategy : KpiCalculationStrategy {
}

protected abstract fun internalIsValid(node: KpiNode, strict: Boolean): Boolean

companion object {
/**
* Checks if `result.score` is in a valid range (0..100). If the score is lower 0 or higher
* than 100, we return 0 or 100 respectively.
*
* @param result
*/
fun getResultInValidRange(result: KpiCalculationResult): KpiCalculationResult {
return when (result) {
is KpiCalculationResult.Success ->
createValidScore(result, result.score) { score ->
KpiCalculationResult.Success(score)
}
is KpiCalculationResult.Incomplete ->
createValidScore(result, result.score) { score ->
KpiCalculationResult.Incomplete(score, result.reason)
}
else -> result
}
}

private fun <T : KpiCalculationResult> createValidScore(
result: T,
score: Int,
createResult: (Int) -> T,
): T {
return when {
score < 0 -> {
logger.warn { "Calculation result score $result is out of bounds." }
createResult(0)
}
score > 100 -> {
logger.warn { "Calculation result score $result is out of bounds." }
createResult(100)
}
else -> result
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ internal object MaximumKPICalculationStrategy : BaseKpiCalculationStrategy() {

override fun internalCalculateKpi(edges: Collection<KpiHierarchyEdge>): KpiCalculationResult {

val max = if (edges.isEmpty()) 0 else edges.maxOf { it.to.score }
val max = edges.maxOfOrNull { it.to.score }

if (max == null) {
return KpiCalculationResult.Empty()
}

return KpiCalculationResult.Success(score = max)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.spha.core.strategy

import de.fraunhofer.iem.spha.core.hierarchy.KpiHierarchyEdge
import de.fraunhofer.iem.spha.model.kpi.KpiStrategyId
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiNode
import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger {}

internal object MinimumKPICalculationStrategy : BaseKpiCalculationStrategy() {

override val kpiStrategyId: KpiStrategyId
get() = KpiStrategyId.MINIMUM_STRATEGY

override fun internalCalculateKpi(edges: Collection<KpiHierarchyEdge>): KpiCalculationResult {

val min = edges.minOfOrNull { it.to.score }

if (min == null) {
return KpiCalculationResult.Empty()
}

return KpiCalculationResult.Success(score = min)
}

/** There is no validity requirement for this strategy. */
override fun internalIsValid(node: KpiNode, strict: Boolean): Boolean {

if (node.edges.size == 1) {
logger.warn {
"Minimum KPI calculation strategy for node $node is planned " +
"for a single child."
}
}

return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger {}

internal object AggregationKPICalculationStrategy : BaseKpiCalculationStrategy() {
internal object WeightedAverageKPICalculationStrategy : BaseKpiCalculationStrategy() {

override val kpiStrategyId: KpiStrategyId
get() = KpiStrategyId.AGGREGATION_STRATEGY
get() = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY

/**
* This function calculates the aggregate sum of all given children. If a child is empty it is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ internal object WeightedMaximumKPICalculationStrategy : BaseKpiCalculationStrate

override fun internalCalculateKpi(edges: Collection<KpiHierarchyEdge>): KpiCalculationResult {

val max = if (edges.isEmpty()) 0 else edges.maxOf { it.to.score * it.actualWeight }.toInt()
val max = edges.maxByOrNull { it.to.score * it.actualWeight }?.to?.score

if (max == null) {
return KpiCalculationResult.Empty()
}

return KpiCalculationResult.Success(score = max)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.spha.core.strategy

import de.fraunhofer.iem.spha.core.hierarchy.KpiHierarchyEdge
import de.fraunhofer.iem.spha.model.kpi.KpiStrategyId
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiNode
import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger {}

internal object WeightedMinimumKPICalculationStrategy : BaseKpiCalculationStrategy() {

override val kpiStrategyId: KpiStrategyId
get() = KpiStrategyId.WEIGHTED_MINIMUM_STRATEGY

override fun internalCalculateKpi(edges: Collection<KpiHierarchyEdge>): KpiCalculationResult {

val min = edges.minByOrNull { it.to.score * it.actualWeight }?.to?.score

if (min == null) {
return KpiCalculationResult.Empty()
}

return KpiCalculationResult.Success(score = min)
}

/** There is no validity requirement for this strategy. */
override fun internalIsValid(node: KpiNode, strict: Boolean): Boolean {

if (node.edges.size == 1) {
logger.warn {
"Maximum KPI calculation strategy for node $node is planned " +
"for a single child."
}
}

return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import io.github.oshai.kotlinlogging.KotlinLogging

private val logger = KotlinLogging.logger {}

internal object RatioKPICalculationStrategy : BaseKpiCalculationStrategy() {
internal object WeightedRatioKPICalculationStrategy : BaseKpiCalculationStrategy() {

override val kpiStrategyId: KpiStrategyId
get() = KpiStrategyId.RATIO_STRATEGY
get() = KpiStrategyId.WEIGHTED_RATIO_STRATEGY

/** Returns smallerValue / biggerValue, regardless in which order the values are given. */
override fun internalCalculateKpi(edges: Collection<KpiHierarchyEdge>): KpiCalculationResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class KpiCalculatorTest {
val root =
KpiNode(
kpiId = KpiId.ROOT,
kpiStrategyId = KpiStrategyId.AGGREGATION_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY,
edges =
listOf(
KpiEdge(
Expand Down Expand Up @@ -106,7 +106,7 @@ class KpiCalculatorTest {
val root =
KpiNode(
kpiId = KpiId.ROOT,
kpiStrategyId = KpiStrategyId.AGGREGATION_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY,
edges =
listOf(
KpiEdge(
Expand Down Expand Up @@ -166,14 +166,14 @@ class KpiCalculatorTest {
val root =
KpiNode(
kpiId = KpiId.ROOT,
kpiStrategyId = KpiStrategyId.AGGREGATION_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY,
edges =
listOf(
KpiEdge(
target =
KpiNode(
kpiId = KpiId.SIGNED_COMMITS_RATIO,
kpiStrategyId = KpiStrategyId.RATIO_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_RATIO_STRATEGY,
edges =
listOf(
KpiEdge(
Expand Down Expand Up @@ -247,7 +247,7 @@ class KpiCalculatorTest {
val root =
KpiNode(
kpiId = KpiId.ROOT,
kpiStrategyId = KpiStrategyId.AGGREGATION_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY,
edges =
listOf(
KpiEdge(
Expand Down Expand Up @@ -284,7 +284,7 @@ class KpiCalculatorTest {
val root =
KpiNode(
kpiId = KpiId.ROOT,
kpiStrategyId = KpiStrategyId.AGGREGATION_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY,
edges =
listOf(
KpiEdge(
Expand Down Expand Up @@ -338,7 +338,7 @@ class KpiCalculatorTest {
val root =
KpiNode(
kpiId = KpiId.ROOT,
kpiStrategyId = KpiStrategyId.AGGREGATION_STRATEGY,
kpiStrategyId = KpiStrategyId.WEIGHTED_AVERAGE_STRATEGY,
edges =
listOf(
KpiEdge(
Expand Down
Loading