From 6f44d67846a753b2c566e94e7c0c0ea305becb37 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Struewer <j.n.struewer@gmail.com> Date: Fri, 2 Aug 2024 16:24:07 +0200 Subject: [PATCH] breaking: backup commit to prepare merge from dev branch. Branch is not in a working state! --- .gitignore | 1 + .../iem/app/kpi/service/KPIService.kt | 3 +- .../AggregationKPICalculationStrategy.kt | 61 ------------ .../strategy/MaximumKPICalculationStrategy.kt | 26 ----- .../strategy/RatioKPICalculationStrategy.kt | 22 ----- .../iem/kpiCalculator/core/KpiCalculator.kt | 90 +++++++++++++++++- .../core/hierarchy/KPIHierarchyEdgeDto.kt | 8 -- .../core/hierarchy/KpiCalculationNode.kt | 94 +++++++++++++------ .../core/hierarchy/KpiHierarchyEdge.kt | 6 ++ .../AggregationKPICalculationStrategy.kt | 51 ++++++++++ .../core/strategy/KPICalculationStrategy.kt | 7 -- .../strategy/MaximumKPICalculationStrategy.kt | 32 +++++++ .../strategy/RatioKPICalculationStrategy.kt | 40 ++++++++ .../RawValueKPICalculationStrategy.kt | 10 -- .../kpiCalculator/core/KpiCalculatorTest.kt | 27 ++++++ .../model/kpi/hierarchy/KpiHierarchy.kt | 26 +++++ .../model/kpi/hierarchy/KpiHierarchyTest.kt | 13 --- 17 files changed, 337 insertions(+), 180 deletions(-) delete mode 100644 app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/AggregationKPICalculationStrategy.kt delete mode 100644 app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/MaximumKPICalculationStrategy.kt delete mode 100644 app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/RatioKPICalculationStrategy.kt delete mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KPIHierarchyEdgeDto.kt create mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiHierarchyEdge.kt create mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/AggregationKPICalculationStrategy.kt delete mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/KPICalculationStrategy.kt create mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/MaximumKPICalculationStrategy.kt create mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RatioKPICalculationStrategy.kt delete mode 100644 kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RawValueKPICalculationStrategy.kt create mode 100644 kpi-calculator/core/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculatorTest.kt diff --git a/.gitignore b/.gitignore index 68465b42..021d4af0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ bin **/.DS_Store src/test/testResults/* app/backend/tools/db/cockroach-data +tools/db diff --git a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/service/KPIService.kt b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/service/KPIService.kt index 2055ca7f..2611f5b4 100644 --- a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/service/KPIService.kt +++ b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/service/KPIService.kt @@ -2,12 +2,11 @@ package de.fraunhofer.iem.app.kpi.service import de.fraunhofer.iem.app.kpi.dto.KPITreeChildResponseDto import de.fraunhofer.iem.app.kpi.dto.KPITreeResponseDto -import de.fraunhofer.iem.app.kpi.strategy.AggregationKPICalculationStrategy import de.fraunhofer.iem.app.logger.getLogger import de.fraunhofer.iem.app.repository.dto.RepositoryDetailsDto import de.fraunhofer.iem.app.tools.occmd.enumeration.Checks import de.fraunhofer.iem.app.tools.occmd.json.RawResultJson -import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KpiCalculationNode +import de.fraunhofer.iem.kpiCalculator.core.strategy.AggregationKPICalculationStrategy import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpiDto import org.springframework.stereotype.Service diff --git a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/AggregationKPICalculationStrategy.kt b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/AggregationKPICalculationStrategy.kt deleted file mode 100644 index 06b807d3..00000000 --- a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/AggregationKPICalculationStrategy.kt +++ /dev/null @@ -1,61 +0,0 @@ -package de.fraunhofer.iem.app.kpi.strategy - -import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KPIHierarchyEdgeDto -import de.fraunhofer.iem.kpiCalculator.core.strategy.KPICalculationStrategy - -class AggregationKPICalculationStrategy : KPICalculationStrategy { - - /** - * This function calculates the aggregate sum of all given children. - * If a child is empty it is removed from the calculation and its - * corresponding edge weight is distributed evenly between the - * remaining children. - * The method returns the KPIs value as well as the updated - * KPIHierarchyEdgeDtos with the actual used weight. - */ - override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Pair<Int, List<KPIHierarchyEdgeDto>> { -// if (children.none { !it.to.isEmpty() } || children.isEmpty()) { -// throw Exception("KPI aggregation of empty children can't be calculated.") -// } -// -// val emptyChildren: List<KPIHierarchyEdgeDto> = children.filter { it.to.isEmpty() } -// -// val notEmptyChildren = children.toMutableList() -// notEmptyChildren.removeAll(emptyChildren) -// -// val additionalWeightPerElement: Double = if (notEmptyChildren.isNotEmpty()) { -// val unusedWeight = emptyChildren.sumOf { it.plannedWeight } -// unusedWeight / notEmptyChildren.size -// } else { -// 0.0 -// } -// -// val weightedEdges: MutableList<KPIHierarchyEdgeDto> = emptyChildren -// .map { -// KPIHierarchyEdgeDto( -// from = it.from, -// to = it.to, -// plannedWeight = it.plannedWeight, -// actualWeight = 0.0 -// ) -// }.toMutableList() -// -// var value = 0 -// -// notEmptyChildren.forEach { child -> -// val actualWeight = (child.plannedWeight + additionalWeightPerElement) -// weightedEdges.add( -// KPIHierarchyEdgeDto( -// from = child.from, -// to = child.to, -// plannedWeight = child.plannedWeight, -// actualWeight = actualWeight -// ) -// ) -// value += (child.to.getValue().toFloat() * actualWeight).toInt() -// } -// -// return Pair(value, weightedEdges) - TODO() - } -} diff --git a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/MaximumKPICalculationStrategy.kt b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/MaximumKPICalculationStrategy.kt deleted file mode 100644 index 0b7ed318..00000000 --- a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/MaximumKPICalculationStrategy.kt +++ /dev/null @@ -1,26 +0,0 @@ -package de.fraunhofer.iem.app.kpi.strategy - -import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KPIHierarchyEdgeDto -import de.fraunhofer.iem.kpiCalculator.core.strategy.KPICalculationStrategy - -class MaximumKPICalculationStrategy : KPICalculationStrategy { - // TODO: Currently it's tailored to the maximum dependency vulnerability score, this should change in the future - @Suppress("MagicNumber") - override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Pair<Int, List<KPIHierarchyEdgeDto>> { -// if (children.none { !it.to.isEmpty() } || children.isEmpty()) { -// throw Exception("KPI maximum of empty children can't be calculated") -// } -// var maximum = 0 -// for (child in children) { -// if (!child.to.isEmpty()) { -// val childValue = child.to.getValue() -// if (childValue > maximum) { -// maximum = childValue -// } -// } -// } -// return Pair((100 - (maximum)), children) -// } - TODO() - } -} diff --git a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/RatioKPICalculationStrategy.kt b/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/RatioKPICalculationStrategy.kt deleted file mode 100644 index 5b552b63..00000000 --- a/app/backend/src/main/kotlin/de/fraunhofer/iem/app/kpi/strategy/RatioKPICalculationStrategy.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.fraunhofer.iem.app.kpi.strategy - -import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KPIHierarchyEdgeDto -import de.fraunhofer.iem.kpiCalculator.core.strategy.KPICalculationStrategy - -class RatioKPICalculationStrategy : KPICalculationStrategy { - @Suppress("MagicNumber") - override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Pair<Int, List<KPIHierarchyEdgeDto>> { -// if (children.size != 2) { -// throw Exception("Requires exactly two children") -// } -// // TODO: this is a dirty workaround to fix a copy by reference bug -// val copiedChildren = children.toList() -// val firstValue = children[0].to.getValue() -// val secondValue = children[1].to.getValue() -// if (firstValue >= secondValue) { -// return Pair(((secondValue.toDouble() / firstValue.toDouble()) * 100).toInt(), copiedChildren) -// } -// return Pair(((firstValue.toDouble() / secondValue.toDouble()) * 100).toInt(), copiedChildren) - TODO() - } -} 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 5ffc7910..bf9fa0c8 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 @@ -1,8 +1,96 @@ package de.fraunhofer.iem.kpiCalculator.core +import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KpiCalculationNode +import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId +import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiStrategyId +import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpiDto +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiHierarchy +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiNode +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiResultNode + +sealed class KpiResult {} + object KpiCalculator { //XXX: Setup Logger -// fun calculateKpis(root: KpiCalculationNode): + fun calculateKpis(hierarchy: KpiHierarchy, rawValueKpiDtos: List<RawValueKpiDto>): KpiResultNode { + val root = hierarchy.rootNode + val connectedHierarchyRoot = connectKpiHierarchyToRawValues(root, rawValueKpiDtos) + depthFirstTraversal(connectedHierarchyRoot) { it.calculateKpi() } + return KpiCalculationNode.to(connectedHierarchyRoot) + } + + private fun depthFirstTraversal( + node: KpiCalculationNode, + seen: MutableSet<KpiCalculationNode> = mutableSetOf(), + action: (node: KpiCalculationNode) -> Unit + ) { + if (seen.contains(node)) { + return + } + + node.hierarchyEdges + .forEach { child -> + depthFirstTraversal(node = child.target, seen = seen, action) + } + + action(node) + seen.add(node) + } + + private fun connectKpiHierarchyToRawValues( + node: KpiNode, + rawValueKpiDtos: List<RawValueKpiDto> + ): KpiCalculationNode { + + val kindToValues = mutableMapOf<KpiId, MutableList<RawValueKpiDto>>() + KpiId.entries.forEach { kindToValues[it] = mutableListOf() } + + rawValueKpiDtos.forEach { + kindToValues[it.kind]!!.add(it) + } + + val calculationRoot = KpiCalculationNode.from(node) + + depthFirstTraversal( + node = calculationRoot + ) { currentNode -> + // TODO: this is complicated as we don't know how many children we will have. + // this scenario might reappear again so we should come up with a better solution + if ( + currentNode.kind == KpiId.MAXIMAL_VULNERABILITY && + currentNode.hierarchyEdges.size == 1 && + currentNode.hierarchyEdges.first().target.kind == KpiId.VULNERABILITY_SCORE && + !kindToValues[KpiId.VULNERABILITY_SCORE].isNullOrEmpty() + ) { + val vulnerabilityCalcNodes = kindToValues[KpiId.VULNERABILITY_SCORE]?.map { + val newNode = KpiCalculationNode( + kind = KpiId.VULNERABILITY_SCORE, + calculationStrategy = KpiStrategyId.RAW_VALUE_STRATEGY + ) + newNode.setScore(it.score) + newNode + } + + if (!vulnerabilityCalcNodes.isNullOrEmpty()) { + currentNode.clearChildren() + val weights = 1.0 / vulnerabilityCalcNodes.size + vulnerabilityCalcNodes.forEach { + currentNode.addChild(it, weights) + } + + } + } else { + val correspondingRawValue = kindToValues[currentNode.kind] + if (!correspondingRawValue.isNullOrEmpty()) { + if (correspondingRawValue.size > 1) { + println("We only expect one RAW value for every KPI") + } + currentNode.setScore(correspondingRawValue.first().score) + } + } + } + return calculationRoot + } } diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KPIHierarchyEdgeDto.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KPIHierarchyEdgeDto.kt deleted file mode 100644 index 388207c6..00000000 --- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KPIHierarchyEdgeDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.fraunhofer.iem.kpiCalculator.core.hierarchy - -data class KPIHierarchyEdgeDto( - val from: KpiCalculationNode, - val to: KpiCalculationNode, - val plannedWeight: Double, - val actualWeight: Double = plannedWeight -) 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 f03aa75f..0d76f230 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 @@ -1,48 +1,82 @@ package de.fraunhofer.iem.kpiCalculator.core.hierarchy -import de.fraunhofer.iem.kpiCalculator.core.strategy.KPICalculationStrategy -import de.fraunhofer.iem.kpiCalculator.core.strategy.RawValueKPICalculationStrategy +import de.fraunhofer.iem.kpiCalculator.core.strategy.AggregationKPICalculationStrategy +import de.fraunhofer.iem.kpiCalculator.core.strategy.MaximumKPICalculationStrategy +import de.fraunhofer.iem.kpiCalculator.core.strategy.RatioKPICalculationStrategy import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId +import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiStrategyId +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiNode +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiResultEdge +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiResultNode -sealed class KpiCalculationResult { - class Kpi(val kind: KpiId, val score: Int) : KpiCalculationResult() - class Error(val reason: String) : KpiCalculationResult() -} -// TODO: fixme -open class KpiCalculationNode(val kind: KpiId, private val calculationStrategy: KPICalculationStrategy) { +internal class KpiCalculationNode(val kind: KpiId, val calculationStrategy: KpiStrategyId) { + + private var score: KpiCalculationResult = KpiCalculationResult.Empty + fun setScore(score: Int) { + this.score = KpiCalculationResult.Success(score) + } - private val _hierarchyEdges: MutableList<KPIHierarchyEdgeDto> = mutableListOf() - val hierarchyEdges: List<KPIHierarchyEdgeDto> + private val _hierarchyEdges: MutableList<KpiHierarchyEdge> = mutableListOf() + val hierarchyEdges: List<KpiHierarchyEdge> get() = _hierarchyEdges - fun hasChildren(): Boolean { - return hierarchyEdges.isNotEmpty() + fun addChild(node: KpiCalculationNode, weight: Double) { + _hierarchyEdges.add(KpiHierarchyEdge(node, weight)) } - fun applyStrategy(): KpiCalculationResult { - return try { - val valueAndEdges = calculationStrategy.calculateKPI(this.hierarchyEdges) - val value = valueAndEdges.first - this._hierarchyEdges.clear() - this._hierarchyEdges.addAll(valueAndEdges.second) + fun clearChildren() { + _hierarchyEdges.clear() + } - KpiCalculationResult.Kpi(kind = this.kind, score = value) - } catch (exception: Exception) { - KpiCalculationResult.Error(exception.toString()) + fun calculateKpi(): KpiCalculationResult { + val strategyData = hierarchyEdges.map { + Pair(it.target.score, it.weight) } - } + score = when (calculationStrategy) { + KpiStrategyId.RAW_VALUE_STRATEGY -> + score + + KpiStrategyId.RATIO_STRATEGY -> + RatioKPICalculationStrategy.calculateKPI(strategyData) + KpiStrategyId.AGGREGATION_STRATEGY -> + AggregationKPICalculationStrategy.calculateKPI(strategyData) - fun addChildKPI(kpiCreateDto: KpiCalculationNode, weight: Double) { - val hierarchyEdge = KPIHierarchyEdgeDto(this, kpiCreateDto, weight) - this._hierarchyEdges.add(hierarchyEdge) + KpiStrategyId.MAXIMUM_STRATEGY -> + MaximumKPICalculationStrategy.calculateKPI(strategyData) + } + + return score } -} + companion object { + fun to(node: KpiCalculationNode): KpiResultNode { + return KpiResultNode( + kpiId = node.kind, + strategyType = node.calculationStrategy, + kpiResult = node.score, + children = node.hierarchyEdges.map { + KpiResultEdge( + target = to(it.target), + weight = it.weight + ) + } + ) + } -class RawKpiCalculationNode( - score: Int, - kind: KpiId -) : KpiCalculationNode(kind, RawValueKPICalculationStrategy(score)) + fun from(node: KpiNode): KpiCalculationNode { + val calcNode = KpiCalculationNode(node.kpiId, calculationStrategy = node.strategyType) + val children = node.children.map { child -> + KpiHierarchyEdge( + target = from(child.target), + weight = child.weight + ) + } + calcNode._hierarchyEdges.addAll(children) + return calcNode + } + } +} 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 new file mode 100644 index 00000000..ddb5b74b --- /dev/null +++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/hierarchy/KpiHierarchyEdge.kt @@ -0,0 +1,6 @@ +package de.fraunhofer.iem.kpiCalculator.core.hierarchy + +internal data class KpiHierarchyEdge( + val target: KpiCalculationNode, + val weight: Double, +) diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/AggregationKPICalculationStrategy.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/AggregationKPICalculationStrategy.kt new file mode 100644 index 00000000..c4fefe35 --- /dev/null +++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/AggregationKPICalculationStrategy.kt @@ -0,0 +1,51 @@ +package de.fraunhofer.iem.kpiCalculator.core.strategy + +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult + + +object AggregationKPICalculationStrategy { + /** + * This function calculates the aggregate sum of all given children. + * If a child is empty it is removed from the calculation and its + * corresponding edge weight is distributed evenly between the + * remaining children. + * The method returns the KPIs value as well as the updated + * KPIHierarchyEdgeDtos with the actual used weight. + */ + fun calculateKPI(childScores: List<Pair<KpiCalculationResult, Double>>): KpiCalculationResult { + + if (childScores.isEmpty()) { + return KpiCalculationResult.Empty + } + + val successScores = childScores.filter { it.first is KpiCalculationResult.Success } + val failed = childScores + .filter { it.first !is KpiCalculationResult.Success } + val missingEdgeWeights = failed.sumOf { it.second } + + val additionalWeight = + if (missingEdgeWeights == 0.0) + 0.0 + else + missingEdgeWeights / successScores.size + + val aggregation = + if (successScores.isEmpty()) + 0 + else + successScores.sumOf { + (it.first as KpiCalculationResult.Success).score * (it.second + additionalWeight) + }.toInt() + + + if (failed.isNotEmpty()) { + return KpiCalculationResult.Incomplete( + score = aggregation, + reason = "There were ${failed.size} elements missing during aggregation.", + missingWeights = missingEdgeWeights + ) + } + + return KpiCalculationResult.Success(score = aggregation) + } +} diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/KPICalculationStrategy.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/KPICalculationStrategy.kt deleted file mode 100644 index 516ea616..00000000 --- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/KPICalculationStrategy.kt +++ /dev/null @@ -1,7 +0,0 @@ -package de.fraunhofer.iem.kpiCalculator.core.strategy - -import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KPIHierarchyEdgeDto - -fun interface KPICalculationStrategy { - fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Pair<Int, List<KPIHierarchyEdgeDto>> -} 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 new file mode 100644 index 00000000..7fe7cf05 --- /dev/null +++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/MaximumKPICalculationStrategy.kt @@ -0,0 +1,32 @@ +package de.fraunhofer.iem.kpiCalculator.core.strategy + +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult + +object MaximumKPICalculationStrategy { + fun calculateKPI(childScores: List<Pair<KpiCalculationResult, Double>>): KpiCalculationResult { + + if (childScores.isEmpty()) { + return KpiCalculationResult.Empty + } + + val successScores = childScores.filterIsInstance<KpiCalculationResult.Success>() + + val max = + if (successScores.isEmpty()) + 0 + else + successScores.maxOf { it.score } + + if (childScores.size != successScores.size) { + val failed = childScores.filter { it.first !is KpiCalculationResult.Success } + val missingWeights = failed.sumOf { it.second } + return KpiCalculationResult.Incomplete( + score = max, + reason = "Missing ${failed.size} elements.", + missingWeights = missingWeights + ) + } + + return KpiCalculationResult.Success(score = max) + } +} 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 new file mode 100644 index 00000000..16aa0128 --- /dev/null +++ b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RatioKPICalculationStrategy.kt @@ -0,0 +1,40 @@ +package de.fraunhofer.iem.kpiCalculator.core.strategy + +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult + +object RatioKPICalculationStrategy { + /** + * Returns smallerValue / biggerValue, regardless in which order the values are given. + */ + fun calculateKPI(childScores: List<Pair<KpiCalculationResult, Double>>): KpiCalculationResult { + + val successScores = + childScores.map { it.first }.filterIsInstance<KpiCalculationResult.Success>() + + if (successScores.size != 2) { + return KpiCalculationResult.Error( + "Ratio calculation strategy called " + + "with ${successScores.size} valid elements, which is illegal." + ) + } + + val firstValue = successScores.first().score + val secondValue = successScores[1].score + + if (firstValue == secondValue && secondValue == 0) { + return KpiCalculationResult.Success(0) + } + try { + if (firstValue >= secondValue) { + return KpiCalculationResult.Success( + score = (secondValue.toDouble() / firstValue.toDouble()).toInt() * 100 + ) + } + return KpiCalculationResult.Success( + score = ((firstValue.toDouble() / secondValue.toDouble()) * 100).toInt() + ) + } catch (e: Exception) { + return KpiCalculationResult.Error(e.message ?: e.toString()) + } + } +} diff --git a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RawValueKPICalculationStrategy.kt b/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RawValueKPICalculationStrategy.kt deleted file mode 100644 index 86c0a134..00000000 --- a/kpi-calculator/core/src/main/kotlin/de/fraunhofer/iem/kpiCalculator/core/strategy/RawValueKPICalculationStrategy.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.fraunhofer.iem.kpiCalculator.core.strategy - -import de.fraunhofer.iem.kpiCalculator.core.hierarchy.KPIHierarchyEdgeDto - -class RawValueKPICalculationStrategy(private val value: Int) : - KPICalculationStrategy { - override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Pair<Int, List<KPIHierarchyEdgeDto>> { - return Pair(this.value, children) - } -} 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 new file mode 100644 index 00000000..dadb1a17 --- /dev/null +++ b/kpi-calculator/core/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/core/KpiCalculatorTest.kt @@ -0,0 +1,27 @@ +package de.fraunhofer.iem.kpiCalculator.core + +import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId +import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpiDto +import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.DefaultHierarchy +import org.junit.jupiter.api.Test + +class KpiCalculatorTest { + + @Test + fun calculateKpis() { + val rawValueKpiDtos = listOf( + RawValueKpiDto(kind = KpiId.VULNERABILITY_SCORE, score = 8), + RawValueKpiDto(kind = KpiId.VULNERABILITY_SCORE, score = 9), + RawValueKpiDto(kind = KpiId.CHECKED_IN_BINARIES, score = 100), + RawValueKpiDto(kind = KpiId.COMMENTS_IN_CODE, score = 80), + RawValueKpiDto(kind = KpiId.NUMBER_OF_COMMITS, score = 90), + RawValueKpiDto(kind = KpiId.IS_DEFAULT_BRANCH_PROTECTED, score = 100), + RawValueKpiDto(kind = KpiId.NUMBER_OF_SIGNED_COMMITS, score = 80), + RawValueKpiDto(kind = KpiId.SECRETS, score = 80), + RawValueKpiDto(kind = KpiId.SAST_USAGE, score = 80), + RawValueKpiDto(kind = KpiId.DOCUMENTATION_INFRASTRUCTURE, score = 80), + ) + val res = KpiCalculator.calculateKpis(DefaultHierarchy.get(), rawValueKpiDtos) + println(res) + } +} 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 be3d6900..16368bd0 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 @@ -8,6 +8,7 @@ val SCHEMA_VERSIONS: Array<String> = arrayOf( "1.0.0" ).sortedArray() +//TODO: add Hierarchy Validator @Serializable data class KpiHierarchy private constructor(val rootNode: KpiNode, val schemaVersion: String) { companion object { @@ -21,3 +22,28 @@ data class KpiNode(val kpiId: KpiId, val strategyType: KpiStrategyId, val childr @Serializable data class KpiEdge(val target: KpiNode, val weight: Double) +@Serializable +data class KpiResultHierarchy private constructor(val rootNode: KpiNode, val schemaVersion: String) { + companion object { + fun create(rootNode: KpiNode) = KpiResultHierarchy(rootNode, SCHEMA_VERSIONS.last()) + } +} + +@Serializable +data class KpiResultNode( + val kpiId: KpiId, + val kpiResult: KpiCalculationResult, + val strategyType: KpiStrategyId, + val children: List<KpiResultEdge> +) + +@Serializable +data class KpiResultEdge(val target: KpiResultNode, val weight: Double) + +@Serializable +sealed class KpiCalculationResult { + data class Success(val score: Int) : KpiCalculationResult() + data class Error(val reason: String) : KpiCalculationResult() + data class Incomplete(val score: Int, val missingWeights: Double, val reason: String) : KpiCalculationResult() + data object Empty : KpiCalculationResult() +} diff --git a/kpi-calculator/model/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchyTest.kt b/kpi-calculator/model/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchyTest.kt index 014b4c5e..b9932654 100644 --- a/kpi-calculator/model/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchyTest.kt +++ b/kpi-calculator/model/src/test/kotlin/de/fraunhofer/iem/kpiCalculator/model/kpi/hierarchy/KpiHierarchyTest.kt @@ -2,7 +2,6 @@ package de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiId import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiStrategyId -import de.fraunhofer.iem.kpiCalculator.model.kpi.RawValueKpiDto import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.junit.jupiter.api.Test @@ -45,16 +44,4 @@ class KpiHierarchyTest { } } - @Test - fun connectHierarchyAndData() { - - val rawValueKpis = listOf( - RawValueKpiDto( - kind = KpiId.COMMENTS_IN_CODE, - score = 40 - ) - ) - val hierarchy = DefaultHierarchy.get() - println(KpiHierarchy.isValid(hierarchy)) - } } -- GitLab