Skip to content

Commit

Permalink
Add cvss calculator (#2580)
Browse files Browse the repository at this point in the history
* Added CVSS calculator
  • Loading branch information
Cheshiriks authored Sep 13, 2023
1 parent c2086c6 commit 2f83f43
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.saveourtool.save.cvsscalculator

import kotlinx.serialization.Serializable

/**
* @property version
* @property attackVector
* @property attackComplexity
* @property privilegeRequired
* @property userInteraction
* @property scopeMetric
* @property confidentiality
* @property integrity
* @property availability
*/
@Serializable
data class BaseMetrics(
var version: Float,
var attackVector: AttackVectorType,
var attackComplexity: AttackComplexityType,
var privilegeRequired: PrivilegesRequiredType,
var userInteraction: UserInteractionType,
var scopeMetric: ScopeType,
var confidentiality: CiaType,
var integrity: CiaType,
var availability: CiaType,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
@file:Suppress("FILE_NAME_MATCH_CLASS")

package com.saveourtool.save.cvsscalculator

import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.ATTACK_COMPLEXITY
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.ATTACK_VECTOR
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.AVAILABILITY
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.CONFIDENTIALITY
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.CVSS_VERSION
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.INTEGRITY
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.PRIVILEGES_REQUIRED
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.SCOPE
import com.saveourtool.save.cvsscalculator.CvssMetrics.Companion.USER_INTERACTION
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.roundToInt

@Suppress("UtilityClassWithPublicConstructor")
private class CvssMetrics {
companion object {
const val ATTACK_COMPLEXITY = "AC"
const val ATTACK_VECTOR = "AV"
const val AVAILABILITY = "A"
const val CONFIDENTIALITY = "C"
const val CVSS_VERSION = "CVSS"
const val INTEGRITY = "I"
const val PRIVILEGES_REQUIRED = "PR"
const val SCOPE = "S"
const val USER_INTERACTION = "UI"
}
}

@Suppress(
"UnsafeCallOnNullableType",
)
private fun String.parsingVector(): BaseMetrics {
val values = this.toMap()
return BaseMetrics(
version = values.getValue(CVSS_VERSION).toFloat(),
attackVector = values.findOrElseThrow(ATTACK_VECTOR, AttackVectorType::value),
attackComplexity = values.findOrElseThrow(ATTACK_COMPLEXITY, AttackComplexityType::value),
privilegeRequired = values.findOrElseThrow(PRIVILEGES_REQUIRED, PrivilegesRequiredType::value),
userInteraction = values.findOrElseThrow(USER_INTERACTION, UserInteractionType::value),
scopeMetric = values.findOrElseThrow(SCOPE, ScopeType::value),
confidentiality = values.findOrElseThrow(CONFIDENTIALITY, CiaType::value),
integrity = values.findOrElseThrow(INTEGRITY, CiaType::value),
availability = values.findOrElseThrow(AVAILABILITY, CiaType::value),
)
}

private inline fun <reified T : Enum<T>> Map<String, String>.findOrElseThrow(vector: String, getValue: (T) -> String): T =
get(vector)?.let { value -> enumValues<T>().find { getValue(it) == value } }
?: throw IllegalArgumentException("No such value for $vector.")

@Suppress("MagicNumber")
private fun String.toMap() = split("/").associate { value ->
value.split(":")
.also { require(it.size == 2) }
.let { it[0] to it[1] }
}

private fun Map<String, Float>.getWeight(key: String): Float = this.getOrElse(key) {
throw IllegalArgumentException("No such weight for value $key.")
}

/**
* @param vector
* @return base score criticality
*/
fun calculateScore(vector: String): Float {
val baseMetrics = vector.parsingVector()
return calculate(baseMetrics)
}

@Suppress(
"FLOAT_IN_ACCURATE_CALCULATIONS",
"MagicNumber",
"UnsafeCallOnNullableType",
)
private fun calculate(baseMetrics: BaseMetrics): Float {
val iss = 1f - (1f - cia.getWeight(baseMetrics.confidentiality.value)) * (1f - cia.getWeight(baseMetrics.integrity.value)) * (1f -
cia.getWeight(baseMetrics.availability.value))
val impact: Float = if (baseMetrics.scopeMetric == ScopeType.CHANGED) {
7.52f * (iss - 0.029f) - 3.25f * (iss - 0.02f).pow(15f)
} else {
6.42f * iss
}
val pr = scope.getOrElse(baseMetrics.scopeMetric.value) { throw IllegalArgumentException("No such weights for Scope type ${baseMetrics.scopeMetric.value}.") }
val exploitability = 8.22f * av.getWeight(baseMetrics.attackVector.value) * ac.getWeight(baseMetrics.attackComplexity.value) *
pr.getWeight(baseMetrics.privilegeRequired.value) *
ui.getWeight(baseMetrics.userInteraction.value)

val baseScore: Float = if (impact <= 0) {
0f
} else {
if (baseMetrics.scopeMetric == ScopeType.UNCHANGED) {
min(impact + exploitability, 10f)
} else {
min((impact + exploitability) * 1.08f, 10f)
}
}

return roundup(baseScore)
}

@Suppress(
"FLOAT_IN_ACCURATE_CALCULATIONS",
"MagicNumber",
)
private fun roundup(number: Float): Float {
val value = (number * 100_000).roundToInt()
return if (value % 10_000 == 0) {
value / 100_000f
} else {
(value / 10_000 + 1) / 10f
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
@file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE")

package com.saveourtool.save.cvsscalculator

import kotlinx.serialization.Serializable

/**
* Type of Attack Vector
*
* @property value abbreviated value from the cvss vector
*/
@Serializable
enum class AttackVectorType(val value: String) {
ADJACENT_NETWORK("A"),
LOCAL("L"),
NETWORK("N"),
PHYSICAL("P"),
;
}

/**
* Type of Attack Complexity
*
* @property value abbreviated value from the cvss vector
*/
@Serializable
enum class AttackComplexityType(val value: String) {
HIGH("H"),
LOW("L"),
;
}

/**
* Type of Privileges Required
*
* @property value abbreviated value from the cvss vector
*/
@Serializable
enum class PrivilegesRequiredType(val value: String) {
HIGH("H"),
LOW("L"),
NONE("N"),
;
}

/**
* Type of User Interaction
*
* @property value abbreviated value from the cvss vector
*/
@Serializable
enum class UserInteractionType(val value: String) {
NONE("N"),
REQUIRED("R"),
;
}

/**
* Type of Scope
*
* @property value abbreviated value from the cvss vector
*/
@Serializable
enum class ScopeType(val value: String) {
CHANGED("C"),
UNCHANGED("U"),
;
}

/**
* Type of CIA
*
* @property value abbreviated value from the cvss vector
*/
@Serializable
enum class CiaType(val value: String) {
HIGH("H"),
LOW("L"),
NONE("N"),
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@file:Suppress(
"HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE",
"MagicNumber",
)

package com.saveourtool.save.cvsscalculator

val av = mapOf(
AttackVectorType.NETWORK.value to 0.85f,
AttackVectorType.ADJACENT_NETWORK.value to 0.62f,
AttackVectorType.LOCAL.value to 0.55f,
AttackVectorType.PHYSICAL.value to 0.2f,
)

val ac = mapOf(
AttackComplexityType.LOW.value to 0.77f,
AttackComplexityType.HIGH.value to 0.44f,
)

// if Scope / Modified Scope is Unchanged
val prSu = mapOf(
PrivilegesRequiredType.NONE.value to 0.85f,
PrivilegesRequiredType.LOW.value to 0.62f,
PrivilegesRequiredType.HIGH.value to 0.27f,
)

// if Scope / Modified Scope is Changed
val prSc = mapOf(
PrivilegesRequiredType.NONE.value to 0.85f,
PrivilegesRequiredType.LOW.value to 0.68f,
PrivilegesRequiredType.HIGH.value to 0.50f,
)

val scope = mapOf(
ScopeType.CHANGED.value to prSc,
ScopeType.UNCHANGED.value to prSu,
)

val ui = mapOf(
UserInteractionType.NONE.value to 0.85f,
UserInteractionType.REQUIRED.value to 0.62f,
)

val cia = mapOf(
CiaType.HIGH.value to 0.56f,
CiaType.LOW.value to 0.22f,
CiaType.NONE.value to 0f,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.saveourtool.save.cvsscalculator

import kotlin.js.JsName
import kotlin.test.Test
import kotlin.test.assertEquals

class CvssCalculatorTest {

@Test
@JsName("parsingVector")
fun `parsing vector`() {

val vector = "CVSS:3.1/AV:A/AC:H/PR:N/UI:R/S:C/C:H/I:N/A:H"
val score = calculateScore(vector);

assertEquals(score, 7.5f)
}

}

0 comments on commit 2f83f43

Please sign in to comment.