use std::path::PathBuf;
use keyhog_core::ScanConfig;
use keyhog_core::{Chunk, ChunkMetadata};
use keyhog_scanner::CompiledScanner;
use serde::Deserialize;
use super::paths::detector_dir;
#[derive(Debug, Deserialize)]
pub struct Contract {
pub schema_version: u32,
pub detector_id: String,
pub service: String,
pub severity: String,
#[serde(default)]
pub positive: Vec<Positive>,
}
#[derive(Debug, Deserialize)]
pub struct Positive {
pub text: String,
pub credential: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct Primary {
pub detector_id: String,
pub text: String,
pub credential: String,
}
pub fn contracts_dir() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("tests");
d.push("contracts");
d
}
pub fn load_contracts() -> Vec<Contract> {
let dir = contracts_dir();
let mut out = Vec::new();
let entries = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read tests/contracts dir {}: {e}", dir.display()));
for entry in entries {
let path = entry.expect("tests/contracts dir entry readable").path();
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read contract {}: {e}", path.display()));
let contract = toml::from_str::<Contract>(&text)
.unwrap_or_else(|e| panic!("parse contract {}: {e}", path.display()));
out.push(contract);
}
assert!(
!out.is_empty(),
"tests/contracts/ has no *.toml, the runner has nothing to drive"
);
out
}
pub fn scanner() -> CompiledScanner {
let detectors =
keyhog_core::load_detectors(&detector_dir()).expect("detectors directory loadable");
CompiledScanner::compile(detectors).expect("scanner compile")
}
pub fn scanner_with(config: ScanConfig) -> CompiledScanner {
let detectors =
keyhog_core::load_detectors(&detector_dir()).expect("detectors directory loadable");
CompiledScanner::compile(detectors)
.expect("scanner compile")
.with_config(config.into())
}
pub fn make_chunk(text: &str, source_type: &str, path: &str) -> Chunk {
Chunk {
data: text.into(),
metadata: ChunkMetadata {
source_type: source_type.into(),
path: Some(path.into()),
..Default::default()
},
}
}
pub fn test_chunk(text: &str, path: &str) -> Chunk {
make_chunk(text, "test", path)
}
pub fn primaries(contracts: &[Contract]) -> Vec<Primary> {
contracts
.iter()
.filter_map(|c| {
c.positive.first().map(|p| Primary {
detector_id: c.detector_id.clone(),
text: p.text.clone(),
credential: p.credential.clone(),
})
})
.collect()
}
pub fn surfaces(scanner: &CompiledScanner, chunk: &Chunk, credential: &str) -> bool {
scanner.clear_fragment_cache();
scanner
.scan(chunk)
.expect("contract surface probe scan succeeds")
.iter()
.any(|m| m.credential.as_ref().contains(credential))
}
pub fn credential_sufficient(
scanner: &CompiledScanner,
source_type: &str,
primary: &Primary,
) -> bool {
let chunk = make_chunk(&primary.credential, source_type, "sufficiency-probe.txt");
scanner.clear_fragment_cache();
scanner
.scan(&chunk)
.expect("credential-sufficiency probe scan succeeds")
.iter()
.any(|m| {
m.credential.as_ref().contains(primary.credential.as_str())
&& !keyhog_scanner::is_entropy_detector(m.detector_id.as_ref())
})
}
pub fn sufficiency_mask(
scanner: &CompiledScanner,
source_type: &str,
primaries: &[Primary],
) -> Vec<bool> {
primaries
.iter()
.map(|p| credential_sufficient(scanner, source_type, p))
.collect()
}