///|
fn json_bool(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn json_finding(finding : Finding) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"rule_id\":")
out.write_string(json_quote(finding.rule_id))
out.write_string(",\"severity\":")
out.write_string(json_quote(severity_name(finding.severity)))
out.write_string(",\"risk\":")
out.write_string(json_quote(risk_name(finding.risk)))
out.write_string(",\"message\":")
out.write_string(json_quote(finding.message))
out.write_string(",\"offset\":")
out.write_string(finding.offset.to_string())
out.write_string(",\"sequence\":")
out.write_string(json_quote(finding.sequence))
out.write_string(",\"decision\":")
out.write_string(
json_quote(
match finding.decision {
Keep => "keep"
Drop => "drop"
Mark => "mark"
},
),
)
out.write_string("}")
out.to_string()
}
///|
fn json_event(event : Event) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"kind\":")
out.write_string(json_quote(event_kind_name(event.kind)))
out.write_string(",\"offset\":")
out.write_string(event.offset.to_string())
out.write_string(",\"complete\":")
out.write_string(json_bool(event.complete))
out.write_string(",\"raw\":")
out.write_string(json_quote(visible(event.raw)))
out.write_string(",\"payload\":")
out.write_string(json_quote(visible(event.payload)))
match event.final_byte {
Some(c) => {
out.write_string(",\"final\":")
out.write_string(json_quote(String::from_array([c])))
}
None => ()
}
out.write_string("}")
out.to_string()
}
///|
fn json_stats(stats : Stats) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"input_chars\":\{stats.input_chars}")
out.write_string(",\"text_chars\":\{stats.text_chars}")
out.write_string(",\"control_events\":\{stats.control_events}")
out.write_string(",\"info\":\{stats.info}")
out.write_string(",\"warnings\":\{stats.warnings}")
out.write_string(",\"dangerous\":\{stats.dangerous}")
out.write_string(",\"critical\":\{stats.critical}")
out.write_string(",\"dropped\":\{stats.dropped}")
out.write_string(",\"marked\":\{stats.marked}")
out.write_string("}")
out.to_string()
}
///|
/// Deterministic JSON report with no dependency on a JSON serialization package.
pub fn report_json(
result : AuditResult,
include_events? : Bool = false,
) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"schema\":\"urn:termproof:report-v1\"")
out.write_string(",\"failed\":")
out.write_string(json_bool(result.failed))
out.write_string(",\"plain_text\":")
out.write_string(json_quote(result.plain_text))
out.write_string(",\"stats\":")
out.write_string(json_stats(result.stats))
out.write_string(",\"findings\":[")
let mut first = true
for finding in result.findings {
if !first {
out.write_char(',')
}
first = false
out.write_string(json_finding(finding))
}
out.write_string("]")
if include_events {
out.write_string(",\"events\":[")
first = true
for event in result.events {
if !first {
out.write_char(',')
}
first = false
out.write_string(json_event(event))
}
out.write_string("]")
}
out.write_string("}")
out.to_string()
}
///|
pub fn report_markdown(result : AuditResult) -> String {
let out = StringBuilder::new()
out.write_string("# TermProof audit report\n\n")
out.write_string(
if result.failed {
"**Result: failed**\n\n"
} else {
"**Result: passed**\n\n"
},
)
out.write_string("| Metric | Count |\n|---|---:|\n")
out.write_string("| Input characters | \{result.stats.input_chars} |\n")
out.write_string("| Control events | \{result.stats.control_events} |\n")
out.write_string("| Warnings | \{result.stats.warnings} |\n")
out.write_string("| Dangerous | \{result.stats.dangerous} |\n")
out.write_string("| Critical | \{result.stats.critical} |\n\n")
if result.findings.length() == 0 {
out.write_string("No terminal-control findings.\n")
} else {
out.write_string("## Findings\n\n")
out.write_string(
"| Offset | Severity | Rule | Risk | Sequence | Description |\n",
)
out.write_string("|---:|---|---|---|---|---|\n")
for finding in result.findings {
out.write_string("| \{finding.offset} | ")
out.write_string(severity_name(finding.severity))
out.write_string(" | ")
out.write_string(markdown_escape(finding.rule_id))
out.write_string(" | ")
out.write_string(markdown_escape(risk_name(finding.risk)))
out.write_string(" | `")
out.write_string(markdown_escape(finding.sequence))
out.write_string("` | ")
out.write_string(markdown_escape(finding.message))
out.write_string(" |\n")
}
}
out.to_string()
}
///|
pub fn report_github_annotations(
result : AuditResult,
file? : String = "terminal.log",
) -> String {
let out = StringBuilder::new()
for finding in result.findings {
let command = match finding.severity {
Info => "notice"
Warning => "warning"
Dangerous | Critical => "error"
}
out.write_string("::")
out.write_string(command)
out.write_string(" file=")
out.write_string(github_property_escape(file))
out.write_string(",title=")
out.write_string(github_property_escape(finding.rule_id))
out.write_string("::")
out.write_string(
github_message_escape(
"\{finding.message} at character \{finding.offset}; sequence \{finding.sequence}",
),
)
out.write_char('\n')
}
out.to_string()
}
///|
pub fn summary_line(result : AuditResult) -> String {
"TermProof: \{result.stats.control_events} controls, " +
"\{result.stats.warnings} warning, \{result.stats.dangerous} dangerous, " +
"\{result.stats.critical} critical; " +
(if result.failed { "FAILED" } else { "PASSED" })
}