1125 lines
39 KiB
Rust
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 {}
|