mockforge_plugin_registry/
security.rs

1//! Plugin security scanning and validation
2
3use crate::Result;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7/// Security scan result
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ScanResult {
10    /// Overall status
11    pub status: ScanStatus,
12
13    /// Security score (0-100)
14    pub score: u8,
15
16    /// Findings by severity
17    pub findings: Vec<Finding>,
18
19    /// Scan metadata
20    pub metadata: ScanMetadata,
21}
22
23/// Scan status
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(rename_all = "lowercase")]
26pub enum ScanStatus {
27    Pass,
28    Warning,
29    Fail,
30}
31
32/// Security finding
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Finding {
35    /// Finding ID
36    pub id: String,
37
38    /// Severity level
39    pub severity: Severity,
40
41    /// Finding category
42    pub category: Category,
43
44    /// Title
45    pub title: String,
46
47    /// Description
48    pub description: String,
49
50    /// Location (file path, line number, etc.)
51    pub location: Option<String>,
52
53    /// Recommendation
54    pub recommendation: String,
55
56    /// References (CVE, CWE, etc.)
57    pub references: Vec<String>,
58}
59
60/// Severity level
61#[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/// Finding category
72#[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/// Scan metadata
87#[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/// Security scanner configuration
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ScannerConfig {
101    /// Enable malware detection
102    pub enable_malware_scan: bool,
103
104    /// Enable dependency vulnerability scanning
105    pub enable_dependency_scan: bool,
106
107    /// Enable static code analysis
108    pub enable_static_analysis: bool,
109
110    /// Enable license compliance check
111    pub enable_license_check: bool,
112
113    /// Maximum file size to scan (bytes)
114    pub max_file_size: u64,
115
116    /// Timeout per file (seconds)
117    pub timeout_per_file: u64,
118
119    /// Allowed licenses
120    pub allowed_licenses: Vec<String>,
121
122    /// Severity threshold for failure
123    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, // 10MB
134            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
148/// Security scanner
149pub struct SecurityScanner {
150    config: ScannerConfig,
151}
152
153impl SecurityScanner {
154    /// Create a new scanner with config
155    pub fn new(config: ScannerConfig) -> Self {
156        Self { config }
157    }
158
159    /// Scan a plugin package
160    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        // 1. Malware detection
169        if self.config.enable_malware_scan {
170            findings.extend(self.scan_for_malware(package_path).await?);
171        }
172
173        // 2. Dependency vulnerability scan
174        if self.config.enable_dependency_scan {
175            findings.extend(self.scan_dependencies(package_path).await?);
176        }
177
178        // 3. Static code analysis
179        if self.config.enable_static_analysis {
180            findings.extend(self.static_analysis(package_path).await?);
181        }
182
183        // 4. License compliance
184        if self.config.enable_license_check {
185            findings.extend(self.check_license_compliance(package_path).await?);
186        }
187
188        // Calculate score and status
189        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        // Check for suspicious patterns
214        // In production, integrate with actual AV/malware scanning service
215        // Examples: ClamAV, VirusTotal API, etc.
216
217        // Placeholder: Check for suspicious file names
218        let _suspicious_patterns = [
219            "backdoor",
220            "keylogger",
221            "trojan",
222            "ransomware",
223            "cryptominer",
224            "rootkit",
225            "exploit",
226        ];
227
228        // This would scan actual files
229        // For now, return empty findings
230
231        Ok(findings)
232    }
233
234    async fn scan_dependencies(&self, _package_path: &Path) -> Result<Vec<Finding>> {
235        let findings = Vec::new();
236
237        // In production, integrate with:
238        // - RustSec Advisory Database (cargo-audit)
239        // - npm audit for JavaScript
240        // - pip-audit for Python
241        // - OSV (Open Source Vulnerabilities) database
242
243        // Placeholder implementation
244
245        Ok(findings)
246    }
247
248    async fn static_analysis(&self, _package_path: &Path) -> Result<Vec<Finding>> {
249        let findings = Vec::new();
250
251        // In production, run:
252        // - cargo clippy for Rust
253        // - eslint for JavaScript
254        // - pylint/bandit for Python
255        // - semgrep for multi-language
256
257        // Check for common issues:
258        // - Unsafe code blocks
259        // - Hardcoded credentials
260        // - SQL injection patterns
261        // - Command injection
262        // - Path traversal vulnerabilities
263
264        Ok(findings)
265    }
266
267    async fn check_license_compliance(&self, _package_path: &Path) -> Result<Vec<Finding>> {
268        let findings = Vec::new();
269
270        // Check if license is in allowed list
271        // Scan for license headers in code files
272        // Check dependency licenses
273
274        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/// Vulnerability database entry
314#[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/// License information
329#[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/// License category
339#[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); // 100 - 20 - 10
381    }
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}