tsafe-cli 1.0.27

Secrets runtime for developers — inject credentials into processes via exec, never into shell history or .env files
//! Rotation-policy and rotate-due command handlers.
//!
//! Implements `tsafe policy` and `tsafe rotate-due` — key-level rotation policy
//! management and over-due reporting for vault secrets.

use anyhow::Result;
use colored::Colorize;
use crate::cli::PolicyAction;
use tsafe_core::audit::AuditEntry;

use crate::helpers::*;

pub(crate) fn cmd_policy(profile: &str, action: PolicyAction) -> Result<()> {
    use tsafe_core::vault::parse_rotation_days;
    match action {
        PolicyAction::Set { key, rotate_every } => {
            if parse_rotation_days(&rotate_every).is_none() {
                anyhow::bail!(
                    "invalid rotation interval '{rotate_every}' — use a format like 90d, 30d, 7d"
                );
            }
            let mut vault = open_vault(profile)?;
            let current_value = vault.get(&key)?;
            let mut tags = vault
                .file()
                .secrets
                .get(&key)
                .map(|e| e.tags.clone())
                .unwrap_or_default();
            tags.insert("rotate_policy".into(), rotate_every.clone());
            vault.set(&key, &current_value, tags)?;
            audit(profile)
                .append(&AuditEntry::success(profile, "policy-set", Some(&key)))
                .ok();
            println!(
                "{} Rotation policy set on '{}': every {}",
                "".green(),
                key,
                rotate_every
            );
            Ok(())
        }
        PolicyAction::Remove { key } => {
            let mut vault = open_vault(profile)?;
            let current_value = vault.get(&key)?;
            let mut tags = vault
                .file()
                .secrets
                .get(&key)
                .map(|e| e.tags.clone())
                .unwrap_or_default();
            tags.remove("rotate_policy");
            vault.set(&key, &current_value, tags)?;
            audit(profile)
                .append(&AuditEntry::success(profile, "policy-remove", Some(&key)))
                .ok();
            println!("{} Rotation policy removed from '{}'", "".green(), key);
            Ok(())
        }
    }
}

pub(crate) fn cmd_rotate_due(profile: &str, json_out: bool, fail: bool) -> Result<()> {
    use serde_json::json;
    use tsafe_core::vault::rotation_due;

    let vault = open_vault(profile)?;
    let due = rotation_due(vault.file());

    if json_out {
        let items: Vec<_> = due
            .iter()
            .map(|(key, days_over, policy)| {
                json!({
                    "key": key,
                    "days_overdue": days_over,
                    "policy": policy,
                })
            })
            .collect();
        let out = json!({
            "profile": profile,
            "overdue_count": due.len(),
            "items": items,
        });
        println!("{}", serde_json::to_string_pretty(&out)?);
    } else if due.is_empty() {
        println!("{} No secrets are overdue for rotation", "".green());
        println!(
            "  Tip: attach a rotation policy with {} (checked by {} and {}).",
            "tsafe policy set KEY --rotate-every 90d".cyan(),
            "tsafe rotate-due".dimmed(),
            "tsafe doctor".dimmed()
        );
    } else {
        println!(
            "{} {} secret(s) overdue for rotation:\n",
            "!".yellow(),
            due.len()
        );
        println!("  {:<30} {:>12} {:>10}", "KEY", "OVERDUE BY", "POLICY");
        println!("  {}", "".repeat(54));
        for (key, days_over, policy) in &due {
            println!("  {:<30} {:>9} d  {:>10}", key.yellow(), days_over, policy);
        }
        println!(
            "\n  Update values: {}   Re-key vault: {}",
            "tsafe set KEY <new-value>".cyan(),
            "tsafe rotate".cyan()
        );
    }

    if fail && !due.is_empty() {
        anyhow::bail!("{} secret(s) overdue for rotation", due.len());
    }
    Ok(())
}