use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use super::helpers::*;
use crate::core::{state, types};
pub(super) fn security_counts(lock: &types::StateLock) -> (usize, usize, usize) {
let mut secret_refs = 0usize;
let mut privileged = 0usize;
let mut tls = 0usize;
for (id, rl) in &lock.resources {
if let Some(val) = rl.details.get("secret_refs") {
secret_refs += match val {
serde_yaml_ng::Value::Number(n) => n.as_u64().unwrap_or(1) as usize,
_ => 1,
};
}
if rl.resource_type == types::ResourceType::Service {
privileged += 1;
}
let id_lower = id.to_lowercase();
if id_lower.contains("tls") || id_lower.contains("ssl") || id_lower.contains("cert") {
tls += 1;
}
}
(secret_refs, privileged, tls)
}
pub(super) fn classify_posture(secret_refs: usize, privileged: usize) -> &'static str {
if secret_refs == 0 && privileged == 0 {
"good"
} else if secret_refs > 5 || privileged > 3 {
"needs-attention"
} else {
"moderate"
}
}
pub(super) fn parse_rfc3339_to_epoch(s: &str) -> Option<u64> {
if s.len() < 19 {
return None;
}
let year: u64 = s.get(0..4)?.parse().ok()?;
let month: u64 = s.get(5..7)?.parse().ok()?;
let day: u64 = s.get(8..10)?.parse().ok()?;
let hour: u64 = s.get(11..13)?.parse().ok()?;
let min: u64 = s.get(14..16)?.parse().ok()?;
let sec: u64 = s.get(17..19)?.parse().ok()?;
let mut days: u64 = 0;
for y in 1970..year {
days += if (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400) {
366
} else {
365
};
}
let table = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30];
let leap = (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400);
let mut md: u64 = 0;
for m in 1..month.min(13) {
md += table[m as usize];
if m == 2 && leap {
md += 1;
}
}
days += md + (day - 1);
Some(days * 86_400 + hour * 3600 + min * 60 + sec)
}
pub(super) fn now_epoch() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub(super) fn freshness_score(generated_at: &str, now: u64) -> u64 {
let epoch = match parse_rfc3339_to_epoch(generated_at) {
Some(e) if now >= e => e,
_ => return 0,
};
let age = now - epoch;
const BUCKETS: &[(u64, u64)] = &[(3600, 100), (86_400, 80), (604_800, 60), (2_592_000, 30)];
BUCKETS
.iter()
.find(|(threshold, _)| age < *threshold)
.map_or(0, |(_, score)| *score)
}
pub(super) fn filtered_machines(state_dir: &Path, machine: Option<&str>) -> Vec<String> {
let all = discover_machines(state_dir);
match machine {
Some(m) => all.into_iter().filter(|n| n == m).collect(),
None => all,
}
}
pub(crate) fn cmd_status_fleet_security_posture_summary(
state_dir: &Path,
machine: Option<&str>,
json: bool,
) -> Result<(), String> {
let machines = filtered_machines(state_dir, machine);
let mut rows: Vec<(String, usize, usize, usize, &str)> = Vec::new();
for m in &machines {
if let Ok(Some(lock)) = state::load_lock(state_dir, m) {
let (sr, priv_c, tls) = security_counts(&lock);
rows.push((m.clone(), sr, priv_c, tls, classify_posture(sr, priv_c)));
}
}
if json {
let entries: Vec<serde_json::Value> = rows.iter().map(|(m, sr, p, t, pos)| {
serde_json::json!({"machine":m,"secret_refs":sr,"privileged":p,"tls_resources":t,"posture":pos})
}).collect();
println!(
"{}",
serde_json::to_string_pretty(
&serde_json::json!({"fleet_security_posture":{"machines":entries}})
)
.unwrap_or_default()
);
} else {
println!("=== Fleet Security Posture Summary ===");
if rows.is_empty() {
println!(" No machine state found.");
}
for (m, sr, p, t, pos) in &rows {
let sym = match *pos {
"good" => green("*"),
"moderate" => yellow("~"),
_ => red("!"),
};
println!(" {sym} {m} — secrets:{sr}, privileged:{p}, tls:{t}, posture:{pos}");
}
}
Ok(())
}
fn score_symbol(score: u64) -> String {
if score >= 60 {
green("*")
} else if score >= 30 {
yellow("~")
} else {
red("!")
}
}
pub(crate) fn cmd_status_machine_resource_freshness_index(
state_dir: &Path,
machine: Option<&str>,
json: bool,
) -> Result<(), String> {
let machines = filtered_machines(state_dir, machine);
let now = now_epoch();
let mut rows: Vec<(String, u64, String)> = Vec::new();
for m in &machines {
if let Ok(Some(lock)) = state::load_lock(state_dir, m) {
let score = freshness_score(&lock.generated_at, now);
rows.push((m.clone(), score, lock.generated_at.clone()));
}
}
if json {
let entries: Vec<serde_json::Value> = rows
.iter()
.map(
|(m, s, ts)| serde_json::json!({"machine":m,"freshness_index":s,"generated_at":ts}),
)
.collect();
println!(
"{}",
serde_json::to_string_pretty(
&serde_json::json!({"freshness_index":{"machines":entries}})
)
.unwrap_or_default()
);
} else {
println!("=== Machine Resource Freshness Index ===");
if rows.is_empty() {
println!(" No machine state found.");
}
for (m, score, ts) in &rows {
let sym = score_symbol(*score);
println!(" {sym} {m} — freshness:{score}/100, generated_at:{ts}");
}
}
Ok(())
}
pub(super) fn collect_type_coverage(
state_dir: &Path,
machines: &[String],
) -> BTreeMap<String, BTreeSet<String>> {
let mut coverage: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for m in machines {
if let Ok(Some(lock)) = state::load_lock(state_dir, m) {
for rl in lock.resources.values() {
coverage
.entry(rl.resource_type.to_string())
.or_default()
.insert(m.clone());
}
}
}
coverage
}
pub(crate) fn cmd_status_fleet_resource_type_coverage(
state_dir: &Path,
machine: Option<&str>,
json: bool,
) -> Result<(), String> {
let machines = filtered_machines(state_dir, machine);
let coverage = collect_type_coverage(state_dir, &machines);
if json {
let entries: Vec<serde_json::Value> = coverage
.iter()
.map(|(rt, ms)| {
let names: Vec<&str> = ms.iter().map(|s| s.as_str()).collect();
serde_json::json!({"resource_type":rt,"machine_count":ms.len(),"machines":names})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(
&serde_json::json!({"resource_type_coverage":{"types":entries}})
)
.unwrap_or_default()
);
} else {
println!("=== Fleet Resource Type Coverage ===");
if coverage.is_empty() {
println!(" No resources found.");
}
for (rt, ms) in &coverage {
let names: Vec<&str> = ms.iter().map(|s| s.as_str()).collect();
println!(
" {:>10} | {} machine(s): {}",
rt,
ms.len(),
names.join(", ")
);
}
}
Ok(())
}