use super::apply_helpers::run_notify;
use super::colors::{green, red, yellow};
use super::drift::DriftScan;
use crate::core::types;
use crate::tripwire::drift;
pub(super) fn census_json(name: &str, census: &drift::DriftCensus) -> serde_json::Value {
let mut value = census.to_json();
if let Some(obj) = value.as_object_mut() {
obj.insert("machine".to_string(), serde_json::json!(name));
}
value
}
pub(super) fn print_drift_summary(scan: &DriftScan, json: bool) -> Result<(), String> {
let sum = |key: &str| -> u64 { scan.censuses.iter().filter_map(|c| c[key].as_u64()).sum() };
let (inspected, skipped) = (sum("inspected"), sum("skipped"));
if json {
let report = serde_json::json!({
"machines_checked": scan.machines_checked,
"drift_count": scan.total_drift,
"unmeasured_count": scan.total_unmeasured,
"resources_inspected": inspected,
"resources_skipped": skipped,
"resources_unmeasured": sum("unmeasured"),
"census": scan.censuses,
"findings": scan.findings,
"unmeasured": scan.unmeasured,
});
let output =
serde_json::to_string_pretty(&report).map_err(|e| format!("JSON error: {e}"))?;
println!("{output}");
} else if scan.total_drift > 0 {
println!();
println!(
"{}",
red(&format!("Drift detected: {} resource(s)", scan.total_drift))
);
if scan.total_unmeasured > 0 {
println!(
" and {} unmeasured: the target did not answer.",
scan.total_unmeasured
);
}
} else if scan.total_unmeasured > 0 {
println!();
println!(
"{}",
yellow(&format!(
"Drift unknown: {} resource(s) unmeasured — the target did not answer.",
scan.total_unmeasured
))
);
println!(" {inspected} resource(s) inspected, {skipped} not inspected.");
} else {
println!("{}", green("No drift detected."));
println!(" {inspected} resource(s) inspected, {skipped} not inspected.");
}
Ok(())
}
pub(super) fn run_drift_alert(alert_cmd: &str, total_drift: usize) -> Result<(), String> {
let status = std::process::Command::new("sh")
.arg("-c")
.arg(alert_cmd)
.env("FORJAR_DRIFT_COUNT", total_drift.to_string())
.status()
.map_err(|e| format!("alert-cmd failed to execute: {e}"))?;
if !status.success() {
eprintln!("alert-cmd exited with code {}", status.code().unwrap_or(-1));
}
Ok(())
}
pub(super) fn send_drift_notification(
config: &types::ForjarConfig,
total_drift: usize,
machine_filter: Option<&str>,
) {
if let Some(ref cmd) = config.policy.notify.on_drift {
let drift_str = total_drift.to_string();
let machine_str = machine_filter.unwrap_or("all");
run_notify(
cmd,
&[("machine", machine_str), ("drift_count", &drift_str)],
);
}
}
pub(super) fn render_finding(
name: &str,
f: &drift::DriftFinding,
label: String,
json: bool,
rows: &mut Vec<serde_json::Value>,
) {
if json {
rows.push(serde_json::json!({
"machine": name,
"resource": f.resource_id,
"detail": f.detail,
"expected_hash": f.expected_hash,
"actual_hash": f.actual_hash,
}));
return;
}
println!(" {label}: {} on {name} ({})", f.resource_id, f.detail);
println!(" Expected: {}", f.expected_hash);
println!(" Actual: {}", f.actual_hash);
}