Render a full terminal report with finding panels. Returns exit code: 0 = pass, 1 = any findings present.
(findings: &[Finding], policy_path: &str, credentials_path: &str)
| 131 | /// Render a full terminal report with finding panels. |
| 132 | /// Returns exit code: 0 = pass, 1 = any findings present. |
| 133 | pub fn render_report(findings: &[Finding], policy_path: &str, credentials_path: &str) -> i32 { |
| 134 | let policy_name = Path::new(policy_path) |
| 135 | .file_name() |
| 136 | .map_or("policy.yaml", |n| n.to_str().unwrap_or("policy.yaml")); |
| 137 | let creds_name = Path::new(credentials_path) |
| 138 | .file_name() |
| 139 | .map_or("credentials.yaml", |n| { |
| 140 | n.to_str().unwrap_or("credentials.yaml") |
| 141 | }); |
| 142 | |
| 143 | println!(); |
| 144 | println!( |
| 145 | "{}", |
| 146 | "\u{250c}\u{2500}\u{2500} OpenShell Policy Prover \u{2500}\u{2500}\u{2510}".blue() |
| 147 | ); |
| 148 | println!(" Policy: {policy_name}"); |
| 149 | println!(" Credentials: {creds_name}"); |
| 150 | println!(); |
| 151 | |
| 152 | let active: Vec<&Finding> = findings.iter().filter(|f| !f.accepted).collect(); |
| 153 | let accepted: Vec<&Finding> = findings.iter().filter(|f| f.accepted).collect(); |
| 154 | |
| 155 | // Per-category summary |
| 156 | let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); |
| 157 | for f in &active { |
| 158 | *counts.entry(f.query.as_str()).or_default() += f.paths.len(); |
| 159 | } |
| 160 | |
| 161 | if active.is_empty() && accepted.is_empty() { |
| 162 | println!("{}", "No findings. Policy posture is clean.".green().bold()); |
| 163 | return 0; |
| 164 | } |
| 165 | |
| 166 | println!("{}", "Finding Summary".bold().underline()); |
| 167 | for (query, count) in &counts { |
| 168 | println!(" {:>40} {count} path(s)", category_label(query).yellow()); |
| 169 | } |
| 170 | if !accepted.is_empty() { |
| 171 | println!(" {:>40} {}", "ACCEPTED".dimmed(), accepted.len()); |
| 172 | } |
| 173 | println!(); |
| 174 | |
| 175 | for (i, finding) in active.iter().enumerate() { |
| 176 | println!( |
| 177 | "--- Finding #{} [{}] ---", |
| 178 | i + 1, |
| 179 | category_label(&finding.query) |
| 180 | ); |
| 181 | println!(" {}", finding.title.bold()); |
| 182 | println!(" {}", finding.description); |
| 183 | println!(); |
| 184 | render_paths(&finding.paths); |
| 185 | if !finding.remediation.is_empty() { |
| 186 | println!(" {}", "Remediation:".bold()); |
| 187 | for r in &finding.remediation { |
| 188 | println!(" - {r}"); |
| 189 | } |
| 190 | println!(); |
no test coverage detected