safe_shell_scanner/
entropy.rs1pub fn shannon_entropy(s: &str) -> f64 {
4 if s.is_empty() {
5 return 0.0;
6 }
7
8 let len = s.len() as f64;
9 let mut freq = [0u32; 256];
10
11 for &b in s.as_bytes() {
12 freq[b as usize] += 1;
13 }
14
15 freq.iter()
16 .filter(|&&count| count > 0)
17 .map(|&count| {
18 let p = count as f64 / len;
19 -p * p.log2()
20 })
21 .sum()
22}
23
24pub fn is_high_entropy(s: &str) -> bool {
27 s.len() >= 16 && shannon_entropy(s) > 4.5
28}
29
30#[cfg(test)]
31mod tests {
32 use super::*;
33
34 #[test]
35 fn empty_string() {
36 assert_eq!(shannon_entropy(""), 0.0);
37 }
38
39 #[test]
40 fn single_char_repeated() {
41 assert_eq!(shannon_entropy("aaaaaaaaaaaaaaaa"), 0.0);
42 }
43
44 #[test]
45 fn low_entropy_string() {
46 assert!(!is_high_entropy("aaaaaaaaaaaaaaaa"));
48 assert!(!is_high_entropy("aabbccddaabbccdd"));
49 }
50
51 #[test]
52 fn high_entropy_string() {
53 assert!(is_high_entropy("aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1v"));
55 }
56
57 #[test]
58 fn too_short_even_if_random() {
59 assert!(!is_high_entropy("aB3dE5f"));
60 }
61
62 #[test]
63 fn real_api_key_like() {
64 assert!(is_high_entropy("sk_live_4eC39HqLyjWDarjtT1zdp7dc"));
65 }
66
67 #[test]
68 fn normal_path_is_low_entropy() {
69 assert!(!is_high_entropy("/usr/local/bin/node"));
70 }
71
72 #[test]
73 fn entropy_increases_with_diversity() {
74 let low = shannon_entropy("aaaa");
75 let high = shannon_entropy("abcd");
76 assert!(high > low);
77 }
78}