use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
const KEY_FIXTURE: &str = "\
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy
NTUxOQAAACAabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01AAAAEAAA
AAAAAAAAtzc2gtZWQyNTUxOQAAACBabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR
-----END OPENSSH PRIVATE KEY-----
";
fn scan_json(path: &std::path::Path) -> Vec<serde_json::Value> {
let out = Command::new(binary())
.args(["scan", "--no-daemon", "--format", "json", "--path"])
.arg(path)
.output()
.expect("spawn keyhog scan");
let stdout = String::from_utf8_lossy(&out.stdout);
let trimmed = stdout.trim();
let val: serde_json::Value = serde_json::from_str(trimmed).unwrap_or_else(|e| {
panic!(
"keyhog scan stdout was not valid JSON ({e}).\nstdout: {trimmed}\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
)
});
val.as_array()
.unwrap_or_else(|| panic!("--format json must emit a JSON array, got: {trimmed}"))
.clone()
}
fn has_private_key_finding(findings: &[serde_json::Value]) -> bool {
findings.iter().any(|f| {
f["detector_id"]
.as_str()
.map(|id| id.contains("private-key"))
.unwrap_or(false)
})
}
fn write_key_file(dir: &std::path::Path, name: &str) -> PathBuf {
let p = dir.join(name);
std::fs::write(&p, KEY_FIXTURE).expect("write key fixture");
p
}
fn require_tool(tool: &str) {
let ok = Command::new(tool)
.arg("--help")
.output()
.map(|o| o.status.success() || !o.stdout.is_empty() || !o.stderr.is_empty())
.unwrap_or(false);
assert!(
ok,
"test prerequisite `{tool}` is not available on PATH; this is an \
environment problem, not the keyhog defect under audit"
);
}
#[test]
fn tar_archive_content_is_scanned_like_zip() {
require_tool("tar");
require_tool("zip");
let dir = TempDir::new().expect("tempdir");
let key = write_key_file(dir.path(), "leak.txt");
let control = scan_json(&key);
assert!(
has_private_key_finding(&control),
"CONTROL FAILED: plain leak.txt should be detected as a private key; \
got {control:#?}"
);
let zip_path = dir.path().join("bundle.zip");
let z = Command::new("zip")
.arg("-qj")
.arg(&zip_path)
.arg(&key)
.output()
.expect("run zip");
assert!(z.status.success(), "zip failed: {z:?}");
let zip_findings = scan_json(&zip_path);
assert!(
has_private_key_finding(&zip_findings),
"CONTROL FAILED: keyhog should detect the key inside a .zip; got {zip_findings:#?}"
);
let tar_path = dir.path().join("bundle.tar");
let t = Command::new("tar")
.arg("-cf")
.arg(&tar_path)
.arg("-C")
.arg(dir.path())
.arg("leak.txt")
.output()
.expect("run tar");
assert!(t.status.success(), "tar failed: {t:?}");
let tar_findings = scan_json(&tar_path);
assert!(
has_private_key_finding(&tar_findings),
"CAPABILITY GAP (AUD-capability-1): keyhog scanned a .tar containing a \
private key and reported NO private-key finding, even though the \
identical .zip is detected. `.tar` is in SKIP_EXTENSIONS \
(crates/sources/src/filesystem/filter.rs:35) and has no unpack branch \
in crates/sources/src/filesystem/extract.rs. Findings: {tar_findings:#?}"
);
}
#[test]
fn tgz_archive_content_is_scanned() {
require_tool("tar");
let dir = TempDir::new().expect("tempdir");
let key = write_key_file(dir.path(), "leak.txt");
assert!(
has_private_key_finding(&scan_json(&key)),
"CONTROL FAILED: plain leak.txt should be detected"
);
let targz_path = dir.path().join("bundle.tar.gz");
let t = Command::new("tar")
.arg("-czf")
.arg(&targz_path)
.arg("-C")
.arg(dir.path())
.arg("leak.txt")
.output()
.expect("run tar -czf");
assert!(t.status.success(), "tar -czf failed: {t:?}");
let findings = scan_json(&targz_path);
assert!(
has_private_key_finding(&findings),
"CAPABILITY GAP (AUD-capability-2): keyhog scanned a .tar.gz containing \
a private key and reported NO private-key finding. The gz path \
(crates/sources/src/filesystem/extract.rs:195 -> extract_compressed_chunks) \
gunzips but never untars, and bare .tgz is skipped via \
crates/sources/src/filesystem/filter.rs:40. Findings: {findings:#?}"
);
}
#[test]
fn plain_gzip_file_content_is_scanned() {
require_tool("gzip");
let dir = TempDir::new().expect("tempdir");
let key = write_key_file(dir.path(), "leak.txt");
assert!(
has_private_key_finding(&scan_json(&key)),
"CONTROL FAILED: plain leak.txt should be detected"
);
let gz_path = dir.path().join("leak.txt.gz");
let raw = std::fs::read(&key).expect("read key");
let g = Command::new("gzip")
.arg("-c")
.arg(&key)
.output()
.expect("run gzip -c");
assert!(g.status.success(), "gzip failed: {g:?}");
assert!(
!g.stdout.is_empty() && g.stdout.len() != raw.len(),
"gzip produced no/identical output: {} bytes",
g.stdout.len()
);
std::fs::write(&gz_path, &g.stdout).expect("write .gz fixture");
let findings = scan_json(&gz_path);
assert!(
has_private_key_finding(&findings),
"CAPABILITY GAP (AUD-capability-3): keyhog scanned a .gz-compressed file \
containing a private key and reported NO private-key finding, even \
though the uncompressed file is detected. extract_compressed_chunks \
(crates/sources/src/filesystem/extract.rs:342) runs but its \
literal-block reassembly never reconstructs the credential. \
Findings: {findings:#?}"
);
}