1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! Homoglyph detection: finds secrets obfuscated with lookalike Unicode characters.
//!
//! Attackers may replace 'a' with Cyrillic 'а' to bypass simple regexes.
//! This module provides a way to match patterns against homoglyph-expanded forms.
use std::collections::HashMap;
use std::sync::OnceLock;
/// Returns a map of ASCII characters to their common Unicode homoglyphs.
fn homoglyph_map() -> &'static HashMap<char, Vec<char>> {
static MAP: OnceLock<HashMap<char, Vec<char>>> = OnceLock::new();
MAP.get_or_init(|| {
let mut m = HashMap::new();
m.insert('a', vec!['а', 'α', 'a']);
m.insert('b', vec!['Ь', 'β', 'b']);
m.insert('c', vec!['с', 'c']);
m.insert('e', vec!['е', 'ε', 'e']);
m.insert('g', vec!['ɡ', 'g']); // U+0261
m.insert('h', vec!['н', 'һ', 'h']); // U+04BB for h
m.insert('i', vec!['і', 'ι', 'i']);
m.insert('j', vec!['ј', 'j']);
m.insert('k', vec!['к', 'κ', 'k']);
m.insert('m', vec!['м', 'm']);
m.insert('n', vec!['п', 'ν', 'n']);
m.insert('o', vec!['о', 'ο', 'o']);
m.insert('p', vec!['р', 'ρ', 'p']);
m.insert('s', vec!['ѕ', 's']);
m.insert('t', vec!['т', 'τ', 't']);
m.insert('u', vec!['υ', 'u']);
// 'l' confuses with the I/1/| cluster: Cyrillic/Greek dotless i and
// fullwidth l. The Greek/Cyrillic o-lookalikes (Ο/ο/о) are an 'o' cluster,
// not 'l', and only add a false-positive/automaton-bloat surface here.
m.insert('l', vec!['і', 'І', 'ι', 'Ι', 'l']);
m.insert('x', vec!['х', 'χ', 'x']);
m.insert('y', vec!['у', 'y']);
m.insert('L', vec!['L']);
m.insert('A', vec!['А', 'Α', 'A']);
m.insert('B', vec!['В', 'Β', 'B']);
m.insert('E', vec!['Е', 'Ε', 'E']);
m.insert('H', vec!['Н', 'Η', 'H']);
m.insert('I', vec!['І', 'Ι', 'I']);
m.insert('J', vec!['Ј', 'J']);
m.insert('K', vec!['К', 'Κ', 'K']);
m.insert('M', vec!['М', 'M']);
m.insert('N', vec!['Ν', 'N']);
m.insert('O', vec!['О', 'Ο', 'O']);
m.insert('P', vec!['Р', 'Ρ', 'P']);
m.insert('S', vec!['С', 'S']);
m.insert('T', vec!['Т', 'Τ', 'T']);
m.insert('X', vec!['Х', 'Χ', 'X']);
m.insert('Y', vec!['Υ', 'Y']);
m
})
}
/// The `(ascii, confusable-glyphs)` entries of [`homoglyph_map`], sorted by the
/// ASCII key for deterministic iteration. Exposed (via the `testing` facade) so a
/// cross-map consistency gate can assert this AC/regex-expand map agrees with the
/// `unicode_hardening` normalize-path folds (`cyrillic_to_latin`/`greek_to_latin`)
/// on every shared codepoint (the two are separate scan paths that must not drift).
pub(crate) fn homoglyph_confusables() -> Vec<(char, Vec<char>)> {
let mut entries: Vec<(char, Vec<char>)> = homoglyph_map()
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect();
entries.sort_by_key(|(k, _)| *k);
entries
}
/// Expand a regex pattern to include homoglyphs.
/// e.g. "ghp_" -> "[gɡg][hнһh][pрρp]_"
pub(crate) fn expand_homoglyphs(pattern: &str) -> String {
let map = homoglyph_map();
// Every mapped ASCII char becomes a `[<ascii><glyphs>]` class (~8 bytes);
// reserve up front so expansion over all detector prefixes does not realloc
// as it grows. Byte-identical to building from an empty String.
let mut expanded = String::with_capacity(pattern.len() * 8);
// Simple implementation: replace ASCII chars with character classes
for ch in pattern.chars() {
if let Some(glyphs) = map.get(&ch) {
expanded.push('[');
expanded.push(ch);
for &g in glyphs {
expanded.push(g);
}
expanded.push(']');
} else {
push_regex_literal_char(&mut expanded, ch);
}
}
expanded
}
fn push_regex_literal_char(out: &mut String, ch: char) {
if matches!(
ch,
'\\' | '.'
| '+'
| '*'
| '?'
| '('
| ')'
| '|'
| '['
| ']'
| '{'
| '}'
| '^'
| '$'
| '#'
| '&'
| '-'
) {
out.push('\\');
}
out.push(ch);
}