use keyhog_core::{Chunk, ChunkMetadata, DetectorSpec, PatternSpec, Severity};
use keyhog_scanner::CompiledScanner;
use std::time::{Duration, Instant};
#[test]
fn test_scan_timeout_respects_deadline() {
let detector = DetectorSpec {
kind: Default::default(),
entropy_floor: Vec::new(),
tests: Vec::new(),
id: "redos-detector".into(),
name: "ReDoS Detector".into(),
service: "test".into(),
severity: Severity::High,
patterns: vec![PatternSpec {
regex: "(a{32,})$".into(),
description: Some("long trailing run".into()),
group: Some(1),
required_literals: Vec::new(),
client_safe: false,
weak_anchor: false,
structural_password_slot: false,
}],
companions: vec![],
verify: None,
keywords: vec!["a".into()],
min_confidence: None,
..keyhog_scanner::testing::named_detector_fixture_defaults()
};
let scanner = CompiledScanner::compile(vec![detector]).unwrap();
let mut data = "a".repeat(1000);
data.push('!');
let chunk = Chunk {
data: data.into(),
metadata: ChunkMetadata::default(),
};
let start = Instant::now();
let timeout = Duration::from_millis(100);
let deadline = start + timeout;
let _matches = keyhog_scanner::testing::scan_with_deadline(&scanner, &chunk, Some(deadline));
assert!(
start.elapsed() < Duration::from_secs(1),
"Scan took too long: {:?}",
start.elapsed()
);
}
#[test]
fn test_inner_loop_deadline_aborts_many_match_pattern() {
let detector = DetectorSpec {
kind: Default::default(),
entropy_floor: Vec::new(),
tests: Vec::new(),
id: "many-match-detector".into(),
name: "Many Match Detector".into(),
service: "test".into(),
severity: Severity::High,
patterns: vec![PatternSpec {
regex: "([a-z])".into(),
description: None,
group: Some(1),
required_literals: Vec::new(),
client_safe: false,
weak_anchor: false,
structural_password_slot: false,
}],
companions: vec![],
verify: None,
keywords: vec![],
min_confidence: None,
..keyhog_scanner::testing::named_detector_fixture_defaults()
};
let scanner = CompiledScanner::compile(vec![detector]).unwrap();
let chunk = Chunk {
data: "a".repeat(1024 * 1024).into(),
metadata: ChunkMetadata::default(),
};
let start = Instant::now();
let deadline = start + Duration::from_millis(5);
let _ = keyhog_scanner::testing::scan_with_deadline(&scanner, &chunk, Some(deadline));
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(100),
"Inner-loop deadline did not abort: scan ran for {:?} \
despite a 5ms deadline. The extractor regex/post-processing \
deadline checks are not firing.",
elapsed
);
}
#[test]
fn test_inner_loop_deadline_aborts_many_match_grouped_pattern() {
let detector = DetectorSpec {
kind: Default::default(),
entropy_floor: Vec::new(),
tests: Vec::new(),
id: "many-match-grouped-detector".into(),
name: "Many Match Grouped Detector".into(),
service: "test".into(),
severity: Severity::High,
patterns: vec![PatternSpec {
regex: "([a-z])".into(),
description: None,
group: Some(1),
required_literals: Vec::new(),
client_safe: false,
weak_anchor: false,
structural_password_slot: false,
}],
companions: vec![],
verify: None,
keywords: vec![],
min_confidence: None,
..keyhog_scanner::testing::named_detector_fixture_defaults()
};
let scanner = CompiledScanner::compile(vec![detector]).unwrap();
let chunk = Chunk {
data: "a".repeat(1024 * 1024).into(),
metadata: ChunkMetadata::default(),
};
let start = Instant::now();
let deadline = start + Duration::from_millis(5);
let _ = keyhog_scanner::testing::scan_with_deadline(&scanner, &chunk, Some(deadline));
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(100),
"Grouped extraction loop ignored the scan deadline: elapsed={elapsed:?}"
);
}
#[test]
fn test_inner_loop_deadline_aborts_many_match_anchored_pattern() {
let detector = DetectorSpec {
kind: Default::default(),
entropy_floor: Vec::new(),
tests: Vec::new(),
id: "many-match-anchored-detector".into(),
name: "Many Match Anchored Detector".into(),
service: "test".into(),
severity: Severity::High,
patterns: vec![PatternSpec {
regex: "sk_live_[A-Za-z0-9]{24}".into(),
description: None,
group: None,
required_literals: Vec::new(),
client_safe: false,
weak_anchor: false,
structural_password_slot: false,
}],
companions: vec![],
verify: None,
keywords: vec!["sk_live_".into()],
min_confidence: None,
..keyhog_scanner::testing::named_detector_fixture_defaults()
};
let scanner = CompiledScanner::compile(vec![detector]).unwrap();
let chunk = Chunk {
data: "sk_live_".repeat(500_000).into(),
metadata: ChunkMetadata::default(),
};
let start = Instant::now();
let deadline = start + Duration::from_millis(5);
let _ = keyhog_scanner::testing::scan_with_deadline(&scanner, &chunk, Some(deadline));
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(250),
"Anchored extraction loop ignored the scan deadline: elapsed={elapsed:?}"
);
}
#[test]
fn test_generic_assignment_deadline_aborts_inside_bridge() {
let detector = DetectorSpec {
kind: Default::default(),
entropy_floor: Vec::new(),
tests: Vec::new(),
id: "prefix-pass-detector".into(),
name: "Prefix Pass Detector".into(),
service: "test".into(),
severity: Severity::High,
patterns: vec![PatternSpec {
regex: "abc".into(),
description: None,
group: None,
required_literals: Vec::new(),
client_safe: false,
weak_anchor: false,
structural_password_slot: false,
}],
companions: vec![],
verify: None,
keywords: vec!["abc".into()],
min_confidence: None,
..keyhog_scanner::testing::named_detector_fixture_defaults()
};
let scanner = CompiledScanner::compile(vec![detector]).unwrap();
let mut data = String::from("abc\n");
let value = "a1b2c3d4e5f60718293a4b5c6d7e8f901a2b3c4d5e6f7081";
for idx in 0..100_000 {
data.push_str("api_key_");
data.push_str(&idx.to_string());
data.push_str(" = \"");
data.push_str(value);
data.push_str("\"\n");
}
let chunk = Chunk {
data: data.into(),
metadata: ChunkMetadata::default(),
};
let start = Instant::now();
let deadline = start + Duration::from_millis(5);
let _ = keyhog_scanner::testing::scan_with_deadline(&scanner, &chunk, Some(deadline));
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(250),
"Generic assignment bridge ignored the scan deadline: elapsed={elapsed:?}"
);
}