Support partially-filled CVSS tables, categorize finding as 'Other' then

This commit is contained in:
maride 2026-01-29 14:49:27 +01:00
parent a354919fca
commit 201caff2fc
3 changed files with 64 additions and 49 deletions

View File

@ -8,6 +8,8 @@
table.cell(str, fill: yellow, align: center)
} else if str == "N" {
table.cell(str, fill: lime, align: center)
} else if str == "-" {
table.cell(str, fill: white, align: center)
} else {
panic("Unknown CIA state: " + str)
}

View File

@ -33,58 +33,62 @@
}
// Create a small CIA table to be included for every finding
#let createTable(attackVector: "N", attackComplexity: "L", privilegesRequired: "N", userInteraction: "N", scope: "U", confidentiality: "H", integrity: "H", availability: "H") = {
#let createTable(attackVector: "-", attackComplexity: "-", privilegesRequired: "-", userInteraction: "-", scope: "-", confidentiality: "-", integrity: "-", availability: "-") = {
// Check values
panicOnInvalid(attackVector, ("N", "A", "L", "P"))
panicOnInvalid(attackComplexity, ("L", "H"))
panicOnInvalid(privilegesRequired, ("N", "L", "H"))
panicOnInvalid(userInteraction, ("N", "R"))
panicOnInvalid(scope, ("U", "C"))
panicOnInvalid(confidentiality, ("H", "L", "N"))
panicOnInvalid(integrity, ("H", "L", "N"))
panicOnInvalid(availability, ("H", "L", "N"))
// Calculate base result, see https://www.first.org/cvss/v3-1/specification-document#7-1-Base-Metrics-Equations
let issLookup = ("H": 0.56, "L": 0.22, "N": 0)
let attackVectorLookup = ("N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2)
let attackComplexityLookup = ("L": 0.77, "H": 0.44)
let privilegesLookup = ("N": 0.85, "L": if scope == "U" { 0.62 } else { 0.68 }, "H": if scope == "U" { 0.27 } else { 0.5 })
let userInteractionLookup = ("N": 0.85, "R": 0.62)
let iss = 1 - ((1 - issLookup.at(confidentiality)) * (1 - issLookup.at(integrity)) * (1 - issLookup.at(availability)))
let impact = if scope == "U" { 6.42 * iss } else { 7.52 * (iss - 0.029) - 3.25 * (iss - 0.02)}
let exploitability = 8.22 * attackVectorLookup.at(attackVector) * attackComplexityLookup.at(attackComplexity) * privilegesLookup.at(privilegesRequired) * userInteractionLookup.at(userInteraction)
let baseScore = if impact <= 0 { 0 } else { if scope == "U" { calc.round(calc.min(impact + exploitability, 10), digits: 1) } else { calc.round(calc.min(1.08 * (impact + exploitability), 10), digits: 1) } }
panicOnInvalid(attackVector, ("N", "A", "L", "P", "-"))
panicOnInvalid(attackComplexity, ("L", "H", "-"))
panicOnInvalid(privilegesRequired, ("N", "L", "H", "-"))
panicOnInvalid(userInteraction, ("N", "R", "-"))
panicOnInvalid(scope, ("U", "C", "-"))
panicOnInvalid(confidentiality, ("H", "L", "N", "-"))
panicOnInvalid(integrity, ("H", "L", "N", "-"))
panicOnInvalid(availability, ("H", "L", "N", "-"))
let status = "?"
if baseScore >= 9.0 {
status = "CRITICAL"
} else if baseScore >= 7.0 {
status = "HIGH"
} else if baseScore >= 4.0 {
status = "MEDIUM"
} else if baseScore >= 0.1 {
status = "LOW"
if((attackVector, attackComplexity, privilegesRequired, userInteraction, scope, confidentiality, integrity, availability).find(x => x == "-") == none) {
// Calculate base result, see https://www.first.org/cvss/v3-1/specification-document#7-1-Base-Metrics-Equations
let issLookup = ("H": 0.56, "L": 0.22, "N": 0)
let attackVectorLookup = ("N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2)
let attackComplexityLookup = ("L": 0.77, "H": 0.44)
let privilegesLookup = ("N": 0.85, "L": if scope == "U" { 0.62 } else { 0.68 }, "H": if scope == "U" { 0.27 } else { 0.5 })
let userInteractionLookup = ("N": 0.85, "R": 0.62)
let iss = 1 - ((1 - issLookup.at(confidentiality)) * (1 - issLookup.at(integrity)) * (1 - issLookup.at(availability)))
let impact = if scope == "U" { 6.42 * iss } else { 7.52 * (iss - 0.029) - 3.25 * (iss - 0.02)}
let exploitability = 8.22 * attackVectorLookup.at(attackVector) * attackComplexityLookup.at(attackComplexity) * privilegesLookup.at(privilegesRequired) * userInteractionLookup.at(userInteraction)
let baseScore = if impact <= 0 { 0 } else { if scope == "U" { calc.round(calc.min(impact + exploitability, 10), digits: 1) } else { calc.round(calc.min(1.08 * (impact + exploitability), 10), digits: 1) } }
if baseScore >= 9.0 {
status = "CRITICAL"
} else if baseScore >= 7.0 {
status = "HIGH"
} else if baseScore >= 4.0 {
status = "MEDIUM"
} else if baseScore >= 0.1 {
status = "LOW"
} else {
status = "NONE"
}
} else {
status = "NONE"
// At least one value is unspecified, so this finding will be categorized as "other" and CVSS Score calculation is skipped
status = "OTHER"
}
block(
[
#block(
spacing: 0.4em,
table(
columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr),
align: center,
stroke: 1pt,
table.cell(colspan: 5)[*Exploitability Metrics*],
table.cell(colspan: 3)[*Impact Metrics*],
table.cell(rowspan: 2, align: bottom)[*#sym.sum*],
[*AV*], [*AC*], [*PR*], [*UI*], [*S*], [*C*], [*I*], [*A*],
attackVector, attackComplexity, privilegesRequired, userInteraction, scope, cia.colorize(confidentiality), cia.colorize(integrity), cia.colorize(availability), coloredCell(status),
)
)
#align(right)[
#text(
stack(
dir: ttb,
table(
columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr),
align: center,
stroke: 1pt,
table.cell(colspan: 5)[*Exploitability Metrics*],
table.cell(colspan: 3)[*Impact Metrics*],
table.cell(rowspan: 2, align: bottom)[*#sym.sum*],
[*AV*], [*AC*], [*PR*], [*UI*], [*S*], [*C*], [*I*], [*A*],
attackVector, attackComplexity, privilegesRequired, userInteraction, scope, cia.colorize(confidentiality), cia.colorize(integrity), cia.colorize(availability), coloredCell(status),
),
v(.25em),
align(
right,
text(
size: 10pt,
fill: gray,
"CVSS:3.1/AV:" + attackVector +
@ -95,8 +99,8 @@
"/C:" + confidentiality +
"/I:" + integrity +
"/A:" + availability
)]
]
)
)
)
updateRiskCategoryStats(status)

View File

@ -6,7 +6,16 @@
== Administration Interfaces reachable
#cvss.createTable(confidentiality: "N", integrity: "N", availability: "N")
#cvss.createTable(
attackVector: "N",
attackComplexity: "L",
privilegesRequired: "N",
userInteraction: "N",
scope: "U",
confidentiality: "N",
integrity: "N",
availability: "N",
)
=== Description