use crate::commands::sync;
use crate::config::Config;
use crate::exec::{Cmd, ScanPolicy, SecretsArg};
use crate::lock;
use crate::logger::*;
use crate::paths::*;
use std::io::{self, Write};
const SCAN_BATCH: usize = 200;
fn managed_plaintext_files(chezmoi: &Cmd) -> io::Result<Vec<String>> {
let outcome = chezmoi.run(
"managed",
&[
"--include=files,symlinks",
"--exclude=encrypted",
"--path-style",
"absolute",
"-0",
],
SecretsArg::Unsupported,
)?;
outcome.report();
let mut files = Vec::new();
for chunk in outcome.stdout.as_bytes().split(|&b| b == 0) {
if chunk.is_empty() {
continue;
}
let path = String::from_utf8_lossy(chunk).into_owned();
if std::path::Path::new(&path).is_file() {
files.push(path);
}
}
Ok(files)
}
fn scan_managed(chezmoi: &Cmd, files: &[String]) -> io::Result<Vec<String>> {
let mut flagged: Vec<String> = Vec::new();
for batch in files.chunks(SCAN_BATCH) {
let mut argv: Vec<&str> = vec!["--dry-run", "--force"];
argv.extend(batch.iter().map(|p| p.as_str()));
let outcome = chezmoi.run(
"add",
&argv,
chezmoi.secrets_arg("add", ScanPolicy::Enforce),
)?;
outcome.report();
for path in outcome.secret_paths(&[]) {
if !flagged.contains(&path) {
flagged.push(path);
}
}
}
Ok(flagged)
}
fn promote_to_encrypted(tracked_enc_file: &str, home: &str, paths: &[String]) -> io::Result<usize> {
if let Some(parent) = std::path::Path::new(tracked_enc_file).parent() {
std::fs::create_dir_all(parent)?;
}
let mut existing: Vec<String> = Vec::new();
if let Ok(file) = std::fs::File::open(tracked_enc_file) {
use std::io::BufRead;
for line in io::BufReader::new(file).lines().map_while(Result::ok) {
let trimmed = line.trim().to_string();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
existing.push(trimmed);
}
}
}
let home_path = canonical_path(home);
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(tracked_enc_file)?;
let mut added = 0usize;
for path in paths {
let canonical = canonical_path(path);
if !canonical.starts_with(&home_path) {
log_warn(&("Skipping path outside home: ".to_owned() + path));
continue;
}
let offset = if home_path.ends_with('/') {
home_path.len()
} else {
home_path.len() + 1
};
let rel_path = if canonical.len() > offset {
canonical[offset..].to_string()
} else {
continue;
};
if existing.contains(&rel_path) {
log_warn(&(rel_path.clone() + " is already tracked as encrypted."));
continue;
}
writeln!(file, "{}", rel_path)?;
existing.push(rel_path.clone());
added += 1;
}
Ok(added)
}
pub fn run_audit_secrets(config: Config, fix: bool) -> io::Result<()> {
log_section("Auditing managed dotfiles for leaked secrets...");
let policy = config.scan_policy();
if policy == ScanPolicy::Off {
log_warn("Secret scanning is disabled (secret_scan: off). Nothing was scanned.");
return Ok(());
}
if !crate::exec::supports_secret_scanning() {
crate::exec::warn_if_unsupported();
return Ok(());
}
let runtime_dir =
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| concat_paths(&config.home, ".cache"));
let Some(_guard) =
lock::acquire_or_report(&concat_paths(&runtime_dir, "dotfiles-sync.lock.dir"))
else {
return Ok(());
};
sync::resolve_config_drift()?;
let temp_config = sync::generate_chezmoi_run_config(&config.home)?;
let chezmoi = Cmd::new().with_config(&temp_config);
let result = audit_with(&chezmoi, &config, fix);
sync::remove_chezmoi_run_config(&temp_config);
result
}
fn audit_with(chezmoi: &Cmd, config: &Config, fix: bool) -> io::Result<()> {
let policy = config.scan_policy();
let files = managed_plaintext_files(chezmoi)?;
if files.is_empty() {
log_success("No plaintext managed files to audit.");
return Ok(());
}
log_step(&format!("Scanning {} plaintext file(s)...", files.len()));
let flagged = scan_managed(chezmoi, &files)?;
if flagged.is_empty() {
log_success("No potential secrets found in managed files.");
return Ok(());
}
log_warn(&format!(
"{} file(s) contain potential secrets and are stored in plaintext.",
flagged.len()
));
if !fix {
log_step("Re-run with --fix to move these into tracked_encrypted and encrypt them.");
return Ok(());
}
log_section("Promoting flagged files to encrypted tracking...");
let added = promote_to_encrypted(&config.tracked_enc_file, &config.home, &flagged)?;
if added == 0 {
log_warn("No new entries were added to the encrypted tracking list.");
return Ok(());
}
log_success(
&("Added ".to_owned() + &added.to_string() + " path(s) to the encrypted tracking list."),
);
let mut argv: Vec<&str> = vec!["--encrypt", "--force"];
argv.extend(flagged.iter().map(|p| p.as_str()));
let outcome = chezmoi.run("add", &argv, chezmoi.secrets_arg("add", policy))?;
outcome.report();
if outcome.success {
log_success("Re-encrypted the promoted files.");
log_step("Run `fp-dotfiles-manager sync` to commit and push the change.");
} else {
log_error("Failed to re-encrypt the promoted files.");
}
Ok(())
}