use std::collections::HashMap;
use crate::policy::ir::PolicyDecision;
use crate::policy::sandbox_types::SandboxPolicy;
use crate::style;
pub fn success(msg: &str) {
println!("{} {}", style::green_bold("✓"), msg);
}
pub fn skip(msg: &str) {
println!("{} {}", style::dim("·"), msg);
}
pub fn warn(msg: &str) {
eprintln!("{} {}", style::err_yellow("!"), msg);
}
pub fn fail(msg: &str) {
eprintln!("{} {}", style::err_red_bold("✗"), msg);
}
pub fn progress(msg: &str) {
println!("{} {}", style::cyan("~"), msg);
}
pub fn info(msg: &str) {
println!("{msg}");
}
pub fn banner() {
println!("{}", style::banner());
println!();
}
pub fn section(title: &str) {
println!("{}", style::header(title));
println!("{}", style::dim(&"─".repeat(title.len())));
}
pub fn banner_section(title: &str) {
banner();
section(title);
println!();
}
pub fn print_tool_header(title: &str, tool_name: &str, arguments: &serde_json::Value) {
println!("{}", style::bold(title));
println!(" {} {}", style::cyan("tool:"), tool_name);
println!(" {} {}", style::cyan("arguments:"), arguments);
}
pub fn print_decision(decision: &PolicyDecision) {
println!(
"{} {}",
style::bold("Decision:"),
style::effect(&decision.effect.to_string())
);
if let Some(ref reason) = decision.reason {
println!("{} {}", style::bold("Reason: "), reason);
}
println!();
if !decision.trace.matched_rules.is_empty() {
println!("{}", style::header("Matched rules:"));
for m in &decision.trace.matched_rules {
let eff = style::effect(&m.effect.to_string());
println!(" [{}] {} -> {}", m.rule_index, m.description, eff);
}
println!();
}
if !decision.trace.skipped_rules.is_empty() {
println!("{}", style::dim("Skipped rules:"));
for s in &decision.trace.skipped_rules {
println!(
" {} {} {}",
style::dim(&format!("[{}]", s.rule_index)),
style::dim(&s.description),
style::dim(&format!("({})", s.reason))
);
}
println!();
}
println!(
"{} {}",
style::bold("Resolution:"),
style::effect(&decision.trace.final_resolution)
);
}
pub fn print_sandbox_summary(sandbox: &SandboxPolicy) {
println!(" {}: {}", style::cyan("default"), sandbox.default.short());
println!(" {}: {:?}", style::cyan("network"), sandbox.network);
for rule in &sandbox.rules {
use crate::policy::sandbox_types::PathMatch;
let path_display = match rule.path_match {
PathMatch::Subpath => format!("{}/**", rule.path),
PathMatch::ChildOf => format!("{}/*", rule.path),
PathMatch::Regex => format!("{} (regex)", rule.path),
PathMatch::Literal => rule.path.clone(),
};
println!(
" {:?} {} in {}",
rule.effect,
rule.caps.short(),
path_display
);
}
}
pub fn print_sandbox_table(sandboxes: &HashMap<String, SandboxPolicy>) {
use crate::policy::sandbox_types::{NetworkPolicy, RuleEffect};
let mut names: Vec<&str> = sandboxes.keys().map(|s| s.as_str()).collect();
names.sort();
let mut paths: Vec<String> = Vec::new();
for name in &names {
for rule in &sandboxes[*name].rules {
let suffix = match rule.path_match {
crate::policy::sandbox_types::PathMatch::Subpath => "/**",
crate::policy::sandbox_types::PathMatch::Literal => "",
crate::policy::sandbox_types::PathMatch::ChildOf => "/*",
crate::policy::sandbox_types::PathMatch::Regex => " (re)",
};
let key = format!("{}{}", rule.path, suffix);
if !paths.contains(&key) {
paths.push(key);
}
}
}
paths.sort_by_key(|p| {
let stripped = p.strip_suffix("/**").unwrap_or(p);
stripped.matches('/').count() + stripped.matches('$').count()
});
let mut domains: Vec<String> = Vec::new();
for name in &names {
if let NetworkPolicy::AllowDomains(ds) = &sandboxes[*name].network {
for d in ds {
if !domains.contains(d) {
domains.push(d.clone());
}
}
}
}
let col_w = names.iter().map(|n| n.len()).max().unwrap_or(5).max(5);
let domain_max = domains.iter().map(|d| d.len()).max().unwrap_or(0);
let path_w = paths
.iter()
.map(|p| p.len())
.max()
.unwrap_or(7)
.max(domain_max)
.max(7);
let hdr: Vec<String> = names.iter().map(|n| lpad(&style::cyan(n), col_w)).collect();
println!(" {} {}", rpad("", path_w), hdr.join(" "));
let def: Vec<String> = names
.iter()
.map(|n| lpad(&style::dim(&sandboxes[*n].default.short()), col_w))
.collect();
println!(
" {} {}",
rpad(&style::dim("default"), path_w),
def.join(" ")
);
let net: Vec<String> = names
.iter()
.map(|n| {
let s = match &sandboxes[*n].network {
NetworkPolicy::Deny => style::red("deny"),
NetworkPolicy::Allow => style::green("allow"),
NetworkPolicy::Localhost => style::yellow("localhost"),
NetworkPolicy::AllowDomains(_) => style::yellow("proxy"),
NetworkPolicy::LocalhostPorts(_) => style::yellow("localhost"),
};
lpad(&s, col_w)
})
.collect();
println!(" {} {}", rpad(&style::dim("net"), path_w), net.join(" "));
for domain in &domains {
let cells: Vec<String> = names
.iter()
.map(|n| {
let cell = match &sandboxes[*n].network {
NetworkPolicy::Allow => style::green("allow"),
NetworkPolicy::AllowDomains(ds) if ds.iter().any(|d| d == domain) => {
style::green("allow")
}
_ => style::dim("·····"),
};
lpad(&cell, col_w)
})
.collect();
println!(
" {} {}",
rpad(&style::dim(domain), path_w),
cells.join(" ")
);
}
for path in &paths {
let cells: Vec<String> = names
.iter()
.map(|n| {
let matched = sandboxes[*n].rules.iter().find(|r| {
let suffix = match r.path_match {
crate::policy::sandbox_types::PathMatch::Subpath => "/**",
crate::policy::sandbox_types::PathMatch::Literal => "",
crate::policy::sandbox_types::PathMatch::ChildOf => "/*",
crate::policy::sandbox_types::PathMatch::Regex => " (re)",
};
format!("{}{}", r.path, suffix) == *path
});
let cell = match matched {
Some(r) => {
let s = r.caps.short();
match r.effect {
RuleEffect::Allow => style::green(&s),
RuleEffect::Deny => style::red(&s),
}
}
None => style::dim("·····"),
};
lpad(&cell, col_w)
})
.collect();
println!(" {} {}", rpad(&style::dim(path), path_w), cells.join(" "));
}
}
fn rpad(s: &str, width: usize) -> String {
console::pad_str(s, width, console::Alignment::Left, None).into_owned()
}
fn lpad(s: &str, width: usize) -> String {
console::pad_str(s, width, console::Alignment::Right, None).into_owned()
}