ferrous_forge/rust_version/
security.rs1use crate::rust_version::{
10 VersionManager, detector::detect_rust_version, file_cache::FileCache, github::GitHubClient,
11 parser::parse_release_notes,
12};
13use crate::{Error, Result};
14use console::style;
15use semver::Version;
16use std::collections::HashMap;
17
18#[derive(Debug, Clone)]
20pub struct SecurityCheckResult {
21 pub is_secure: bool,
23 pub current_version: Version,
25 pub issues: Vec<SecurityIssue>,
27 pub recommended_version: Option<Version>,
29 pub offline_mode: bool,
31}
32
33#[derive(Debug, Clone)]
35pub struct SecurityIssue {
36 pub severity: crate::rust_version::parser::Severity,
38 pub description: String,
40 pub cve_id: Option<String>,
42 pub fixed_in: Option<Version>,
44 pub url: Option<String>,
46}
47
48impl SecurityCheckResult {
49 pub fn has_critical_issues(&self) -> bool {
51 self.issues
52 .iter()
53 .any(|i| i.severity == crate::rust_version::parser::Severity::Critical)
54 }
55
56 pub fn highest_severity(&self) -> Option<&crate::rust_version::parser::Severity> {
58 self.issues.iter().map(|i| &i.severity).max()
59 }
60
61 pub fn severity_counts(&self) -> HashMap<String, usize> {
63 let mut counts = HashMap::new();
64 for issue in &self.issues {
65 *counts.entry(issue.severity.to_string()).or_insert(0) += 1;
66 }
67 counts
68 }
69}
70
71pub struct SecurityChecker {
73 version_manager: VersionManager,
74 cache: FileCache,
75}
76
77impl SecurityChecker {
78 pub fn new() -> Result<Self> {
84 let version_manager = VersionManager::new()?;
85 let cache = FileCache::default()?;
86
87 Ok(Self {
88 version_manager,
89 cache,
90 })
91 }
92
93 pub async fn check_current_version(&self) -> Result<SecurityCheckResult> {
100 let current = detect_rust_version()?;
101 let offline_mode = self.cache.should_use_offline();
102
103 let issues = if offline_mode {
104 self.check_offline(¤t.version).await?
105 } else {
106 self.check_online(¤t.version).await?
107 };
108
109 let recommended_version = if !issues.is_empty() {
111 self.find_recommended_version(¤t.version).await?
112 } else {
113 None
114 };
115
116 let is_secure = issues.is_empty();
117
118 Ok(SecurityCheckResult {
119 is_secure,
120 current_version: current.version.clone(),
121 issues,
122 recommended_version,
123 offline_mode,
124 })
125 }
126
127 async fn check_offline(&self, current_version: &Version) -> Result<Vec<SecurityIssue>> {
129 tracing::info!("Running security check in offline mode");
130
131 let cache_key = "recent_releases_30";
133 let cached = self
134 .cache
135 .get(cache_key)
136 .ok_or_else(|| Error::network("No cached data available for offline security check"))?;
137
138 let releases: Vec<crate::rust_version::GitHubRelease> =
139 serde_json::from_slice(&cached.data)
140 .map_err(|e| Error::parse(format!("Failed to parse cached releases: {e}")))?;
141
142 self.analyze_security_issues(current_version, &releases)
143 }
144
145 async fn check_online(&self, current_version: &Version) -> Result<Vec<SecurityIssue>> {
147 let client = GitHubClient::new(None)?;
148
149 let releases = client.get_releases(30).await?;
151
152 if let Ok(data) = serde_json::to_vec(&releases) {
154 let _ = self
155 .cache
156 .set("recent_releases_30", data, "application/json");
157 }
158
159 self.analyze_security_issues(current_version, &releases)
160 }
161
162 fn analyze_security_issues(
164 &self,
165 current_version: &Version,
166 releases: &[crate::rust_version::GitHubRelease],
167 ) -> Result<Vec<SecurityIssue>> {
168 let mut issues = Vec::new();
169
170 for release in releases {
171 if release.version <= *current_version {
173 continue;
174 }
175
176 let parsed = parse_release_notes(&release.tag_name, &release.body);
177
178 for advisory in parsed.security_advisories {
179 issues.push(SecurityIssue {
180 severity: advisory.severity,
181 description: advisory.description,
182 cve_id: advisory.id,
183 fixed_in: Some(release.version.clone()),
184 url: Some(release.html_url.clone()),
185 });
186 }
187 }
188
189 issues.sort_by(|a, b| b.severity.cmp(&a.severity));
191
192 Ok(issues)
193 }
194
195 async fn find_recommended_version(
197 &self,
198 _current_version: &Version,
199 ) -> Result<Option<Version>> {
200 let latest = self.version_manager.get_latest_stable().await?;
202 Ok(Some(latest.version))
203 }
204
205 pub fn display_results(result: &SecurityCheckResult) {
207 println!();
208
209 if result.is_secure {
210 println!("{}", style("✅ Security Check Passed").green().bold());
211 println!(
212 " Your Rust version {} has no known security vulnerabilities.",
213 style(&result.current_version).green()
214 );
215 } else {
216 let severity = result.highest_severity();
217 let header = match severity {
218 Some(s) if *s == crate::rust_version::parser::Severity::Critical => {
219 style("🚨 CRITICAL SECURITY ISSUES FOUND").red().bold()
220 }
221 Some(s) if *s == crate::rust_version::parser::Severity::High => {
222 style("⚠️ HIGH SEVERITY SECURITY ISSUES FOUND")
223 .yellow()
224 .bold()
225 }
226 _ => style("⚠️ Security Issues Found").yellow().bold(),
227 };
228
229 println!("{}", header);
230 println!();
231 println!(
232 " Your Rust version {} has {} known security issue{}.",
233 style(&result.current_version).red(),
234 result.issues.len(),
235 if result.issues.len() == 1 { "" } else { "s" }
236 );
237
238 let counts = result.severity_counts();
240 let mut parts = Vec::new();
241 for (sev, count) in &counts {
242 let styled = match sev.as_str() {
243 "CRITICAL" => style(format!("{} CRITICAL", count)).red().bold(),
244 "HIGH" => style(format!("{} HIGH", count)).yellow().bold(),
245 "MEDIUM" => style(format!("{} MEDIUM", count)).yellow(),
246 "LOW" => style(format!("{} LOW", count)).dim(),
247 _ => style(format!("{} {}", count, sev)),
248 };
249 parts.push(styled.to_string());
250 }
251
252 if !parts.is_empty() {
253 println!(" Severity breakdown: {}", parts.join(", "));
254 }
255
256 println!();
257 println!("{}", style(" Security Issues:").bold());
258
259 for (_i, issue) in result.issues.iter().take(5).enumerate() {
260 let sev_icon = match issue.severity {
261 crate::rust_version::parser::Severity::Critical => "🔴",
262 crate::rust_version::parser::Severity::High => "🟠",
263 crate::rust_version::parser::Severity::Medium => "🟡",
264 crate::rust_version::parser::Severity::Low => "🔵",
265 crate::rust_version::parser::Severity::Unknown => "⚪",
266 };
267
268 println!();
269 println!(
270 " {} {} {}",
271 sev_icon,
272 style(format!("[{}]", issue.severity)).bold(),
273 issue.description
274 );
275
276 if let Some(ref cve) = issue.cve_id {
277 println!(" CVE: {}", style(cve).cyan());
278 }
279
280 if let Some(ref fixed) = issue.fixed_in {
281 println!(" Fixed in: {}", style(fixed).green());
282 }
283
284 if let Some(ref url) = issue.url {
285 println!(" URL: {}", style(url).dim());
286 }
287 }
288
289 if result.issues.len() > 5 {
290 println!("\n ... and {} more issues", result.issues.len() - 5);
291 }
292
293 println!();
294 if let Some(ref recommended) = result.recommended_version {
295 println!(
296 "{} {}",
297 style("🔧 Recommended Action:").bold(),
298 "Update to the latest version"
299 );
300 println!(
301 " Latest secure version: {}",
302 style(recommended).green().bold()
303 );
304 println!(" Update command: {}", style("rustup update").cyan());
305 }
306 }
307
308 if result.offline_mode {
309 println!();
310 println!(
311 "{}",
312 style("📴 Running in offline mode (using cached data)").dim()
313 );
314 }
315
316 println!();
317 }
318}
319
320pub async fn quick_security_check() -> Result<bool> {
328 let checker = SecurityChecker::new()?;
329
330 match checker.check_current_version().await {
331 Ok(result) => {
332 if !result.is_secure {
333 SecurityChecker::display_results(&result);
334 }
335 Ok(!result.has_critical_issues())
336 }
337 Err(e) => {
338 tracing::warn!("Security check failed: {}", e);
339 Ok(true)
341 }
342 }
343}
344
345#[cfg(test)]
346#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn test_security_check_result_helpers() {
352 let result = SecurityCheckResult {
353 is_secure: false,
354 current_version: Version::new(1, 70, 0),
355 issues: vec![
356 SecurityIssue {
357 severity: crate::rust_version::parser::Severity::Critical,
358 description: "Critical bug".to_string(),
359 cve_id: Some("CVE-2023-1".to_string()),
360 fixed_in: Some(Version::new(1, 71, 0)),
361 url: None,
362 },
363 SecurityIssue {
364 severity: crate::rust_version::parser::Severity::High,
365 description: "High bug".to_string(),
366 cve_id: None,
367 fixed_in: Some(Version::new(1, 71, 0)),
368 url: None,
369 },
370 ],
371 recommended_version: Some(Version::new(1, 71, 0)),
372 offline_mode: false,
373 };
374
375 assert!(result.has_critical_issues());
376 assert_eq!(
377 result.highest_severity(),
378 Some(&crate::rust_version::parser::Severity::Critical)
379 );
380
381 let counts = result.severity_counts();
382 assert_eq!(counts.get("CRITICAL"), Some(&1));
383 assert_eq!(counts.get("HIGH"), Some(&1));
384 }
385
386 #[test]
387 fn test_security_check_result_secure() {
388 let result = SecurityCheckResult {
389 is_secure: true,
390 current_version: Version::new(1, 70, 0),
391 issues: vec![],
392 recommended_version: None,
393 offline_mode: false,
394 };
395
396 assert!(!result.has_critical_issues());
397 assert!(result.highest_severity().is_none());
398 assert!(result.severity_counts().is_empty());
399 }
400}