aptu_core/security/
ignore.rs1use std::fs;
9use std::path::PathBuf;
10
11use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13
14use super::Finding;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SecurityConfig {
24 #[serde(default)]
26 pub ignore_patterns: Vec<String>,
27
28 #[serde(default)]
30 pub ignore_paths: Vec<String>,
31}
32
33impl Default for SecurityConfig {
34 fn default() -> Self {
39 Self {
40 ignore_patterns: vec![],
41 ignore_paths: vec![
42 "tests/".to_string(),
43 "test/".to_string(),
44 "benches/".to_string(),
45 "fixtures/".to_string(),
46 "vendor/".to_string(),
47 ],
48 }
49 }
50}
51
52impl SecurityConfig {
53 #[must_use]
57 #[deprecated(since = "0.6.0", note = "Use `SecurityConfig::default()` instead")]
58 pub fn with_defaults() -> Self {
59 Self::default()
60 }
61
62 #[must_use]
66 pub fn empty() -> Self {
67 Self {
68 ignore_patterns: vec![],
69 ignore_paths: vec![],
70 }
71 }
72
73 #[must_use]
86 pub fn should_ignore_path(&self, file_path: &str) -> bool {
87 self.ignore_paths
88 .iter()
89 .any(|prefix| file_path.starts_with(prefix))
90 }
91
92 #[must_use]
100 pub fn load() -> Self {
101 if let Some(path) = Self::config_path() {
102 match Self::load_from_path(&path) {
103 Ok(config) => config,
104 Err(e) => {
105 tracing::warn!("Failed to load security config: {:#}", e);
106 Self::default()
107 }
108 }
109 } else {
110 tracing::warn!("Config directory not available, using default security config");
111 Self::default()
112 }
113 }
114
115 #[must_use]
119 pub fn config_path() -> Option<PathBuf> {
120 dirs::config_dir().map(|dir| dir.join("aptu").join("security.toml"))
121 }
122
123 fn load_from_path(path: &PathBuf) -> Result<Self> {
133 if !path.exists() {
134 return Ok(Self::default());
135 }
136
137 let contents = fs::read_to_string(path)
138 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
139
140 toml::from_str(&contents)
141 .with_context(|| format!("Failed to parse config file: {}", path.display()))
142 }
143
144 #[must_use]
158 pub fn should_ignore(&self, finding: &Finding) -> bool {
159 if self.ignore_patterns.contains(&finding.pattern_id) {
161 return true;
162 }
163
164 for prefix in &self.ignore_paths {
166 if finding.file_path.starts_with(prefix) {
167 return true;
168 }
169 }
170
171 false
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::security::{Confidence, Severity};
179
180 #[test]
181 fn test_security_config_default_has_sensible_paths() {
182 let config = SecurityConfig::default();
183 assert!(config.ignore_patterns.is_empty());
184 assert_eq!(config.ignore_paths.len(), 5);
185 assert!(config.ignore_paths.contains(&"tests/".to_string()));
186 assert!(config.ignore_paths.contains(&"test/".to_string()));
187 assert!(config.ignore_paths.contains(&"benches/".to_string()));
188 assert!(config.ignore_paths.contains(&"fixtures/".to_string()));
189 assert!(config.ignore_paths.contains(&"vendor/".to_string()));
190 }
191
192 #[test]
193 fn test_empty_config() {
194 let config = SecurityConfig::empty();
195 assert!(config.ignore_patterns.is_empty());
196 assert!(config.ignore_paths.is_empty());
197 }
198
199 #[test]
200 #[allow(deprecated)]
201 fn test_with_defaults_deprecated() {
202 let config = SecurityConfig::with_defaults();
204 assert!(config.ignore_patterns.is_empty());
205 assert_eq!(config.ignore_paths.len(), 5);
206 }
207
208 #[test]
209 fn test_should_ignore_path_method() {
210 let config = SecurityConfig::default();
211
212 assert!(config.should_ignore_path("tests/unit/test.rs"));
214 assert!(config.should_ignore_path("test/fixtures/data.rs"));
215 assert!(config.should_ignore_path("vendor/lib.rs"));
216
217 assert!(!config.should_ignore_path("src/main.rs"));
219 assert!(!config.should_ignore_path("src/test.rs"));
220 }
221
222 #[test]
223 fn test_should_ignore_pattern() {
224 let config = SecurityConfig {
225 ignore_patterns: vec!["test-pattern".to_string(), "another-pattern".to_string()],
226 ignore_paths: vec![],
227 };
228
229 let finding = Finding {
230 pattern_id: "test-pattern".to_string(),
231 description: "Test".to_string(),
232 severity: Severity::Low,
233 confidence: Confidence::Low,
234 file_path: "src/main.rs".to_string(),
235 line_number: 1,
236 matched_text: "test".to_string(),
237 cwe: None,
238 };
239
240 assert!(config.should_ignore(&finding));
241 }
242
243 #[test]
244 fn test_should_ignore_path() {
245 let config = SecurityConfig {
246 ignore_patterns: vec![],
247 ignore_paths: vec!["test/".to_string(), "vendor/".to_string()],
248 };
249
250 let finding = Finding {
251 pattern_id: "pattern".to_string(),
252 description: "Test".to_string(),
253 severity: Severity::Low,
254 confidence: Confidence::Low,
255 file_path: "test/fixtures/data.rs".to_string(),
256 line_number: 1,
257 matched_text: "test".to_string(),
258 cwe: None,
259 };
260
261 assert!(config.should_ignore(&finding));
262 }
263
264 #[test]
265 fn test_should_not_ignore() {
266 let config = SecurityConfig {
267 ignore_patterns: vec!["other-pattern".to_string()],
268 ignore_paths: vec!["vendor/".to_string()],
269 };
270
271 let finding = Finding {
272 pattern_id: "real-pattern".to_string(),
273 description: "Test".to_string(),
274 severity: Severity::High,
275 confidence: Confidence::High,
276 file_path: "src/main.rs".to_string(),
277 line_number: 42,
278 matched_text: "code".to_string(),
279 cwe: Some("CWE-123".to_string()),
280 };
281
282 assert!(!config.should_ignore(&finding));
283 }
284
285 #[test]
286 fn test_should_ignore_path_prefix() {
287 let config = SecurityConfig {
288 ignore_patterns: vec![],
289 ignore_paths: vec!["test/".to_string()],
290 };
291
292 let finding1 = Finding {
294 pattern_id: "pattern".to_string(),
295 description: "Test".to_string(),
296 severity: Severity::Low,
297 confidence: Confidence::Low,
298 file_path: "test/unit/test.rs".to_string(),
299 line_number: 1,
300 matched_text: "test".to_string(),
301 cwe: None,
302 };
303 assert!(config.should_ignore(&finding1));
304
305 let finding2 = Finding {
307 pattern_id: "pattern".to_string(),
308 description: "Test".to_string(),
309 severity: Severity::Low,
310 confidence: Confidence::Low,
311 file_path: "src/test.rs".to_string(),
312 line_number: 1,
313 matched_text: "test".to_string(),
314 cwe: None,
315 };
316 assert!(!config.should_ignore(&finding2));
317 }
318
319 #[test]
320 fn test_config_serialization() {
321 let config = SecurityConfig {
322 ignore_patterns: vec!["pattern1".to_string(), "pattern2".to_string()],
323 ignore_paths: vec!["test/".to_string(), "vendor/".to_string()],
324 };
325
326 let toml = toml::to_string(&config).expect("serialize");
327 let deserialized: SecurityConfig = toml::from_str(&toml).expect("deserialize");
328
329 assert_eq!(config.ignore_patterns, deserialized.ignore_patterns);
330 assert_eq!(config.ignore_paths, deserialized.ignore_paths);
331 }
332
333 #[test]
334 fn test_load_nonexistent_file_returns_defaults() {
335 let path = PathBuf::from("/nonexistent/path/security.toml");
336 let config = SecurityConfig::load_from_path(&path).expect("load default");
337 assert!(config.ignore_patterns.is_empty());
339 assert_eq!(config.ignore_paths.len(), 5);
340 }
341
342 #[test]
343 fn test_config_path() {
344 if let Some(path) = SecurityConfig::config_path() {
345 assert!(path.ends_with("aptu/security.toml"));
346 }
347 }
349}