penretem/helper.typ

136 lines
5.2 KiB
Typst

#let panicOnPlaceholder = state("panicOnPlaceholder", true)
#let hasCIATable = state("hasCIATable", false)
#let hasCVSSTable = state("hasCVSSTable", false)
// Function panics if value is not in the allowed array
#let panicOnInvalid(value, allowed) = {
if allowed.find(x => x == value) == none {
panic("Value " + value + " is not in " + allowed.join(", "))
}
}
// Statistics for the Finding risk categories
#let riskCategoryStats = (
Critical: state("riskCriticalStat", 0),
High: state("riskHighStat", 0),
Medium: state("riskMediumStat", 0),
Low: state("riskLowStat", 0),
None: state("riskInformativeStat", 0),
Other: state("riskOtherStat", 0)
)
// Function to update the statistics
#let updateRiskCategoryStats(status) = {
// Update status
if status == "Critical" {
context(riskCategoryStats.Critical.update(riskCategoryStats.Critical.get() + 1))
} else if status == "High" {
context(riskCategoryStats.High.update(riskCategoryStats.High.get() + 1))
} else if status == "Medium" {
context(riskCategoryStats.Medium.update(riskCategoryStats.Medium.get() + 1))
} else if status == "Low" {
context(riskCategoryStats.Low.update(riskCategoryStats.Low.get() + 1))
} else if status == "None" {
context(riskCategoryStats.None.update(riskCategoryStats.None.get() + 1))
} else if status == "Other" {
context(riskCategoryStats.Other.update(riskCategoryStats.Other.get() + 1))
} else {
panic("Unknown state: " + status)
}
}
// Return the table cell formatted according to its content - for use with CIA values
#let ciacolor(str) = {
if str == "H" {
table.cell(str, fill: red, align: center)
} else if str == "L" {
table.cell(str, fill: yellow, align: center)
} else if str == "N" {
table.cell(str, fill: lime, align: center)
} else {
panic("Unknown CIA state: " + str)
}
}
// Return the table cell formatted according to its content - for the CVSS result
#let cvsscolor(str) = {
if str == "Critical" {
table.cell(str, fill: red, align: center)
} else if str == "High" {
table.cell(str, fill: orange, align: center)
} else if str == "Medium" {
table.cell(str, fill: yellow, align: center)
} else if str == "Low" {
table.cell(str, fill: lime, align: center)
} else if str == "None" {
table.cell(str, fill: white, align: center)
} else {
panic("Unknown CVSS state: " + str)
}
}
// Create a small CIA table to be included for every finding
#let cvsstable(attackVector: "N", attackComplexity: "L", privilegesRequired: "N", userInteraction: "N", scope: "U", confidentiality: "H", integrity: "H", availability: "H") = {
// 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) } }
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"
} else {
status = "None"
}
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, ciacolor(confidentiality), ciacolor(integrity), ciacolor(availability), cvsscolor(status),
)
updateRiskCategoryStats(status)
hasCVSSTable.update(true)
}
// Return the value with a colorful background so it is visibly a placeholder.
// Has the ability to panic if panicOnPlaceholder is set to true.
#let placeholder(value) = {
highlight(
fill: rgb(0xff, 0xa2, 0x9c, 0xff),
value
)
context(
if panicOnPlaceholder.get() {
panic("Found placeholder and panicOnPlaceholder is set.")
}
)
}