1use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Dependency {
10 pub name: String,
12 pub current_version: String,
14 pub latest_version: Option<String>,
16 pub is_outdated: bool,
18 pub vulnerabilities: Vec<Vulnerability>,
20 pub dep_type: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Vulnerability {
27 pub cve_id: String,
29 pub severity: VulnerabilitySeverity,
31 pub description: String,
33 pub affected_versions: Vec<String>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum VulnerabilitySeverity {
40 Low,
42 Medium,
44 High,
46 Critical,
48}
49
50impl std::fmt::Display for VulnerabilitySeverity {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 VulnerabilitySeverity::Low => write!(f, "Low"),
54 VulnerabilitySeverity::Medium => write!(f, "Medium"),
55 VulnerabilitySeverity::High => write!(f, "High"),
56 VulnerabilitySeverity::Critical => write!(f, "Critical"),
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct DependencyScanResult {
64 pub dependencies: Vec<Dependency>,
66 pub outdated_count: usize,
68 pub vulnerable_count: usize,
70 pub total_vulnerabilities: usize,
72}
73
74#[derive(Debug, Clone)]
76pub struct DependencyUpdateSuggestion {
77 pub dependency: Dependency,
79 pub reason: UpdateReason,
81 pub risk_level: UpdateRiskLevel,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum UpdateReason {
88 Outdated,
90 SecurityVulnerability,
92 OutdatedAndVulnerable,
94}
95
96impl std::fmt::Display for UpdateReason {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 match self {
99 UpdateReason::Outdated => write!(f, "Outdated"),
100 UpdateReason::SecurityVulnerability => write!(f, "Security Vulnerability"),
101 UpdateReason::OutdatedAndVulnerable => write!(f, "Outdated and Vulnerable"),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub enum UpdateRiskLevel {
109 Low,
111 Medium,
113 High,
115}
116
117impl std::fmt::Display for UpdateRiskLevel {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 UpdateRiskLevel::Low => write!(f, "Low"),
121 UpdateRiskLevel::Medium => write!(f, "Medium"),
122 UpdateRiskLevel::High => write!(f, "High"),
123 }
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct DependencyUpdatePrResult {
130 pub pr_number: u32,
132 pub pr_url: String,
134 pub updated_dependencies: Vec<String>,
136 pub branch_name: String,
138}
139
140#[derive(Debug, Clone)]
142pub struct DependencyUpdateVerificationResult {
143 pub build_passed: bool,
145 pub status_message: String,
147 pub tests_passed: bool,
149 pub issues: Vec<String>,
151}
152
153#[derive(Debug, Clone)]
155pub struct DependencyManager {
156 pub owner: String,
158 pub repo: String,
160}
161
162impl DependencyManager {
163 pub fn new(owner: String, repo: String) -> Self {
165 Self { owner, repo }
166 }
167
168 pub fn scan_dependencies(&self) -> Result<DependencyScanResult, DependencyError> {
170 let dependencies = vec![
172 Dependency {
173 name: "tokio".to_string(),
174 current_version: "1.35.0".to_string(),
175 latest_version: Some("1.36.0".to_string()),
176 is_outdated: true,
177 vulnerabilities: vec![],
178 dep_type: "runtime".to_string(),
179 },
180 Dependency {
181 name: "serde".to_string(),
182 current_version: "1.0.190".to_string(),
183 latest_version: Some("1.0.195".to_string()),
184 is_outdated: true,
185 vulnerabilities: vec![],
186 dep_type: "runtime".to_string(),
187 },
188 Dependency {
189 name: "log4j".to_string(),
190 current_version: "2.14.0".to_string(),
191 latest_version: Some("2.21.0".to_string()),
192 is_outdated: true,
193 vulnerabilities: vec![Vulnerability {
194 cve_id: "CVE-2021-44228".to_string(),
195 severity: VulnerabilitySeverity::Critical,
196 description: "Remote code execution vulnerability".to_string(),
197 affected_versions: vec!["2.14.0".to_string()],
198 }],
199 dep_type: "runtime".to_string(),
200 },
201 Dependency {
202 name: "openssl".to_string(),
203 current_version: "1.1.1k".to_string(),
204 latest_version: Some("3.0.0".to_string()),
205 is_outdated: true,
206 vulnerabilities: vec![Vulnerability {
207 cve_id: "CVE-2023-0286".to_string(),
208 severity: VulnerabilitySeverity::High,
209 description: "X.509 certificate verification bypass".to_string(),
210 affected_versions: vec!["1.1.1k".to_string()],
211 }],
212 dep_type: "runtime".to_string(),
213 },
214 ];
215
216 let outdated_count = dependencies.iter().filter(|d| d.is_outdated).count();
217 let vulnerable_count = dependencies.iter().filter(|d| !d.vulnerabilities.is_empty()).count();
218 let total_vulnerabilities: usize = dependencies.iter().map(|d| d.vulnerabilities.len()).sum();
219
220 Ok(DependencyScanResult {
221 dependencies,
222 outdated_count,
223 vulnerable_count,
224 total_vulnerabilities,
225 })
226 }
227
228 pub fn suggest_updates(&self, scan_result: &DependencyScanResult) -> Result<Vec<DependencyUpdateSuggestion>, DependencyError> {
230 let mut suggestions = Vec::new();
231
232 for dep in &scan_result.dependencies {
233 let reason = if !dep.vulnerabilities.is_empty() && dep.is_outdated {
234 UpdateReason::OutdatedAndVulnerable
235 } else if !dep.vulnerabilities.is_empty() {
236 UpdateReason::SecurityVulnerability
237 } else if dep.is_outdated {
238 UpdateReason::Outdated
239 } else {
240 continue;
241 };
242
243 let risk_level = if !dep.vulnerabilities.is_empty()
244 || dep.latest_version.as_ref().is_some_and(|v| is_major_version_bump(&dep.current_version, v))
245 {
246 UpdateRiskLevel::High
247 } else if dep.latest_version.as_ref().is_some_and(|v| is_minor_version_bump(&dep.current_version, v)) {
248 UpdateRiskLevel::Medium
249 } else {
250 UpdateRiskLevel::Low
251 };
252
253 suggestions.push(DependencyUpdateSuggestion {
254 dependency: dep.clone(),
255 reason,
256 risk_level,
257 });
258 }
259
260 Ok(suggestions)
261 }
262
263 pub fn create_update_pr(&self, suggestions: &[DependencyUpdateSuggestion]) -> Result<DependencyUpdatePrResult, DependencyError> {
265 if suggestions.is_empty() {
266 return Err(DependencyError::NoUpdatesAvailable);
267 }
268
269 let updated_deps: Vec<String> = suggestions.iter().map(|s| s.dependency.name.clone()).collect();
270 let branch_name = format!("deps/update-{}", updated_deps.join("-"));
271
272 Ok(DependencyUpdatePrResult {
273 pr_number: 42,
274 pr_url: format!("https://github.com/{}/{}/pull/42", self.owner, self.repo),
275 updated_dependencies: updated_deps,
276 branch_name,
277 })
278 }
279
280 pub fn verify_update(&self, _pr_number: u32) -> Result<DependencyUpdateVerificationResult, DependencyError> {
282 Ok(DependencyUpdateVerificationResult {
283 build_passed: true,
284 status_message: "Build passed successfully".to_string(),
285 tests_passed: true,
286 issues: vec![],
287 })
288 }
289
290 pub fn track_vulnerabilities(&self, scan_result: &DependencyScanResult) -> Result<VulnerabilityReport, DependencyError> {
292 let mut vulnerabilities_by_severity: HashMap<VulnerabilitySeverity, Vec<Vulnerability>> = HashMap::new();
293
294 for dep in &scan_result.dependencies {
295 for vuln in &dep.vulnerabilities {
296 vulnerabilities_by_severity
297 .entry(vuln.severity)
298 .or_default()
299 .push(vuln.clone());
300 }
301 }
302
303 let critical_count = vulnerabilities_by_severity.get(&VulnerabilitySeverity::Critical).map_or(0, |v| v.len());
304 let high_count = vulnerabilities_by_severity.get(&VulnerabilitySeverity::High).map_or(0, |v| v.len());
305 let medium_count = vulnerabilities_by_severity.get(&VulnerabilitySeverity::Medium).map_or(0, |v| v.len());
306 let low_count = vulnerabilities_by_severity.get(&VulnerabilitySeverity::Low).map_or(0, |v| v.len());
307
308 Ok(VulnerabilityReport {
309 total_vulnerabilities: scan_result.total_vulnerabilities,
310 critical_count,
311 high_count,
312 medium_count,
313 low_count,
314 vulnerabilities_by_severity,
315 })
316 }
317}
318
319#[derive(Debug, Clone)]
321pub struct VulnerabilityReport {
322 pub total_vulnerabilities: usize,
324 pub critical_count: usize,
326 pub high_count: usize,
328 pub medium_count: usize,
330 pub low_count: usize,
332 pub vulnerabilities_by_severity: HashMap<VulnerabilitySeverity, Vec<Vulnerability>>,
334}
335
336#[derive(Debug, thiserror::Error)]
338pub enum DependencyError {
339 #[error("No updates available")]
341 NoUpdatesAvailable,
342
343 #[error("Dependency not found: {0}")]
345 DependencyNotFound(String),
346
347 #[error("Invalid version format: {0}")]
349 InvalidVersion(String),
350
351 #[error("API error: {0}")]
353 ApiError(String),
354
355 #[error("Build verification failed: {0}")]
357 BuildVerificationFailed(String),
358}
359
360fn is_major_version_bump(current: &str, latest: &str) -> bool {
362 let current_major = current.split('.').next().unwrap_or("0");
363 let latest_major = latest.split('.').next().unwrap_or("0");
364 current_major != latest_major
365}
366
367fn is_minor_version_bump(current: &str, latest: &str) -> bool {
369 let current_parts: Vec<&str> = current.split('.').collect();
370 let latest_parts: Vec<&str> = latest.split('.').collect();
371
372 if current_parts.len() < 2 || latest_parts.len() < 2 {
373 return false;
374 }
375
376 current_parts[0] == latest_parts[0] && current_parts[1] != latest_parts[1]
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_dependency_manager_creation() {
385 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
386 assert_eq!(manager.owner, "owner");
387 assert_eq!(manager.repo, "repo");
388 }
389
390 #[test]
391 fn test_scan_dependencies_returns_results() {
392 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
393 let result = manager.scan_dependencies().unwrap();
394 assert!(!result.dependencies.is_empty());
395 assert!(result.outdated_count > 0);
396 }
397
398 #[test]
399 fn test_scan_dependencies_identifies_vulnerabilities() {
400 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
401 let result = manager.scan_dependencies().unwrap();
402 assert!(result.vulnerable_count > 0);
403 assert!(result.total_vulnerabilities > 0);
404 }
405
406 #[test]
407 fn test_suggest_updates_returns_suggestions() {
408 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
409 let scan_result = manager.scan_dependencies().unwrap();
410 let suggestions = manager.suggest_updates(&scan_result).unwrap();
411 assert!(!suggestions.is_empty());
412 }
413
414 #[test]
415 fn test_suggest_updates_identifies_security_vulnerabilities() {
416 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
417 let scan_result = manager.scan_dependencies().unwrap();
418 let suggestions = manager.suggest_updates(&scan_result).unwrap();
419 let security_suggestions: Vec<_> = suggestions
420 .iter()
421 .filter(|s| matches!(s.reason, UpdateReason::SecurityVulnerability | UpdateReason::OutdatedAndVulnerable))
422 .collect();
423 assert!(!security_suggestions.is_empty());
424 }
425
426 #[test]
427 fn test_create_update_pr_with_suggestions() {
428 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
429 let scan_result = manager.scan_dependencies().unwrap();
430 let suggestions = manager.suggest_updates(&scan_result).unwrap();
431 let pr_result = manager.create_update_pr(&suggestions).unwrap();
432 assert!(pr_result.pr_number > 0);
433 assert!(!pr_result.updated_dependencies.is_empty());
434 }
435
436 #[test]
437 fn test_create_update_pr_fails_with_no_suggestions() {
438 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
439 let result = manager.create_update_pr(&[]);
440 assert!(result.is_err());
441 }
442
443 #[test]
444 fn test_verify_update_returns_result() {
445 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
446 let result = manager.verify_update(42).unwrap();
447 assert!(result.build_passed);
448 assert!(result.tests_passed);
449 }
450
451 #[test]
452 fn test_track_vulnerabilities_returns_report() {
453 let manager = DependencyManager::new("owner".to_string(), "repo".to_string());
454 let scan_result = manager.scan_dependencies().unwrap();
455 let report = manager.track_vulnerabilities(&scan_result).unwrap();
456 assert_eq!(report.total_vulnerabilities, scan_result.total_vulnerabilities);
457 assert!(report.critical_count > 0 || report.high_count > 0);
458 }
459
460 #[test]
461 fn test_vulnerability_severity_ordering() {
462 assert!(VulnerabilitySeverity::Low < VulnerabilitySeverity::Medium);
463 assert!(VulnerabilitySeverity::Medium < VulnerabilitySeverity::High);
464 assert!(VulnerabilitySeverity::High < VulnerabilitySeverity::Critical);
465 }
466
467 #[test]
468 fn test_update_risk_level_ordering() {
469 assert!(UpdateRiskLevel::Low < UpdateRiskLevel::Medium);
470 assert!(UpdateRiskLevel::Medium < UpdateRiskLevel::High);
471 }
472
473 #[test]
474 fn test_is_major_version_bump() {
475 assert!(is_major_version_bump("1.0.0", "2.0.0"));
476 assert!(!is_major_version_bump("1.0.0", "1.1.0"));
477 assert!(!is_major_version_bump("1.0.0", "1.0.1"));
478 }
479
480 #[test]
481 fn test_is_minor_version_bump() {
482 assert!(is_minor_version_bump("1.0.0", "1.1.0"));
483 assert!(!is_minor_version_bump("1.0.0", "2.0.0"));
484 assert!(!is_minor_version_bump("1.0.0", "1.0.1"));
485 }
486
487 #[test]
488 fn test_dependency_clone() {
489 let dep = Dependency {
490 name: "test".to_string(),
491 current_version: "1.0.0".to_string(),
492 latest_version: Some("2.0.0".to_string()),
493 is_outdated: true,
494 vulnerabilities: vec![],
495 dep_type: "runtime".to_string(),
496 };
497 let cloned = dep.clone();
498 assert_eq!(dep, cloned);
499 }
500
501 #[test]
502 fn test_vulnerability_display() {
503 assert_eq!(VulnerabilitySeverity::Low.to_string(), "Low");
504 assert_eq!(VulnerabilitySeverity::Medium.to_string(), "Medium");
505 assert_eq!(VulnerabilitySeverity::High.to_string(), "High");
506 assert_eq!(VulnerabilitySeverity::Critical.to_string(), "Critical");
507 }
508
509 #[test]
510 fn test_update_reason_display() {
511 assert_eq!(UpdateReason::Outdated.to_string(), "Outdated");
512 assert_eq!(UpdateReason::SecurityVulnerability.to_string(), "Security Vulnerability");
513 assert_eq!(UpdateReason::OutdatedAndVulnerable.to_string(), "Outdated and Vulnerable");
514 }
515
516 #[test]
517 fn test_update_risk_level_display() {
518 assert_eq!(UpdateRiskLevel::Low.to_string(), "Low");
519 assert_eq!(UpdateRiskLevel::Medium.to_string(), "Medium");
520 assert_eq!(UpdateRiskLevel::High.to_string(), "High");
521 }
522}