use crate::rust_version::{
VersionManager, detector::detect_rust_version, file_cache::FileCache, github::GitHubClient,
parser::parse_release_notes,
};
use crate::{Error, Result};
use console::style;
use semver::Version;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SecurityCheckResult {
pub is_secure: bool,
pub current_version: Version,
pub issues: Vec<SecurityIssue>,
pub recommended_version: Option<Version>,
pub offline_mode: bool,
}
#[derive(Debug, Clone)]
pub struct SecurityIssue {
pub severity: crate::rust_version::parser::Severity,
pub description: String,
pub cve_id: Option<String>,
pub fixed_in: Option<Version>,
pub url: Option<String>,
}
impl SecurityCheckResult {
pub fn has_critical_issues(&self) -> bool {
self.issues
.iter()
.any(|i| i.severity == crate::rust_version::parser::Severity::Critical)
}
pub fn highest_severity(&self) -> Option<&crate::rust_version::parser::Severity> {
self.issues.iter().map(|i| &i.severity).max()
}
pub fn severity_counts(&self) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for issue in &self.issues {
*counts.entry(issue.severity.to_string()).or_insert(0) += 1;
}
counts
}
}
pub struct SecurityChecker {
version_manager: VersionManager,
cache: FileCache,
}
impl SecurityChecker {
pub fn new() -> Result<Self> {
let version_manager = VersionManager::new()?;
let cache = FileCache::create_default()?;
Ok(Self {
version_manager,
cache,
})
}
pub async fn check_current_version(&self) -> Result<SecurityCheckResult> {
let current = detect_rust_version().await?;
let offline_mode = self.cache.should_use_offline();
let issues = if offline_mode {
self.check_offline(¤t.version).await?
} else {
self.check_online(¤t.version).await?
};
let recommended_version = if !issues.is_empty() {
self.find_recommended_version(¤t.version).await?
} else {
None
};
let is_secure = issues.is_empty();
Ok(SecurityCheckResult {
is_secure,
current_version: current.version.clone(),
issues,
recommended_version,
offline_mode,
})
}
async fn check_offline(&self, current_version: &Version) -> Result<Vec<SecurityIssue>> {
tracing::info!("Running security check in offline mode");
let cache_key = "recent_releases_30";
let cached = self
.cache
.get(cache_key)
.ok_or_else(|| Error::network("No cached data available for offline security check"))?;
let releases: Vec<crate::rust_version::GitHubRelease> =
serde_json::from_slice(&cached.data)
.map_err(|e| Error::parse(format!("Failed to parse cached releases: {e}")))?;
self.analyze_security_issues(current_version, &releases)
}
async fn check_online(&self, current_version: &Version) -> Result<Vec<SecurityIssue>> {
let client = GitHubClient::new(None)?;
let releases = client.get_releases(30).await?;
if let Ok(data) = serde_json::to_vec(&releases) {
let _ = self
.cache
.set("recent_releases_30", data, "application/json");
}
self.analyze_security_issues(current_version, &releases)
}
fn analyze_security_issues(
&self,
current_version: &Version,
releases: &[crate::rust_version::GitHubRelease],
) -> Result<Vec<SecurityIssue>> {
let mut issues = Vec::new();
for release in releases {
if release.version <= *current_version {
continue;
}
let parsed = parse_release_notes(&release.tag_name, &release.body);
for advisory in parsed.security_advisories {
issues.push(SecurityIssue {
severity: advisory.severity,
description: advisory.description,
cve_id: advisory.id,
fixed_in: Some(release.version.clone()),
url: Some(release.html_url.clone()),
});
}
}
issues.sort_by(|a, b| b.severity.cmp(&a.severity));
Ok(issues)
}
async fn find_recommended_version(
&self,
_current_version: &Version,
) -> Result<Option<Version>> {
let latest = self.version_manager.get_latest_stable().await?;
Ok(Some(latest.version))
}
pub fn display_results(result: &SecurityCheckResult) {
println!();
if result.is_secure {
println!("{}", style("✅ Security Check Passed").green().bold());
println!(
" Your Rust version {} has no known security vulnerabilities.",
style(&result.current_version).green()
);
} else {
let severity = result.highest_severity();
let header = match severity {
Some(s) if *s == crate::rust_version::parser::Severity::Critical => {
style("🚨 CRITICAL SECURITY ISSUES FOUND").red().bold()
}
Some(s) if *s == crate::rust_version::parser::Severity::High => {
style("⚠️ HIGH SEVERITY SECURITY ISSUES FOUND")
.yellow()
.bold()
}
_ => style("⚠️ Security Issues Found").yellow().bold(),
};
println!("{}", header);
println!();
println!(
" Your Rust version {} has {} known security issue{}.",
style(&result.current_version).red(),
result.issues.len(),
if result.issues.len() == 1 { "" } else { "s" }
);
let counts = result.severity_counts();
let mut parts = Vec::new();
for (sev, count) in &counts {
let styled = match sev.as_str() {
"CRITICAL" => style(format!("{} CRITICAL", count)).red().bold(),
"HIGH" => style(format!("{} HIGH", count)).yellow().bold(),
"MEDIUM" => style(format!("{} MEDIUM", count)).yellow(),
"LOW" => style(format!("{} LOW", count)).dim(),
_ => style(format!("{} {}", count, sev)),
};
parts.push(styled.to_string());
}
if !parts.is_empty() {
println!(" Severity breakdown: {}", parts.join(", "));
}
println!();
println!("{}", style(" Security Issues:").bold());
for issue in result.issues.iter().take(5) {
let sev_icon = match issue.severity {
crate::rust_version::parser::Severity::Critical => "🔴",
crate::rust_version::parser::Severity::High => "🟠",
crate::rust_version::parser::Severity::Medium => "🟡",
crate::rust_version::parser::Severity::Low => "🔵",
crate::rust_version::parser::Severity::Unknown => "⚪",
};
println!();
println!(
" {} {} {}",
sev_icon,
style(format!("[{}]", issue.severity)).bold(),
issue.description
);
if let Some(ref cve) = issue.cve_id {
println!(" CVE: {}", style(cve).cyan());
}
if let Some(ref fixed) = issue.fixed_in {
println!(" Fixed in: {}", style(fixed).green());
}
if let Some(ref url) = issue.url {
println!(" URL: {}", style(url).dim());
}
}
if result.issues.len() > 5 {
println!("\n ... and {} more issues", result.issues.len() - 5);
}
println!();
if let Some(ref recommended) = result.recommended_version {
println!(
"{} Update to the latest version",
style("🔧 Recommended Action:").bold()
);
println!(
" Latest secure version: {}",
style(recommended).green().bold()
);
println!(" Update command: {}", style("rustup update").cyan());
}
}
if result.offline_mode {
println!();
println!(
"{}",
style("📴 Running in offline mode (using cached data)").dim()
);
}
println!();
}
}
pub async fn quick_security_check() -> Result<bool> {
let checker = SecurityChecker::new()?;
match checker.check_current_version().await {
Ok(result) => {
if !result.is_secure {
SecurityChecker::display_results(&result);
}
Ok(!result.has_critical_issues())
}
Err(e) => {
tracing::warn!("Security check failed: {}", e);
Ok(true)
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_security_check_result_helpers() {
let result = SecurityCheckResult {
is_secure: false,
current_version: Version::new(1, 70, 0),
issues: vec![
SecurityIssue {
severity: crate::rust_version::parser::Severity::Critical,
description: "Critical bug".to_string(),
cve_id: Some("CVE-2023-1".to_string()),
fixed_in: Some(Version::new(1, 71, 0)),
url: None,
},
SecurityIssue {
severity: crate::rust_version::parser::Severity::High,
description: "High bug".to_string(),
cve_id: None,
fixed_in: Some(Version::new(1, 71, 0)),
url: None,
},
],
recommended_version: Some(Version::new(1, 71, 0)),
offline_mode: false,
};
assert!(result.has_critical_issues());
assert_eq!(
result.highest_severity(),
Some(&crate::rust_version::parser::Severity::Critical)
);
let counts = result.severity_counts();
assert_eq!(counts.get("CRITICAL"), Some(&1));
assert_eq!(counts.get("HIGH"), Some(&1));
}
#[test]
fn test_security_check_result_secure() {
let result = SecurityCheckResult {
is_secure: true,
current_version: Version::new(1, 70, 0),
issues: vec![],
recommended_version: None,
offline_mode: false,
};
assert!(!result.has_critical_issues());
assert!(result.highest_severity().is_none());
assert!(result.severity_counts().is_empty());
}
}