use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::hash::Hash;
use std::io::Read;
use std::path::Path;
use tracing::info;
pub struct SecurityScanner {
vulnerability_db: VulnerabilityDatabase,
config: ScanConfig,
}
impl SecurityScanner {
pub fn new(config: ScanConfig) -> Self {
Self {
vulnerability_db: VulnerabilityDatabase::new(),
config,
}
}
pub async fn scan_image(&self, image: &ContainerImage) -> Result<ScanReport, Error> {
info!("Scanning image: {}", image.name);
let vulnerabilities: _ = self.scan_vulnerabilities(image).await?;
let compliance_issues: _ = self.scan_compliance(image).await?;
let secrets: _ = self.scan_secrets(image).await?;
let risk_score: _ =
self.calculate_risk_score(&vulnerabilities, &compliance_issues, &secrets);
let report: _ = ScanReport {
image_name: image.name.clone(),
image_digest: image.digest.clone(),
scan_timestamp: std::time::SystemTime::now(),
vulnerabilities: vulnerabilities.clone(),
compliance_issues: compliance_issues.clone(),
secrets: secrets.clone(),
risk_score,
recommendations: self.generate_recommendations(&vulnerabilities, &compliance_issues),
};
info!(
"Completed scan for image: {}. Risk score: {}",
image.name, risk_score
);
Ok(report)
}
async fn scan_vulnerabilities(
&self,
image: &ContainerImage,
) -> Result<Vec<Vulnerability>, Error> {
let mut vulnerabilities = Vec::new();
for layer in &image.layers {
let layer_vulns: _ = self.scan_layer_vulnerabilities(layer).await?;
vulnerabilities.extend(layer_vulns);
}
vulnerabilities.sort_by(|a, b| a.severity.cmp(&b.severity));
vulnerabilities.dedup_by(|a, b| a.id == b.id);
Ok(vulnerabilities)
}
async fn scan_layer_vulnerabilities(
&self,
layer: &ImageLayer,
) -> Result<Vec<Vulnerability>, Error> {
let mut vulnerabilities = Vec::new();
for vuln in self.vulnerability_db.get_vulnerabilities() {
if self.is_vulnerability_relevant(vuln, layer) {
vulnerabilities.push(vuln.clone());
}
}
Ok(vulnerabilities)
}
fn is_vulnerability_relevant(&self, vuln: &Vulnerability, layer: &ImageLayer) -> bool {
if layer.is_base_layer() {
return vuln
.affected_packages
.iter()
.any(|pkg| layer.packages.contains(pkg));
}
vuln.affected_packages
.iter()
.any(|pkg| layer.packages.contains(pkg))
}
async fn scan_compliance(&self, image: &ContainerImage) -> Result<Vec<ComplianceIssue>, Error> {
let mut issues = Vec::new();
if !self.has_non_root_user(image) {
issues.push(ComplianceIssue {
rule_id: "CIS-4.1".to_string(),
severity: ComplianceSeverity::High,
description: "Container is running as root user".to_string(),
remediation: "Create a non-root user and switch to it".to_string(),
});
}
if !self.has_read_only_root(image) {
issues.push(ComplianceIssue {
rule_id: "CIS-4.6".to_string(),
severity: ComplianceSeverity::Medium,
description: "Root filesystem is not read-only".to_string(),
remediation: "Mount root filesystem as read-only".to_string(),
});
}
if !self.drops_capabilities(image) {
issues.push(ComplianceIssue {
rule_id: "CIS-4.4".to_string(),
severity: ComplianceSeverity::Medium,
description: "Linux capabilities are not dropped".to_string(),
remediation: "Drop unnecessary Linux capabilities".to_string(),
});
}
if !self.has_health_check(image) {
issues.push(ComplianceIssue {
rule_id: "CIS-4.8".to_string(),
severity: ComplianceSeverity::Low,
description: "No health check defined".to_string(),
remediation: "Add a HEALTHCHECK instruction".to_string(),
});
}
Ok(issues)
}
async fn scan_secrets(&self, image: &ContainerImage) -> Result<Vec<Secret>, Error> {
let mut secrets = Vec::new();
for layer in &image.layers {
let layer_secrets: _ = self.scan_layer_secrets(layer).await?;
secrets.extend(layer_secrets);
}
Ok(secrets)
}
async fn scan_layer_secrets(&self, layer: &ImageLayer) -> Result<Vec<Secret>, Error> {
let mut secrets = Vec::new();
let secret_patterns: _ = vec![
(
"AWS_ACCESS_KEY_ID",
r#"(?i)aws_access_key_id\s*[=:]\s*['\"]?[A-Z0-9]{16,}['\"]?"#,
),
(
"AWS_SECRET_ACCESS_KEY",
r#"(?i)aws_secret_access_key\s*[=:]\s*['\"]?[A-Z0-9/+=]{40,}['\"]?"#,
),
("PRIVATE_KEY", r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
(
"API_KEY",
r#"(?i)(api[_-]?key|apikey)\s*[=:]\s*['\"]?[a-zA-Z0-9]{32,}['\"]?"#,
),
(
"PASSWORD",
r#"(?i)password\s*[=:]\s*['\"]?[a-zA-Z0-9]{8,}['\"]?"#,
),
];
for file_path in &layer.files {
if let Ok(content) = self.read_file(file_path) {
for (secret_type, pattern) in &secret_patterns {
if let Ok(regex) = regex::Regex::new(pattern) {
if regex.is_match(&content) {
secrets.push(Secret {
secret_type: secret_type.to_string(),
file_path: file_path.clone(),
line_number: self.find_line_number(&content, ®ex),
remediation: format!("Remove or encrypt {}", secret_type),
});
}
}
}
}
}
Ok(secrets)
}
fn read_file(&self, path: &str) -> Result<String, std::io::Error> {
Ok(String::new())
}
fn find_line_number(&self, content: &str, regex: ®ex::Regex) -> Option<u32> {
for (line_num, line) in content.lines().enumerate() {
if regex.is_match(line) {
return Some(line_num as u32 + 1);
}
}
None
}
fn calculate_risk_score(
&self,
vulnerabilities: &[Vulnerability],
compliance_issues: &[ComplianceIssue],
secrets: &[Secret],
) -> f64 {
let mut score = 0.0;
for vuln in vulnerabilities {
score += match vuln.severity {
VulnerabilitySeverity::Critical => 10.0,
VulnerabilitySeverity::High => 7.5,
VulnerabilitySeverity::Medium => 5.0,
VulnerabilitySeverity::Low => 2.5,
VulnerabilitySeverity::Info => 1.0,
};
}
for issue in compliance_issues {
score += match issue.severity {
ComplianceSeverity::High => 5.0,
ComplianceSeverity::Medium => 3.0,
ComplianceSeverity::Low => 1.0,
};
}
for secret in secrets {
score += 8.0; }
(score / 100.0_f64).min(100.0_f64)
}
fn generate_recommendations(
&self,
vulnerabilities: &[Vulnerability],
compliance_issues: &[ComplianceIssue],
) -> Vec<String> {
let mut recommendations = Vec::new();
if vulnerabilities
.iter()
.any(|v| v.severity == VulnerabilitySeverity::Critical)
{
recommendations.push("Address critical vulnerabilities immediately".to_string());
}
if compliance_issues
.iter()
.any(|i| i.severity == ComplianceSeverity::High)
{
recommendations.push("Implement high-priority compliance recommendations".to_string());
}
recommendations.extend(vec![
"Use multi-stage builds to minimize attack surface".to_string(),
"Scan images before deployment".to_string(),
"Keep base images up to date".to_string(),
"Use minimal base images (alpine, distroless)".to_string(),
"Implement least privilege principle".to_string(),
]);
recommendations
}
fn has_non_root_user(&self, image: &ContainerImage) -> bool {
image.layers.iter().any(|layer| {
layer.files.contains(&"/etc/passwd".to_string())
&& layer.files.contains(&"/etc/shadow".to_string())
})
}
fn has_read_only_root(&self, image: &ContainerImage) -> bool {
false
}
fn drops_capabilities(&self, image: &ContainerImage) -> bool {
false
}
fn has_health_check(&self, image: &ContainerImage) -> bool {
image.healthcheck.is_some()
}
}
#[derive(Debug, Clone)]
pub struct ContainerImage {
pub name: String,
pub digest: String,
pub layers: Vec<ImageLayer>,
pub healthcheck: Option<HealthCheckConfig>,
}
#[derive(Debug, Clone)]
pub struct ImageLayer {
pub id: String,
pub size: u64,
pub packages: Vec<String>,
pub files: Vec<String>,
pub is_base_layer: bool,
}
impl ImageLayer {
pub fn is_base_layer(&self) -> bool {
self.is_base_layer
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Vulnerability {
pub id: String,
pub severity: VulnerabilitySeverity,
pub description: String,
pub affected_packages: Vec<String>,
pub fixed_version: Option<String>,
pub cvss_score: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VulnerabilitySeverity {
Critical,
High,
Medium,
Low,
Info,
}
#[derive(Debug, Clone)]
pub struct ComplianceIssue {
pub rule_id: String,
pub severity: ComplianceSeverity,
pub description: String,
pub remediation: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ComplianceSeverity {
High,
Medium,
Low,
}
#[derive(Debug, Clone)]
pub struct Secret {
pub secret_type: String,
pub file_path: String,
pub line_number: Option<u32>,
pub remediation: String,
}
#[derive(Debug, Clone)]
pub struct ScanReport {
pub image_name: String,
pub image_digest: String,
pub scan_timestamp: std::time::SystemTime,
pub vulnerabilities: Vec<Vulnerability>,
pub compliance_issues: Vec<ComplianceIssue>,
pub secrets: Vec<Secret>,
pub risk_score: f64,
pub recommendations: Vec<String>,
}
impl ScanReport {
pub fn vulnerability_count(&self) -> usize {
self.vulnerabilities.len()
}
pub fn critical_vulnerability_count(&self) -> usize {
self.vulnerabilities
.iter()
.filter(|v| v.severity == VulnerabilitySeverity::Critical)
.count()
}
pub fn high_severity_issue_count(&self) -> usize {
let vuln_count: _ = self
.vulnerabilities
.iter()
.filter(|v| v.severity == VulnerabilitySeverity::High)
.count();
let compliance_count: _ = self
.compliance_issues
.iter()
.filter(|i| i.severity == ComplianceSeverity::High)
.count();
vuln_count + compliance_count
}
pub fn passed(&self) -> bool {
self.risk_score < 70.0
&& self.critical_vulnerability_count() == 0
&& self.secrets.is_empty()
}
}
struct VulnerabilityDatabase {
vulnerabilities: Vec<Vulnerability>,
}
impl VulnerabilityDatabase {
fn new() -> Self {
let vulnerabilities: _ = Self::load_vulnerabilities();
Self { vulnerabilities }
}
fn load_vulnerabilities() -> Vec<Vulnerability> {
vec![
Vulnerability {
id: "CVE-2023-1234".to_string(),
severity: VulnerabilitySeverity::High,
description: "Buffer overflow in package X".to_string(),
affected_packages: vec!["package-x".to_string()],
fixed_version: Some("1.2.3".to_string()),
cvss_score: Some(7.5),
},
Vulnerability {
id: "CVE-2023-5678".to_string(),
severity: VulnerabilitySeverity::Critical,
description: "Remote code execution in package Y".to_string(),
affected_packages: vec!["package-y".to_string()],
fixed_version: Some("2.0.0".to_string()),
cvss_score: Some(9.8),
},
]
}
fn get_vulnerabilities(&self) -> &[Vulnerability] {
&self.vulnerabilities
}
}
#[derive(Debug, Clone)]
pub struct ScanConfig {
pub scan_vulnerabilities: bool,
pub scan_compliance: bool,
pub scan_secrets: bool,
pub min_severity: VulnerabilitySeverity,
}
#[derive(Debug, Clone)]
pub struct HealthCheckConfig {
pub test: Vec<String>,
pub interval: u32,
pub timeout: u32,
pub retries: u32,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Scan failed: {0}")]
ScanFailed(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("Other error: {0}")]
Other(String),
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::SystemTime;
#[test]
fn test_vulnerability_severity_ordering() {
assert!(VulnerabilitySeverity::Critical > VulnerabilitySeverity::High);
assert!(VulnerabilitySeverity::High > VulnerabilitySeverity::Medium);
assert!(VulnerabilitySeverity::Medium > VulnerabilitySeverity::Low);
assert!(VulnerabilitySeverity::Low > VulnerabilitySeverity::Info);
}
#[test]
fn test_compliance_severity_ordering() {
assert!(ComplianceSeverity::High > ComplianceSeverity::Medium);
assert!(ComplianceSeverity::Medium > ComplianceSeverity::Low);
}
#[test]
fn test_scan_report_passing() {
let report: _ = ScanReport {
image_name: "test-image".to_string(),
image_digest: "sha256:123".to_string(),
scan_timestamp: std::time::SystemTime::now(),
vulnerabilities: vec![Vulnerability {
id: "CVE-2023-1234".to_string(),
severity: VulnerabilitySeverity::Medium,
description: "Test".to_string(),
affected_packages: vec![],
fixed_version: None,
cvss_score: None,
}],
compliance_issues: vec![],
secrets: vec![],
risk_score: 50.0,
recommendations: vec![],
};
assert!(report.passed());
}
#[test]
fn test_scan_report_failing() {
let report: _ = ScanReport {
image_name: "test-image".to_string(),
image_digest: "sha256:123".to_string(),
scan_timestamp: std::time::SystemTime::now(),
vulnerabilities: vec![Vulnerability {
id: "CVE-2023-1234".to_string(),
severity: VulnerabilitySeverity::Critical,
description: "Test".to_string(),
affected_packages: vec![],
fixed_version: None,
cvss_score: None,
}],
compliance_issues: vec![],
secrets: vec![Secret {
secret_type: "AWS_ACCESS_KEY_ID".to_string(),
file_path: "/etc/secret".to_string(),
line_number: Some(1),
remediation: "Remove secret".to_string(),
}],
risk_score: 80.0,
recommendations: vec![],
};
assert!(!report.passed());
}
}