zeph_tools/filter/
security.rs1use std::sync::LazyLock;
5
6use regex::Regex;
7
8static SECURITY_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
9 [
10 r"warning:.*unused.*Result",
11 r"warning:.*must be used",
12 r"thread '.*' panicked at",
13 r"warning:.*unsafe",
14 r"dereference of raw pointer",
15 r"(?i)authentication failed",
16 r"(?i)unauthorized",
17 r"(?i)permission denied",
18 r"(?i)(401|403)\s+(Unauthorized|Forbidden)",
19 r"(?i)weak cipher",
20 r"(?i)deprecated algorithm",
21 r"(?i)insecure hash",
22 r"(?i)SQL injection",
23 r"(?i)unsafe query",
24 r"RUSTSEC-\d{4}-\d{4}",
25 r"(?i)security advisory",
26 r"(?i)vulnerability detected",
27 ]
28 .iter()
29 .map(|s| Regex::new(s).unwrap())
30 .collect()
31});
32
33#[must_use]
35pub fn compile_extra_patterns(patterns: &[String]) -> Vec<Regex> {
36 patterns
37 .iter()
38 .filter_map(|s| match Regex::new(s) {
39 Ok(re) => Some(re),
40 Err(e) => {
41 tracing::warn!(pattern = %s, error = %e, "invalid security extra_pattern, skipping");
42 None
43 }
44 })
45 .collect()
46}
47
48#[must_use]
49pub fn extract_security_lines<'a>(text: &'a str, extra: &[Regex]) -> Vec<&'a str> {
50 text.lines()
51 .filter(|line| {
52 SECURITY_PATTERNS.iter().any(|pat| pat.is_match(line))
53 || extra.iter().any(|pat| pat.is_match(line))
54 })
55 .collect()
56}
57
58pub fn append_security_warnings(filtered: &mut String, raw_output: &str, extra: &[Regex]) {
59 let security_lines = extract_security_lines(raw_output, extra);
60 if security_lines.is_empty() {
61 return;
62 }
63 filtered.push_str("\n\n--- Security Warnings (preserved) ---\n");
64 for line in &security_lines {
65 filtered.push_str(line);
66 filtered.push('\n');
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn detects_panic() {
76 let lines = extract_security_lines("thread 'main' panicked at 'oops'\nnormal line", &[]);
77 assert_eq!(lines.len(), 1);
78 assert!(lines[0].contains("panicked"));
79 }
80
81 #[test]
82 fn detects_rustsec() {
83 let lines = extract_security_lines("RUSTSEC-2024-0001 advisory here", &[]);
84 assert_eq!(lines.len(), 1);
85 }
86
87 #[test]
88 fn detects_auth_failure() {
89 let lines = extract_security_lines("Error: Authentication failed for user admin", &[]);
90 assert_eq!(lines.len(), 1);
91 }
92
93 #[test]
94 fn detects_permission_denied() {
95 let lines = extract_security_lines("Permission denied (publickey)", &[]);
96 assert_eq!(lines.len(), 1);
97 }
98
99 #[test]
100 fn detects_http_status_codes() {
101 let lines = extract_security_lines("HTTP 401 Unauthorized", &[]);
102 assert_eq!(lines.len(), 1);
103 let lines = extract_security_lines("HTTP 403 Forbidden", &[]);
104 assert_eq!(lines.len(), 1);
105 }
106
107 #[test]
108 fn detects_sql_injection() {
109 let lines = extract_security_lines("WARNING: potential SQL injection detected", &[]);
110 assert_eq!(lines.len(), 1);
111 }
112
113 #[test]
114 fn detects_unsafe_warnings() {
115 let lines = extract_security_lines("warning: use of unsafe block in function foo", &[]);
116 assert_eq!(lines.len(), 1);
117 }
118
119 #[test]
120 fn detects_vulnerability() {
121 let lines = extract_security_lines("vulnerability detected in dep xyz", &[]);
122 assert_eq!(lines.len(), 1);
123 }
124
125 #[test]
126 fn detects_weak_crypto() {
127 let lines = extract_security_lines(
128 "weak cipher suite selected\ninsecure hash MD5 used\ndeprecated algorithm RC4",
129 &[],
130 );
131 assert_eq!(lines.len(), 3);
132 }
133
134 #[test]
135 fn no_false_positives() {
136 let lines = extract_security_lines(
137 "Compiling zeph v0.9.0\nFinished dev [unoptimized] target(s) in 2.3s",
138 &[],
139 );
140 assert!(lines.is_empty());
141 }
142
143 #[test]
144 fn extra_patterns_work() {
145 let extra = compile_extra_patterns(&["TODO: security review".to_owned()]);
146 let lines = extract_security_lines("TODO: security review needed here", &extra);
147 assert_eq!(lines.len(), 1);
148 }
149
150 #[test]
151 fn compile_extra_warns_on_invalid() {
152 let extra = compile_extra_patterns(&["valid".to_owned(), "[invalid".to_owned()]);
153 assert_eq!(extra.len(), 1);
154 }
155
156 #[test]
157 fn append_does_nothing_on_clean_output() {
158 let mut filtered = "clean output".to_owned();
159 append_security_warnings(&mut filtered, "no warnings here", &[]);
160 assert_eq!(filtered, "clean output");
161 }
162
163 #[test]
164 fn append_adds_security_section() {
165 let mut filtered = "filtered result".to_owned();
166 append_security_warnings(&mut filtered, "thread 'main' panicked at 'oops'", &[]);
167 assert!(filtered.contains("--- Security Warnings (preserved) ---"));
168 assert!(filtered.contains("panicked"));
169 }
170
171 #[test]
172 fn integration_filter_removes_security_restored() {
173 let raw = "normal output\nthread 'main' panicked at 'assertion failed'\nmore normal";
174 let mut filtered = "normal output\nmore normal".to_owned();
175 append_security_warnings(&mut filtered, raw, &[]);
176 assert!(filtered.contains("panicked"));
177 }
178}