1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum VulnerabilitySeverity {
18 Informational,
19 Low,
20 Medium,
21 High,
22 Critical,
23}
24
25impl VulnerabilitySeverity {
26 pub fn as_str(&self) -> &'static str {
27 match self {
28 VulnerabilitySeverity::Informational => "informational",
29 VulnerabilitySeverity::Low => "low",
30 VulnerabilitySeverity::Medium => "medium",
31 VulnerabilitySeverity::High => "high",
32 VulnerabilitySeverity::Critical => "critical",
33 }
34 }
35
36 pub fn try_parse(s: &str) -> Option<Self> {
37 match s.to_lowercase().as_str() {
38 "informational" => Some(VulnerabilitySeverity::Informational),
39 "low" => Some(VulnerabilitySeverity::Low),
40 "medium" => Some(VulnerabilitySeverity::Medium),
41 "high" => Some(VulnerabilitySeverity::High),
42 "critical" => Some(VulnerabilitySeverity::Critical),
43 _ => None,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Vulnerability {
51 pub id: String, pub title: String,
53 pub description: String,
54 pub severity: VulnerabilitySeverity,
55 pub affected_version: String,
56 pub fixed_version: Option<String>,
57 pub advisory_url: Option<String>,
58 pub published_date: String,
59 pub discovered_at: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub enum LicenseType {
65 MIT,
66 Apache2,
67 GPL2,
68 GPL3,
69 BSD,
70 ISC,
71 MPL2,
72 Custom(String),
73 Unknown,
74}
75
76impl LicenseType {
77 pub fn from_spdx(spdx_id: &str) -> Self {
78 match spdx_id.to_uppercase().as_str() {
79 "MIT" => LicenseType::MIT,
80 "APACHE-2.0" => LicenseType::Apache2,
81 "GPL-2.0" => LicenseType::GPL2,
82 "GPL-3.0" => LicenseType::GPL3,
83 "BSD-2-CLAUSE" | "BSD-3-CLAUSE" => LicenseType::BSD,
84 "ISC" => LicenseType::ISC,
85 "MPL-2.0" => LicenseType::MPL2,
86 _ => LicenseType::Custom(spdx_id.to_string()),
87 }
88 }
89
90 pub fn is_permissive(&self) -> bool {
91 matches!(
92 self,
93 LicenseType::MIT | LicenseType::Apache2 | LicenseType::BSD | LicenseType::ISC
94 )
95 }
96
97 pub fn is_copyleft(&self) -> bool {
98 matches!(
99 self,
100 LicenseType::GPL2 | LicenseType::GPL3 | LicenseType::MPL2
101 )
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LicenseCompliance {
108 pub dependency: String,
109 pub version: String,
110 pub license: LicenseType,
111 pub is_approved: bool,
112 pub issue: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SecurityScanResult {
118 pub plugin_id: String,
119 pub plugin_version: String,
120 pub scan_timestamp: String,
121 pub vulnerabilities: Vec<Vulnerability>,
122 pub license_issues: Vec<LicenseCompliance>,
123 pub dependency_count: usize,
124 pub vulnerable_dependency_count: usize,
125 pub high_severity_count: usize,
126 pub critical_severity_count: usize,
127 pub overall_risk: RiskLevel,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
132#[serde(rename_all = "lowercase")]
133pub enum RiskLevel {
134 Safe,
135 Low,
136 Medium,
137 High,
138 Critical,
139}
140
141impl RiskLevel {
142 pub fn description(&self) -> &'static str {
143 match self {
144 RiskLevel::Safe => "No vulnerabilities detected",
145 RiskLevel::Low => "Minor vulnerabilities, low risk",
146 RiskLevel::Medium => "Moderate vulnerabilities present",
147 RiskLevel::High => "Significant security concerns",
148 RiskLevel::Critical => "Critical vulnerabilities - do not use",
149 }
150 }
151}
152
153pub struct VulnerabilityScanner {
155 known_vulnerabilities: Vec<Vulnerability>,
156 approved_licenses: Vec<LicenseType>,
157 max_allowed_severity: VulnerabilitySeverity,
158}
159
160impl VulnerabilityScanner {
161 pub fn new() -> Self {
163 Self {
164 known_vulnerabilities: Vec::new(),
165 approved_licenses: vec![
166 LicenseType::MIT,
167 LicenseType::Apache2,
168 LicenseType::BSD,
169 LicenseType::ISC,
170 ],
171 max_allowed_severity: VulnerabilitySeverity::High,
172 }
173 }
174
175 pub fn with_severity_threshold(max_severity: VulnerabilitySeverity) -> Self {
177 let mut scanner = Self::new();
178 scanner.max_allowed_severity = max_severity;
179 scanner
180 }
181
182 pub fn register_vulnerability(&mut self, vuln: Vulnerability) {
184 self.known_vulnerabilities.push(vuln);
185 }
186
187 pub fn approve_license(&mut self, license: LicenseType) {
189 if !self.approved_licenses.contains(&license) {
190 self.approved_licenses.push(license);
191 }
192 }
193
194 pub fn scan_plugin(
196 &self,
197 plugin_id: &str,
198 version: &str,
199 dependencies: Vec<(&str, &str)>, ) -> SecurityScanResult {
201 let mut vulnerabilities = Vec::new();
202 let mut high_count = 0;
203 let mut critical_count = 0;
204
205 for (_dep_name, dep_version) in &dependencies {
207 for vuln in &self.known_vulnerabilities {
208 if vuln.affected_version == *dep_version {
209 if vuln.severity >= VulnerabilitySeverity::High {
210 high_count += 1;
211 }
212 if vuln.severity == VulnerabilitySeverity::Critical {
213 critical_count += 1;
214 }
215 vulnerabilities.push(vuln.clone());
216 }
217 }
218 }
219
220 let vulnerable_dep_count = dependencies.len();
221 let risk_level = self.assess_risk_level(
222 vulnerabilities.len(),
223 high_count,
224 critical_count,
225 vulnerable_dep_count,
226 );
227
228 SecurityScanResult {
229 plugin_id: plugin_id.to_string(),
230 plugin_version: version.to_string(),
231 scan_timestamp: chrono::Utc::now().to_rfc3339(),
232 vulnerabilities,
233 license_issues: Vec::new(),
234 dependency_count: dependencies.len(),
235 vulnerable_dependency_count: vulnerable_dep_count,
236 high_severity_count: high_count,
237 critical_severity_count: critical_count,
238 overall_risk: risk_level,
239 }
240 }
241
242 pub fn check_license_compliance(
244 &self,
245 dependencies: Vec<(&str, &str, &str)>, ) -> Vec<LicenseCompliance> {
247 dependencies
248 .into_iter()
249 .map(|(name, version, license_spdx)| {
250 let license_type = LicenseType::from_spdx(license_spdx);
251 let is_approved = self.approved_licenses.contains(&license_type);
252
253 LicenseCompliance {
254 dependency: name.to_string(),
255 version: version.to_string(),
256 license: license_type,
257 is_approved,
258 issue: if !is_approved {
259 Some(format!("License {} not approved", license_spdx))
260 } else {
261 None
262 },
263 }
264 })
265 .collect()
266 }
267
268 fn assess_risk_level(
270 &self,
271 total_vulns: usize,
272 high_count: usize,
273 critical_count: usize,
274 _dep_count: usize,
275 ) -> RiskLevel {
276 if critical_count > 0 {
277 RiskLevel::Critical
278 } else if high_count > 2 {
279 RiskLevel::High
280 } else if high_count > 0 {
281 RiskLevel::Medium
282 } else if total_vulns > 5 {
283 RiskLevel::Low
284 } else {
285 RiskLevel::Safe
286 }
287 }
288
289 pub fn is_acceptable(&self, result: &SecurityScanResult) -> bool {
291 if result.critical_severity_count > 0 {
292 return false;
293 }
294
295 result.high_severity_count
296 <= (match self.max_allowed_severity {
297 VulnerabilitySeverity::Informational => 10,
298 VulnerabilitySeverity::Low => 5,
299 VulnerabilitySeverity::Medium => 2,
300 VulnerabilitySeverity::High => 1,
301 VulnerabilitySeverity::Critical => 0,
302 })
303 }
304}
305
306impl Default for VulnerabilityScanner {
307 fn default() -> Self {
308 Self::new()
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct SecurityAuditReport {
315 pub plugin_id: String,
316 pub plugin_version: String,
317 pub report_timestamp: String,
318 pub scan_result: SecurityScanResult,
319 pub license_compliances: Vec<LicenseCompliance>,
320 pub recommendations: Vec<String>,
321 pub approved: bool,
322}
323
324impl SecurityAuditReport {
325 pub fn generate_recommendations(&mut self) {
327 self.recommendations.clear();
328
329 if self.scan_result.critical_severity_count > 0 {
331 self.recommendations.push(
332 "CRITICAL: Do not publish. Address critical vulnerabilities immediately."
333 .to_string(),
334 );
335 }
336
337 if self.scan_result.high_severity_count > 2 {
338 self.recommendations.push(
339 "HIGH RISK: Multiple high-severity vulnerabilities detected. Consider patching."
340 .to_string(),
341 );
342 }
343
344 let unapproved_licenses: Vec<_> = self
346 .license_compliances
347 .iter()
348 .filter(|lc| !lc.is_approved)
349 .collect();
350
351 if !unapproved_licenses.is_empty() {
352 self.recommendations.push(format!(
353 "LICENSE: {} unapproved license(s) found. Review and update.",
354 unapproved_licenses.len()
355 ));
356 }
357
358 if self.scan_result.dependency_count > 50 {
360 self.recommendations.push(
361 "DEPENDENCY: High number of dependencies increases attack surface. Consider minimizing."
362 .to_string(),
363 );
364 }
365
366 self.approved = self.scan_result.critical_severity_count == 0
368 && unapproved_licenses.is_empty()
369 && self.scan_result.high_severity_count <= 1;
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_vulnerability_severity_ordering() {
379 assert!(VulnerabilitySeverity::Critical > VulnerabilitySeverity::High);
380 assert!(VulnerabilitySeverity::High > VulnerabilitySeverity::Medium);
381 assert!(VulnerabilitySeverity::Medium > VulnerabilitySeverity::Low);
382 }
383
384 #[test]
385 fn test_vulnerability_severity_to_str() {
386 assert_eq!(VulnerabilitySeverity::Critical.as_str(), "critical");
387 assert_eq!(VulnerabilitySeverity::Low.as_str(), "low");
388 }
389
390 #[test]
391 fn test_vulnerability_severity_try_parse() {
392 assert_eq!(
393 VulnerabilitySeverity::try_parse("critical"),
394 Some(VulnerabilitySeverity::Critical)
395 );
396 assert_eq!(VulnerabilitySeverity::try_parse("invalid"), None);
397 }
398
399 #[test]
400 fn test_license_type_permissive() {
401 assert!(LicenseType::MIT.is_permissive());
402 assert!(LicenseType::Apache2.is_permissive());
403 assert!(!LicenseType::GPL3.is_permissive());
404 }
405
406 #[test]
407 fn test_license_type_copyleft() {
408 assert!(LicenseType::GPL3.is_copyleft());
409 assert!(LicenseType::MPL2.is_copyleft());
410 assert!(!LicenseType::MIT.is_copyleft());
411 }
412
413 #[test]
414 fn test_license_from_spdx() {
415 assert_eq!(LicenseType::from_spdx("MIT"), LicenseType::MIT);
416 assert_eq!(LicenseType::from_spdx("Apache-2.0"), LicenseType::Apache2);
417 assert_eq!(LicenseType::from_spdx("GPL-3.0"), LicenseType::GPL3);
418 }
419
420 #[test]
421 fn test_scanner_creation() {
422 let scanner = VulnerabilityScanner::new();
423 assert_eq!(scanner.approved_licenses.len(), 4); }
425
426 #[test]
427 fn test_scanner_register_vulnerability() {
428 let mut scanner = VulnerabilityScanner::new();
429 let vuln = Vulnerability {
430 id: "CVE-2024-0001".to_string(),
431 title: "Test Vulnerability".to_string(),
432 description: "A test vulnerability".to_string(),
433 severity: VulnerabilitySeverity::High,
434 affected_version: "1.0.0".to_string(),
435 fixed_version: Some("1.0.1".to_string()),
436 advisory_url: Some("https://example.com".to_string()),
437 published_date: "2024-01-01".to_string(),
438 discovered_at: chrono::Utc::now().to_rfc3339(),
439 };
440
441 scanner.register_vulnerability(vuln);
442 assert_eq!(scanner.known_vulnerabilities.len(), 1);
443 }
444
445 #[test]
446 fn test_risk_level_ordering() {
447 assert!(RiskLevel::Critical > RiskLevel::High);
448 assert!(RiskLevel::High > RiskLevel::Medium);
449 assert!(RiskLevel::Safe < RiskLevel::Low);
450 }
451
452 #[test]
453 fn test_risk_level_descriptions() {
454 assert!(!RiskLevel::Safe.description().is_empty());
455 assert!(!RiskLevel::Critical.description().is_empty());
456 }
457
458 #[test]
459 fn test_scan_plugin_no_vulnerabilities() {
460 let scanner = VulnerabilityScanner::new();
461 let result = scanner.scan_plugin("test-plugin", "1.0.0", vec![]);
462
463 assert_eq!(result.critical_severity_count, 0);
464 assert_eq!(result.high_severity_count, 0);
465 assert_eq!(result.overall_risk, RiskLevel::Safe);
466 }
467
468 #[test]
469 fn test_scan_result_acceptable() {
470 let result = SecurityScanResult {
471 plugin_id: "test".to_string(),
472 plugin_version: "1.0.0".to_string(),
473 scan_timestamp: "2024-01-01".to_string(),
474 vulnerabilities: Vec::new(),
475 license_issues: Vec::new(),
476 dependency_count: 5,
477 vulnerable_dependency_count: 0,
478 high_severity_count: 0,
479 critical_severity_count: 0,
480 overall_risk: RiskLevel::Safe,
481 };
482
483 let scanner = VulnerabilityScanner::new();
484 assert!(scanner.is_acceptable(&result));
485 }
486
487 #[test]
488 fn test_license_compliance_check() {
489 let scanner = VulnerabilityScanner::new();
490 let deps = vec![("dep1", "1.0.0", "MIT"), ("dep2", "2.0.0", "GPL-3.0")];
491
492 let compliances = scanner.check_license_compliance(deps);
493 assert_eq!(compliances.len(), 2);
494 assert!(compliances[0].is_approved); assert!(!compliances[1].is_approved); }
497
498 #[test]
499 fn test_audit_report_recommendations() {
500 let mut report = SecurityAuditReport {
501 plugin_id: "test".to_string(),
502 plugin_version: "1.0.0".to_string(),
503 report_timestamp: chrono::Utc::now().to_rfc3339(),
504 scan_result: SecurityScanResult {
505 plugin_id: "test".to_string(),
506 plugin_version: "1.0.0".to_string(),
507 scan_timestamp: chrono::Utc::now().to_rfc3339(),
508 vulnerabilities: Vec::new(),
509 license_issues: Vec::new(),
510 dependency_count: 5,
511 vulnerable_dependency_count: 0,
512 high_severity_count: 0,
513 critical_severity_count: 0,
514 overall_risk: RiskLevel::Safe,
515 },
516 license_compliances: Vec::new(),
517 recommendations: Vec::new(),
518 approved: false,
519 };
520
521 report.generate_recommendations();
522 assert!(report.approved);
523 }
524
525 #[test]
526 fn test_audit_report_critical_vulnerability() {
527 let mut report = SecurityAuditReport {
528 plugin_id: "test".to_string(),
529 plugin_version: "1.0.0".to_string(),
530 report_timestamp: chrono::Utc::now().to_rfc3339(),
531 scan_result: SecurityScanResult {
532 plugin_id: "test".to_string(),
533 plugin_version: "1.0.0".to_string(),
534 scan_timestamp: chrono::Utc::now().to_rfc3339(),
535 vulnerabilities: Vec::new(),
536 license_issues: Vec::new(),
537 dependency_count: 5,
538 vulnerable_dependency_count: 0,
539 high_severity_count: 0,
540 critical_severity_count: 1, overall_risk: RiskLevel::Critical,
542 },
543 license_compliances: Vec::new(),
544 recommendations: Vec::new(),
545 approved: false,
546 };
547
548 report.generate_recommendations();
549 assert!(!report.approved);
550 assert!(!report.recommendations.is_empty());
551 }
552}