1use std::io::Write;
2
3use crate::verdict::{Action, Finding, Severity, Verdict};
4
5const SCHEMA_VERSION: u32 = 1;
6
7#[derive(serde::Serialize)]
9pub struct JsonOutput<'a> {
10 pub schema_version: u32,
11 pub action: Action,
12 pub findings: &'a [Finding],
13 pub tier_reached: u8,
14 pub bypass_requested: bool,
15 pub bypass_honored: bool,
16 pub interactive_detected: bool,
17 pub policy_path_used: &'a Option<String>,
18 pub timings_ms: &'a crate::verdict::Timings,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub urls_extracted_count: Option<usize>,
21}
22
23pub fn write_json(verdict: &Verdict, mut w: impl Write) -> std::io::Result<()> {
25 let output = JsonOutput {
26 schema_version: SCHEMA_VERSION,
27 action: verdict.action,
28 findings: &verdict.findings,
29 tier_reached: verdict.tier_reached,
30 bypass_requested: verdict.bypass_requested,
31 bypass_honored: verdict.bypass_honored,
32 interactive_detected: verdict.interactive_detected,
33 policy_path_used: &verdict.policy_path_used,
34 timings_ms: &verdict.timings_ms,
35 urls_extracted_count: verdict.urls_extracted_count,
36 };
37 serde_json::to_writer(&mut w, &output)?;
38 writeln!(w)?;
39 Ok(())
40}
41
42pub fn write_human(verdict: &Verdict, mut w: impl Write) -> std::io::Result<()> {
44 if verdict.findings.is_empty() {
45 return Ok(());
46 }
47
48 let action_str = match verdict.action {
49 Action::Allow => return Ok(()),
50 Action::Warn => "WARNING",
51 Action::Block => "BLOCKED",
52 };
53
54 writeln!(w, "tirith: {action_str}")?;
55
56 for finding in &verdict.findings {
57 let severity_color = match finding.severity {
58 Severity::Critical => "\x1b[91m", Severity::High => "\x1b[31m", Severity::Medium => "\x1b[33m", Severity::Low => "\x1b[36m", };
63 let reset = "\x1b[0m";
64
65 writeln!(
66 w,
67 " {}[{}]{} {} — {}",
68 severity_color, finding.severity, reset, finding.rule_id, finding.title
69 )?;
70 writeln!(w, " {}", finding.description)?;
71 }
72
73 if verdict.action == Action::Block {
74 writeln!(w, " Set TIRITH=0 to bypass (use with caution)")?;
75 }
76
77 Ok(())
78}
79
80pub fn write_human_auto(verdict: &Verdict) -> std::io::Result<()> {
83 let stderr = std::io::stderr();
84 let is_tty = is_terminal::is_terminal(&stderr);
85
86 if is_tty {
87 write_human(verdict, stderr.lock())
88 } else {
89 write_human_no_color(verdict, stderr.lock())
90 }
91}
92
93fn write_human_no_color(verdict: &Verdict, mut w: impl Write) -> std::io::Result<()> {
95 if verdict.findings.is_empty() {
96 return Ok(());
97 }
98
99 let action_str = match verdict.action {
100 Action::Allow => return Ok(()),
101 Action::Warn => "WARNING",
102 Action::Block => "BLOCKED",
103 };
104
105 writeln!(w, "tirith: {action_str}")?;
106
107 for finding in &verdict.findings {
108 writeln!(
109 w,
110 " [{}] {} — {}",
111 finding.severity, finding.rule_id, finding.title
112 )?;
113 writeln!(w, " {}", finding.description)?;
114 }
115
116 if verdict.action == Action::Block {
117 writeln!(w, " Set TIRITH=0 to bypass (use with caution)")?;
118 }
119
120 Ok(())
121}