///|
pub(all) struct LineRisk {
line : Int
mut findings : Int
mut info : Int
mut warnings : Int
mut dangerous : Int
mut critical : Int
mut risks : Array[RiskKind]
} derive(Debug, Eq)
///|
pub(all) struct AuditOverview {
result : AuditResult
profile : AuditProfile
remediation : RemediationPlan
line_index : LineIndex
located : Array[LocatedFinding]
line_risks : Array[LineRisk]
} derive(Debug, Eq)
///|
fn empty_line_risk(line : Int) -> LineRisk {
{
line,
findings: 0,
info: 0,
warnings: 0,
dangerous: 0,
critical: 0,
risks: [],
}
}
///|
fn line_risk_index(items : Array[LineRisk], line : Int) -> Int {
let mut i = 0
while i < items.length() {
if items[i].line == line {
return i
}
i += 1
}
-1
}
///|
fn line_risk_has_kind(item : LineRisk, risk : RiskKind) -> Bool {
for existing in item.risks {
if existing == risk {
return true
}
}
false
}
///|
fn update_line_risk(item : LineRisk, located : LocatedFinding) -> LineRisk {
let next = item
next.findings += 1
match located.finding.severity {
Info => next.info += 1
Warning => next.warnings += 1
Dangerous => next.dangerous += 1
Critical => next.critical += 1
}
if !line_risk_has_kind(next, located.finding.risk) {
next.risks.push(located.finding.risk)
}
next
}
///|
fn add_line_risk(items : Array[LineRisk], located : LocatedFinding) -> Unit {
let line = located.position.line
let index = line_risk_index(items, line)
if index < 0 {
items.push(update_line_risk(empty_line_risk(line), located))
} else {
items[index] = update_line_risk(items[index], located)
}
}
///|
fn build_line_risks(located : Array[LocatedFinding]) -> Array[LineRisk] {
let items : Array[LineRisk] = []
for item in located {
add_line_risk(items, item)
}
items
}
///|
pub fn LineRisk::highest(self : LineRisk) -> Severity {
if self.critical > 0 {
Critical
} else if self.dangerous > 0 {
Dangerous
} else if self.warnings > 0 {
Warning
} else {
Info
}
}
///|
fn line_risk_score(item : LineRisk) -> Int {
severity_rank(item.highest()) * 100 + item.findings * 10 + item.risks.length()
}
///|
pub fn top_line_risk(overview : AuditOverview) -> LineRisk? {
if overview.line_risks.length() == 0 {
return None
}
let mut best = overview.line_risks[0]
let mut score = line_risk_score(best)
let mut i = 1
while i < overview.line_risks.length() {
let next_score = line_risk_score(overview.line_risks[i])
if next_score > score {
best = overview.line_risks[i]
score = next_score
}
i += 1
}
Some(best)
}
///|
pub fn overview_from_result(
source : String,
result : AuditResult,
) -> AuditOverview {
let index = build_line_index(source)
let located = locate_findings(source, result.findings)
{
result,
profile: profile_audit(result),
remediation: remediation_plan(result),
line_index: index,
located,
line_risks: build_line_risks(located),
}
}
///|
pub fn audit_overview(
source : String,
policy? : Policy = Policy::strict(),
) -> AuditOverview {
overview_from_result(source, audit(source, policy~))
}
///|
pub fn AuditOverview::gate(self : AuditOverview) -> String {
self.remediation.gate
}
///|
pub fn AuditOverview::passed(self : AuditOverview) -> Bool {
self.gate() == "pass"
}
///|
pub fn AuditOverview::failed(self : AuditOverview) -> Bool {
self.result.failed || self.remediation.blocked
}
///|
pub fn AuditOverview::summary(self : AuditOverview) -> String {
let top_line = match top_line_risk(self) {
Some(line) => "line \{line.line}"
None => "none"
}
"\{profile_summary(self.profile)}; gate=\{self.gate()}; top-line=\{top_line}"
}
///|
pub fn overview_summary(overview : AuditOverview) -> String {
overview.summary()
}
///|
fn write_overview_text_findings(
out : StringBuilder,
overview : AuditOverview,
) -> Unit {
if overview.located.length() == 0 {
out.write_string("findings: none\n")
return
}
out.write_string("findings:\n")
for item in overview.located {
out.write_string("- ")
out.write_string(item.summary())
out.write_char('\n')
}
}
///|
fn write_overview_text_lines(
out : StringBuilder,
overview : AuditOverview,
) -> Unit {
if overview.line_risks.length() == 0 {
return
}
out.write_string("line risks:\n")
for item in overview.line_risks {
out.write_string("- line ")
out.write_string(item.line.to_string())
out.write_string(": ")
out.write_string(item.findings.to_string())
out.write_string(" finding(s), highest=")
out.write_string(severity_name(item.highest()))
out.write_char('\n')
}
}
///|
pub fn overview_text(overview : AuditOverview) -> String {
let out = StringBuilder::new()
out.write_string(overview.summary())
out.write_char('\n')
out.write_string(profile_text(overview.profile))
out.write_string(remediation_text(overview.remediation))
write_overview_text_findings(out, overview)
write_overview_text_lines(out, overview)
out.to_string()
}
///|
pub fn audit_overview_text(
source : String,
policy? : Policy = Policy::strict(),
) -> String {
overview_text(audit_overview(source, policy~))
}
///|
fn write_line_risk_markdown(out : StringBuilder, line_risk : LineRisk) -> Unit {
out.write_string("| ")
out.write_string(line_risk.line.to_string())
out.write_string(" | ")
out.write_string(line_risk.findings.to_string())
out.write_string(" | ")
out.write_string(markdown_escape(severity_name(line_risk.highest())))
out.write_string(" | ")
let mut first = true
for risk in line_risk.risks {
if !first {
out.write_string(", ")
}
first = false
out.write_string(markdown_escape(risk_name(risk)))
}
out.write_string(" |\n")
}
///|
fn write_overview_markdown_header(
out : StringBuilder,
overview : AuditOverview,
) -> Unit {
out.write_string("# TermProof audit overview\n\n")
out.write_string("| Field | Value |\n|---|---|\n")
out.write_string("| Gate | ")
out.write_string(markdown_escape(overview.gate()))
out.write_string(" |\n")
out.write_string("| Failed | ")
out.write_string(if overview.failed() { "yes" } else { "no" })
out.write_string(" |\n")
out.write_string("| Lines | ")
out.write_string(overview.line_index.line_count().to_string())
out.write_string(" |\n")
out.write_string("| Findings | ")
out.write_string(overview.profile.total_findings.to_string())
out.write_string(" |\n")
out.write_string("| Highest severity | ")
out.write_string(
markdown_escape(severity_name(overview.profile.highest_severity)),
)
out.write_string(" |\n")
out.write_string("| Exposure | ")
out.write_string(markdown_escape(overview.profile.score.label))
out.write_string(" |\n\n")
}
///|
fn write_overview_markdown_lines(
out : StringBuilder,
overview : AuditOverview,
) -> Unit {
if overview.line_risks.length() == 0 {
return
}
out.write_string("## Line risk map\n\n")
out.write_string("| Line | Findings | Highest | Risks |\n")
out.write_string("|---:|---:|---|---|\n")
for item in overview.line_risks {
write_line_risk_markdown(out, item)
}
out.write_char('\n')
}
///|
fn write_overview_markdown_context(
out : StringBuilder,
overview : AuditOverview,
) -> Unit {
if overview.located.length() == 0 {
return
}
out.write_string("## Context snippets\n\n")
for item in overview.located {
out.write_string("### ")
out.write_string(markdown_escape(item.summary()))
out.write_string("\n\n")
out.write_string("```text\n")
out.write_string(
overview.line_index.window_at(item.finding.offset, context=1),
)
out.write_string("```\n\n")
}
}
///|
pub fn overview_markdown(overview : AuditOverview) -> String {
let out = StringBuilder::new()
write_overview_markdown_header(out, overview)
out.write_string(profile_markdown(overview.profile))
out.write_char('\n')
out.write_string(remediation_markdown(overview.remediation))
out.write_char('\n')
if overview.located.length() == 0 {
out.write_string("## Findings\n\nNo terminal-control findings.\n")
} else {
out.write_string("## Findings with source locations\n\n")
out.write_string(located_findings_markdown(overview.located))
out.write_char('\n')
}
write_overview_markdown_lines(out, overview)
write_overview_markdown_context(out, overview)
out.to_string()
}
///|
pub fn audit_overview_markdown(
source : String,
policy? : Policy = Policy::strict(),
) -> String {
overview_markdown(audit_overview(source, policy~))
}
///|
fn write_line_risk_json(out : StringBuilder, line_risk : LineRisk) -> Unit {
out.write_string("{\"line\":")
out.write_string(line_risk.line.to_string())
out.write_string(",\"findings\":")
out.write_string(line_risk.findings.to_string())
out.write_string(",\"highest\":")
out.write_string(json_quote(severity_name(line_risk.highest())))
out.write_string(",\"risks\":[")
let mut first = true
for risk in line_risk.risks {
if !first {
out.write_char(',')
}
first = false
out.write_string(json_quote(risk_name(risk)))
}
out.write_string("]}")
}
///|
pub fn overview_json(overview : AuditOverview) -> String {
let out = StringBuilder::new()
out.write_string("{\"schema\":\"urn:termproof:overview-v1\"")
out.write_string(",\"summary\":")
out.write_string(json_quote(overview.summary()))
out.write_string(",\"gate\":")
out.write_string(json_quote(overview.gate()))
out.write_string(",\"failed\":")
out.write_string(if overview.failed() { "true" } else { "false" })
out.write_string(",\"line_count\":")
out.write_string(overview.line_index.line_count().to_string())
out.write_string(",\"profile\":")
out.write_string(profile_json(overview.profile))
out.write_string(",\"remediation\":")
out.write_string(remediation_json(overview.remediation))
out.write_string(",\"located_findings\":")
out.write_string(located_findings_json(overview.located))
out.write_string(",\"line_risks\":[")
let mut first = true
for item in overview.line_risks {
if !first {
out.write_char(',')
}
first = false
write_line_risk_json(out, item)
}
out.write_string("]}")
out.to_string()
}
///|
pub fn audit_overview_json(
source : String,
policy? : Policy = Policy::strict(),
) -> String {
overview_json(audit_overview(source, policy~))
}