246 lines
7.9 KiB
Typst
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)
|
|
} |