mod support;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use support::paths::detector_dir;
use keyhog_core::{Chunk, ChunkMetadata, DetectorFile};
use keyhog_scanner::telemetry::{self, ScanTelemetry};
use keyhog_scanner::{CompiledScanner, ScanBackend};
struct InlineCase {
detector_id: String,
toml_file: String,
positive: Option<String>,
negative: Option<String>,
}
fn load_inline_cases() -> Vec<InlineCase> {
let dir = detector_dir();
let mut files: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("detectors/ must be readable ({}): {e}", dir.display()))
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("toml"))
.collect();
files.sort();
let mut cases = Vec::new();
for path in &files {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let file: DetectorFile = toml::from_str(&text)
.unwrap_or_else(|e| panic!("malformed detector TOML {}: {e}", path.display()));
let stem = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("<unknown>")
.to_string();
for t in &file.detector.tests {
cases.push(InlineCase {
detector_id: file.detector.id.clone(),
toml_file: stem.clone(),
positive: t.test_positive.clone(),
negative: t.test_negative.clone(),
});
}
}
cases
}
fn scanner() -> CompiledScanner {
let detectors = keyhog_core::load_detectors(&detector_dir())
.unwrap_or_else(|e| panic!("detectors/ must load into the scanner: {e}"));
CompiledScanner::compile(detectors).expect("on-disk corpus must compile into one scanner")
}
fn make_chunk(text: &str) -> Chunk {
Chunk {
data: text.into(),
metadata: ChunkMetadata {
source_type: "filesystem".into(),
path: Some("application.conf".into()),
..Default::default()
},
}
}
#[test]
fn inline_test_fixtures_are_present() {
let cases = load_inline_cases();
let with_positive = cases.iter().filter(|c| c.positive.is_some()).count();
assert!(
with_positive >= 11,
"expected >= 11 detectors shipping an inline test_positive, found {with_positive} \
The inline `[[detector.tests]]` self-test corpus shrank (or the loader broke)"
);
}
#[test]
fn every_detector_owns_a_complete_inline_truth_pair() {
let detectors = keyhog_core::load_detectors(&detector_dir())
.expect("detectors/ must load while checking inline truth ownership");
let mut missing = Vec::new();
for detector in &detectors {
let owns_pair = detector.tests.iter().any(|case| {
case.test_positive
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
&& case
.test_negative
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
});
if !owns_pair {
missing.push(detector.id.as_str());
}
}
assert!(
missing.is_empty(),
"{} detector(s) have no complete detector-owned positive/negative truth pair: {}",
missing.len(),
missing.join(", ")
);
}
#[test]
fn every_inline_positive_fires_its_own_detector() {
let scanner = scanner();
let cases = load_inline_cases();
let mut failures: Vec<String> = Vec::new();
let mut checked = 0usize;
for case in &cases {
let Some(positive) = &case.positive else {
continue;
};
scanner.clear_fragment_cache();
let matches = scanner.scan(&make_chunk(positive));
let fired = matches
.iter()
.any(|m| m.detector_id.as_ref() == case.detector_id);
if !fired {
let ids: Vec<&str> = matches.iter().map(|m| m.detector_id.as_ref()).collect();
let trace = Arc::new(ScanTelemetry::new());
trace.enable_dogfood();
scanner.clear_fragment_cache();
telemetry::with_scan_telemetry(&trace, || {
let _ = scanner.scan(&make_chunk(positive));
});
let suppressions = trace.drain().dogfood_events;
failures.push(format!(
"{} ({}): inline test_positive {:?} did not fire detector {:?}; scanner saw {:?}; suppression trace {:?}",
case.detector_id,
case.toml_file,
positive,
case.detector_id,
ids,
suppressions,
));
}
checked += 1;
}
assert!(
failures.is_empty(),
"{} of {checked} inline positives failed to fire their own detector:\n - {}",
failures.len(),
failures.join("\n - ")
);
assert!(
checked >= 11,
"expected >= 11 inline positive cases, ran {checked}"
);
}
#[test]
fn agenta_assignment_is_not_a_tripadvisor_alias() {
let matches = scanner().scan(&make_chunk(
"AGENTA_API_KEY=7b3e5d8c1a9f4e2b6c8d3a5e9f1b7c4d",
));
assert!(
matches
.iter()
.any(|matched| matched.detector_id.as_ref() == "agenta-api-key"),
"Agenta assignment must retain its detector: {matches:?}"
);
assert!(
!matches
.iter()
.any(|matched| matched.detector_id.as_ref() == "tripadvisor-api-key"),
"TA_API_KEY must require a word boundary: {matches:?}"
);
}
#[test]
fn anchored_generic_service_detectors_remain_named_through_resolution() {
let cases = load_inline_cases();
let detectors = keyhog_core::embedded_detector_specs().to_vec();
let scanner = CompiledScanner::compile(detectors.clone()).expect("compile embedded detectors");
let anchored_generic_ids: std::collections::BTreeSet<&str> = detectors
.iter()
.filter(|detector| {
detector.service == "generic" && detector.kind == keyhog_core::DetectorKind::Regex
})
.map(|detector| detector.id.as_str())
.collect();
assert_eq!(anchored_generic_ids.len(), 5);
let mut checked = 0usize;
for case in cases {
if !anchored_generic_ids.contains(case.detector_id.as_str()) {
continue;
}
let Some(positive) = &case.positive else {
continue;
};
scanner.clear_fragment_cache();
let raw = scanner.scan(&make_chunk(positive));
let active = scanner
.try_resolve_matches(raw.clone())
.expect("active compiled plan must classify every finding");
let embedded = keyhog_scanner::resolution::try_resolve_matches(raw)
.expect("embedded plan must classify every finding");
for (surface, resolved) in [("active", active), ("embedded", embedded)] {
assert!(
resolved
.iter()
.any(|matched| matched.detector_id.as_ref() == case.detector_id),
"{surface} resolution dropped anchored generic-service detector {} for {:?}; retained {:?}",
case.detector_id,
positive,
resolved
.iter()
.map(|matched| matched.detector_id.as_ref())
.collect::<Vec<_>>()
);
}
checked += 1;
}
assert_eq!(
checked, 5,
"each anchored generic-service detector has one inline positive"
);
}
#[test]
fn corrected_primary_role_regressions_have_exact_backend_parity() {
let scanner = scanner();
let acquired_gpu_backends: Vec<_> = scanner
.gpu_backend_candidates()
.into_iter()
.filter(|candidate| candidate.available)
.map(|candidate| candidate.backend)
.collect();
assert!(
!keyhog_scanner::hw_probe::probe_hardware().gpu_available
|| !acquired_gpu_backends.is_empty(),
"physical GPU probe succeeded but no compiled GPU peer was acquired"
);
let corrected: std::collections::BTreeSet<&str> = [
"alertmanager-credentials",
"amazon-music-api-credentials",
"basic-auth-credentials",
"bearer-authorization",
"checkmarx-api-credentials",
"cli-password-flag",
"goto-connect-api-credentials",
"huawei-cloud-api-credentials",
"rapyd-api-credentials",
"saltstack-credentials",
"sql-password",
"twilio-api-key",
"url-credentials",
"vonage-video-api",
]
.into_iter()
.collect();
let mut checked = 0usize;
for case in load_inline_cases() {
if !corrected.contains(case.detector_id.as_str()) {
continue;
}
let Some(positive) = case.positive else {
continue;
};
let chunk = make_chunk(&positive);
scanner.clear_fragment_cache();
let mut cpu = scanner.scan_with_backend(&chunk, ScanBackend::CpuFallback);
scanner.clear_fragment_cache();
let mut simd = scanner.scan_with_backend(&chunk, ScanBackend::SimdCpu);
cpu.sort();
simd.sort();
assert_eq!(cpu, simd, "CPU/SIMD finding drift for {}", case.detector_id);
for backend in &acquired_gpu_backends {
scanner.clear_fragment_cache();
let mut gpu = scanner.scan_with_backend(&chunk, *backend);
gpu.sort();
assert_eq!(
cpu,
gpu,
"CPU/{} finding drift for {}",
backend.label(),
case.detector_id
);
}
let resolved = scanner
.try_resolve_matches(cpu)
.expect("active plan resolves corrected inline findings");
assert!(
resolved
.iter()
.any(|matched| matched.detector_id.as_ref() == case.detector_id),
"{} lost its own positive during final resolution",
case.detector_id
);
checked += 1;
}
assert_eq!(
checked, 17,
"the fourteen corrected detectors own seventeen inline positives"
);
}
#[test]
fn every_inline_negative_does_not_fire_its_own_detector() {
let scanner = scanner();
let cases = load_inline_cases();
let mut failures: Vec<String> = Vec::new();
let mut checked = 0usize;
for case in &cases {
let Some(negative) = &case.negative else {
continue;
};
scanner.clear_fragment_cache();
let matches = scanner.scan(&make_chunk(negative));
let wrongly_fired: Vec<&str> = matches
.iter()
.filter(|m| m.detector_id.as_ref() == case.detector_id)
.map(|m| m.credential.as_ref())
.collect();
if !wrongly_fired.is_empty() {
failures.push(format!(
"{} ({}): inline test_negative {:?} WRONGLY fired detector {:?} on {:?}",
case.detector_id, case.toml_file, negative, case.detector_id, wrongly_fired
));
}
checked += 1;
}
assert!(
failures.is_empty(),
"{} of {checked} inline negatives wrongly fired their own detector:\n - {}",
failures.len(),
failures.join("\n - ")
);
assert!(
checked >= 11,
"expected >= 11 inline negative cases, ran {checked}"
);
}
#[test]
fn every_embedded_id_is_present_exactly_once_on_disk() {
let embedded = keyhog_core::load_embedded_detectors_or_fail()
.expect("embedded corpus must parse (baked in by build.rs)");
let dir = detector_dir();
let mut on_disk: BTreeMap<String, usize> = BTreeMap::new();
for entry in std::fs::read_dir(&dir)
.expect("detectors/ readable")
.flatten()
{
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("toml") {
continue;
}
let text = std::fs::read_to_string(&path).expect("read detector toml");
let file: DetectorFile = toml::from_str(&text)
.unwrap_or_else(|e| panic!("malformed detector TOML {}: {e}", path.display()));
*on_disk.entry(file.detector.id).or_insert(0) += 1;
}
let mut missing: Vec<String> = Vec::new();
let mut dup: Vec<String> = Vec::new();
for d in &embedded {
match on_disk.get(d.id.as_str()).copied().unwrap_or(0) {
0 => missing.push(d.id.clone()),
1 => {}
n => dup.push(format!("{} (x{n})", d.id)),
}
}
assert!(
missing.is_empty(),
"embedded id(s) absent from the on-disk detectors/ tree (stale embed): {missing:?}"
);
assert!(
dup.is_empty(),
"id(s) declared by more than one on-disk TOML (one silently shadows the other): {dup:?}"
);
assert_eq!(
embedded.len(),
on_disk.len(),
"embedded detector count ({}) must equal the distinct on-disk id count ({})",
embedded.len(),
on_disk.len()
);
assert!(
embedded.len() >= 800,
"embedded detector population collapsed to {} (<800)",
embedded.len()
);
}