penretem/helper.typ

246 lines
7.9 KiB
Typst

#let panicOnPlaceholder = state("panicOnPlaceholder", true)
#let hasCIATable = state("hasCIATable", false)
#let hasCVSSTable = state("hasCVSSTable", false)
#let usesTLP = state("usesTLP", 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(v => v + 1))
} else if status == "High" {
context(riskCategoryStats.High.update(v => v + 1))
} else if status == "Medium" {
context(riskCategoryStats.Medium.update(v => v + 1))
} else if status == "Low" {
context(riskCategoryStats.Low.update(v => v + 1))
} else if status == "None" {
context(riskCategoryStats.None.update(v => v + 1))
} else if status == "Other" {
context(riskCategoryStats.Other.update(v => v + 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"
}
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, ciacolor(confidentiality), ciacolor(integrity), ciacolor(availability), cvsscolor(status),
)
)
#align(right)[
#text(
size: 10pt,
fill: gray,
"CVSS:3.1/AV:" + attackVector +
"/AC:" + attackComplexity +
"/PR:" + privilegesRequired +
"/UI:" + userInteraction +
"/S:" + scope +
"/C:" + confidentiality +
"/I:" + integrity +
"/A:" + availability
)]
]
)
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.")
}
)
}
// confidentialMark draws a "CONFIDENTIAL" stamp, used on the cover page
#let confidentialMark() = {
rect(
height: 100%,
width: 100%,
stroke: (paint: red, thickness: 2pt, dash: "solid"),
align(center + horizon,
text(
size: 18pt,
weight: "semibold",
fill: red,
"CONFIDENTIAL"
)
)
)
}
#let draftMark() = {
rect(
height: 100%,
width: 100%,
stroke: (paint: blue, thickness: 2pt, dash: "solid"),
align(center + horizon,
text(
size: 18pt,
weight: "semibold",
fill: blue,
"DRAFT"
)
)
)
}
#let tlpLightMap = (
"RED": (color: rgb("#FF2B2B"), title: "TLP:RED", content: "recipient only"),
"AMBER": (color: rgb("#FFC000"), title: "TLP:AMBER", content: "organisation\nand its clients"),
"AMBER+STRICT": (color: rgb("#FFC000"), title: "TLP:AMBER+STRICT", content: "organisation only"),
"GREEN": (color: rgb("#33FF00"), title: "TLP:GREEN", content: "within community"),
"CLEAR": (color: rgb("#FFFFFF"), title: "TLP:CLEAR", content: "public")
)
// tlpLabel draws an inline TLP label with appropiate color and black backgropund
// light may be one of "RED", "AMBER", "AMBER+STRICT", "GREEN", or "CLEAR"
#let tlpLabel(light) = {
light = upper(light)
// check argument
panicOnInvalid(light, tlpLightMap.keys())
highlight(
fill: black,
text(
weight: "semibold",
fill: tlpLightMap.at(light).color,
tlpLightMap.at(light).title
)
)
}
// tlpMark draws a Traffic Light Protocol mark, used on the cover page
// light may be one of "RED", "AMBER", "AMBER+STRICT", "GREEN", or "CLEAR"
#let tlpMark(light) = {
light = upper(light)
// check argument
panicOnInvalid(light, tlpLightMap.keys())
rect(
height: 100%,
width: 100%,
stroke: (paint: tlpLightMap.at(light).color.darken(10%), thickness: 2pt, dash: "solid"),
align(center + horizon,
grid(
columns: (80%),
rows: (18pt, auto),
gutter: 8pt,
[
#set text(size: if light == "AMBER+STRICT" { 13pt } else { 18pt })
#tlpLabel(light)
],
text(
size: 12pt,
fill: if light == "CLEAR" { black } else { tlpLightMap.at(light).color.darken(10%) },
tlpLightMap.at(light).content
)
)
)
)
usesTLP.update(true)
}