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) table.cell(str, fill: yellow, align: center)
} else if str == "N" { } else if str == "N" {
table.cell(str, fill: lime, align: center) table.cell(str, fill: lime, align: center)
} else if str == "-" {
table.cell(str, fill: white, align: center)
} else { } else {
panic("Unknown CIA state: " + str) panic("Unknown CIA state: " + str)
} }

View File

@ -33,17 +33,19 @@
} }
// Create a small CIA table to be included for every finding // 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 // Check values
panicOnInvalid(attackVector, ("N", "A", "L", "P")) panicOnInvalid(attackVector, ("N", "A", "L", "P", "-"))
panicOnInvalid(attackComplexity, ("L", "H")) panicOnInvalid(attackComplexity, ("L", "H", "-"))
panicOnInvalid(privilegesRequired, ("N", "L", "H")) panicOnInvalid(privilegesRequired, ("N", "L", "H", "-"))
panicOnInvalid(userInteraction, ("N", "R")) panicOnInvalid(userInteraction, ("N", "R", "-"))
panicOnInvalid(scope, ("U", "C")) panicOnInvalid(scope, ("U", "C", "-"))
panicOnInvalid(confidentiality, ("H", "L", "N")) panicOnInvalid(confidentiality, ("H", "L", "N", "-"))
panicOnInvalid(integrity, ("H", "L", "N")) panicOnInvalid(integrity, ("H", "L", "N", "-"))
panicOnInvalid(availability, ("H", "L", "N")) panicOnInvalid(availability, ("H", "L", "N", "-"))
let status = "?"
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 // 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 issLookup = ("H": 0.56, "L": 0.22, "N": 0)
let attackVectorLookup = ("N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2) let attackVectorLookup = ("N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2)
@ -55,7 +57,6 @@
let exploitability = 8.22 * attackVectorLookup.at(attackVector) * attackComplexityLookup.at(attackComplexity) * privilegesLookup.at(privilegesRequired) * userInteractionLookup.at(userInteraction) 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) } } 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) } }
let status = "?"
if baseScore >= 9.0 { if baseScore >= 9.0 {
status = "CRITICAL" status = "CRITICAL"
} else if baseScore >= 7.0 { } else if baseScore >= 7.0 {
@ -67,11 +68,13 @@
} else { } else {
status = "NONE" status = "NONE"
} }
} else {
// At least one value is unspecified, so this finding will be categorized as "other" and CVSS Score calculation is skipped
status = "OTHER"
}
block( stack(
[ dir: ttb,
#block(
spacing: 0.4em,
table( table(
columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr), columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr),
align: center, align: center,
@ -81,10 +84,11 @@
table.cell(rowspan: 2, align: bottom)[*#sym.sum*], table.cell(rowspan: 2, align: bottom)[*#sym.sum*],
[*AV*], [*AC*], [*PR*], [*UI*], [*S*], [*C*], [*I*], [*A*], [*AV*], [*AC*], [*PR*], [*UI*], [*S*], [*C*], [*I*], [*A*],
attackVector, attackComplexity, privilegesRequired, userInteraction, scope, cia.colorize(confidentiality), cia.colorize(integrity), cia.colorize(availability), coloredCell(status), attackVector, attackComplexity, privilegesRequired, userInteraction, scope, cia.colorize(confidentiality), cia.colorize(integrity), cia.colorize(availability), coloredCell(status),
) ),
) v(.25em),
#align(right)[ align(
#text( right,
text(
size: 10pt, size: 10pt,
fill: gray, fill: gray,
"CVSS:3.1/AV:" + attackVector + "CVSS:3.1/AV:" + attackVector +
@ -95,8 +99,8 @@
"/C:" + confidentiality + "/C:" + confidentiality +
"/I:" + integrity + "/I:" + integrity +
"/A:" + availability "/A:" + availability
)] )
] )
) )
updateRiskCategoryStats(status) updateRiskCategoryStats(status)

View File

@ -6,7 +6,16 @@
== Administration Interfaces reachable == 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 === Description