Skip to content

Commit

Permalink
feat: added report command (#12)
Browse files Browse the repository at this point in the history
* feat: added report command
* chore: addressed PR feedback
  • Loading branch information
janniclas authored Nov 25, 2024
1 parent 88020ab commit ee1a0ae
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/main/kotlin/de/fraunhofer/iem/spha/cli/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import de.fraunhofer.iem.spha.cli.commands.CalculateKpiCommand
import de.fraunhofer.iem.spha.cli.commands.ReportCommand
import de.fraunhofer.iem.spha.cli.commands.TransformToolResultCommand
import de.fraunhofer.iem.spha.cli.transformer.RawKpiTransformer
import de.fraunhofer.iem.spha.cli.transformer.Tool2RawKpiTransformer
Expand All @@ -38,7 +39,7 @@ fun main(args: Array<String>) {

try {
MainSphaToolCommand()
.subcommands(TransformToolResultCommand(), CalculateKpiCommand())
.subcommands(TransformToolResultCommand(), CalculateKpiCommand(), ReportCommand())
.main(args)
} catch (e: Exception) {
val logger = KotlinLogging.logger {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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.cli.commands

import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.options.switch
import de.fraunhofer.iem.spha.cli.SphaToolCommandBase
import de.fraunhofer.iem.spha.cli.reporter.getMarkdown
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiResultHierarchy
import java.nio.file.FileSystem
import java.nio.file.Files
import kotlin.io.path.createDirectories
import kotlin.io.path.inputStream
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

internal class ReportCommand :
SphaToolCommandBase(
name = "report",
help = "Takes a KpiResultHierarchy and generates a human readable report from it..",
),
KoinComponent {

private val fileSystem by inject<FileSystem>()

private val resultHierarchy by
option(
"-r",
"--resultHierarchy",
help = "Path to the result hierarchy for which the report is generated.",
)
.required()

private val exportFormat by option().switch("--markdown" to "markdown").required()

private val output by
option(
"-o",
"--output",
help = "The output directory where the result of the operation is stored.",
)
.required()

override fun run() {
super.run()

val resultHierarchy = readResultHierarchy()
val report =
when (exportFormat) {
"markdown" -> resultHierarchy.getMarkdown()
else -> {
Logger.error { "Unknown exportFormat: $exportFormat" }
null
}
}

report?.let { writeReport(it) }
}

private fun writeReport(report: String) {
val outputFilePath = fileSystem.getPath(output)

val directory = outputFilePath.toAbsolutePath().parent
directory.createDirectories()

Files.newBufferedWriter(outputFilePath).use { it.write(report) }
}

@OptIn(ExperimentalSerializationApi::class)
private fun readResultHierarchy(): KpiResultHierarchy {

fileSystem.getPath(resultHierarchy).inputStream().use {
return Json.decodeFromStream<KpiResultHierarchy>(it)
}
}
}
74 changes: 74 additions & 0 deletions src/main/kotlin/de/fraunhofer/iem/spha/cli/reporter/Reporter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.cli.reporter

import de.fraunhofer.iem.spha.model.kpi.KpiId
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiResultHierarchy
import de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiResultNode

fun KpiId.getName(): String {
return when (this) {
KpiId.CHECKED_IN_BINARIES -> "Checked in binaries"
KpiId.NUMBER_OF_COMMITS -> "Number of commits"
KpiId.CODE_VULNERABILITY_SCORE -> "Code vulnerability scores"
KpiId.CONTAINER_VULNERABILITY_SCORE -> "Container vulnerability scores"
KpiId.NUMBER_OF_SIGNED_COMMITS -> "Number of signed commits"
KpiId.IS_DEFAULT_BRANCH_PROTECTED -> "Default branch protection"
KpiId.SECRETS -> "Secrets"
KpiId.SAST_USAGE -> "SAST usage"
KpiId.COMMENTS_IN_CODE -> "Code comments"
KpiId.DOCUMENTATION_INFRASTRUCTURE -> "Documentation infrastructure"
KpiId.LIB_DAYS_DEV -> "LibDays for dev dependencies"
KpiId.LIB_DAYS_PROD -> "LibDays for prod dependencies"
KpiId.SIGNED_COMMITS_RATIO -> "Ratio signed to unsigned commits"
KpiId.INTERNAL_QUALITY -> "⭐ Internal quality"
KpiId.EXTERNAL_QUALITY -> "⭐ External quality"
KpiId.PROCESS_COMPLIANCE -> "✅ Process compliance"
KpiId.PROCESS_TRANSPARENCY -> "\uD83D\uDD0E Process transparency"
KpiId.SECURITY -> "\uD83D\uDD12 Security"
KpiId.MAXIMAL_VULNERABILITY -> "\uD83D\uDEA8 Maximum vulnerability score"
KpiId.DOCUMENTATION -> "\uD83D\uDCD6 Documentation"
KpiId.ROOT -> "\uD83D\uDC96 Software Product Health Score"
}
}

fun KpiResultNode.getScoreVisualization(): String? {
return when (this.kpiResult) {
is KpiCalculationResult.Empty ->
"${this.kpiId.getName()}: ${(this.kpiResult as KpiCalculationResult.Empty).reason}"
is KpiCalculationResult.Error ->
"${this.kpiId.getName()}: ${(this.kpiResult as KpiCalculationResult.Error).reason}"
is KpiCalculationResult.Incomplete ->
"${this.kpiId.getName()}: ${(this.kpiResult as KpiCalculationResult.Incomplete).score} / 100"
is KpiCalculationResult.Success ->
"${this.kpiId.getName()}: ${(this.kpiResult as KpiCalculationResult.Success).score} / 100"
}
}

fun KpiResultHierarchy.getMarkdown(): String? {

val topLevelScore = this.rootNode.getScoreVisualization()

val firstLevelScores =
this.rootNode.children
.map { edge -> edge.target.getScoreVisualization() }
.reduce { a, b ->
"""$a
| $b"""
.trimMargin()
}

return """# $topLevelScore
| ## Top level KPI Scores
| $firstLevelScores
"""
.trimMargin()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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.cli.commands

import com.github.ajalt.clikt.testing.test
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import de.fraunhofer.iem.spha.cli.appModules
import java.nio.file.FileSystem
import kotlin.io.path.writeText
import kotlin.test.Test
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
import kotlinx.serialization.ExperimentalSerializationApi
import org.junit.jupiter.api.extension.RegisterExtension
import org.koin.core.logger.Level
import org.koin.test.KoinTest
import org.koin.test.junit5.KoinTestExtension
import org.koin.test.mock.declare

class ReportCommandTest : KoinTest {
@JvmField
@RegisterExtension
val koinTestRule =
KoinTestExtension.create {
printLogger(Level.DEBUG)
modules(appModules)
}

@OptIn(ExperimentalSerializationApi::class)
@Test
fun testMarkdown_Integration() {
val fileSystem =
declare<FileSystem> { Jimfs.newFileSystem(Configuration.forCurrentPlatform()) }

fileSystem.provider().createDirectory(fileSystem.getPath("result"))
fileSystem
.getPath("./result/kpis.json")
.writeText(
"{\"rootNode\":{\"kpiId\":\"ROOT\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Incomplete\",\"score\":999,\"reason\":\"Incomplete results.\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"PROCESS_TRANSPARENCY\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"SIGNED_COMMITS_RATIO\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_RATIO_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"NUMBER_OF_COMMITS\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":1.0,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"NUMBER_OF_SIGNED_COMMITS\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":1.0,\"actualWeight\":0.0}]},\"plannedWeight\":1.0,\"actualWeight\":0.0}]},\"plannedWeight\":0.1,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"PROCESS_COMPLIANCE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"CHECKED_IN_BINARIES\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"SIGNED_COMMITS_RATIO\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_RATIO_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"NUMBER_OF_COMMITS\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":1.0,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"NUMBER_OF_SIGNED_COMMITS\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":1.0,\"actualWeight\":0.0}]},\"plannedWeight\":0.2,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"IS_DEFAULT_BRANCH_PROTECTED\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.3,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"DOCUMENTATION\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"DOCUMENTATION_INFRASTRUCTURE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.6,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"COMMENTS_IN_CODE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.4,\"actualWeight\":0.0}]},\"plannedWeight\":0.3,\"actualWeight\":0.0}]},\"plannedWeight\":0.1,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"SECURITY\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Incomplete\",\"score\":0,\"reason\":\"Incomplete results.\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"SECRETS\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"MAXIMAL_VULNERABILITY\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Success\",\"score\":0},\"strategyType\":\"MINIMUM_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"CODE_VULNERABILITY_SCORE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Success\",\"score\":10},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.2},{\"target\":{\"kpiId\":\"CODE_VULNERABILITY_SCORE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Success\",\"score\":34},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.2},{\"target\":{\"kpiId\":\"CODE_VULNERABILITY_SCORE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Success\",\"score\":0},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.2},{\"target\":{\"kpiId\":\"CODE_VULNERABILITY_SCORE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Success\",\"score\":14},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.2},{\"target\":{\"kpiId\":\"CODE_VULNERABILITY_SCORE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Success\",\"score\":47},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.2,\"actualWeight\":0.2}]},\"plannedWeight\":0.35,\"actualWeight\":1.0},{\"target\":{\"kpiId\":\"MAXIMAL_VULNERABILITY\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"MAXIMUM_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"CONTAINER_VULNERABILITY_SCORE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":1.0,\"actualWeight\":0.0}]},\"plannedWeight\":0.35,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"CHECKED_IN_BINARIES\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.1,\"actualWeight\":0.0}]},\"plannedWeight\":0.4,\"actualWeight\":1.0},{\"target\":{\"kpiId\":\"INTERNAL_QUALITY\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"DOCUMENTATION\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"DOCUMENTATION_INFRASTRUCTURE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.6,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"COMMENTS_IN_CODE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.4,\"actualWeight\":0.0}]},\"plannedWeight\":1.0,\"actualWeight\":0.0}]},\"plannedWeight\":0.15,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"EXTERNAL_QUALITY\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"DOCUMENTATION\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"WEIGHTED_AVERAGE_STRATEGY\",\"children\":[{\"target\":{\"kpiId\":\"DOCUMENTATION_INFRASTRUCTURE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.6,\"actualWeight\":0.0},{\"target\":{\"kpiId\":\"COMMENTS_IN_CODE\",\"kpiResult\":{\"type\":\"de.fraunhofer.iem.spha.model.kpi.hierarchy.KpiCalculationResult.Empty\"},\"strategyType\":\"RAW_VALUE_STRATEGY\",\"children\":[]},\"plannedWeight\":0.4,\"actualWeight\":0.0}]},\"plannedWeight\":1.0,\"actualWeight\":0.0}]},\"plannedWeight\":0.25,\"actualWeight\":0.0}]},\"schemaVersion\":\"1.0.0\"}"
)

val command = ReportCommand()
command.test("-r ./result/kpis.json -o ./result/output.md --markdown")

fileSystem.provider().newInputStream(fileSystem.getPath("./result/output.md")).use {
val fileContent = it.bufferedReader().readText()
println(fileContent)
assertNotEquals("", fileContent)
assertTrue(fileContent.contains("999"))
}
}
}
Loading

0 comments on commit ee1a0ae

Please sign in to comment.