cc_audit/engine/
traits.rs1use crate::parser::ParsedContent;
4use crate::rules::{Finding, Severity};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Default)]
9pub struct EngineConfig {
10 pub deobfuscate: bool,
12 pub malware_scan: bool,
14 pub cve_scan: bool,
16 pub min_severity: Option<Severity>,
18 pub skip_rules: Vec<String>,
20 pub context: Option<String>,
22}
23
24impl EngineConfig {
25 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn with_deobfuscation(mut self, enabled: bool) -> Self {
32 self.deobfuscate = enabled;
33 self
34 }
35
36 pub fn with_malware_scan(mut self, enabled: bool) -> Self {
38 self.malware_scan = enabled;
39 self
40 }
41
42 pub fn with_cve_scan(mut self, enabled: bool) -> Self {
44 self.cve_scan = enabled;
45 self
46 }
47
48 pub fn with_min_severity(mut self, severity: Severity) -> Self {
50 self.min_severity = Some(severity);
51 self
52 }
53
54 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#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct AnalysisResult {
64 pub findings: Vec<Finding>,
66 pub deobfuscated: bool,
68 pub rules_applied: usize,
70 pub metadata: AnalysisMetadata,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct AnalysisMetadata {
77 pub duration_ms: u64,
79 pub patterns_matched: usize,
81 pub detected_context: Option<String>,
83}
84
85impl AnalysisResult {
86 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 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 pub fn has_findings(&self) -> bool {
108 !self.findings.is_empty()
109 }
110
111 pub fn highest_severity(&self) -> Option<Severity> {
113 self.findings.iter().map(|f| f.severity).max()
114 }
115}
116
117pub trait DetectionEngine: Send + Sync {
122 fn analyze(&self, content: &ParsedContent, config: &EngineConfig) -> AnalysisResult;
124
125 fn name(&self) -> &str;
127
128 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}