aptu_core/security/
detection.rs1#[must_use]
20pub fn needs_security_scan(file_paths: &[String], labels: &[String], description: &str) -> bool {
21 if labels.iter().any(|label| {
23 let lower = label.to_lowercase();
24 lower.contains("security")
25 || lower.contains("vulnerability")
26 || lower.contains("cve")
27 || lower.contains("exploit")
28 }) {
29 return true;
30 }
31
32 let desc_lower = description.to_lowercase();
34 if desc_lower.contains("security")
35 || desc_lower.contains("vulnerability")
36 || desc_lower.contains("exploit")
37 || desc_lower.contains("injection")
38 || desc_lower.contains("xss")
39 || desc_lower.contains("csrf")
40 || desc_lower.contains("authentication")
41 || desc_lower.contains("authorization")
42 || desc_lower.contains("crypto")
43 || desc_lower.contains("password")
44 || desc_lower.contains("secret")
45 || desc_lower.contains("token")
46 || desc_lower.contains("jwt")
47 || desc_lower.contains("oauth")
48 || desc_lower.contains("session")
49 || desc_lower.contains("mfa")
50 {
51 return true;
52 }
53
54 for path in file_paths {
56 let path_lower = path.to_lowercase();
57
58 if path_lower.contains("/auth")
60 || path_lower.contains("/security")
61 || path_lower.contains("/crypto")
62 || path_lower.contains("/password")
63 || path_lower.contains("/session")
64 || path_lower.contains("/oauth")
65 || path_lower.contains("/jwt")
66 {
67 return true;
68 }
69
70 let path_obj = std::path::Path::new(&path_lower);
72 if path_obj
73 .extension()
74 .is_some_and(|ext| ext.eq_ignore_ascii_case("env"))
75 || path_lower.ends_with(".env.example")
76 || path_lower.contains("config")
77 || path_lower.contains("secret")
78 || path_lower.contains("credential")
79 {
80 return true;
81 }
82
83 if path_obj
85 .extension()
86 .is_some_and(|ext| ext.eq_ignore_ascii_case("sql"))
87 || path_lower.contains("migration")
88 || path_lower.contains("database")
89 {
90 return true;
91 }
92
93 if path_lower.contains("login")
95 || path_lower.contains("signin")
96 || path_lower.contains("signup")
97 || path_lower.contains("register")
98 {
99 return true;
100 }
101 }
102
103 false
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_security_label_triggers_scan() {
113 assert!(needs_security_scan(&[], &["security".to_string()], ""));
114 assert!(needs_security_scan(&[], &["vulnerability".to_string()], ""));
115 assert!(needs_security_scan(
116 &[],
117 &["bug".to_string(), "Security Fix".to_string()],
118 ""
119 ));
120 }
121
122 #[test]
123 fn test_description_keywords_trigger_scan() {
124 assert!(needs_security_scan(
125 &[],
126 &[],
127 "Fix security vulnerability in auth"
128 ));
129 assert!(needs_security_scan(
130 &[],
131 &[],
132 "Prevent SQL injection attack"
133 ));
134 assert!(needs_security_scan(
135 &[],
136 &[],
137 "Update password hashing algorithm"
138 ));
139 assert!(needs_security_scan(&[], &[], "Remove hardcoded API token"));
140 }
141
142 #[test]
143 fn test_sensitive_file_paths_trigger_scan() {
144 assert!(needs_security_scan(
145 &["src/auth/login.rs".to_string()],
146 &[],
147 ""
148 ));
149 assert!(needs_security_scan(
150 &["config/secrets.yml".to_string()],
151 &[],
152 ""
153 ));
154 assert!(needs_security_scan(&[".env.example".to_string()], &[], ""));
155 assert!(needs_security_scan(
156 &["migrations/001_users.sql".to_string()],
157 &[],
158 ""
159 ));
160 assert!(needs_security_scan(
161 &["src/security/scanner.rs".to_string()],
162 &[],
163 ""
164 ));
165 }
166
167 #[test]
168 fn test_no_scan_for_regular_changes() {
169 assert!(!needs_security_scan(
170 &["README.md".to_string()],
171 &[],
172 "Update documentation"
173 ));
174 assert!(!needs_security_scan(
175 &["src/utils.rs".to_string()],
176 &["enhancement".to_string()],
177 "Add helper function"
178 ));
179 assert!(!needs_security_scan(
180 &["tests/test_utils.rs".to_string()],
181 &["test".to_string()],
182 "Add unit tests"
183 ));
184 }
185
186 #[test]
187 fn test_case_insensitive_matching() {
188 assert!(needs_security_scan(&[], &["SECURITY".to_string()], ""));
189 assert!(needs_security_scan(&[], &[], "SECURITY FIX"));
190 assert!(needs_security_scan(
191 &["SRC/AUTH/LOGIN.RS".to_string()],
192 &[],
193 ""
194 ));
195 }
196
197 #[test]
198 fn test_multiple_conditions() {
199 assert!(needs_security_scan(
201 &["src/auth/login.rs".to_string()],
202 &["security".to_string()],
203 "Fix authentication bug"
204 ));
205 }
206
207 #[test]
208 fn test_crypto_related_changes() {
209 assert!(needs_security_scan(
210 &["src/crypto/hash.rs".to_string()],
211 &[],
212 ""
213 ));
214 assert!(needs_security_scan(
215 &[],
216 &[],
217 "Update cryptographic library"
218 ));
219 }
220
221 #[test]
222 fn test_identity_related_keywords() {
223 assert!(needs_security_scan(&[], &[], "Update JWT token handling"));
224 assert!(needs_security_scan(&[], &[], "Fix OAuth2 flow"));
225 assert!(needs_security_scan(
226 &[],
227 &[],
228 "Session management improvements"
229 ));
230 assert!(needs_security_scan(&[], &[], "Add MFA support"));
231 assert!(needs_security_scan(
232 &["src/session/store.rs".to_string()],
233 &[],
234 ""
235 ));
236 assert!(needs_security_scan(
237 &["src/oauth/provider.rs".to_string()],
238 &[],
239 ""
240 ));
241 assert!(needs_security_scan(
242 &["src/jwt/validator.rs".to_string()],
243 &[],
244 ""
245 ));
246 }
247}