use std::sync::Arc;
use std::time::{Duration, Instant};
use keyhog_core::{MatchLocation, RawMatch, Severity};
use keyhog_scanner::resolution::resolve_matches;
use sha2::{Digest, Sha256};
const SPAN_ID: &str = "private-key";
const PROBE_ID: &str = "generic-password";
fn credential_hash(credential: &str) -> [u8; 32] {
Sha256::digest(credential.as_bytes()).into()
}
fn at(detector_id: &str, file: &str, offset: usize, width: usize, line: usize) -> RawMatch {
let credential = "x".repeat(width);
let hash = credential_hash(&credential);
RawMatch {
detector_id: Arc::from(detector_id),
detector_name: Arc::from(detector_id),
service: Arc::from("test"),
severity: Severity::High,
credential: keyhog_core::SensitiveString::from(credential),
credential_hash: hash.into(),
companions: std::collections::HashMap::new(),
location: MatchLocation {
source: Arc::from("test"),
file_path: Some(Arc::from(file)),
line: Some(line),
offset,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: Some(0.5),
}
}
fn span(file: &str, start: usize, width: usize) -> RawMatch {
at(SPAN_ID, file, start, width, start + 1)
}
fn probe(file: &str, start: usize, width: usize) -> RawMatch {
at(PROBE_ID, file, start, width, start + 1)
}
fn probe_kept(resolved: &[RawMatch], offset: usize) -> bool {
resolved
.iter()
.any(|m| m.detector_id.as_ref() == PROBE_ID && m.location.offset == offset)
}
fn span_kept(resolved: &[RawMatch], offset: usize) -> bool {
resolved
.iter()
.any(|m| m.detector_id.as_ref() == SPAN_ID && m.location.offset == offset)
}
fn count_probes(resolved: &[RawMatch]) -> usize {
resolved
.iter()
.filter(|m| m.detector_id.as_ref() == PROBE_ID)
.count()
}
fn count_spans(resolved: &[RawMatch]) -> usize {
resolved
.iter()
.filter(|m| m.detector_id.as_ref() == SPAN_ID)
.count()
}
const FILE: &str = "keys.pem";
#[test]
fn nested_probe_in_single_span_is_suppressed() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 120, 50)]);
assert!(span_kept(&resolved, 100), "the block span itself survives");
assert!(
!probe_kept(&resolved, 120),
"a probe fully nested in the private-key block must be suppressed"
);
}
#[test]
fn probe_at_exact_span_bounds_is_suppressed() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 100, 200)]);
assert!(
!probe_kept(&resolved, 100),
"a probe coextensive with the span (inclusive bounds) is nested → suppressed"
);
}
#[test]
fn probe_at_span_start_boundary_is_suppressed() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 100, 50)]);
assert!(
!probe_kept(&resolved, 100),
"left-aligned nested probe is suppressed"
);
}
#[test]
fn probe_at_span_end_boundary_is_suppressed() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 290, 10)]);
assert!(
!probe_kept(&resolved, 290),
"right-aligned nested probe is suppressed"
);
}
#[test]
fn nested_probe_in_one_of_many_spans_is_suppressed() {
let resolved = resolve_matches(vec![
span(FILE, 0, 100),
span(FILE, 1000, 100),
span(FILE, 2000, 100),
probe(FILE, 1020, 50),
]);
assert_eq!(count_spans(&resolved), 3, "all three block spans survive");
assert!(
!probe_kept(&resolved, 1020),
"a probe nested in any one of the spans is suppressed"
);
}
#[test]
fn wide_early_span_suppresses_probe_past_a_narrow_later_span_start() {
let resolved = resolve_matches(vec![
span(FILE, 0, 1000),
span(FILE, 500, 10),
probe(FILE, 505, 395),
]);
assert!(
!probe_kept(&resolved, 505),
"the wide early span must suppress a probe whose start is past a narrow later span"
);
}
#[test]
fn probe_contained_in_both_overlapping_spans_is_suppressed() {
let resolved = resolve_matches(vec![
span(FILE, 0, 1000),
span(FILE, 500, 1000),
probe(FILE, 600, 100),
]);
assert!(
!probe_kept(&resolved, 600),
"a probe inside overlapping spans is suppressed"
);
}
#[test]
fn probe_entirely_before_span_is_kept() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 0, 50)]);
assert!(
probe_kept(&resolved, 0),
"a probe before the span is not nested → kept"
);
}
#[test]
fn probe_entirely_after_span_is_kept() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 400, 50)]);
assert!(
probe_kept(&resolved, 400),
"a probe after the span is not nested → kept"
);
}
#[test]
fn probe_extending_one_byte_past_span_end_is_kept() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 150, 151)]);
assert!(
probe_kept(&resolved, 150),
"a probe whose end exceeds the span by one byte is NOT contained → kept"
);
}
#[test]
fn probe_starting_one_byte_before_span_is_kept() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 99, 101)]);
assert!(
probe_kept(&resolved, 99),
"a probe starting one byte before the span is NOT contained → kept"
);
}
#[test]
fn probe_straddling_span_left_edge_is_kept() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 50, 100)]);
assert!(
probe_kept(&resolved, 50),
"left-straddling probe is not nested → kept"
);
}
#[test]
fn probe_straddling_span_right_edge_is_kept() {
let resolved = resolve_matches(vec![span(FILE, 100, 200), probe(FILE, 250, 100)]);
assert!(
probe_kept(&resolved, 250),
"right-straddling probe is not nested → kept"
);
}
#[test]
fn probe_enclosing_two_separate_spans_is_kept() {
let resolved = resolve_matches(vec![
span(FILE, 100, 100),
span(FILE, 400, 100),
probe(FILE, 50, 550),
]);
assert!(
probe_kept(&resolved, 50),
"a probe enclosing the spans is contained in neither → kept"
);
}
#[test]
fn span_in_other_file_does_not_suppress_probe() {
let resolved = resolve_matches(vec![span("b.txt", 100, 200), probe("a.txt", 120, 50)]);
assert!(
probe_kept(&resolved, 120),
"a private-key span in a different file must not suppress this file's probe"
);
}
#[test]
fn only_same_file_span_suppresses_when_two_files_each_have_a_span() {
let resolved = resolve_matches(vec![
span("a.txt", 0, 500),
span("b.txt", 0, 500),
probe("a.txt", 100, 50),
probe("c.txt", 100, 50),
]);
assert!(
!resolved.iter().any(|m| m.detector_id.as_ref() == PROBE_ID
&& m.location.file_path.as_deref() == Some("a.txt")),
"the probe in a.txt (which has a covering span) is suppressed"
);
assert!(
resolved.iter().any(|m| m.detector_id.as_ref() == PROBE_ID
&& m.location.file_path.as_deref() == Some("c.txt")),
"the probe in c.txt (no span) is kept"
);
}
#[test]
fn probe_without_file_path_is_kept() {
let mut no_path = probe(FILE, 120, 50);
no_path.location.file_path = None;
let resolved = resolve_matches(vec![span(FILE, 100, 200), no_path]);
assert!(
resolved
.iter()
.any(|m| m.detector_id.as_ref() == PROBE_ID && m.location.file_path.is_none()),
"a probe with no file path is never nested → kept"
);
}
#[test]
fn block_detector_nested_in_another_block_is_not_suppressed() {
let mut inner = at(SPAN_ID, FILE, 120, 50, 99_999);
inner.detector_id = Arc::from("ssh-private-key");
inner.detector_name = Arc::from("ssh-private-key");
let resolved = resolve_matches(vec![span(FILE, 100, 200), inner]);
assert!(span_kept(&resolved, 100), "outer block span kept");
assert!(
resolved
.iter()
.any(|m| m.detector_id.as_ref() == "ssh-private-key"),
"a nested BLOCK detector is never suppressed by the nesting pass"
);
}
#[test]
fn no_private_key_spans_keeps_every_probe() {
let resolved = resolve_matches(vec![
probe(FILE, 0, 50),
probe(FILE, 1000, 50),
probe(FILE, 2000, 50),
]);
assert_eq!(
count_probes(&resolved),
3,
"with no spans, every probe is kept"
);
}
#[test]
fn single_match_is_returned_unchanged() {
let resolved = resolve_matches(vec![probe(FILE, 120, 50)]);
assert_eq!(resolved.len(), 1);
assert!(probe_kept(&resolved, 120));
}
#[test]
fn suppression_is_independent_of_input_order() {
let forward = vec![
span(FILE, 0, 100),
span(FILE, 1000, 100),
probe(FILE, 20, 30),
probe(FILE, 1020, 30),
probe(FILE, 5000, 30),
];
let mut reversed = forward.clone();
reversed.reverse();
let a = resolve_matches(forward);
let b = resolve_matches(reversed);
assert_eq!(a, b, "resolution must be order-independent");
assert!(!probe_kept(&a, 20) && !probe_kept(&a, 1020));
assert!(probe_kept(&a, 5000));
assert_eq!(count_spans(&a), 2);
}
#[test]
fn many_spans_each_with_a_nested_probe_suppresses_all_probes() {
const N: usize = 2_000;
let mut input = Vec::with_capacity(N * 2);
for i in 0..N {
let start = i * 1_000;
input.push(span(FILE, start, 200));
input.push(probe(FILE, start + 20, 50)); }
let resolved = resolve_matches(input);
assert_eq!(count_spans(&resolved), N, "every block span survives");
assert_eq!(
count_probes(&resolved),
0,
"every nested probe is suppressed"
);
}
#[test]
fn interleaved_nested_and_outside_probes_partition_correctly() {
const N: usize = 1_000;
let mut input = Vec::with_capacity(N * 3);
for i in 0..N {
let start = i * 1_000;
input.push(span(FILE, start, 200));
input.push(probe(FILE, start + 20, 50)); input.push(probe(FILE, start + 500, 50)); }
let resolved = resolve_matches(input);
assert_eq!(count_spans(&resolved), N, "all spans kept");
assert_eq!(
count_probes(&resolved),
N,
"exactly the N gap probes survive; the N nested probes are gone"
);
for i in [0usize, N / 2, N - 1] {
let start = i * 1_000;
assert!(
!probe_kept(&resolved, start + 20),
"nested probe {i} suppressed"
);
assert!(probe_kept(&resolved, start + 500), "gap probe {i} kept");
}
}
#[test]
fn adversarial_many_spans_and_nested_probes_resolve_within_time_bound() {
const N: usize = 40_000;
let mut input = Vec::with_capacity(N * 2);
for i in 0..N {
let start = i * 1_000;
input.push(span(FILE, start, 200));
input.push(probe(FILE, start + 20, 50));
}
let begin = Instant::now();
let resolved = resolve_matches(input);
let elapsed = begin.elapsed();
assert!(
elapsed < Duration::from_secs(5),
"nesting suppression over {N} spans + {N} probes took {elapsed:?}; the prefix-max \
index resolves this in milliseconds, exceeding 5s means the O(matches × spans) \
scan was reintroduced (algorithmic-DoS, Law 7)"
);
assert_eq!(
count_spans(&resolved),
N,
"all {N} spans survive the fast path"
);
assert_eq!(
count_probes(&resolved),
0,
"all {N} nested probes are suppressed"
);
}