use comfy_table::{
modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Attribute, Cell, Color, ContentArrangement,
Table,
};
use longline::domain::Decision;
use longline::policy;
fn source_color(s: policy::RuleSource) -> Color {
match s {
policy::RuleSource::BuiltIn => Color::DarkGrey,
policy::RuleSource::Global => Color::Blue,
policy::RuleSource::Project => Color::Cyan,
}
}
fn source_cell(s: policy::RuleSource) -> Cell {
let label = match s {
policy::RuleSource::BuiltIn => "builtin",
policy::RuleSource::Global => "global",
policy::RuleSource::Project => "project",
};
Cell::new(label).fg(source_color(s))
}
fn decision_color(d: Decision) -> Color {
match d {
Decision::Allow => Color::Green,
Decision::Ask => Color::Yellow,
Decision::Deny => Color::Red,
}
}
fn decision_cell(d: Decision) -> Cell {
Cell::new(d).fg(decision_color(d))
}
pub fn rules_table(rules: &[&policy::Rule]) -> Table {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("DECISION").add_attribute(Attribute::Bold),
Cell::new("LEVEL").add_attribute(Attribute::Bold),
Cell::new("ID").add_attribute(Attribute::Bold),
Cell::new("DESCRIPTION").add_attribute(Attribute::Bold),
Cell::new("SOURCE").add_attribute(Attribute::Bold),
]);
for rule in rules {
table.add_row(vec![
decision_cell(rule.decision),
Cell::new(rule.level),
Cell::new(&rule.id),
Cell::new(&rule.reason),
source_cell(rule.source),
]);
}
table
}
pub fn rules_table_verbose(rules: &[&policy::Rule]) -> Table {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("DECISION").add_attribute(Attribute::Bold),
Cell::new("LEVEL").add_attribute(Attribute::Bold),
Cell::new("ID").add_attribute(Attribute::Bold),
Cell::new("MATCH").add_attribute(Attribute::Bold),
Cell::new("PATTERN").add_attribute(Attribute::Bold),
Cell::new("DESCRIPTION").add_attribute(Attribute::Bold),
Cell::new("SOURCE").add_attribute(Attribute::Bold),
]);
for rule in rules {
let (match_type, pattern) = format_matcher(&rule.matcher);
table.add_row(vec![
decision_cell(rule.decision),
Cell::new(rule.level),
Cell::new(&rule.id),
Cell::new(match_type),
Cell::new(pattern),
Cell::new(&rule.reason),
source_cell(rule.source),
]);
}
table
}
fn format_matcher(matcher: &policy::Matcher) -> (String, String) {
match matcher {
policy::Matcher::Command {
command,
flags,
args,
} => {
let mut parts = vec![format!("cmd={}", format_string_or_list(command))];
if let Some(f) = flags {
let mut flag_items = Vec::new();
if !f.any_of.is_empty() {
flag_items.extend(f.any_of.iter().cloned());
}
if !f.all_of.is_empty() {
flag_items.extend(f.all_of.iter().cloned());
}
if !flag_items.is_empty() {
parts.push(format!("flags={{{}}}", flag_items.join(", ")));
}
}
if let Some(a) = args {
if !a.any_of.is_empty() {
parts.push(format!("args={{{}}}", a.any_of.join(", ")));
}
}
("command".to_string(), parts.join(" "))
}
policy::Matcher::Pipeline { pipeline } => {
let stages: Vec<String> = pipeline
.stages
.iter()
.map(|s| format_string_or_list(&s.command))
.collect();
("pipeline".to_string(), stages.join(" | "))
}
policy::Matcher::Redirect { redirect } => {
let mut parts = Vec::new();
if let Some(op) = &redirect.op {
parts.push(format!("op={}", format_string_or_list(op)));
}
if let Some(target) = &redirect.target {
parts.push(format!("target={}", format_string_or_list(target)));
}
("redirect".to_string(), parts.join(" "))
}
}
}
fn format_string_or_list(sol: &policy::StringOrList) -> String {
match sol {
policy::StringOrList::Single(s) => s.clone(),
policy::StringOrList::List { any_of } => format!("{{{}}}", any_of.join(", ")),
}
}
pub fn print_rules_grouped_by_decision(rules: &[&policy::Rule], verbose: bool) {
for decision in &[Decision::Deny, Decision::Ask, Decision::Allow] {
let group: Vec<&policy::Rule> = rules
.iter()
.filter(|r| r.decision == *decision)
.copied()
.collect();
if group.is_empty() {
continue;
}
let header = match decision {
Decision::Deny => yansi::Paint::red("DENY").bold(),
Decision::Ask => yansi::Paint::yellow("ASK").bold(),
Decision::Allow => yansi::Paint::green("ALLOW").bold(),
};
println!("\n{header}");
let refs: Vec<&policy::Rule> = group.to_vec();
if verbose {
println!("{}", rules_table_verbose(&refs));
} else {
println!("{}", rules_table(&refs));
}
}
}
pub fn check_table(rows: &[(Decision, String, String)]) -> Table {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("DECISION").add_attribute(Attribute::Bold),
Cell::new("RULE").add_attribute(Attribute::Bold),
Cell::new("COMMAND").add_attribute(Attribute::Bold),
]);
for (decision, rule_label, cmd) in rows {
table.add_row(vec![
decision_cell(*decision),
Cell::new(rule_label),
Cell::new(cmd),
]);
}
table
}
fn trust_color(t: policy::TrustLevel) -> Color {
match t {
policy::TrustLevel::Minimal => Color::Cyan,
policy::TrustLevel::Standard => Color::Green,
policy::TrustLevel::Full => Color::Yellow,
}
}
pub fn allowlist_table(commands: &[policy::AllowlistEntry]) -> Table {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
Cell::new("ALLOWLISTED COMMANDS").add_attribute(Attribute::Bold),
Cell::new("TRUST").add_attribute(Attribute::Bold),
Cell::new("SOURCE").add_attribute(Attribute::Bold),
]);
for entry in commands {
table.add_row(vec![
Cell::new(&entry.command).fg(Color::Green),
Cell::new(entry.trust).fg(trust_color(entry.trust)),
source_cell(entry.source),
]);
}
table
}
pub fn print_allowlist_summary(commands: &[policy::AllowlistEntry]) {
if commands.is_empty() {
println!("Allowlist: (none)");
return;
}
let display: Vec<&str> = commands
.iter()
.take(10)
.map(|e| e.command.as_str())
.collect();
let suffix = if commands.len() > 10 {
format!(", ... ({} total)", commands.len())
} else {
String::new()
};
println!("Allowlist: {}{}", display.join(", "), suffix);
}
pub fn print_rules_grouped_by_level(rules: &[&policy::Rule], verbose: bool) {
for level_val in &[
policy::SafetyLevel::Critical,
policy::SafetyLevel::High,
policy::SafetyLevel::Strict,
] {
let group: Vec<&policy::Rule> = rules
.iter()
.filter(|r| r.level == *level_val)
.copied()
.collect();
if group.is_empty() {
continue;
}
let header = yansi::Paint::new(level_val.to_string().to_uppercase()).bold();
println!("\n{header}");
let refs: Vec<&policy::Rule> = group.to_vec();
if verbose {
println!("{}", rules_table_verbose(&refs));
} else {
println!("{}", rules_table(&refs));
}
}
}