sil_confusable/
detector.rs1use crate::unicode_map::to_ascii_equivalent;
2
3pub fn detect_confusables(input: &str) -> Vec<String> {
4 let mut flags = vec![];
5
6 let normalized = to_ascii_equivalent(input);
7
8 if normalized != input {
9 flags.push("VISUAL_MISMATCH_DETECTED".to_string());
10 }
11
12 if has_cross_script_mix(input) {
13 flags.push("CROSS_SCRIPT_MIXING".to_string());
14 }
15
16 if looks_like_common_target(input) {
17 flags.push("POTENTIAL_IMPERSONATION".to_string());
18 }
19
20 flags
21}
22
23fn has_cross_script_mix(input: &str) -> bool {
24 let mut has_latin = false;
25 let mut has_cyrillic = false;
26 let mut has_greek = false;
27
28 for c in input.chars() {
29 let code = c as u32;
30 match code {
31 0x0041..=0x005A | 0x0061..=0x007A => has_latin = true,
32 0x0400..=0x04FF => has_cyrillic = true,
33 0x0370..=0x03FF => has_greek = true,
34 _ => {}
35 }
36 }
37
38 (has_latin && has_cyrillic) || (has_latin && has_greek) || (has_cyrillic && has_greek)
39}
40
41fn looks_like_common_target(input: &str) -> bool {
42 let normalized = to_ascii_equivalent(input).to_lowercase();
43 let targets = [
44 "paypal",
45 "google",
46 "facebook",
47 "amazon",
48 "apple",
49 "microsoft",
50 "netflix",
51 "github",
52 "gmail",
53 "whatsapp",
54 ];
55 targets.contains(&normalized.as_str())
56}