use cleanlib_client::types::{PolicyDecision, Verdict};
use cleanlib_client::{CustomerState, Tier};
use comfy_table::{presets, ContentArrangement, Table};
use is_terminal::IsTerminal;
use owo_colors::{OwoColorize, Style};
use super::sanitize::{mask_engine_tag, mask_fixture_label};
use super::output::{decision_tier, decision_tier_str_with_severity, DecisionTier};
#[derive(Debug, Clone, Copy, Default)]
#[allow(dead_code)]
pub struct RenderOpts {
pub verbose: bool,
pub no_color: bool,
}
#[allow(dead_code)]
impl RenderOpts {
pub fn use_color(&self) -> bool {
!self.no_color
&& std::env::var("NO_COLOR").is_err()
&& std::io::stdout().is_terminal()
}
}
pub fn stdout_is_tty() -> bool {
std::io::stdout().is_terminal()
}
pub fn style_decision(decision: &str) -> String {
let display = mask_engine_tag(decision);
if !stdout_is_tty() {
return display;
}
let style: Style = match decision_tier(decision) {
DecisionTier::Allow => Style::new().green().bold(),
DecisionTier::Deny => Style::new().red().bold(),
DecisionTier::Warn => {
if decision == "RISK_ACCEPTANCE_REQUIRED" {
Style::new().bright_yellow().bold()
} else {
Style::new().yellow().bold()
}
}
DecisionTier::Other => {
if decision == "INSUFFICIENT_DATA" {
Style::new().blue().to_owned()
} else {
Style::new()
}
}
};
display.style(style).to_string()
}
pub fn render_verdict(verdict: &Verdict, _opts: &RenderOpts) {
render_customer_state(&verdict.source);
let tier_str = decision_tier_str_with_severity(&verdict.verdict, verdict.severity.as_deref(), verdict.composite_score);
println!("decision: {}", style_decision(tier_str));
println!("verdict_id: {}", verdict.verdict_id);
println!("verdict: {}", style_decision(&verdict.verdict));
println!("confidence: {:.2}", verdict.confidence);
println!("composite_score: {}", verdict.composite_score);
if !verdict.reasoning.is_empty() {
println!("reasoning: {}", verdict.reasoning);
}
if !verdict.suggested_actions.is_empty() {
let masked: Vec<String> = verdict
.suggested_actions
.iter()
.map(|a| mask_engine_tag(a))
.filter(|a| !mask_fixture_label(a).is_empty())
.collect();
if let [single] = masked.as_slice() {
println!("suggested: {}", single);
} else if !masked.is_empty() {
println!("suggested:");
for action in &masked {
println!(" - {}", action);
}
}
}
if let Some(stale) = &verdict.stale_since_at {
println!("stale_since_at: {}", stale);
if let Some(reason) = &verdict.staleness_reason {
println!("staleness_reason: {}", reason);
}
}
if let Some(at) = &verdict.computed_at {
println!("as_of: {}", at);
}
}
fn render_customer_state(source: &str) {
let state = CustomerState::from_wire(source);
let header = format!("{} {}", state.emoji(), state.label());
let header = if stdout_is_tty() {
let style = match state.tier() {
Tier::Block => Style::new().red().bold(),
Tier::Warn => Style::new().yellow().bold(),
Tier::Clean => Style::new().green().bold(),
};
header.style(style).to_string()
} else {
header
};
println!("{header}");
println!(" {}", state.copy());
}
pub fn render_decisions(decisions: &[PolicyDecision]) {
if decisions.is_empty() {
println!("(no decisions returned)");
return;
}
if decisions.len() == 1 {
let d = &decisions[0];
let masked_reason = mask_engine_tag(&d.reason);
let suffix = if masked_reason.is_empty() {
String::new()
} else {
format!(" — {}", masked_reason)
};
println!(
"{}: {}@{} → {}{}",
d.ecosystem,
d.package,
d.version,
style_decision(&d.decision),
suffix
);
return;
}
let mut table = Table::new();
let preset = if stdout_is_tty() {
presets::UTF8_BORDERS_ONLY
} else {
presets::NOTHING
};
table
.load_preset(preset)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec!["ECOSYSTEM", "PACKAGE", "VERSION", "DECISION", "REASON"]);
for d in decisions {
table.add_row(vec![
d.ecosystem.clone(),
d.package.clone(),
d.version.clone(),
style_decision(&d.decision),
mask_engine_tag(&d.reason),
]);
}
println!("{table}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decision_tier_phase1_vocab() {
assert_eq!(decision_tier("ALLOWED_NO_FINDINGS"), DecisionTier::Allow);
assert_eq!(decision_tier("VECTOR_VERDICT"), DecisionTier::Deny);
assert_eq!(decision_tier("DM_THRESHOLD_BLOCK"), DecisionTier::Deny);
assert_eq!(decision_tier("RISK_ACCEPTANCE_REQUIRED"), DecisionTier::Warn);
}
#[test]
fn decision_tier_universal_envelope() {
assert_eq!(decision_tier("ALLOW"), DecisionTier::Allow);
assert_eq!(decision_tier("WARN"), DecisionTier::Warn);
assert_eq!(decision_tier("DENY"), DecisionTier::Deny);
}
#[test]
fn decision_tier_unknown_is_other() {
assert_eq!(decision_tier("MAYBE"), DecisionTier::Other);
assert_eq!(decision_tier(""), DecisionTier::Other);
}
#[test]
fn render_opts_no_color_overrides_tty() {
let opts = RenderOpts { verbose: false, no_color: true };
assert!(!opts.use_color());
}
#[test]
fn style_decision_masks_vector_verdict() {
let out = style_decision("VECTOR_VERDICT");
assert!(!out.contains("VECTOR_VERDICT"), "raw codename leaked: {out}");
assert!(out.contains("Engine signal"));
}
#[test]
fn style_decision_masks_dm_threshold_block() {
let out = style_decision("DM_THRESHOLD_BLOCK");
assert!(!out.contains("DM_THRESHOLD_BLOCK"), "raw codename leaked: {out}");
assert!(out.contains("Policy decision"));
}
#[test]
fn style_decision_passes_through_allow_deny_warn() {
assert!(style_decision("ALLOW").contains("ALLOW"));
assert!(style_decision("DENY").contains("DENY"));
assert!(style_decision("WARN").contains("WARN"));
}
#[test]
fn customer_state_header_resolves_from_wire_source() {
let s = CustomerState::from_wire("CVE_FINDING_ON_RANSOMWARE");
assert_eq!(s.label(), "Ransomware-linked");
assert_eq!(s.tier(), Tier::Block);
assert_eq!(CustomerState::from_wire("MALICIOUS_TRIAGE").label(), "Malicious");
assert_eq!(CustomerState::from_wire("CVE_FINDING").label(), "Vulnerable");
}
#[test]
fn customer_state_header_never_leaks_wire_codename() {
for src in ["CVE_FINDING", "CVE_FINDING_ON_KEV", "MALICIOUS_TRIAGE", "DM_THRESHOLD_BLOCK"] {
let s = CustomerState::from_wire(src);
assert!(!s.label().contains(src));
assert!(!s.copy().contains(src));
}
}
#[test]
fn style_decision_masks_allowed_no_findings_to_plain() {
let out = style_decision("ALLOWED_NO_FINDINGS");
assert!(out.contains("ALLOWED_NO_FINDINGS"));
}
#[test]
fn style_decision_masks_insufficient_data_to_customer_label() {
let out = style_decision("INSUFFICIENT_DATA");
assert!(
!out.contains("INSUFFICIENT_DATA"),
"raw wire codename leaked in text render: {out}"
);
assert!(
out.contains("Not yet assessed"),
"canonical customer label missing: {out}"
);
}
}