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}