mod support;
use support::paths::detector_dir;
use keyhog_core::{Chunk, ChunkMetadata, RawMatch};
use keyhog_scanner::resolution::resolve_matches;
use keyhog_scanner::CompiledScanner;
const B64_BODY: &str = "MIIEpQIBAAKCAQEA7n2K9xR4vQ1mWcZ8hLbF3jD5sT6yU0pN2aG4eH7iO9kB1lM3rV5wX8zC0dQ2fS4gJ6kP8mR0tU2wY4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZ0aB2cD4eF6gH8iJ0kL2mN4oP6qR8sT0uV2wX4yZ6aB8cD0eF2gH4iJ6kL8mN0oP2qR4sT6uV8wX0yZ2aB4cD6eF8gH0iJ2kL4";
fn short_block(label: &str, marker: &str) -> String {
format!("-----BEGIN {label}-----\nMIIBVAIBADANBgkqhkiG9w0BAQEF{marker}Po0kjAB\n-----END {label}-----")
}
fn long_block(label: &str, marker: &str) -> String {
format!("-----BEGIN {label}-----\n{marker}{B64_BODY}\n-----END {label}-----")
}
fn scan_raw(text: &str) -> Vec<RawMatch> {
let detectors = keyhog_core::load_detectors(&detector_dir()).expect("load detectors");
let scanner = CompiledScanner::compile(detectors).expect("compile scanner");
let chunk = Chunk {
data: text.into(),
metadata: ChunkMetadata {
source_type: "filesystem".into(),
path: Some("keys.pem".into()),
..Default::default()
},
};
scanner
.scan(&chunk)
.expect("GitHub App PEM scan should succeed")
}
fn github_app_credentials(text: &str) -> Vec<String> {
scan_raw(text)
.into_iter()
.filter(|m| m.detector_id.as_ref() == "github-app-private-key")
.map(|m| m.credential.as_ref().to_string())
.collect()
}
fn github_app_credentials_resolved(text: &str) -> Vec<String> {
resolve_matches(scan_raw(text))
.into_iter()
.filter(|m| m.detector_id.as_ref() == "github-app-private-key")
.map(|m| m.credential.as_ref().to_string())
.collect()
}
#[test]
fn two_adjacent_short_rsa_blocks_do_not_trip_github_app() {
let text = format!(
"{}\n\n{}",
short_block("RSA PRIVATE KEY", "AAAA1111"),
short_block("RSA PRIVATE KEY", "BBBB2222")
);
let creds = github_app_credentials(&text);
assert!(
creds.is_empty(),
"two short (<200B) RSA blocks must NOT satisfy the {{200,}} floor by spanning both; \
github-app matched {creds:?}"
);
}
#[test]
fn no_github_app_credential_spans_both_short_rsa_keys() {
let text = format!(
"{}\n\n{}",
short_block("RSA PRIVATE KEY", "AAAA1111"),
short_block("RSA PRIVATE KEY", "BBBB2222")
);
for cred in github_app_credentials(&text) {
assert!(
!(cred.contains("AAAA1111") && cred.contains("BBBB2222")),
"a single github-app match must never contain BOTH keys' bodies: {cred:?}"
);
}
}
#[test]
fn two_adjacent_short_openssh_blocks_do_not_trip_github_app() {
let text = format!(
"{}\n\n{}",
short_block("OPENSSH PRIVATE KEY", "CCCC3333"),
short_block("OPENSSH PRIVATE KEY", "DDDD4444")
);
assert!(
github_app_credentials(&text).is_empty(),
"two short OPENSSH blocks must not merge into a github-app match"
);
}
#[test]
fn single_short_rsa_block_does_not_trip_github_app() {
let creds = github_app_credentials(&short_block("RSA PRIVATE KEY", "EEEE5555"));
assert!(
creds.is_empty(),
"a single sub-200B block is below the detector floor; got {creds:?}"
);
}
#[test]
fn resolved_two_short_rsa_keys_have_no_github_app_finding() {
let text = format!(
"{}\n\n{}",
short_block("RSA PRIVATE KEY", "FFFF6666"),
short_block("RSA PRIVATE KEY", "GGGG7777")
);
assert!(
github_app_credentials_resolved(&text).is_empty(),
"no github-app finding survives resolution for two short distinct keys"
);
}
#[test]
fn single_long_rsa_block_matches_github_app() {
let creds = github_app_credentials(&long_block("RSA PRIVATE KEY", "LONGKEY1"));
assert_eq!(
creds.len(),
1,
"a real ≥200B RSA key must still match the detector exactly once; got {creds:?}"
);
assert!(
creds[0].contains("LONGKEY1"),
"the match captures the real key body"
);
}
#[test]
fn long_block_match_is_bounded_to_one_block() {
let creds = github_app_credentials(&long_block("RSA PRIVATE KEY", "BOUNDED1"));
assert_eq!(creds.len(), 1);
assert_eq!(
creds[0].matches("-----BEGIN RSA PRIVATE KEY-----").count(),
1,
"credential must contain exactly one BEGIN marker: {:?}",
creds[0]
);
assert_eq!(
creds[0].matches("-----END RSA PRIVATE KEY-----").count(),
1,
"credential must contain exactly one END marker"
);
}
#[test]
fn two_adjacent_long_rsa_blocks_match_as_two_separate_keys() {
let text = format!(
"{}\n\n{}",
long_block("RSA PRIVATE KEY", "FIRSTKEY"),
long_block("RSA PRIVATE KEY", "OTHERKEY")
);
let creds = github_app_credentials(&text);
for cred in &creds {
assert!(
!(cred.contains("FIRSTKEY") && cred.contains("OTHERKEY")),
"no single match may span both long keys: {cred:?}"
);
assert_eq!(
cred.matches("-----BEGIN RSA PRIVATE KEY-----").count(),
1,
"each match is a single block: {cred:?}"
);
}
assert_eq!(
creds.len(),
2,
"two distinct long keys → two distinct matches; got {creds:?}"
);
}
#[test]
fn context_anchored_long_block_matches_single_block() {
let text = format!(
"GITHUB_APP_PRIVATE_KEY=\"{}\"",
long_block("RSA PRIVATE KEY", "CTXKEY01")
);
let creds = github_app_credentials(&text);
assert!(
creds.iter().any(|c| c.contains("CTXKEY01")),
"context-anchored long key must match; got {creds:?}"
);
for cred in &creds {
assert_eq!(
cred.matches("-----BEGIN RSA PRIVATE KEY-----").count(),
1,
"context-anchored match is bounded to one block: {cred:?}"
);
}
}
#[test]
fn long_block_between_two_short_blocks_matches_only_the_long_one() {
let text = format!(
"{}\n\n{}\n\n{}",
short_block("RSA PRIVATE KEY", "PRESHRT1"),
long_block("RSA PRIVATE KEY", "MIDLONG1"),
short_block("RSA PRIVATE KEY", "POSTSHT1")
);
let creds = github_app_credentials(&text);
assert_eq!(
creds.len(),
1,
"only the long middle key matches; got {creds:?}"
);
assert!(creds[0].contains("MIDLONG1"));
assert!(
!creds[0].contains("PRESHRT1") && !creds[0].contains("POSTSHT1"),
"the long-key match must not reach into the short neighbors: {:?}",
creds[0]
);
}
#[test]
fn pem_begin_marker_is_backed_by_the_private_key_detector() {
let marker = keyhog_scanner::testing::pem_begin_marker();
let path = detector_dir().join("private-key.toml");
let toml = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read private-key detector {}: {e}", path.display()));
assert!(
toml.contains(marker),
"PEM marker {marker:?} (credential_shapes::PEM_BEGIN_MARKER) is absent from its \
authoritative private-key.toml pattern, the single-owner const drifted from the \
detector that surfaces a PEM key"
);
}