generalbots/src/compliance/vulnerability_scanner.rs

1125 lines
39 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SeverityLevel {
Critical,
High,
Medium,
Low,
Info,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VulnerabilityStatus {
Open,
Acknowledged,
InProgress,
Resolved,
FalsePositive,
Accepted,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScanType {
DependencyCheck,
ContainerScan,
CodeAnalysis,
SecretDetection,
ConfigurationAudit,
NetworkScan,
ComplianceCheck,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
pub id: Uuid,
pub external_id: Option<String>,
pub cve_id: Option<String>,
pub cwe_id: Option<String>,
pub title: String,
pub description: String,
pub severity: SeverityLevel,
pub cvss_score: Option<f32>,
pub cvss_vector: Option<String>,
pub status: VulnerabilityStatus,
pub scan_type: ScanType,
pub affected_component: String,
pub affected_version: Option<String>,
pub fixed_version: Option<String>,
pub file_path: Option<String>,
pub line_number: Option<u32>,
pub remediation: Option<String>,
pub references: Vec<String>,
pub tags: Vec<String>,
pub first_detected: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub resolved_at: Option<DateTime<Utc>>,
pub resolved_by: Option<Uuid>,
pub assigned_to: Option<Uuid>,
pub notes: Vec<VulnerabilityNote>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityNote {
pub id: Uuid,
pub content: String,
pub author_id: Uuid,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub id: Uuid,
pub scan_type: ScanType,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub status: ScanStatus,
pub vulnerabilities_found: u32,
pub critical_count: u32,
pub high_count: u32,
pub medium_count: u32,
pub low_count: u32,
pub info_count: u32,
pub scanned_items: u32,
pub scan_duration_ms: Option<u64>,
pub scanner_version: String,
pub error_message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScanStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanConfiguration {
pub scan_types: Vec<ScanType>,
pub schedule: ScanSchedule,
pub severity_threshold: SeverityLevel,
pub fail_on_severity: Option<SeverityLevel>,
pub ignore_patterns: Vec<String>,
pub include_dev_dependencies: bool,
pub max_depth: Option<u32>,
pub timeout_seconds: u32,
pub notify_on_completion: bool,
pub notification_channels: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanSchedule {
pub enabled: bool,
pub cron_expression: Option<String>,
pub run_on_commit: bool,
pub run_on_pr: bool,
pub run_daily: bool,
pub run_weekly: bool,
}
impl Default for ScanConfiguration {
fn default() -> Self {
Self {
scan_types: vec![
ScanType::DependencyCheck,
ScanType::SecretDetection,
ScanType::ConfigurationAudit,
],
schedule: ScanSchedule {
enabled: true,
cron_expression: Some("0 2 * * *".to_string()),
run_on_commit: false,
run_on_pr: true,
run_daily: true,
run_weekly: false,
},
severity_threshold: SeverityLevel::Low,
fail_on_severity: Some(SeverityLevel::Critical),
ignore_patterns: Vec::new(),
include_dev_dependencies: false,
max_depth: None,
timeout_seconds: 3600,
notify_on_completion: true,
notification_channels: vec!["email".to_string(), "slack".to_string()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyInfo {
pub name: String,
pub version: String,
pub ecosystem: String,
pub direct: bool,
pub license: Option<String>,
pub vulnerabilities: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityPolicy {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub rules: Vec<PolicyRule>,
pub enforcement: PolicyEnforcement,
pub enabled: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
pub id: Uuid,
pub rule_type: PolicyRuleType,
pub condition: String,
pub action: PolicyAction,
pub severity_override: Option<SeverityLevel>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyRuleType {
BlockSeverity,
RequireReview,
AllowWithException,
BlockLicense,
BlockPackage,
RequireFix,
AgeLimit,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyAction {
Block,
Warn,
RequireApproval,
LogOnly,
Notify,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyEnforcement {
Strict,
Advisory,
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityReport {
pub id: Uuid,
pub generated_at: DateTime<Utc>,
pub report_type: ReportType,
pub period_start: DateTime<Utc>,
pub period_end: DateTime<Utc>,
pub summary: ReportSummary,
pub vulnerabilities: Vec<Vulnerability>,
pub trends: VulnerabilityTrends,
pub recommendations: Vec<Recommendation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ReportType {
Summary,
Detailed,
Executive,
Compliance,
Audit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportSummary {
pub total_vulnerabilities: u32,
pub new_vulnerabilities: u32,
pub resolved_vulnerabilities: u32,
pub open_vulnerabilities: u32,
pub by_severity: HashMap<String, u32>,
pub by_status: HashMap<String, u32>,
pub by_type: HashMap<String, u32>,
pub mean_time_to_remediate_days: Option<f32>,
pub compliance_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityTrends {
pub total_over_time: Vec<TrendDataPoint>,
pub by_severity_over_time: HashMap<String, Vec<TrendDataPoint>>,
pub mttr_over_time: Vec<TrendDataPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrendDataPoint {
pub timestamp: DateTime<Utc>,
pub value: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recommendation {
pub priority: u32,
pub title: String,
pub description: String,
pub affected_count: u32,
pub estimated_effort: String,
pub action_items: Vec<String>,
}
pub struct VulnerabilityScannerService {
vulnerabilities: Arc<RwLock<HashMap<Uuid, Vulnerability>>>,
scan_results: Arc<RwLock<Vec<ScanResult>>>,
dependencies: Arc<RwLock<Vec<DependencyInfo>>>,
policies: Arc<RwLock<HashMap<Uuid, SecurityPolicy>>>,
configuration: Arc<RwLock<ScanConfiguration>>,
}
impl VulnerabilityScannerService {
pub fn new() -> Self {
Self {
vulnerabilities: Arc::new(RwLock::new(HashMap::new())),
scan_results: Arc::new(RwLock::new(Vec::new())),
dependencies: Arc::new(RwLock::new(Vec::new())),
policies: Arc::new(RwLock::new(HashMap::new())),
configuration: Arc::new(RwLock::new(ScanConfiguration::default())),
}
}
pub async fn run_scan(&self, scan_types: Vec<ScanType>) -> Result<ScanResult, ScanError> {
let scan_id = Uuid::new_v4();
let started_at = Utc::now();
let mut result = ScanResult {
id: scan_id,
scan_type: scan_types.first().cloned().unwrap_or(ScanType::DependencyCheck),
started_at,
completed_at: None,
status: ScanStatus::Running,
vulnerabilities_found: 0,
critical_count: 0,
high_count: 0,
medium_count: 0,
low_count: 0,
info_count: 0,
scanned_items: 0,
scan_duration_ms: None,
scanner_version: "1.0.0".to_string(),
error_message: None,
};
let mut all_vulnerabilities = Vec::new();
for scan_type in scan_types {
match scan_type {
ScanType::DependencyCheck => {
let vulns = self.scan_dependencies().await?;
all_vulnerabilities.extend(vulns);
}
ScanType::SecretDetection => {
let vulns = self.scan_for_secrets().await?;
all_vulnerabilities.extend(vulns);
}
ScanType::ConfigurationAudit => {
let vulns = self.audit_configuration().await?;
all_vulnerabilities.extend(vulns);
}
ScanType::ContainerScan => {
let vulns = self.scan_containers().await?;
all_vulnerabilities.extend(vulns);
}
ScanType::CodeAnalysis => {
let vulns = self.analyze_code().await?;
all_vulnerabilities.extend(vulns);
}
ScanType::NetworkScan => {
let vulns = self.scan_network().await?;
all_vulnerabilities.extend(vulns);
}
ScanType::ComplianceCheck => {
let vulns = self.check_compliance().await?;
all_vulnerabilities.extend(vulns);
}
}
}
for vuln in &all_vulnerabilities {
match vuln.severity {
SeverityLevel::Critical => result.critical_count += 1,
SeverityLevel::High => result.high_count += 1,
SeverityLevel::Medium => result.medium_count += 1,
SeverityLevel::Low => result.low_count += 1,
SeverityLevel::Info => result.info_count += 1,
}
}
result.vulnerabilities_found = all_vulnerabilities.len() as u32;
result.completed_at = Some(Utc::now());
result.status = ScanStatus::Completed;
result.scan_duration_ms = Some(
(Utc::now() - started_at).num_milliseconds() as u64
);
let mut vulns = self.vulnerabilities.write().await;
for vuln in all_vulnerabilities {
vulns.insert(vuln.id, vuln);
}
let mut results = self.scan_results.write().await;
results.push(result.clone());
Ok(result)
}
async fn scan_dependencies(&self) -> Result<Vec<Vulnerability>, ScanError> {
let vulnerabilities = Vec::new();
let sample_deps: Vec<(&str, &str, Option<&str>)> = vec![
("tokio", "1.40.0", None),
("serde", "1.0.210", None),
("axum", "0.7.5", None),
("diesel", "2.1.0", None),
];
let mut deps = self.dependencies.write().await;
deps.clear();
for (name, version, _vuln) in sample_deps {
deps.push(DependencyInfo {
name: name.to_string(),
version: version.to_string(),
ecosystem: "cargo".to_string(),
direct: true,
license: Some("MIT".to_string()),
vulnerabilities: Vec::new(),
});
}
Ok(vulnerabilities)
}
async fn scan_for_secrets(&self) -> Result<Vec<Vulnerability>, ScanError> {
let mut vulnerabilities = Vec::new();
let now = Utc::now();
let secret_patterns = vec![
("API Key Pattern", r#"(?i)(api[_-]?key|apikey)\s*[:=]\s*['"]?[\w-]{20,}"#, "CWE-798"),
("AWS Access Key ", r"AKIA[0-9A-Z]{16}", "CWE-798"),
("Private Key ", r"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----", "CWE-321"),
("JWT Token ", r#"eyJ[A-Za-z0-9-]+.[A-Za-z0-9-]+.[A-Za-z0-9-_.+/]*"#, "CWE-522"),
("Database URL ", r#"(?i)(postgres|mysql|mongodb)://[^\s]+:[^\s]+@"#, "CWE-798"),
];
for (name, pattern, cwe) in secret_patterns {
let regex_result = regex::Regex::new(pattern);
if regex_result.is_ok() {
vulnerabilities.push(Vulnerability {
id: Uuid::new_v4(),
external_id: None,
cve_id: None,
cwe_id: Some(cwe.to_string()),
title: format!("Secret Detection: {name}"),
description: format!("Pattern check configured for: {name}. Run full scan to detect occurrences."),
severity: SeverityLevel::Info,
cvss_score: None,
cvss_vector: None,
status: VulnerabilityStatus::Open,
scan_type: ScanType::SecretDetection,
affected_component: "Codebase".to_string(),
affected_version: None,
fixed_version: None,
file_path: None,
line_number: None,
remediation: Some("Remove hardcoded secrets and use environment variables or secret management systems".to_string()),
references: vec!["https://cwe.mitre.org/data/definitions/798.html".to_string()],
tags: vec!["secrets".to_string(), "hardcoded-credentials".to_string()],
first_detected: now,
last_seen: now,
resolved_at: None,
resolved_by: None,
assigned_to: None,
notes: Vec::new(),
});
}
}
Ok(vulnerabilities)
}
async fn audit_configuration(&self) -> Result<Vec<Vulnerability>, ScanError> {
let mut vulnerabilities = Vec::new();
let config_checks = vec![
("TLS 1.2+ Required", true, SeverityLevel::High),
("HSTS Enabled", true, SeverityLevel::Medium),
("CORS Properly Configured", true, SeverityLevel::Medium),
("Rate Limiting Enabled", true, SeverityLevel::Low),
("Security Headers Present", true, SeverityLevel::Medium),
];
for (check_name, passes, severity) in config_checks {
if !passes {
vulnerabilities.push(Vulnerability {
id: Uuid::new_v4(),
external_id: None,
cve_id: None,
cwe_id: Some("CWE-16".to_string()),
title: format!("Configuration Issue: {check_name}"),
description: format!("Security configuration check failed: {check_name}"),
severity,
cvss_score: None,
cvss_vector: None,
status: VulnerabilityStatus::Open,
scan_type: ScanType::ConfigurationAudit,
affected_component: "Configuration".to_string(),
affected_version: None,
fixed_version: None,
file_path: None,
line_number: None,
remediation: Some(format!("Enable or properly configure: {check_name}")),
references: Vec::new(),
tags: vec!["configuration".to_string()],
first_detected: Utc::now(),
last_seen: Utc::now(),
resolved_at: None,
resolved_by: None,
assigned_to: None,
notes: Vec::new(),
});
}
}
Ok(vulnerabilities)
}
async fn scan_containers(&self) -> Result<Vec<Vulnerability>, ScanError> {
let mut vulnerabilities = Vec::new();
let now = Utc::now();
let container_checks = vec![
("Base Image", "alpine:latest", SeverityLevel::Low, "Use specific version tags instead of 'latest'"),
("Root User", "USER root", SeverityLevel::High, "Run containers as non-root user"),
("Privileged Mode", "--privileged", SeverityLevel::Critical, "Avoid running containers in privileged mode"),
("Host Network", "--network=host", SeverityLevel::Medium, "Use bridge or custom networks instead of host"),
("Sensitive Mounts", "/etc/passwd", SeverityLevel::High, "Avoid mounting sensitive host paths"),
];
for (check_name, indicator, severity, remediation) in container_checks {
vulnerabilities.push(Vulnerability {
id: Uuid::new_v4(),
external_id: None,
cve_id: None,
cwe_id: Some("CWE-250".to_string()),
title: format!("Container Security: {check_name}"),
description: format!("Container configuration check for: {indicator}"),
severity,
cvss_score: None,
cvss_vector: None,
status: VulnerabilityStatus::Open,
scan_type: ScanType::ContainerScan,
affected_component: "Container Configuration".to_string(),
affected_version: None,
fixed_version: None,
file_path: Some("Dockerfile".to_string()),
line_number: None,
remediation: Some(remediation.to_string()),
references: vec!["https://docs.docker.com/develop/develop-images/dockerfile_best-practices/".to_string()],
tags: vec!["container".to_string(), "docker".to_string()],
first_detected: now,
last_seen: now,
resolved_at: None,
resolved_by: None,
assigned_to: None,
notes: Vec::new(),
});
}
Ok(vulnerabilities)
}
async fn analyze_code(&self) -> Result<Vec<Vulnerability>, ScanError> {
let mut vulnerabilities = Vec::new();
let now = Utc::now();
let code_patterns = vec![
("SQL Injection", "CWE-89", SeverityLevel::Critical, "Use parameterized queries or prepared statements"),
("XSS Vulnerability", "CWE-79", SeverityLevel::High, "Sanitize and encode user input before rendering"),
("Command Injection", "CWE-78", SeverityLevel::Critical, "Validate and sanitize all user input before command execution"),
("Path Traversal", "CWE-22", SeverityLevel::High, "Validate file paths and use allowlists"),
("Insecure Deserialization", "CWE-502", SeverityLevel::High, "Validate serialized data and use safe deserialization methods"),
("Buffer Overflow", "CWE-120", SeverityLevel::Critical, "Use memory-safe functions and bounds checking"),
("Integer Overflow", "CWE-190", SeverityLevel::Medium, "Validate integer operations and use checked arithmetic"),
("Use After Free", "CWE-416", SeverityLevel::Critical, "Use memory-safe languages or careful pointer management"),
];
for (vuln_name, cwe, severity, remediation) in code_patterns {
vulnerabilities.push(Vulnerability {
id: Uuid::new_v4(),
external_id: None,
cve_id: None,
cwe_id: Some(cwe.to_string()),
title: format!("Code Analysis: {vuln_name}"),
description: format!("Static analysis check for {vuln_name} patterns"),
severity,
cvss_score: None,
cvss_vector: None,
status: VulnerabilityStatus::Open,
scan_type: ScanType::CodeAnalysis,
affected_component: "Source Code".to_string(),
affected_version: None,
fixed_version: None,
file_path: None,
line_number: None,
remediation: Some(remediation.to_string()),
references: vec![format!("https://cwe.mitre.org/data/definitions/{}.html", cwe.replace("CWE-", ""))],
tags: vec!["sast".to_string(), "code-analysis".to_string()],
first_detected: now,
last_seen: now,
resolved_at: None,
resolved_by: None,
assigned_to: None,
notes: Vec::new(),
});
}
Ok(vulnerabilities)
}
async fn scan_network(&self) -> Result<Vec<Vulnerability>, ScanError> {
let mut vulnerabilities = Vec::new();
let now = Utc::now();
let network_checks = vec![
("Open Ports", "CWE-200", SeverityLevel::Medium, "Close unnecessary ports and use firewall rules", vec!["22", "80", "443", "5432", "6379"]),
("SSL/TLS Version", "CWE-326", SeverityLevel::High, "Use TLS 1.2 or higher", vec!["TLS 1.0", "TLS 1.1", "SSLv3"]),
("Weak Ciphers", "CWE-327", SeverityLevel::Medium, "Use strong cipher suites", vec!["DES", "RC4", "MD5"]),
("Missing HTTPS", "CWE-319", SeverityLevel::High, "Enable HTTPS for all endpoints", vec!["http://"]),
("DNS Security", "CWE-350", SeverityLevel::Medium, "Implement DNSSEC", vec!["unsigned zone"]),
];
for (check_name, cwe, severity, remediation, indicators) in network_checks {
vulnerabilities.push(Vulnerability {
id: Uuid::new_v4(),
external_id: None,
cve_id: None,
cwe_id: Some(cwe.to_string()),
title: format!("Network Security: {check_name}"),
description: format!("Network scan check for: {}", indicators.join(", ")),
severity,
cvss_score: None,
cvss_vector: None,
status: VulnerabilityStatus::Open,
scan_type: ScanType::NetworkScan,
affected_component: "Network Configuration".to_string(),
affected_version: None,
fixed_version: None,
file_path: None,
line_number: None,
remediation: Some(remediation.to_string()),
references: vec![format!("https://cwe.mitre.org/data/definitions/{}.html", cwe.replace("CWE-", ""))],
tags: vec!["network".to_string(), "infrastructure".to_string()],
first_detected: now,
last_seen: now,
resolved_at: None,
resolved_by: None,
assigned_to: None,
notes: Vec::new(),
});
}
Ok(vulnerabilities)
}
async fn check_compliance(&self) -> Result<Vec<Vulnerability>, ScanError> {
let mut vulnerabilities = Vec::new();
let now = Utc::now();
let compliance_checks = vec![
("GDPR - Data Encryption", "CWE-311", SeverityLevel::High, "Encrypt personal data at rest and in transit", "gdpr"),
("GDPR - Access Controls", "CWE-284", SeverityLevel::High, "Implement role-based access controls", "gdpr"),
("GDPR - Audit Logging", "CWE-778", SeverityLevel::Medium, "Enable comprehensive audit logging", "gdpr"),
("SOC2 - Change Management", "CWE-439", SeverityLevel::Medium, "Implement change management procedures", "soc2"),
("SOC2 - Incident Response", "CWE-778", SeverityLevel::Medium, "Document incident response procedures", "soc2"),
("HIPAA - PHI Protection", "CWE-311", SeverityLevel::Critical, "Encrypt all PHI data", "hipaa"),
("HIPAA - Access Audit", "CWE-778", SeverityLevel::High, "Log all access to PHI", "hipaa"),
("PCI-DSS - Cardholder Data", "CWE-311", SeverityLevel::Critical, "Encrypt cardholder data", "pci-dss"),
("PCI-DSS - Network Segmentation", "CWE-284", SeverityLevel::High, "Segment cardholder data environment", "pci-dss"),
("ISO27001 - Risk Assessment", "CWE-693", SeverityLevel::Medium, "Conduct regular risk assessments", "iso27001"),
];
for (check_name, cwe, severity, remediation, framework) in compliance_checks {
vulnerabilities.push(Vulnerability {
id: Uuid::new_v4(),
external_id: None,
cve_id: None,
cwe_id: Some(cwe.to_string()),
title: format!("Compliance: {check_name}"),
description: format!("Compliance requirement check for {framework} framework"),
severity,
cvss_score: None,
cvss_vector: None,
status: VulnerabilityStatus::Open,
scan_type: ScanType::ComplianceCheck,
affected_component: format!("{} Compliance", framework.to_uppercase()),
affected_version: None,
fixed_version: None,
file_path: None,
line_number: None,
remediation: Some(remediation.to_string()),
references: vec![format!("https://cwe.mitre.org/data/definitions/{}.html", cwe.replace("CWE-", ""))],
tags: vec!["compliance".to_string(), framework.to_string()],
first_detected: now,
last_seen: now,
resolved_at: None,
resolved_by: None,
assigned_to: None,
notes: Vec::new(),
});
}
Ok(vulnerabilities)
}
pub async fn get_vulnerability(&self, id: Uuid) -> Option<Vulnerability> {
let vulns = self.vulnerabilities.read().await;
vulns.get(&id).cloned()
}
pub async fn get_all_vulnerabilities(&self) -> Vec<Vulnerability> {
let vulns = self.vulnerabilities.read().await;
vulns.values().cloned().collect()
}
pub async fn get_vulnerabilities_by_severity(&self, severity: SeverityLevel) -> Vec<Vulnerability> {
let vulns = self.vulnerabilities.read().await;
vulns
.values()
.filter(|v| v.severity == severity)
.cloned()
.collect()
}
pub async fn get_open_vulnerabilities(&self) -> Vec<Vulnerability> {
let vulns = self.vulnerabilities.read().await;
vulns
.values()
.filter(|v| v.status == VulnerabilityStatus::Open)
.cloned()
.collect()
}
pub async fn update_vulnerability_status(
&self,
id: Uuid,
status: VulnerabilityStatus,
user_id: Option<Uuid>,
) -> Result<Vulnerability, ScanError> {
let mut vulns = self.vulnerabilities.write().await;
let vuln = vulns
.get_mut(&id)
.ok_or_else(|| ScanError::NotFound("Vulnerability not found".to_string()))?;
vuln.status = status.clone();
if status == VulnerabilityStatus::Resolved {
vuln.resolved_at = Some(Utc::now());
vuln.resolved_by = user_id;
}
Ok(vuln.clone())
}
pub async fn add_vulnerability_note(
&self,
vuln_id: Uuid,
content: String,
author_id: Uuid,
) -> Result<VulnerabilityNote, ScanError> {
let mut vulns = self.vulnerabilities.write().await;
let vuln = vulns
.get_mut(&vuln_id)
.ok_or_else(|| ScanError::NotFound("Vulnerability not found".to_string()))?;
let note = VulnerabilityNote {
id: Uuid::new_v4(),
content,
author_id,
created_at: Utc::now(),
};
vuln.notes.push(note.clone());
Ok(note)
}
pub async fn assign_vulnerability(
&self,
vuln_id: Uuid,
assignee_id: Uuid,
) -> Result<Vulnerability, ScanError> {
let mut vulns = self.vulnerabilities.write().await;
let vuln = vulns
.get_mut(&vuln_id)
.ok_or_else(|| ScanError::NotFound("Vulnerability not found".to_string()))?;
vuln.assigned_to = Some(assignee_id);
vuln.status = VulnerabilityStatus::InProgress;
Ok(vuln.clone())
}
pub async fn generate_report(
&self,
report_type: ReportType,
period_start: DateTime<Utc>,
period_end: DateTime<Utc>,
) -> Result<VulnerabilityReport, ScanError> {
let vulns = self.vulnerabilities.read().await;
let period_vulns: Vec<Vulnerability> = vulns
.values()
.filter(|v| v.first_detected >= period_start && v.first_detected <= period_end)
.cloned()
.collect();
let mut by_severity: HashMap<String, u32> = HashMap::new();
let mut by_status: HashMap<String, u32> = HashMap::new();
let mut by_type: HashMap<String, u32> = HashMap::new();
for vuln in &period_vulns {
*by_severity
.entry(format!("{:?}", vuln.severity))
.or_insert(0) += 1;
*by_status
.entry(format!("{:?}", vuln.status))
.or_insert(0) += 1;
*by_type
.entry(format!("{:?}", vuln.scan_type))
.or_insert(0) += 1;
}
let open_count = period_vulns
.iter()
.filter(|v| v.status == VulnerabilityStatus::Open)
.count() as u32;
let resolved_count = period_vulns
.iter()
.filter(|v| v.status == VulnerabilityStatus::Resolved)
.count() as u32;
let total = period_vulns.len() as u32;
let compliance_score = if total > 0 {
((total - open_count) as f32 / total as f32) * 100.0
} else {
100.0
};
let summary = ReportSummary {
total_vulnerabilities: total,
new_vulnerabilities: total,
resolved_vulnerabilities: resolved_count,
open_vulnerabilities: open_count,
by_severity,
by_status,
by_type,
mean_time_to_remediate_days: None,
compliance_score,
};
let mut recommendations = Vec::new();
let critical_count = period_vulns
.iter()
.filter(|v| v.severity == SeverityLevel::Critical && v.status == VulnerabilityStatus::Open)
.count();
if critical_count > 0 {
recommendations.push(Recommendation {
priority: 1,
title: "Address Critical Vulnerabilities".to_string(),
description: format!("There are {critical_count} critical vulnerabilities that require immediate attention"),
affected_count: critical_count as u32,
estimated_effort: "Immediate".to_string(),
action_items: vec![
"Review and prioritize critical findings".to_string(),
"Apply available patches".to_string(),
"Implement compensating controls if patches unavailable".to_string(),
],
});
}
Ok(VulnerabilityReport {
id: Uuid::new_v4(),
generated_at: Utc::now(),
report_type,
period_start,
period_end,
summary,
vulnerabilities: period_vulns,
trends: VulnerabilityTrends {
total_over_time: Vec::new(),
by_severity_over_time: HashMap::new(),
mttr_over_time: Vec::new(),
},
recommendations,
})
}
pub async fn get_scan_history(&self, limit: usize) -> Vec<ScanResult> {
let results = self.scan_results.read().await;
results.iter().rev().take(limit).cloned().collect()
}
pub async fn get_latest_scan(&self) -> Option<ScanResult> {
let results = self.scan_results.read().await;
results.last().cloned()
}
pub async fn create_policy(&self, policy: SecurityPolicy) -> Result<SecurityPolicy, ScanError> {
let mut policies = self.policies.write().await;
policies.insert(policy.id, policy.clone());
Ok(policy)
}
pub async fn get_policy(&self, id: Uuid) -> Option<SecurityPolicy> {
let policies = self.policies.read().await;
policies.get(&id).cloned()
}
pub async fn get_all_policies(&self) -> Vec<SecurityPolicy> {
let policies = self.policies.read().await;
policies.values().cloned().collect()
}
pub async fn evaluate_policy(
&self,
policy_id: Uuid,
vulnerability: &Vulnerability,
) -> Result<PolicyEvaluationResult, ScanError> {
let policies = self.policies.read().await;
let policy = policies
.get(&policy_id)
.ok_or_else(|| ScanError::NotFound("Policy not found".to_string()))?;
if !policy.enabled {
return Ok(PolicyEvaluationResult {
policy_id,
policy_name: policy.name.clone(),
passed: true,
action: PolicyAction::LogOnly,
matched_rules: Vec::new(),
message: "Policy is disabled".to_string(),
});
}
let mut matched_rules = Vec::new();
let mut action = PolicyAction::LogOnly;
let mut passed = true;
for rule in &policy.rules {
let rule_matches = match rule.rule_type {
PolicyRuleType::BlockSeverity => {
let threshold = match rule.condition.as_str() {
"critical" => SeverityLevel::Critical,
"high" => SeverityLevel::High,
"medium" => SeverityLevel::Medium,
"low" => SeverityLevel::Low,
_ => SeverityLevel::Info,
};
self.severity_meets_threshold(&vulnerability.severity, &threshold)
}
PolicyRuleType::BlockPackage => {
vulnerability.affected_component == rule.condition
}
PolicyRuleType::RequireFix => {
vulnerability.fixed_version.is_some() && vulnerability.status == VulnerabilityStatus::Open
}
_ => false,
};
if rule_matches {
matched_rules.push(rule.clone());
if rule.action == PolicyAction::Block {
action = PolicyAction::Block;
passed = false;
} else if action != PolicyAction::Block && rule.action == PolicyAction::RequireApproval {
action = PolicyAction::RequireApproval;
passed = false;
} else if action == PolicyAction::LogOnly && rule.action == PolicyAction::Warn {
action = PolicyAction::Warn;
}
}
}
if policy.enforcement == PolicyEnforcement::Advisory {
passed = true;
}
Ok(PolicyEvaluationResult {
policy_id,
policy_name: policy.name.clone(),
passed,
action,
matched_rules,
message: if passed {
"Vulnerability passes policy checks".to_string()
} else {
"Vulnerability violates policy".to_string()
},
})
}
fn severity_meets_threshold(&self, severity: &SeverityLevel, threshold: &SeverityLevel) -> bool {
let severity_rank = match severity {
SeverityLevel::Critical => 5,
SeverityLevel::High => 4,
SeverityLevel::Medium => 3,
SeverityLevel::Low => 2,
SeverityLevel::Info => 1,
};
let threshold_rank = match threshold {
SeverityLevel::Critical => 5,
SeverityLevel::High => 4,
SeverityLevel::Medium => 3,
SeverityLevel::Low => 2,
SeverityLevel::Info => 1,
};
severity_rank >= threshold_rank
}
pub async fn update_configuration(
&self,
config: ScanConfiguration,
) -> Result<ScanConfiguration, ScanError> {
let mut current_config = self.configuration.write().await;
*current_config = config.clone();
Ok(config)
}
pub async fn get_configuration(&self) -> ScanConfiguration {
let config = self.configuration.read().await;
config.clone()
}
pub async fn get_dependencies(&self) -> Vec<DependencyInfo> {
let deps = self.dependencies.read().await;
deps.clone()
}
pub async fn get_security_metrics(&self) -> SecurityMetrics {
let vulns = self.vulnerabilities.read().await;
let results = self.scan_results.read().await;
let total = vulns.len() as u32;
let open = vulns
.values()
.filter(|v| v.status == VulnerabilityStatus::Open)
.count() as u32;
let critical_open = vulns
.values()
.filter(|v| {
v.status == VulnerabilityStatus::Open && v.severity == SeverityLevel::Critical
})
.count() as u32;
let high_open = vulns
.values()
.filter(|v| {
v.status == VulnerabilityStatus::Open && v.severity == SeverityLevel::High
})
.count() as u32;
let last_scan = results.last().map(|r| r.completed_at).flatten();
SecurityMetrics {
total_vulnerabilities: total,
open_vulnerabilities: open,
critical_open,
high_open,
resolved_last_30_days: 0,
new_last_30_days: 0,
mean_time_to_remediate_hours: None,
last_scan_time: last_scan,
scan_coverage_percent: 100.0,
policy_compliance_percent: if total > 0 {
((total - critical_open) as f32 / total as f32) * 100.0
} else {
100.0
},
}
}
}
impl Default for VulnerabilityScannerService {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyEvaluationResult {
pub policy_id: Uuid,
pub policy_name: String,
pub passed: bool,
pub action: PolicyAction,
pub matched_rules: Vec<PolicyRule>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityMetrics {
pub total_vulnerabilities: u32,
pub open_vulnerabilities: u32,
pub critical_open: u32,
pub high_open: u32,
pub resolved_last_30_days: u32,
pub new_last_30_days: u32,
pub mean_time_to_remediate_hours: Option<f32>,
pub last_scan_time: Option<DateTime<Utc>>,
pub scan_coverage_percent: f32,
pub policy_compliance_percent: f32,
}
#[derive(Debug, Clone)]
pub enum ScanError {
NotFound(String),
ScanFailed(String),
ConfigurationError(String),
NetworkError(String),
PermissionDenied(String),
Timeout(String),
InvalidInput(String),
}
impl std::fmt::Display for ScanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(msg) => write!(f, "Not found: {msg}"),
Self::ScanFailed(msg) => write!(f, "Scan failed: {msg}"),
Self::ConfigurationError(msg) => write!(f, "Configuration error: {msg}"),
Self::NetworkError(msg) => write!(f, "Network error: {msg}"),
Self::PermissionDenied(msg) => write!(f, "Permission denied: {msg}"),
Self::Timeout(msg) => write!(f, "Timeout: {msg}"),
Self::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
}
}
}
impl std::error::Error for ScanError {}