agentshield/output/
console.rs1use crate::rules::policy::PolicyVerdict;
2use crate::rules::{Finding, Severity};
3
4const RESET: &str = "\x1b[0m";
6const BOLD: &str = "\x1b[1m";
7const DIM: &str = "\x1b[2m";
8const RED: &str = "\x1b[31m";
9const GREEN: &str = "\x1b[32m";
10const YELLOW: &str = "\x1b[33m";
11const BLUE: &str = "\x1b[34m";
12const MAGENTA: &str = "\x1b[35m";
13const CYAN: &str = "\x1b[36m";
14
15pub fn render(findings: &[Finding], verdict: &PolicyVerdict) -> String {
17 let use_color = std::env::var("NO_COLOR").is_err();
18 let mut output = String::new();
19
20 if findings.is_empty() {
21 if use_color {
22 output.push_str(&format!(
23 "\n {GREEN}{BOLD}No security findings detected.{RESET}\n\n"
24 ));
25 } else {
26 output.push_str("\n No security findings detected.\n\n");
27 }
28 return output;
29 }
30
31 let mut sorted: Vec<&Finding> = findings.iter().collect();
33 sorted.sort_by(|a, b| {
34 b.severity.cmp(&a.severity).then_with(|| {
35 let a_file = a.location.as_ref().map(|l| &l.file);
36 let b_file = b.location.as_ref().map(|l| &l.file);
37 a_file.cmp(&b_file)
38 })
39 });
40
41 output.push_str(&format!(
42 "\n {bold}{} finding(s) detected:{reset}\n\n",
43 findings.len(),
44 bold = if use_color { BOLD } else { "" },
45 reset = if use_color { RESET } else { "" },
46 ));
47
48 for finding in &sorted {
49 let severity_tag = if use_color {
50 match finding.severity {
51 Severity::Critical => format!("{RED}{BOLD}[CRITICAL]{RESET}"),
52 Severity::High => format!("{MAGENTA}{BOLD}[HIGH] {RESET}"),
53 Severity::Medium => format!("{YELLOW}{BOLD}[MEDIUM] {RESET}"),
54 Severity::Low => format!("{BLUE}[LOW] {RESET}"),
55 Severity::Info => format!("{DIM}[INFO] {RESET}"),
56 }
57 } else {
58 match finding.severity {
59 Severity::Critical => "[CRITICAL]".into(),
60 Severity::High => "[HIGH] ".into(),
61 Severity::Medium => "[MEDIUM] ".into(),
62 Severity::Low => "[LOW] ".into(),
63 Severity::Info => "[INFO] ".into(),
64 }
65 };
66
67 let location = finding
68 .location
69 .as_ref()
70 .map(|l| format!("{}:{}", l.file.display(), l.line))
71 .unwrap_or_else(|| "-".into());
72
73 output.push_str(&format!(
74 " {} {bold}{}{reset} {}\n",
75 severity_tag,
76 finding.rule_id,
77 finding.message,
78 bold = if use_color { BOLD } else { "" },
79 reset = if use_color { RESET } else { "" },
80 ));
81 output.push_str(&format!(
82 " {dim}at {}{reset}\n",
83 location,
84 dim = if use_color { DIM } else { "" },
85 reset = if use_color { RESET } else { "" },
86 ));
87 if let Some(remediation) = &finding.remediation {
88 output.push_str(&format!(
89 " {cyan}fix: {}{reset}\n",
90 remediation,
91 cyan = if use_color { CYAN } else { "" },
92 reset = if use_color { RESET } else { "" },
93 ));
94 }
95 output.push('\n');
96 }
97
98 let (status, status_color) = if verdict.pass {
100 ("PASS", if use_color { GREEN } else { "" })
101 } else {
102 ("FAIL", if use_color { RED } else { "" })
103 };
104 output.push_str(&format!(
105 " Result: {sc}{bold}{}{reset} (threshold: {}, highest: {})\n\n",
106 status,
107 verdict.fail_threshold,
108 verdict
109 .highest_severity
110 .map(|s| s.to_string())
111 .unwrap_or_else(|| "none".into()),
112 sc = status_color,
113 bold = if use_color { BOLD } else { "" },
114 reset = if use_color { RESET } else { "" },
115 ));
116
117 output
118}