///|
fn marker_for(finding : Finding) -> String {
"[termproof:\{finding.rule_id}:\{risk_name(finding.risk)}]"
}
///|
fn count_text_chars(input : String) -> Int {
input.iter().count()
}
///|
fn push_bidi_findings(
event : Event,
policy : Policy,
findings : Array[Finding],
stats : Stats,
) -> Unit {
let mut relative = 0
for c in event.raw {
if is_bidi_control(c) {
let severity = Dangerous
findings.push({
rule_id: "TP-UNI-001",
severity,
risk: BidirectionalText,
message: "Unicode bidirectional control can disguise the visual order of text",
offset: event.offset + relative,
sequence: visible(String::from_array([c])),
decision: if policy.remove_bidi {
if policy.mark_dangerous {
Mark
} else {
Drop
}
} else {
Keep
},
})
stats.dangerous += 1
if policy.remove_bidi {
stats.dropped += 1
}
}
relative += 1
}
}
///|
fn update_severity_stats(stats : Stats, severity : Severity) -> Unit {
match severity {
Info => stats.info += 1
Warning => stats.warnings += 1
Dangerous => stats.dangerous += 1
Critical => stats.critical += 1
}
}
///|
/// Parse, classify, and sanitize terminal output according to `policy`.
///
/// `plain_text` never contains terminal controls. `safe_terminal` may retain SGR
/// or OSC 8 only when explicitly allowed by the selected policy.
pub fn audit(
input : String,
policy? : Policy = Policy::strict(),
) -> AuditResult {
let events = parse(input)
let findings : Array[Finding] = []
let plain = StringBuilder::new(size_hint=input.length())
let safe = StringBuilder::new(size_hint=input.length())
let stats = empty_stats(input.iter().count())
for event in events {
if event.kind == Text {
let clean = if policy.remove_bidi {
strip_bidi_controls(event.raw)
} else {
event.raw
}
plain.write_string(clean)
safe.write_string(clean)
stats.text_chars += count_text_chars(clean)
push_bidi_findings(event, policy, findings, stats)
} else {
stats.control_events += 1
guard classify(event, policy) is Some(finding) else { continue }
findings.push(finding)
update_severity_stats(stats, finding.severity)
match finding.decision {
Keep => safe.write_string(event.raw)
Drop => stats.dropped += 1
Mark => {
stats.marked += 1
safe.write_string(marker_for(finding))
plain.write_string(marker_for(finding))
}
}
}
}
let mut failed = false
for finding in findings {
if severity_rank(finding.severity) >= severity_rank(policy.fail_at) {
failed = true
}
}
{
plain_text: plain.to_string(),
safe_terminal: safe.to_string(),
events,
findings,
stats,
failed,
}
}
///|
pub fn sanitize(input : String) -> String {
audit(input, policy=Policy::plain()).plain_text
}
///|
pub fn sanitize_for_ci(input : String) -> String {
audit(input, policy=Policy::ci()).safe_terminal
}
///|
pub fn has_terminal_controls(input : String) -> Bool {
for event in parse(input) {
if event.kind != Text {
return true
}
}
false
}