use std::path::Path;
use serde_json::Value;
use crate::context::CrossLayerContext;
pub const DEFAULT_MIN_LENGTH: usize = 8;
pub fn run_and_persist(
ctx: &CrossLayerContext,
min_length: usize,
db_path: &Path,
abort: Option<&std::sync::atomic::AtomicBool>,
strings_file_seed: Option<&Path>,
) -> anyhow::Result<Value> {
#[cfg(not(feature = "mcp"))]
let _ = abort;
let _tempfile_guard: Option<tempfile::NamedTempFile>;
let strings_file: std::path::PathBuf = match strings_file_seed {
Some(p) => {
_tempfile_guard = None;
p.to_path_buf()
}
None => {
let tf = create_strings_tempfile()?;
let p = tf.path().to_path_buf();
_tempfile_guard = Some(tf);
p
}
};
let mut buf: Vec<u8> = Vec::new();
crate::commands::trufflehog(ctx, min_length, &mut buf)?;
let strings_written = buf.split(|&b| b == b'\n').count();
std::fs::write(&strings_file, &buf)?;
let th_cmd = format!(
"trufflehog filesystem {} --json --no-verification",
strings_file.display()
);
if !which_in_path("trufflehog") || strings_written == 0 {
return Ok(serde_json::json!({
"ran": false,
"strings_file": strings_file.display().to_string(),
"command": th_cmd,
}));
}
let th_args: Vec<std::ffi::OsString> = vec![
"filesystem".into(),
strings_file.as_os_str().into(),
"--json".into(),
"--no-update".into(),
"--no-verification".into(),
];
#[cfg(feature = "mcp")]
let result = {
let timeout_secs = crate::mcp::subprocess::subprocess_timeout_secs();
crate::mcp::subprocess::run_command_with_timeout(
"trufflehog",
&th_args,
"trufflehog",
timeout_secs,
abort,
)
.map_err(|e| anyhow::anyhow!("{}", e.into_mcp_error().message))
};
#[cfg(not(feature = "mcp"))]
let result = std::process::Command::new("trufflehog")
.args(&th_args)
.output()
.map_err(anyhow::Error::new);
match result {
Ok(out) => {
let stdout_str = String::from_utf8_lossy(&out.stdout);
let cred_count =
crate::commands::write_credentials_db(&stdout_str, db_path).unwrap_or(0);
let hit_count = stdout_str
.lines()
.filter(|l| !l.trim().is_empty())
.count();
let verified_count = if cred_count > 0 {
rusqlite::Connection::open_with_flags(
db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)
.and_then(|db| {
db.query_row(
"SELECT COUNT(*) FROM credentials WHERE verified=1",
[],
|r| r.get::<_, i64>(0),
)
})
.unwrap_or(0)
} else {
0
};
#[allow(clippy::as_conversions, clippy::cast_possible_wrap, reason = "display-only unverified-secret count; saturating_sub is exact here (hit_count >= verified_count by construction). Compute outside the json! macro so the `as` lint allow can attach to the let-binding (attribute-on-expression is unstable).")]
let unverified_count = (hit_count as i64).saturating_sub(verified_count);
Ok(serde_json::json!({
"ran": true,
"hit_count": hit_count,
"verified_count": verified_count,
"unverified_count": unverified_count,
"credentials_written": cred_count,
"db_table": "credentials",
"note": if verified_count == 0 {
"all hits unverified (pattern matches only) — query credentials table and confirm manually"
} else {
"verified=1 rows are live secrets; verified=0 are pattern matches requiring confirmation"
},
}))
}
Err(e) => Ok(serde_json::json!({"ran": false, "error": e.to_string()})),
}
}
fn which_in_path(name: &str) -> bool {
std::env::var_os("PATH")
.map(|path| std::env::split_paths(&path).any(|dir| dir.join(name).is_file()))
.unwrap_or(false)
}
fn create_strings_tempfile() -> std::io::Result<tempfile::NamedTempFile> {
tempfile::Builder::new()
.prefix("droidsaw-strings-")
.suffix(".txt")
.tempfile()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strings_tempfile_unlinks_on_drop() {
let tf = create_strings_tempfile().expect("create tempfile");
let path = tf.path().to_path_buf();
assert!(
path.exists(),
"strings tempfile not on disk at create: {}",
path.display()
);
let basename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
assert!(
basename.starts_with("droidsaw-strings-"),
"expected prefix preserved for ops-grep affordance; got {basename}"
);
assert!(
basename.ends_with(".txt"),
"expected .txt suffix preserved; got {basename}"
);
drop(tf);
assert!(
!path.exists(),
"strings tempfile NOT unlinked on Drop — leak fix regressed: {}",
path.display()
);
}
}