From df6622b8c547e3a3915932f1f4ef6c9649a5ed2d Mon Sep 17 00:00:00 2001
From: Jan-Niclas Struewer <j.n.struewer@gmail.com>
Date: Thu, 8 Aug 2024 14:57:35 +0200
Subject: [PATCH] feature: added explicit value for actual edge weights after
 KPI calculation

---
 .../iem/app/kpi/enumeration/KpiKind.kt        | 16 ++---
 app/gradle/libs.versions.toml                 |  2 +-
 .../iem/kpiCalculator/core/KpiCalculator.kt   |  2 +-
 .../core/hierarchy/KpiCalculationNode.kt      | 58 +++++++++++++++----
 .../core/hierarchy/KpiHierarchyEdge.kt        |  3 +-
 .../strategy/MaximumKPICalculationStrategy.kt |  2 +-
 .../strategy/RatioKPICalculationStrategy.kt   |  2 +-
 .../kpiCalculator/core/KpiCalculatorTest.kt   |  6 ++
 .../model/kpi/hierarchy/DefaultHierarchy.kt   |  3 -
 .../model/kpi/hierarchy/KpiHierarchy.kt       |  2 +-
 10 files changed, 65 insertions(+), 31 deletions(-)

diff --git a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/enumeration/KpiKind.kt b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/enumeration/KpiKind.kt
index 4a0539c2..67f339a1 100644
--- a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/enumeration/KpiKind.kt
+++ b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/enumeration/KpiKind.kt
@@ -7,22 +7,18 @@ import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult
 import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiResultNode
 
 fun KpiResultNode.toViewModel(): KPITreeResponseDto {
-    var additionalWeight = 0.0
-    val score = when (this.kpiResult) {
-        is KpiCalculationResult.Success -> (this.kpiResult as KpiCalculationResult.Success).score
-        is KpiCalculationResult.Incomplete -> {
-            additionalWeight = (this.kpiResult as KpiCalculationResult.Incomplete).additionalWeights
-            (this.kpiResult as KpiCalculationResult.Incomplete).score
-        }
-
+    val score = when (val result = this.kpiResult) {
+        is KpiCalculationResult.Success -> result.score
+        is KpiCalculationResult.Incomplete -> result.score
         else -> -1
     }
     val isEmpty = score == -1
+
     return this.kpiId.toViewModel(score, this.children.map {
         KPITreeChildResponseDto(
             kpi = it.target.toViewModel(),
-            plannedWeight = it.weight,
-            actualWeight = it.weight + additionalWeight
+            plannedWeight = it.plannedWeight,
+            actualWeight = it.actualWeight
         )
     }, isEmpty)
 
diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml
index eb30cf3a..0009636e 100644
--- a/app/gradle/libs.versions.toml
+++ b/app/gradle/libs.versions.toml
@@ -2,7 +2,7 @@
 kotlin = "2.0.10"
 springBoot = "3.3.2"
 kotlinCoroutine = "1.9.0-RC"
-ktor = "3.0.0-beta-2"
+ktor = "2.3.12"
 kpiCalculator = "0.0.2-SNAPSHOT"
 kotlinJackson = "2.17.1"
 springOpenApi = "2.5.0"
diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculator.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculator.kt
index 5f2abb90..8ae82a12 100644
--- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculator.kt
+++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculator.kt
@@ -81,7 +81,7 @@ object KpiCalculator {
                         calculationStrategy = KpiStrategyId.RAW_VALUE_STRATEGY,
                         parent = parent
                     )
-                    newNode.setScore(rawValue.score)
+                    newNode.setResult(rawValue.score)
                     newNode
                 }
 
diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiCalculationNode.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiCalculationNode.kt
index a83b3275..252bfbfe 100644
--- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiCalculationNode.kt
+++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiCalculationNode.kt
@@ -17,9 +17,9 @@ internal class KpiCalculationNode(
     val parent: KpiCalculationNode?
 ) {
 
-    private var score: KpiCalculationResult = KpiCalculationResult.Empty()
-    fun setScore(score: Int) {
-        this.score = KpiCalculationResult.Success(score)
+    private var result: KpiCalculationResult = KpiCalculationResult.Empty()
+    fun setResult(result: Int) {
+        this.result = KpiCalculationResult.Success(result)
     }
 
 
@@ -28,7 +28,7 @@ internal class KpiCalculationNode(
         get() = _hierarchyEdges
 
     fun addChild(node: KpiCalculationNode, weight: Double) {
-        _hierarchyEdges.add(KpiHierarchyEdge(to = node, from = this, weight = weight))
+        _hierarchyEdges.add(KpiHierarchyEdge(to = node, from = this, plannedWeight = weight))
     }
 
     fun removeChild(node: KpiCalculationNode) {
@@ -36,16 +36,16 @@ internal class KpiCalculationNode(
     }
 
     fun getWeight(node: KpiCalculationNode): Double? {
-        return hierarchyEdges.find { it.to == node }?.weight
+        return hierarchyEdges.find { it.to == node }?.actualWeight
     }
 
     fun calculateKpi(): KpiCalculationResult {
         val strategyData = hierarchyEdges.map {
-            Pair(it.to.score, it.weight)
+            Pair(it.to.result, it.actualWeight)
         }
-        score = when (calculationStrategy) {
+        result = when (calculationStrategy) {
             KpiStrategyId.RAW_VALUE_STRATEGY ->
-                score
+                result
 
             KpiStrategyId.RATIO_STRATEGY ->
                 RatioKPICalculationStrategy.calculateKpi(strategyData)
@@ -56,8 +56,41 @@ internal class KpiCalculationNode(
             KpiStrategyId.MAXIMUM_STRATEGY ->
                 MaximumKPICalculationStrategy.calculateKpi(strategyData)
         }
+        updateEdgeWeights(result)
+        return result
+    }
+
+    private fun updateEdgeWeights(result: KpiCalculationResult) {
+        val updatedEdges = hierarchyEdges.map { edge ->
+
+            if (result is KpiCalculationResult.Success) {
+                return@map edge
+            }
+
+            val targetResult = edge.to.result
+
+            if (result is KpiCalculationResult.Incomplete
+                && (targetResult !is KpiCalculationResult.Empty
+                    && targetResult !is KpiCalculationResult.Error)
+            ) {
+                return@map KpiHierarchyEdge(
+                    from = this,
+                    to = edge.to,
+                    plannedWeight = edge.plannedWeight,
+                    actualWeight = edge.plannedWeight + result.additionalWeights
+                )
+            }
+
+            return@map KpiHierarchyEdge(
+                from = this,
+                to = edge.to,
+                plannedWeight = edge.plannedWeight,
+                actualWeight = 0.0
+            )
+        }
 
-        return score
+        _hierarchyEdges.clear()
+        _hierarchyEdges.addAll(updatedEdges)
     }
 
     companion object {
@@ -65,11 +98,12 @@ internal class KpiCalculationNode(
             return KpiResultNode(
                 kpiId = node.kind,
                 strategyType = node.calculationStrategy,
-                kpiResult = node.score,
+                kpiResult = node.result,
                 children = node.hierarchyEdges.map {
                     KpiResultEdge(
                         target = to(it.to),
-                        weight = it.weight
+                        plannedWeight = it.plannedWeight,
+                        actualWeight = it.actualWeight
                     )
                 }
             )
@@ -87,7 +121,7 @@ internal class KpiCalculationNode(
                 KpiHierarchyEdge(
                     to = from(child.target, calcNode),
                     from = calcNode,
-                    weight = child.weight
+                    plannedWeight = child.weight
                 )
             }
             calcNode._hierarchyEdges.addAll(children)
diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiHierarchyEdge.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiHierarchyEdge.kt
index 03c1a0df..1bb42bed 100644
--- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiHierarchyEdge.kt
+++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiHierarchyEdge.kt
@@ -3,5 +3,6 @@ package de.fraunhofer.iem.kpiCalculator.core.hierarchy
 internal data class KpiHierarchyEdge(
     val from: KpiCalculationNode,
     val to: KpiCalculationNode,
-    val weight: Double,
+    val plannedWeight: Double,
+    val actualWeight: Double = plannedWeight,
 )
diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/MaximumKPICalculationStrategy.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/MaximumKPICalculationStrategy.kt
index 16480821..a1b42e04 100644
--- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/MaximumKPICalculationStrategy.kt
+++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/MaximumKPICalculationStrategy.kt
@@ -2,7 +2,7 @@ package de.fraunhofer.iem.kpiCalculator.core.strategy
 
 import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult
 
-object MaximumKPICalculationStrategy : KpiCalculationStrategy {
+internal object MaximumKPICalculationStrategy : KpiCalculationStrategy {
     override fun calculateKpi(
         successScores: List<Pair<KpiCalculationResult.Success, Double>>,
         failed: List<Pair<KpiCalculationResult, Double>>,
diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RatioKPICalculationStrategy.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RatioKPICalculationStrategy.kt
index 10a03cd0..2a94ee32 100644
--- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RatioKPICalculationStrategy.kt
+++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RatioKPICalculationStrategy.kt
@@ -2,7 +2,7 @@ package de.fraunhofer.iem.kpiCalculator.core.strategy
 
 import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult
 
-object RatioKPICalculationStrategy : KpiCalculationStrategy {
+internal object RatioKPICalculationStrategy : KpiCalculationStrategy {
     /**
      * Returns smallerValue / biggerValue, regardless in which order the values are given.
      */
diff --git a/kpi-calculator/core/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculatorTest.kt b/kpi-calculator/core/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculatorTest.kt
index 4b8a4103..e80a516b 100644
--- a/kpi-calculator/core/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculatorTest.kt
+++ b/kpi-calculator/core/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculatorTest.kt
@@ -183,6 +183,12 @@ class KpiCalculatorTest {
 
         if (result is KpiCalculationResult.Incomplete) {
             assertEquals(85, result.score)
+            val sastResult = res.rootNode.children.find { it.target.kpiId == KpiId.SAST_USAGE } ?: fail()
+            assertEquals(0.0, sastResult.actualWeight)
+            val vulnerabilityEdges = res.rootNode.children.filter { it.target.kpiId == KpiId.VULNERABILITY_SCORE }
+            assertEquals(2, vulnerabilityEdges.size)
+            assertEquals(vulnerabilityEdges.first().actualWeight, 0.5)
+            assertEquals(vulnerabilityEdges[1].actualWeight, 0.5)
         } else {
             fail()
         }
diff --git a/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/DefaultHierarchy.kt b/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/DefaultHierarchy.kt
index 33150b58..d9a79f6a 100644
--- a/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/DefaultHierarchy.kt
+++ b/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/DefaultHierarchy.kt
@@ -89,9 +89,6 @@ object DefaultHierarchy {
             children = listOf()
         )
 
-        //XXX: this is different as for all other KPIs as we don't know how many children there will be
-        // there is one child for every found vulnerability. This needs to be kept in mind during
-        // mapping of hierarchy to data.
         val maxDepVulnerability = KpiNode(
             kpiId = KpiId.MAXIMAL_VULNERABILITY,
             strategyType = KpiStrategyId.MAXIMUM_STRATEGY,
diff --git a/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchy.kt b/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchy.kt
index 147dfead..0505b5f1 100644
--- a/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchy.kt
+++ b/kpi-calculator/model/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchy.kt
@@ -38,7 +38,7 @@ data class KpiResultNode(
 )
 
 @Serializable
-data class KpiResultEdge(val target: KpiResultNode, val weight: Double)
+data class KpiResultEdge(val target: KpiResultNode, val plannedWeight: Double, val actualWeight: Double)
 
 @Serializable
 sealed class KpiCalculationResult {
-- 
GitLab