tsafe-cli 1.0.26

Secrets runtime for developers — inject credentials into processes via exec, never into shell history or .env files
//! `tsafe kp-pull` handler — import secrets from a KeePass `.kdbx` file.

use anyhow::{Context as _, Result};
use colored::Colorize;
use crate::tsafe_keepass::{pull_entries, KeePassConfig, KeePassError};
use tsafe_core::pullconfig::PullSource;
use tsafe_core::{audit::AuditEntry, events::emit_event};

use crate::helpers::*;
use crate::cli::PullOnError;

/// Pull secrets from a KeePass `.kdbx` file into the local vault.
pub(crate) fn cmd_keepass_pull(
    profile: &str,
    src: &PullSource,
    overwrite: bool,
    on_error: PullOnError,
) -> Result<()> {
    cmd_keepass_pull_ns(profile, src, overwrite, on_error, None)
}

/// Inner implementation that supports an optional namespace prefix (ADR-012).
///
/// When `ns` is `Some("infra")`, every key name extracted from the database
/// is stored as `infra.KEY_NAME` in the local vault.
pub(crate) fn cmd_keepass_pull_ns(
    profile: &str,
    src: &PullSource,
    overwrite: bool,
    on_error: PullOnError,
    ns: Option<&str>,
) -> Result<()> {
    let cfg = match KeePassConfig::from_pull_source(src) {
        Ok(c) => c,
        Err(KeePassError::PasswordRequired(env_var)) => {
            return Err(anyhow::anyhow!(
                "KeePass password is required but env var '{env_var}' is not set\n\
                 \n  Fix: export {env_var}=<master-password>  (or configure keyfile_path)",
            ));
        }
        Err(e) => {
            return Err(anyhow::anyhow!("{e}").context("failed to build KeePass config"));
        }
    };

    let entries = match pull_entries(&cfg)
        .with_context(|| format!("failed to read KeePass database '{}'", cfg.path))
    {
        Ok(e) => e,
        Err(err) => match on_error {
            PullOnError::FailAll => return Err(err),
            PullOnError::SkipFailed | PullOnError::WarnOnly => {
                eprintln!("{} KeePass pull failed: {err}", "!".yellow());
                return Ok(());
            }
        },
    };

    if entries.is_empty() {
        println!(
            "{} No entries found in the KeePass database matching the filter",
            "i".blue()
        );
        return Ok(());
    }

    let mut vault = open_vault(profile)?;
    let mut imported = 0usize;
    let mut skipped = 0usize;

    for (raw_key, value) in &entries {
        // ADR-012: apply per-source namespace prefix when declared in the manifest.
        let key = match ns {
            Some(prefix) => format!("{prefix}.{raw_key}"),
            None => raw_key.clone(),
        };
        let exists = vault.list().contains(&key.as_str());
        if exists && !overwrite {
            skipped += 1;
            continue;
        }
        vault
            .set(&key, value, std::collections::HashMap::new())
            .with_context(|| format!("failed to store key '{key}' in vault"))?;
        imported += 1;
    }

    audit(profile)
        .append(&AuditEntry::success(profile, "kp-pull", None))
        .ok();
    emit_event(profile, "kp-pull", None);

    println!(
        "{} Imported {imported} secret(s) from KeePass database '{}'{}",
        "".green(),
        cfg.path,
        if skipped > 0 {
            format!(" ({skipped} skipped — use --overwrite to replace)")
        } else {
            String::new()
        }
    );
    Ok(())
}