Skip to main content

cc_audit/engine/
traits.rs

1//! Detection engine traits for the detection layer (L5).
2
3use crate::parser::ParsedContent;
4use crate::rules::{Finding, Severity};
5use serde::{Deserialize, Serialize};
6
7/// Configuration for the detection engine.
8#[derive(Debug, Clone, Default)]
9pub struct EngineConfig {
10    /// Enable deobfuscation.
11    pub deobfuscate: bool,
12    /// Enable malware database scanning.
13    pub malware_scan: bool,
14    /// Enable CVE database scanning.
15    pub cve_scan: bool,
16    /// Minimum severity to report.
17    pub min_severity: Option<Severity>,
18    /// Rules to skip.
19    pub skip_rules: Vec<String>,
20    /// Context to provide to rules.
21    pub context: Option<String>,
22}
23
24impl EngineConfig {
25    /// Create a new engine config with defaults.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Enable deobfuscation.
31    pub fn with_deobfuscation(mut self, enabled: bool) -> Self {
32        self.deobfuscate = enabled;
33        self
34    }
35
36    /// Enable malware scanning.
37    pub fn with_malware_scan(mut self, enabled: bool) -> Self {
38        self.malware_scan = enabled;
39        self
40    }
41
42    /// Enable CVE scanning.
43    pub fn with_cve_scan(mut self, enabled: bool) -> Self {
44        self.cve_scan = enabled;
45        self
46    }
47
48    /// Set minimum severity.
49    pub fn with_min_severity(mut self, severity: Severity) -> Self {
50        self.min_severity = Some(severity);
51        self
52    }
53
54    /// Add rules to skip.
55    pub fn skip_rule(mut self, rule_id: &str) -> Self {
56        self.skip_rules.push(rule_id.to_string());
57        self
58    }
59}
60
61/// Result of analyzing content with a detection engine.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct AnalysisResult {
64    /// Findings from the analysis.
65    pub findings: Vec<Finding>,
66    /// Whether the content was deobfuscated.
67    pub deobfuscated: bool,
68    /// Number of rules applied.
69    pub rules_applied: usize,
70    /// Analysis metadata.
71    pub metadata: AnalysisMetadata,
72}
73
74/// Metadata about the analysis.
75#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct AnalysisMetadata {
77    /// Time taken for analysis in milliseconds.
78    pub duration_ms: u64,
79    /// Number of patterns matched.
80    pub patterns_matched: usize,
81    /// Context detected (if any).
82    pub detected_context: Option<String>,
83}
84
85impl AnalysisResult {
86    /// Create a new empty analysis result.
87    pub fn empty() -> Self {
88        Self {
89            findings: Vec::new(),
90            deobfuscated: false,
91            rules_applied: 0,
92            metadata: AnalysisMetadata::default(),
93        }
94    }
95
96    /// Create a result with findings.
97    pub fn with_findings(findings: Vec<Finding>) -> Self {
98        Self {
99            findings,
100            deobfuscated: false,
101            rules_applied: 0,
102            metadata: AnalysisMetadata::default(),
103        }
104    }
105
106    /// Check if any findings were detected.
107    pub fn has_findings(&self) -> bool {
108        !self.findings.is_empty()
109    }
110
111    /// Get the highest severity finding.
112    pub fn highest_severity(&self) -> Option<Severity> {
113        self.findings.iter().map(|f| f.severity).max()
114    }
115}
116
117/// Trait for detection engines (L5).
118///
119/// Each engine analyzes parsed content and produces findings.
120/// Engines can be composed together for comprehensive analysis.
121pub trait DetectionEngine: Send + Sync {
122    /// Analyze parsed content and return findings.
123    fn analyze(&self, content: &ParsedContent, config: &EngineConfig) -> AnalysisResult;
124
125    /// Get the name of this engine.
126    fn name(&self) -> &str;
127
128    /// Check if this engine can analyze the given content type.
129    fn can_analyze(&self, content: &ParsedContent) -> bool;
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_engine_config_builder() {
138        let config = EngineConfig::new()
139            .with_deobfuscation(true)
140            .with_malware_scan(true)
141            .skip_rule("PI-001");
142
143        assert!(config.deobfuscate);
144        assert!(config.malware_scan);
145        assert!(config.skip_rules.contains(&"PI-001".to_string()));
146    }
147
148    #[test]
149    fn test_analysis_result_empty() {
150        let result = AnalysisResult::empty();
151        assert!(!result.has_findings());
152        assert!(result.highest_severity().is_none());
153    }
154
155    #[test]
156    fn test_analysis_result_with_findings() {
157        use crate::rules::{Category, Confidence, Location};
158
159        let finding = Finding {
160            id: "TEST-001".to_string(),
161            severity: Severity::Medium,
162            category: Category::PromptInjection,
163            confidence: Confidence::Firm,
164            name: "Test Finding".to_string(),
165            location: Location {
166                file: "test.md".to_string(),
167                line: 1,
168                column: None,
169            },
170            code: "test code".to_string(),
171            message: "Test finding message".to_string(),
172            recommendation: "Fix it".to_string(),
173            fix_hint: None,
174            cwe_ids: Vec::new(),
175            rule_severity: None,
176            client: None,
177            context: None,
178        };
179
180        let result = AnalysisResult::with_findings(vec![finding]);
181        assert!(result.has_findings());
182        assert_eq!(result.highest_severity(), Some(Severity::Medium));
183    }
184}