ricecoder_github/managers/
dependency_manager.rs

1//! Dependency Management
2//!
3//! Manages dependency scanning, updates, and security tracking for GitHub repositories.
4
5use std::collections::HashMap;
6
7/// Represents a dependency in a project
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Dependency {
10    /// Name of the dependency
11    pub name: String,
12    /// Current version
13    pub current_version: String,
14    /// Latest available version
15    pub latest_version: Option<String>,
16    /// Whether this dependency is outdated
17    pub is_outdated: bool,
18    /// Known security vulnerabilities
19    pub vulnerabilities: Vec<Vulnerability>,
20    /// Dependency type (e.g., "runtime", "dev", "build")
21    pub dep_type: String,
22}
23
24/// Represents a security vulnerability in a dependency
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Vulnerability {
27    /// CVE identifier
28    pub cve_id: String,
29    /// Severity level
30    pub severity: VulnerabilitySeverity,
31    /// Description of the vulnerability
32    pub description: String,
33    /// Affected versions
34    pub affected_versions: Vec<String>,
35}
36
37/// Severity level for vulnerabilities
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum VulnerabilitySeverity {
40    /// Low severity
41    Low,
42    /// Medium severity
43    Medium,
44    /// High severity
45    High,
46    /// Critical severity
47    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/// Result of dependency scanning
62#[derive(Debug, Clone)]
63pub struct DependencyScanResult {
64    /// All dependencies found
65    pub dependencies: Vec<Dependency>,
66    /// Number of outdated dependencies
67    pub outdated_count: usize,
68    /// Number of dependencies with vulnerabilities
69    pub vulnerable_count: usize,
70    /// Total number of vulnerabilities found
71    pub total_vulnerabilities: usize,
72}
73
74/// Suggestion for a dependency update
75#[derive(Debug, Clone)]
76pub struct DependencyUpdateSuggestion {
77    /// The dependency to update
78    pub dependency: Dependency,
79    /// Reason for the suggestion
80    pub reason: UpdateReason,
81    /// Risk level of the update
82    pub risk_level: UpdateRiskLevel,
83}
84
85/// Reason for suggesting a dependency update
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum UpdateReason {
88    /// Dependency is outdated
89    Outdated,
90    /// Security vulnerability found
91    SecurityVulnerability,
92    /// Both outdated and has vulnerabilities
93    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/// Risk level for a dependency update
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub enum UpdateRiskLevel {
109    /// Low risk (patch version update)
110    Low,
111    /// Medium risk (minor version update)
112    Medium,
113    /// High risk (major version update)
114    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/// Result of a dependency update PR creation
128#[derive(Debug, Clone)]
129pub struct DependencyUpdatePrResult {
130    /// PR number
131    pub pr_number: u32,
132    /// PR URL
133    pub pr_url: String,
134    /// Dependencies updated in this PR
135    pub updated_dependencies: Vec<String>,
136    /// Branch name
137    pub branch_name: String,
138}
139
140/// Result of dependency update verification
141#[derive(Debug, Clone)]
142pub struct DependencyUpdateVerificationResult {
143    /// Whether the build passed
144    pub build_passed: bool,
145    /// Build status message
146    pub status_message: String,
147    /// Tests passed
148    pub tests_passed: bool,
149    /// Any warnings or issues found
150    pub issues: Vec<String>,
151}
152
153/// Dependency Manager for scanning and managing dependencies
154#[derive(Debug, Clone)]
155pub struct DependencyManager {
156    /// Repository owner
157    pub owner: String,
158    /// Repository name
159    pub repo: String,
160}
161
162impl DependencyManager {
163    /// Creates a new DependencyManager
164    pub fn new(owner: String, repo: String) -> Self {
165        Self { owner, repo }
166    }
167
168    /// Scans the repository for dependencies
169    pub fn scan_dependencies(&self) -> Result<DependencyScanResult, DependencyError> {
170        // Generate realistic dependencies for testing
171        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    /// Suggests dependency updates based on scan results
229    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    /// Creates a PR for dependency updates
264    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    /// Verifies that dependency updates don't break builds
281    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    /// Tracks security vulnerabilities in dependencies
291    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/// Report of vulnerabilities found
320#[derive(Debug, Clone)]
321pub struct VulnerabilityReport {
322    /// Total number of vulnerabilities
323    pub total_vulnerabilities: usize,
324    /// Number of critical vulnerabilities
325    pub critical_count: usize,
326    /// Number of high severity vulnerabilities
327    pub high_count: usize,
328    /// Number of medium severity vulnerabilities
329    pub medium_count: usize,
330    /// Number of low severity vulnerabilities
331    pub low_count: usize,
332    /// Vulnerabilities grouped by severity
333    pub vulnerabilities_by_severity: HashMap<VulnerabilitySeverity, Vec<Vulnerability>>,
334}
335
336/// Error type for dependency operations
337#[derive(Debug, thiserror::Error)]
338pub enum DependencyError {
339    /// No updates available
340    #[error("No updates available")]
341    NoUpdatesAvailable,
342
343    /// Dependency not found
344    #[error("Dependency not found: {0}")]
345    DependencyNotFound(String),
346
347    /// Invalid version format
348    #[error("Invalid version format: {0}")]
349    InvalidVersion(String),
350
351    /// API error
352    #[error("API error: {0}")]
353    ApiError(String),
354
355    /// Build verification failed
356    #[error("Build verification failed: {0}")]
357    BuildVerificationFailed(String),
358}
359
360/// Helper function to check if version bump is major
361fn 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
367/// Helper function to check if version bump is minor
368fn 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}