Skip to main content

zeph_tools/filter/
security.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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/// Pre-compile extra security patterns from user config strings.
34#[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}