use keyhog_scanner::testing::unicode_hardening::{
char_normalization_is_drop, char_normalization_is_keep, contains_evasion, cyrillic_to_latin,
detect_unicode_attacks, fullwidth_to_ascii, greek_to_latin, is_combining_mark, is_fullwidth,
is_rtl_override, is_zero_width, normalize_homoglyphs, strip_interior_evasion_controls,
};
use std::borrow::Cow;
use std::collections::BTreeSet;
const CANON: &str = "ghp_aBcD1234EfGh5678IjKl";
fn interleave(s: &str, sep: char) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if i > 0 {
out.push(sep);
}
out.push(ch);
}
out
}
#[test]
fn cyrillic_homoglyphs_map_to_latin_twins() {
let evil = "\u{043E}\u{0440}\u{0435}\u{041D}";
assert_eq!(normalize_homoglyphs(evil), "opeH");
assert_eq!(cyrillic_to_latin('\u{0440}'), Some('p'));
assert_eq!(cyrillic_to_latin('\u{04BB}'), Some('h')); assert_eq!(cyrillic_to_latin('\u{041D}'), Some('H')); assert_eq!(cyrillic_to_latin('a'), None); }
#[test]
fn cyrillic_disguised_prefix_normalizes_to_canonical() {
let evil = format!("g\u{04BB}p_aBcD1234EfGh5678IjKl");
assert_eq!(normalize_homoglyphs(&evil), CANON);
}
#[test]
fn greek_homoglyphs_map_to_latin_twins() {
let evil = "\u{03B1}\u{03B2}\u{03B5}\u{03BF}\u{03C1}\u{03C4}";
assert_eq!(normalize_homoglyphs(evil), "abeopt");
assert_eq!(greek_to_latin('\u{03C1}'), Some('p')); assert_eq!(greek_to_latin('a'), None);
}
#[test]
fn fullwidth_ascii_maps_to_halfwidth() {
let evil = "\u{FF47}\u{FF48}\u{FF50}\u{FF3F}aBcD1234EfGh5678IjKl";
assert_eq!(normalize_homoglyphs(evil), CANON);
assert_eq!(fullwidth_to_ascii('\u{FF10}'), '0');
assert_eq!(fullwidth_to_ascii('\u{FF5E}'), '~');
assert!(!is_fullwidth('\u{FF61}')); }
#[test]
fn newly_added_zero_width_ranges_are_stripped() {
for &cp in &[
'\u{2065}',
'\u{206A}',
'\u{206F}',
'\u{180B}',
'\u{180C}',
'\u{180D}',
'\u{180F}',
'\u{115F}',
'\u{1160}',
'\u{3164}',
'\u{FFA0}',
'\u{1BCA0}',
'\u{1BCA3}',
'\u{1D173}',
'\u{1D17A}',
'\u{FFF0}',
'\u{FFF8}',
] {
assert!(is_zero_width(cp), "{:04X} must be zero-width", cp as u32);
}
for &cp in &[
'\u{2065}',
'\u{206C}',
'\u{180F}',
'\u{115F}',
'\u{3164}',
'\u{FFA0}',
'\u{1BCA1}',
'\u{1D175}',
'\u{FFF4}',
] {
assert!(char_normalization_is_drop(cp), "{:04X}", cp as u32);
}
}
#[test]
fn zero_width_negative_twins_are_not_stripped() {
for &cp in &[
'\u{2070}', '\u{FFA1}', '\u{1161}', '\u{3163}', '\u{1D17B}', ] {
assert!(
!is_zero_width(cp),
"{:04X} must NOT be zero-width",
cp as u32
);
}
assert!(char_normalization_is_keep('\u{2070}'));
assert!(char_normalization_is_keep('\u{FFA1}'));
}
#[test]
fn hangul_filler_split_credential_is_recovered() {
let evil = interleave(CANON, '\u{3164}');
assert_ne!(evil, CANON); assert_eq!(normalize_homoglyphs(&evil), CANON);
}
#[test]
fn mixed_invisible_splice_is_recovered() {
let seps = [
'\u{200B}', '\u{2065}', '\u{206A}', '\u{180F}', '\u{FFA0}', '\u{FEFF}',
];
let mut evil = String::new();
for (i, ch) in CANON.chars().enumerate() {
evil.push(seps[i % seps.len()]);
evil.push(ch);
}
assert_eq!(normalize_homoglyphs(&evil), CANON);
}
#[test]
fn classic_zero_width_joiner_still_stripped() {
let evil = "ghp\u{200D}_aBcD1234EfGh5678IjKl";
assert_eq!(normalize_homoglyphs(evil), CANON);
}
#[test]
fn combining_marks_are_stripped_across_blocks() {
assert!(is_combining_mark('\u{0301}'));
assert!(is_combining_mark('\u{1DC0}'));
assert!(!is_combining_mark('a'));
let evil = "g\u{0301}h\u{1DC0}p_aBcD1234EfGh5678IjKl";
assert_eq!(normalize_homoglyphs(evil), CANON);
}
#[test]
fn tag_block_is_stripped() {
assert!(is_zero_width('\u{E0000}'));
assert!(is_zero_width('\u{E007F}')); assert!(is_zero_width('\u{E0041}')); let evil = "ghp_\u{E0041}\u{E0042}aBcD1234EfGh5678IjKl";
assert_eq!(normalize_homoglyphs(evil), CANON);
}
#[test]
fn bidi_controls_are_stripped() {
for &cp in &['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'] {
assert!(is_rtl_override(cp), "{:04X}", cp as u32);
assert!(char_normalization_is_drop(cp));
}
for &cp in &['\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}', '\u{061C}'] {
assert!(is_zero_width(cp), "{:04X}", cp as u32);
assert!(char_normalization_is_drop(cp));
}
let evil = "ghp_\u{202E}aBcD1234EfGh5678IjKl";
assert_eq!(normalize_homoglyphs(evil), CANON);
}
#[test]
fn plain_ascii_is_borrowed_unchanged() {
let s = "ghp_aBcD1234EfGh5678IjKl and normal_code = fn(x) { y }";
let out = normalize_homoglyphs(s);
assert!(
matches!(out, Cow::Borrowed(_)),
"clean ASCII must not allocate"
);
assert_eq!(out, s);
}
#[test]
fn structural_whitespace_is_preserved() {
let s = "col1\tcol2\r\nnext\tline";
let out = normalize_homoglyphs(s);
assert_eq!(out, s);
assert!(matches!(out, Cow::Borrowed(_)));
}
#[test]
fn legitimate_cjk_text_is_unchanged() {
let s = "日本語のコメント 中文注释 한국어";
assert_eq!(normalize_homoglyphs(s), s);
}
#[test]
fn detector_reports_every_evasion_class() {
let evil = "g\u{04BB}p_\u{200B}\u{3164}a\u{0301}\u{E0041}\u{202E}b\u{FF10}\u{2065}";
let hits = detect_unicode_attacks(evil);
let kinds: BTreeSet<_> = hits.iter().map(|m| format!("{:?}", m.kind)).collect();
assert!(kinds.contains("CyrillicHomoglyph"), "{kinds:?}");
assert!(kinds.contains("ZeroWidth"), "{kinds:?}");
assert!(kinds.contains("Decomposed"), "{kinds:?}");
assert!(kinds.contains("RTLOverride"), "{kinds:?}");
assert!(kinds.contains("Fullwidth"), "{kinds:?}");
}
#[test]
fn contains_evasion_agrees_with_normalized_char() {
assert!(contains_evasion("ghp\u{2065}_x")); assert!(contains_evasion("g\u{04BB}p")); assert!(!contains_evasion("ghp_plain_ascii_token"));
}
#[test]
fn evasion_anchor_ac_is_present_and_strips_interior_control() {
let evil = "AKIA\tQYLPMN5HFIQR7XYA";
let stripped = strip_interior_evasion_controls(evil);
assert_eq!(stripped, "AKIAQYLPMN5HFIQR7XYA");
assert!(matches!(stripped, Cow::Owned(_)));
}
#[test]
fn structural_control_outside_credential_is_kept() {
let s = "value\tother";
assert_eq!(strip_interior_evasion_controls(s), s);
let s2 = "ghp_abcdef\r\nnext";
assert_eq!(strip_interior_evasion_controls(s2), s2);
}