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