1use crate::Result;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ScanResult {
10    pub status: ScanStatus,
12
13    pub score: u8,
15
16    pub findings: Vec<Finding>,
18
19    pub metadata: ScanMetadata,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(rename_all = "lowercase")]
26pub enum ScanStatus {
27    Pass,
28    Warning,
29    Fail,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Finding {
35    pub id: String,
37
38    pub severity: Severity,
40
41    pub category: Category,
43
44    pub title: String,
46
47    pub description: String,
49
50    pub location: Option<String>,
52
53    pub recommendation: String,
55
56    pub references: Vec<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
62#[serde(rename_all = "lowercase")]
63pub enum Severity {
64    Info,
65    Low,
66    Medium,
67    High,
68    Critical,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum Category {
75    Malware,
76    VulnerableDependency,
77    InsecureCoding,
78    DataExfiltration,
79    SupplyChain,
80    Licensing,
81    Configuration,
82    Obfuscation,
83    Other,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ScanMetadata {
89    pub scan_id: String,
90    pub scanner_version: String,
91    pub scan_started_at: String,
92    pub scan_completed_at: String,
93    pub duration_ms: u64,
94    pub scanned_files: u32,
95    pub scanned_bytes: u64,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ScannerConfig {
101    pub enable_malware_scan: bool,
103
104    pub enable_dependency_scan: bool,
106
107    pub enable_static_analysis: bool,
109
110    pub enable_license_check: bool,
112
113    pub max_file_size: u64,
115
116    pub timeout_per_file: u64,
118
119    pub allowed_licenses: Vec<String>,
121
122    pub fail_on_severity: Severity,
124}
125
126impl Default for ScannerConfig {
127    fn default() -> Self {
128        Self {
129            enable_malware_scan: true,
130            enable_dependency_scan: true,
131            enable_static_analysis: true,
132            enable_license_check: true,
133            max_file_size: 10 * 1024 * 1024, timeout_per_file: 30,
135            allowed_licenses: vec![
136                "MIT".to_string(),
137                "Apache-2.0".to_string(),
138                "BSD-2-Clause".to_string(),
139                "BSD-3-Clause".to_string(),
140                "ISC".to_string(),
141                "MPL-2.0".to_string(),
142            ],
143            fail_on_severity: Severity::High,
144        }
145    }
146}
147
148pub struct SecurityScanner {
150    config: ScannerConfig,
151}
152
153impl SecurityScanner {
154    pub fn new(config: ScannerConfig) -> Self {
156        Self { config }
157    }
158
159    pub async fn scan_plugin(&self, package_path: &Path) -> Result<ScanResult> {
161        let start_time = std::time::Instant::now();
162        let scan_id = uuid::Uuid::new_v4().to_string();
163
164        let mut findings = Vec::new();
165        let scanned_files = 0;
166        let scanned_bytes = 0;
167
168        if self.config.enable_malware_scan {
170            findings.extend(self.scan_for_malware(package_path).await?);
171        }
172
173        if self.config.enable_dependency_scan {
175            findings.extend(self.scan_dependencies(package_path).await?);
176        }
177
178        if self.config.enable_static_analysis {
180            findings.extend(self.static_analysis(package_path).await?);
181        }
182
183        if self.config.enable_license_check {
185            findings.extend(self.check_license_compliance(package_path).await?);
186        }
187
188        let score = self.calculate_security_score(&findings);
190        let status = self.determine_status(&findings);
191
192        let duration = start_time.elapsed();
193
194        Ok(ScanResult {
195            status,
196            score,
197            findings,
198            metadata: ScanMetadata {
199                scan_id,
200                scanner_version: env!("CARGO_PKG_VERSION").to_string(),
201                scan_started_at: chrono::Utc::now().to_rfc3339(),
202                scan_completed_at: chrono::Utc::now().to_rfc3339(),
203                duration_ms: duration.as_millis() as u64,
204                scanned_files,
205                scanned_bytes,
206            },
207        })
208    }
209
210    async fn scan_for_malware(&self, _package_path: &Path) -> Result<Vec<Finding>> {
211        let findings = Vec::new();
212
213        let _suspicious_patterns = [
219            "backdoor",
220            "keylogger",
221            "trojan",
222            "ransomware",
223            "cryptominer",
224            "rootkit",
225            "exploit",
226        ];
227
228        Ok(findings)
232    }
233
234    async fn scan_dependencies(&self, _package_path: &Path) -> Result<Vec<Finding>> {
235        let findings = Vec::new();
236
237        Ok(findings)
246    }
247
248    async fn static_analysis(&self, _package_path: &Path) -> Result<Vec<Finding>> {
249        let findings = Vec::new();
250
251        Ok(findings)
265    }
266
267    async fn check_license_compliance(&self, _package_path: &Path) -> Result<Vec<Finding>> {
268        let findings = Vec::new();
269
270        Ok(findings)
275    }
276
277    fn calculate_security_score(&self, findings: &[Finding]) -> u8 {
278        let mut score: u8 = 100;
279
280        for finding in findings {
281            let deduction = match finding.severity {
282                Severity::Critical => 30,
283                Severity::High => 20,
284                Severity::Medium => 10,
285                Severity::Low => 5,
286                Severity::Info => 0,
287            };
288            score = score.saturating_sub(deduction);
289        }
290
291        score
292    }
293
294    fn determine_status(&self, findings: &[Finding]) -> ScanStatus {
295        let has_critical = findings.iter().any(|f| f.severity >= self.config.fail_on_severity);
296
297        if has_critical {
298            ScanStatus::Fail
299        } else if findings.iter().any(|f| f.severity >= Severity::Medium) {
300            ScanStatus::Warning
301        } else {
302            ScanStatus::Pass
303        }
304    }
305}
306
307impl Default for SecurityScanner {
308    fn default() -> Self {
309        Self::new(ScannerConfig::default())
310    }
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct Vulnerability {
316    pub id: String,
317    pub package: String,
318    pub versions: Vec<String>,
319    pub severity: Severity,
320    pub title: String,
321    pub description: String,
322    pub cvss_score: Option<f32>,
323    pub cve: Option<String>,
324    pub patched_versions: Vec<String>,
325    pub references: Vec<String>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct LicenseInfo {
331    pub spdx_id: String,
332    pub name: String,
333    pub approved: bool,
334    pub osi_approved: bool,
335    pub category: LicenseCategory,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(rename_all = "lowercase")]
341pub enum LicenseCategory {
342    Permissive,
343    Copyleft,
344    Proprietary,
345    Unknown,
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_security_score_calculation() {
354        let scanner = SecurityScanner::default();
355
356        let findings = vec![
357            Finding {
358                id: "1".to_string(),
359                severity: Severity::High,
360                category: Category::Malware,
361                title: "Suspicious code".to_string(),
362                description: "Test".to_string(),
363                location: None,
364                recommendation: "Remove".to_string(),
365                references: vec![],
366            },
367            Finding {
368                id: "2".to_string(),
369                severity: Severity::Medium,
370                category: Category::InsecureCoding,
371                title: "Weak encryption".to_string(),
372                description: "Test".to_string(),
373                location: None,
374                recommendation: "Use strong encryption".to_string(),
375                references: vec![],
376            },
377        ];
378
379        let score = scanner.calculate_security_score(&findings);
380        assert_eq!(score, 70); }
382
383    #[test]
384    fn test_status_determination() {
385        let scanner = SecurityScanner::default();
386
387        let critical_findings = vec![Finding {
388            id: "1".to_string(),
389            severity: Severity::Critical,
390            category: Category::Malware,
391            title: "Malware detected".to_string(),
392            description: "Test".to_string(),
393            location: None,
394            recommendation: "Remove".to_string(),
395            references: vec![],
396        }];
397
398        assert_eq!(scanner.determine_status(&critical_findings), ScanStatus::Fail);
399
400        let medium_findings = vec![Finding {
401            id: "1".to_string(),
402            severity: Severity::Medium,
403            category: Category::InsecureCoding,
404            title: "Code issue".to_string(),
405            description: "Test".to_string(),
406            location: None,
407            recommendation: "Fix".to_string(),
408            references: vec![],
409        }];
410
411        assert_eq!(scanner.determine_status(&medium_findings), ScanStatus::Warning);
412
413        let low_findings = vec![Finding {
414            id: "1".to_string(),
415            severity: Severity::Low,
416            category: Category::Configuration,
417            title: "Config issue".to_string(),
418            description: "Test".to_string(),
419            location: None,
420            recommendation: "Update".to_string(),
421            references: vec![],
422        }];
423
424        assert_eq!(scanner.determine_status(&low_findings), ScanStatus::Pass);
425    }
426}