Skip to main content

cc_audit/rules/
types.rs

1use crate::scoring::RiskScore;
2use serde::{Deserialize, Serialize};
3
4/// Rule severity level - determines how findings affect CI exit code.
5/// This is separate from detection Severity (Critical/High/Medium/Low).
6#[derive(
7    Debug,
8    Clone,
9    Copy,
10    PartialEq,
11    Eq,
12    PartialOrd,
13    Ord,
14    Serialize,
15    Deserialize,
16    Default,
17    clap::ValueEnum,
18)]
19#[serde(rename_all = "lowercase")]
20pub enum RuleSeverity {
21    /// Warning: Report only, does not cause CI failure (exit 0)
22    Warn,
23    /// Error: Causes CI failure (exit 1)
24    #[default]
25    Error,
26}
27
28impl RuleSeverity {
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            RuleSeverity::Warn => "warn",
32            RuleSeverity::Error => "error",
33        }
34    }
35}
36
37impl std::fmt::Display for RuleSeverity {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{}", self.as_str().to_uppercase())
40    }
41}
42
43#[derive(
44    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, clap::ValueEnum,
45)]
46#[serde(rename_all = "lowercase")]
47pub enum Severity {
48    Low,
49    Medium,
50    High,
51    Critical,
52}
53
54/// Confidence level for findings. Higher confidence means less likely to be a false positive.
55#[derive(
56    Debug,
57    Clone,
58    Copy,
59    PartialEq,
60    Eq,
61    PartialOrd,
62    Ord,
63    Serialize,
64    Deserialize,
65    Default,
66    clap::ValueEnum,
67)]
68#[serde(rename_all = "lowercase")]
69pub enum Confidence {
70    /// Tentative: May be a false positive, requires review
71    Tentative,
72    /// Firm: Likely a real issue, but context-dependent
73    #[default]
74    Firm,
75    /// Certain: Very high confidence, unlikely to be a false positive
76    Certain,
77}
78
79impl Confidence {
80    pub fn as_str(&self) -> &'static str {
81        match self {
82            Confidence::Tentative => "tentative",
83            Confidence::Firm => "firm",
84            Confidence::Certain => "certain",
85        }
86    }
87}
88
89impl std::fmt::Display for Confidence {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "{}", self.as_str())
92    }
93}
94
95impl Severity {
96    pub fn as_str(&self) -> &'static str {
97        match self {
98            Severity::Low => "low",
99            Severity::Medium => "medium",
100            Severity::High => "high",
101            Severity::Critical => "critical",
102        }
103    }
104}
105
106impl std::fmt::Display for Severity {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{}", self.as_str().to_uppercase())
109    }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
113#[serde(rename_all = "lowercase")]
114pub enum Category {
115    Exfiltration,
116    PrivilegeEscalation,
117    Persistence,
118    PromptInjection,
119    Overpermission,
120    Obfuscation,
121    SupplyChain,
122    SecretLeak,
123}
124
125impl Category {
126    pub fn as_str(&self) -> &'static str {
127        match self {
128            Category::Exfiltration => "exfiltration",
129            Category::PrivilegeEscalation => "privilege_escalation",
130            Category::Persistence => "persistence",
131            Category::PromptInjection => "prompt_injection",
132            Category::Overpermission => "overpermission",
133            Category::Obfuscation => "obfuscation",
134            Category::SupplyChain => "supply_chain",
135            Category::SecretLeak => "secret_leak",
136        }
137    }
138}
139
140#[derive(Debug, Clone)]
141pub struct Rule {
142    pub id: &'static str,
143    pub name: &'static str,
144    pub description: &'static str,
145    pub severity: Severity,
146    pub category: Category,
147    pub confidence: Confidence,
148    pub patterns: Vec<regex::Regex>,
149    pub exclusions: Vec<regex::Regex>,
150    pub message: &'static str,
151    pub recommendation: &'static str,
152    /// Optional concrete fix hint (e.g., command to run, code pattern to use)
153    pub fix_hint: Option<&'static str>,
154    /// CWE IDs associated with this rule (e.g., ["CWE-200", "CWE-78"])
155    pub cwe_ids: &'static [&'static str],
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct Location {
160    pub file: String,
161    pub line: usize,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub column: Option<usize>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct Finding {
168    pub id: String,
169    pub severity: Severity,
170    pub category: Category,
171    pub confidence: Confidence,
172    pub name: String,
173    pub location: Location,
174    pub code: String,
175    pub message: String,
176    pub recommendation: String,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub fix_hint: Option<String>,
179    /// CWE IDs associated with this finding
180    #[serde(default, skip_serializing_if = "Vec::is_empty")]
181    pub cwe_ids: Vec<String>,
182    /// Rule severity level (error/warn) - determines CI exit code behavior.
183    /// This is assigned based on configuration, not the rule definition.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub rule_severity: Option<RuleSeverity>,
186}
187
188impl Finding {
189    pub fn new(rule: &Rule, location: Location, code: String) -> Self {
190        Self {
191            id: rule.id.to_string(),
192            severity: rule.severity,
193            category: rule.category,
194            confidence: rule.confidence,
195            name: rule.name.to_string(),
196            location,
197            code,
198            message: rule.message.to_string(),
199            recommendation: rule.recommendation.to_string(),
200            fix_hint: rule.fix_hint.map(|s| s.to_string()),
201            cwe_ids: rule.cwe_ids.iter().map(|s| s.to_string()).collect(),
202            rule_severity: None, // Assigned later based on config
203        }
204    }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct Summary {
209    pub critical: usize,
210    pub high: usize,
211    pub medium: usize,
212    pub low: usize,
213    pub passed: bool,
214    /// Number of findings with RuleSeverity::Error
215    #[serde(default)]
216    pub errors: usize,
217    /// Number of findings with RuleSeverity::Warn
218    #[serde(default)]
219    pub warnings: usize,
220}
221
222impl Summary {
223    /// Creates a Summary from findings.
224    /// Note: This method does not set errors/warnings counts.
225    /// Use `from_findings_with_rule_severity` when rule_severity is assigned.
226    pub fn from_findings(findings: &[Finding]) -> Self {
227        let (critical, high, medium, low) =
228            findings
229                .iter()
230                .fold((0, 0, 0, 0), |(c, h, m, l), f| match f.severity {
231                    Severity::Critical => (c + 1, h, m, l),
232                    Severity::High => (c, h + 1, m, l),
233                    Severity::Medium => (c, h, m + 1, l),
234                    Severity::Low => (c, h, m, l + 1),
235                });
236
237        Self {
238            critical,
239            high,
240            medium,
241            low,
242            passed: critical == 0 && high == 0,
243            errors: 0,
244            warnings: 0,
245        }
246    }
247
248    /// Creates a Summary from findings with rule_severity counts.
249    /// The `passed` field is determined by whether there are any errors.
250    pub fn from_findings_with_rule_severity(findings: &[Finding]) -> Self {
251        let (critical, high, medium, low, errors, warnings) =
252            findings
253                .iter()
254                .fold((0, 0, 0, 0, 0, 0), |(c, h, m, l, e, w), f| {
255                    let (new_c, new_h, new_m, new_l) = match f.severity {
256                        Severity::Critical => (c + 1, h, m, l),
257                        Severity::High => (c, h + 1, m, l),
258                        Severity::Medium => (c, h, m + 1, l),
259                        Severity::Low => (c, h, m, l + 1),
260                    };
261                    let (new_e, new_w) = match f.rule_severity {
262                        Some(RuleSeverity::Error) | None => (e + 1, w), // Default to error
263                        Some(RuleSeverity::Warn) => (e, w + 1),
264                    };
265                    (new_c, new_h, new_m, new_l, new_e, new_w)
266                });
267
268        Self {
269            critical,
270            high,
271            medium,
272            low,
273            passed: errors == 0, // Pass only if no errors
274            errors,
275            warnings,
276        }
277    }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct ScanResult {
282    pub version: String,
283    pub scanned_at: String,
284    pub target: String,
285    pub summary: Summary,
286    pub findings: Vec<Finding>,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub risk_score: Option<RiskScore>,
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_severity_as_str() {
297        assert_eq!(Severity::Low.as_str(), "low");
298        assert_eq!(Severity::Medium.as_str(), "medium");
299        assert_eq!(Severity::High.as_str(), "high");
300        assert_eq!(Severity::Critical.as_str(), "critical");
301    }
302
303    #[test]
304    fn test_severity_display() {
305        assert_eq!(format!("{}", Severity::Low), "LOW");
306        assert_eq!(format!("{}", Severity::Medium), "MEDIUM");
307        assert_eq!(format!("{}", Severity::High), "HIGH");
308        assert_eq!(format!("{}", Severity::Critical), "CRITICAL");
309    }
310
311    #[test]
312    fn test_severity_ordering() {
313        assert!(Severity::Low < Severity::Medium);
314        assert!(Severity::Medium < Severity::High);
315        assert!(Severity::High < Severity::Critical);
316    }
317
318    #[test]
319    fn test_category_as_str() {
320        assert_eq!(Category::Exfiltration.as_str(), "exfiltration");
321        assert_eq!(
322            Category::PrivilegeEscalation.as_str(),
323            "privilege_escalation"
324        );
325        assert_eq!(Category::Persistence.as_str(), "persistence");
326        assert_eq!(Category::PromptInjection.as_str(), "prompt_injection");
327        assert_eq!(Category::Overpermission.as_str(), "overpermission");
328        assert_eq!(Category::Obfuscation.as_str(), "obfuscation");
329        assert_eq!(Category::SupplyChain.as_str(), "supply_chain");
330        assert_eq!(Category::SecretLeak.as_str(), "secret_leak");
331    }
332
333    #[test]
334    fn test_summary_from_empty_findings() {
335        let findings: Vec<Finding> = vec![];
336        let summary = Summary::from_findings(&findings);
337        assert_eq!(summary.critical, 0);
338        assert_eq!(summary.high, 0);
339        assert_eq!(summary.medium, 0);
340        assert_eq!(summary.low, 0);
341        assert!(summary.passed);
342    }
343
344    #[test]
345    fn test_summary_from_findings_with_critical() {
346        let findings = vec![Finding {
347            id: "EX-001".to_string(),
348            severity: Severity::Critical,
349            category: Category::Exfiltration,
350            confidence: Confidence::Certain,
351            name: "Test".to_string(),
352            location: Location {
353                file: "test.sh".to_string(),
354                line: 1,
355                column: None,
356            },
357            code: "test".to_string(),
358            message: "test".to_string(),
359            recommendation: "test".to_string(),
360            fix_hint: None,
361            cwe_ids: vec![],
362            rule_severity: None,
363        }];
364        let summary = Summary::from_findings(&findings);
365        assert_eq!(summary.critical, 1);
366        assert!(!summary.passed);
367    }
368
369    #[test]
370    fn test_summary_from_findings_all_severities() {
371        let findings = vec![
372            Finding {
373                id: "C-001".to_string(),
374                severity: Severity::Critical,
375                category: Category::Exfiltration,
376                confidence: Confidence::Certain,
377                name: "Critical".to_string(),
378                location: Location {
379                    file: "test.sh".to_string(),
380                    line: 1,
381                    column: None,
382                },
383                code: "test".to_string(),
384                message: "test".to_string(),
385                recommendation: "test".to_string(),
386                fix_hint: None,
387                cwe_ids: vec![],
388                rule_severity: None,
389            },
390            Finding {
391                id: "H-001".to_string(),
392                severity: Severity::High,
393                category: Category::PrivilegeEscalation,
394                confidence: Confidence::Firm,
395                name: "High".to_string(),
396                location: Location {
397                    file: "test.sh".to_string(),
398                    line: 2,
399                    column: None,
400                },
401                code: "test".to_string(),
402                message: "test".to_string(),
403                recommendation: "test".to_string(),
404                fix_hint: None,
405                cwe_ids: vec![],
406                rule_severity: None,
407            },
408            Finding {
409                id: "M-001".to_string(),
410                severity: Severity::Medium,
411                category: Category::Persistence,
412                confidence: Confidence::Tentative,
413                name: "Medium".to_string(),
414                location: Location {
415                    file: "test.sh".to_string(),
416                    line: 3,
417                    column: Some(5),
418                },
419                code: "test".to_string(),
420                message: "test".to_string(),
421                recommendation: "test".to_string(),
422                fix_hint: None,
423                cwe_ids: vec![],
424                rule_severity: None,
425            },
426            Finding {
427                id: "L-001".to_string(),
428                severity: Severity::Low,
429                category: Category::Overpermission,
430                confidence: Confidence::Firm,
431                name: "Low".to_string(),
432                location: Location {
433                    file: "test.sh".to_string(),
434                    line: 4,
435                    column: None,
436                },
437                code: "test".to_string(),
438                message: "test".to_string(),
439                recommendation: "test".to_string(),
440                fix_hint: None,
441                cwe_ids: vec![],
442                rule_severity: None,
443            },
444        ];
445        let summary = Summary::from_findings(&findings);
446        assert_eq!(summary.critical, 1);
447        assert_eq!(summary.high, 1);
448        assert_eq!(summary.medium, 1);
449        assert_eq!(summary.low, 1);
450        assert!(!summary.passed);
451    }
452
453    #[test]
454    fn test_summary_passes_with_only_medium_low() {
455        let findings = vec![
456            Finding {
457                id: "M-001".to_string(),
458                severity: Severity::Medium,
459                category: Category::Persistence,
460                confidence: Confidence::Firm,
461                name: "Medium".to_string(),
462                location: Location {
463                    file: "test.sh".to_string(),
464                    line: 1,
465                    column: None,
466                },
467                code: "test".to_string(),
468                message: "test".to_string(),
469                recommendation: "test".to_string(),
470                fix_hint: None,
471                cwe_ids: vec![],
472                rule_severity: None,
473            },
474            Finding {
475                id: "L-001".to_string(),
476                severity: Severity::Low,
477                category: Category::Overpermission,
478                confidence: Confidence::Firm,
479                name: "Low".to_string(),
480                location: Location {
481                    file: "test.sh".to_string(),
482                    line: 2,
483                    column: None,
484                },
485                code: "test".to_string(),
486                message: "test".to_string(),
487                recommendation: "test".to_string(),
488                fix_hint: None,
489                cwe_ids: vec![],
490                rule_severity: None,
491            },
492        ];
493        let summary = Summary::from_findings(&findings);
494        assert!(summary.passed);
495    }
496
497    #[test]
498    fn test_finding_new() {
499        let rule = Rule {
500            id: "TEST-001",
501            name: "Test Rule",
502            description: "A test rule",
503            severity: Severity::High,
504            category: Category::Exfiltration,
505            confidence: Confidence::Certain,
506            patterns: vec![],
507            exclusions: vec![],
508            message: "Test message",
509            recommendation: "Test recommendation",
510            fix_hint: Some("Test fix hint"),
511            cwe_ids: &["CWE-200", "CWE-78"],
512        };
513        let location = Location {
514            file: "test.sh".to_string(),
515            line: 42,
516            column: Some(10),
517        };
518        let finding = Finding::new(&rule, location, "test code".to_string());
519
520        assert_eq!(finding.id, "TEST-001");
521        assert_eq!(finding.name, "Test Rule");
522        assert_eq!(finding.severity, Severity::High);
523        assert_eq!(finding.category, Category::Exfiltration);
524        assert_eq!(finding.confidence, Confidence::Certain);
525        assert_eq!(finding.location.file, "test.sh");
526        assert_eq!(finding.location.line, 42);
527        assert_eq!(finding.location.column, Some(10));
528        assert_eq!(finding.code, "test code");
529        assert_eq!(finding.message, "Test message");
530        assert_eq!(finding.recommendation, "Test recommendation");
531        assert_eq!(finding.cwe_ids, vec!["CWE-200", "CWE-78"]);
532    }
533
534    #[test]
535    fn test_confidence_as_str() {
536        assert_eq!(Confidence::Tentative.as_str(), "tentative");
537        assert_eq!(Confidence::Firm.as_str(), "firm");
538        assert_eq!(Confidence::Certain.as_str(), "certain");
539    }
540
541    #[test]
542    fn test_confidence_display() {
543        assert_eq!(format!("{}", Confidence::Tentative), "tentative");
544        assert_eq!(format!("{}", Confidence::Firm), "firm");
545        assert_eq!(format!("{}", Confidence::Certain), "certain");
546    }
547
548    #[test]
549    fn test_confidence_ordering() {
550        assert!(Confidence::Tentative < Confidence::Firm);
551        assert!(Confidence::Firm < Confidence::Certain);
552    }
553
554    #[test]
555    fn test_confidence_default() {
556        assert_eq!(Confidence::default(), Confidence::Firm);
557    }
558
559    #[test]
560    fn test_confidence_serialization() {
561        let confidence = Confidence::Certain;
562        let json = serde_json::to_string(&confidence).unwrap();
563        assert_eq!(json, "\"certain\"");
564
565        let deserialized: Confidence = serde_json::from_str(&json).unwrap();
566        assert_eq!(deserialized, Confidence::Certain);
567    }
568
569    #[test]
570    fn test_severity_serialization() {
571        let severity = Severity::Critical;
572        let json = serde_json::to_string(&severity).unwrap();
573        assert_eq!(json, "\"critical\"");
574
575        let deserialized: Severity = serde_json::from_str(&json).unwrap();
576        assert_eq!(deserialized, Severity::Critical);
577    }
578
579    #[test]
580    fn test_category_serialization() {
581        let category = Category::PromptInjection;
582        let json = serde_json::to_string(&category).unwrap();
583        assert_eq!(json, "\"promptinjection\"");
584
585        let deserialized: Category = serde_json::from_str(&json).unwrap();
586        assert_eq!(deserialized, Category::PromptInjection);
587    }
588
589    #[test]
590    fn test_location_without_column_serialization() {
591        let location = Location {
592            file: "test.sh".to_string(),
593            line: 10,
594            column: None,
595        };
596        let json = serde_json::to_string(&location).unwrap();
597        assert!(!json.contains("column"));
598    }
599
600    #[test]
601    fn test_location_with_column_serialization() {
602        let location = Location {
603            file: "test.sh".to_string(),
604            line: 10,
605            column: Some(5),
606        };
607        let json = serde_json::to_string(&location).unwrap();
608        assert!(json.contains("\"column\":5"));
609    }
610
611    // ========== RuleSeverity Tests ==========
612
613    #[test]
614    fn test_rule_severity_default_is_error() {
615        assert_eq!(RuleSeverity::default(), RuleSeverity::Error);
616    }
617
618    #[test]
619    fn test_rule_severity_as_str() {
620        assert_eq!(RuleSeverity::Error.as_str(), "error");
621        assert_eq!(RuleSeverity::Warn.as_str(), "warn");
622    }
623
624    #[test]
625    fn test_rule_severity_display() {
626        assert_eq!(format!("{}", RuleSeverity::Error), "ERROR");
627        assert_eq!(format!("{}", RuleSeverity::Warn), "WARN");
628    }
629
630    #[test]
631    fn test_rule_severity_ordering() {
632        // Warn < Error (warn is less severe)
633        assert!(RuleSeverity::Warn < RuleSeverity::Error);
634    }
635
636    #[test]
637    fn test_rule_severity_serialization() {
638        let error = RuleSeverity::Error;
639        let json = serde_json::to_string(&error).unwrap();
640        assert_eq!(json, "\"error\"");
641
642        let warn = RuleSeverity::Warn;
643        let json = serde_json::to_string(&warn).unwrap();
644        assert_eq!(json, "\"warn\"");
645
646        let deserialized: RuleSeverity = serde_json::from_str("\"error\"").unwrap();
647        assert_eq!(deserialized, RuleSeverity::Error);
648
649        let deserialized: RuleSeverity = serde_json::from_str("\"warn\"").unwrap();
650        assert_eq!(deserialized, RuleSeverity::Warn);
651    }
652
653    // ========== Summary with RuleSeverity Tests ==========
654
655    fn create_test_finding(
656        id: &str,
657        severity: Severity,
658        rule_severity: Option<RuleSeverity>,
659    ) -> Finding {
660        Finding {
661            id: id.to_string(),
662            severity,
663            category: Category::Exfiltration,
664            confidence: Confidence::Firm,
665            name: "Test".to_string(),
666            location: Location {
667                file: "test.sh".to_string(),
668                line: 1,
669                column: None,
670            },
671            code: "test".to_string(),
672            message: "test".to_string(),
673            recommendation: "test".to_string(),
674            fix_hint: None,
675            cwe_ids: vec![],
676            rule_severity,
677        }
678    }
679
680    #[test]
681    fn test_summary_with_rule_severity_empty() {
682        let findings: Vec<Finding> = vec![];
683        let summary = Summary::from_findings_with_rule_severity(&findings);
684        assert_eq!(summary.errors, 0);
685        assert_eq!(summary.warnings, 0);
686        assert!(summary.passed);
687    }
688
689    #[test]
690    fn test_summary_with_rule_severity_all_errors() {
691        let findings = vec![
692            create_test_finding("E-001", Severity::Critical, Some(RuleSeverity::Error)),
693            create_test_finding("E-002", Severity::High, Some(RuleSeverity::Error)),
694        ];
695        let summary = Summary::from_findings_with_rule_severity(&findings);
696        assert_eq!(summary.errors, 2);
697        assert_eq!(summary.warnings, 0);
698        assert!(!summary.passed);
699    }
700
701    #[test]
702    fn test_summary_with_rule_severity_all_warnings() {
703        let findings = vec![
704            create_test_finding("W-001", Severity::Critical, Some(RuleSeverity::Warn)),
705            create_test_finding("W-002", Severity::High, Some(RuleSeverity::Warn)),
706        ];
707        let summary = Summary::from_findings_with_rule_severity(&findings);
708        assert_eq!(summary.errors, 0);
709        assert_eq!(summary.warnings, 2);
710        assert!(summary.passed); // No errors, so passed
711    }
712
713    #[test]
714    fn test_summary_with_rule_severity_mixed() {
715        let findings = vec![
716            create_test_finding("E-001", Severity::Critical, Some(RuleSeverity::Error)),
717            create_test_finding("W-001", Severity::High, Some(RuleSeverity::Warn)),
718            create_test_finding("W-002", Severity::Medium, Some(RuleSeverity::Warn)),
719        ];
720        let summary = Summary::from_findings_with_rule_severity(&findings);
721        assert_eq!(summary.errors, 1);
722        assert_eq!(summary.warnings, 2);
723        assert!(!summary.passed); // Has errors, so failed
724        // Also check severity counts
725        assert_eq!(summary.critical, 1);
726        assert_eq!(summary.high, 1);
727        assert_eq!(summary.medium, 1);
728    }
729
730    #[test]
731    fn test_summary_with_rule_severity_none_defaults_to_error() {
732        let findings = vec![
733            create_test_finding("N-001", Severity::Low, None), // None defaults to Error
734        ];
735        let summary = Summary::from_findings_with_rule_severity(&findings);
736        assert_eq!(summary.errors, 1);
737        assert_eq!(summary.warnings, 0);
738        assert!(!summary.passed);
739    }
740
741    #[test]
742    fn test_finding_rule_severity_not_serialized_when_none() {
743        let finding = create_test_finding("TEST-001", Severity::High, None);
744        let json = serde_json::to_string(&finding).unwrap();
745        assert!(!json.contains("rule_severity"));
746    }
747
748    #[test]
749    fn test_finding_rule_severity_serialized_when_some() {
750        let finding = create_test_finding("TEST-001", Severity::High, Some(RuleSeverity::Warn));
751        let json = serde_json::to_string(&finding).unwrap();
752        assert!(json.contains("\"rule_severity\":\"warn\""));
753    }
754}