use crate::parser::ParsedContent;
use crate::rules::{Finding, Severity};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default)]
pub struct EngineConfig {
pub deobfuscate: bool,
pub malware_scan: bool,
pub cve_scan: bool,
pub min_severity: Option<Severity>,
pub skip_rules: Vec<String>,
pub context: Option<String>,
}
impl EngineConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_deobfuscation(mut self, enabled: bool) -> Self {
self.deobfuscate = enabled;
self
}
pub fn with_malware_scan(mut self, enabled: bool) -> Self {
self.malware_scan = enabled;
self
}
pub fn with_cve_scan(mut self, enabled: bool) -> Self {
self.cve_scan = enabled;
self
}
pub fn with_min_severity(mut self, severity: Severity) -> Self {
self.min_severity = Some(severity);
self
}
pub fn skip_rule(mut self, rule_id: &str) -> Self {
self.skip_rules.push(rule_id.to_string());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisResult {
pub findings: Vec<Finding>,
pub deobfuscated: bool,
pub rules_applied: usize,
pub metadata: AnalysisMetadata,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnalysisMetadata {
pub duration_ms: u64,
pub patterns_matched: usize,
pub detected_context: Option<String>,
}
impl AnalysisResult {
pub fn empty() -> Self {
Self {
findings: Vec::new(),
deobfuscated: false,
rules_applied: 0,
metadata: AnalysisMetadata::default(),
}
}
pub fn with_findings(findings: Vec<Finding>) -> Self {
Self {
findings,
deobfuscated: false,
rules_applied: 0,
metadata: AnalysisMetadata::default(),
}
}
pub fn has_findings(&self) -> bool {
!self.findings.is_empty()
}
pub fn highest_severity(&self) -> Option<Severity> {
self.findings.iter().map(|f| f.severity).max()
}
}
pub trait DetectionEngine: Send + Sync {
fn analyze(&self, content: &ParsedContent, config: &EngineConfig) -> AnalysisResult;
fn name(&self) -> &str;
fn can_analyze(&self, content: &ParsedContent) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_engine_config_builder() {
let config = EngineConfig::new()
.with_deobfuscation(true)
.with_malware_scan(true)
.skip_rule("PI-001");
assert!(config.deobfuscate);
assert!(config.malware_scan);
assert!(config.skip_rules.contains(&"PI-001".to_string()));
}
#[test]
fn test_analysis_result_empty() {
let result = AnalysisResult::empty();
assert!(!result.has_findings());
assert!(result.highest_severity().is_none());
}
#[test]
fn test_analysis_result_with_findings() {
use crate::rules::{Category, Confidence, Location};
let finding = Finding {
id: "TEST-001".to_string(),
severity: Severity::Medium,
category: Category::PromptInjection,
confidence: Confidence::Firm,
name: "Test Finding".to_string(),
location: Location {
file: "test.md".to_string(),
line: 1,
column: None,
},
code: "test code".to_string(),
message: "Test finding message".to_string(),
recommendation: "Fix it".to_string(),
fix_hint: None,
cwe_ids: Vec::new(),
rule_severity: None,
client: None,
context: None,
};
let result = AnalysisResult::with_findings(vec![finding]);
assert!(result.has_findings());
assert_eq!(result.highest_severity(), Some(Severity::Medium));
}
}