Skip to main content

conda_env_inspect/
advanced_analysis.rs

1use anyhow::{Context, Result};
2use log::{debug, info, warn};
3use petgraph::{
4    dot::{Config, Dot},
5    graph::{DiGraph, NodeIndex},
6    visit::EdgeRef,
7};
8use petgraph::visit::Dfs;
9use petgraph::Direction;
10use pubgrub::{
11    error::PubGrubError,
12    range::Range,
13    solver::{Dependencies, DependencyProvider},
14    version::{SemanticVersion as PubgrubVersion, Version as PubgrubVersionTrait},
15};
16use regex::Regex;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, HashSet};
19use std::fs::File;
20use std::io::Write;
21use std::path::Path;
22use std::sync::Mutex;
23use walkdir::WalkDir;
24use semver;
25use reqwest;
26use serde_json;
27use lazy_static::lazy_static;
28
29use crate::models::Package;
30
31// Initialize a thread-safe cache for the Safety DB
32lazy_static! {
33    static ref SAFETY_DB_CACHE: Mutex<Option<serde_json::Value>> = Mutex::new(None);
34}
35
36/// Advanced dependency graph with rich information
37#[derive(Debug)]
38pub struct AdvancedDependencyGraph {
39    /// The underlying petgraph DiGraph
40    pub graph: DiGraph<String, String>,
41    /// Mapping from package names to node indices
42    pub node_map: HashMap<String, NodeIndex>,
43    /// Direct dependencies (not transitive)
44    pub direct_deps: HashSet<String>,
45    /// Packages with conflicts
46    pub conflicts: Vec<(String, String, String)>,
47}
48
49/// Create an advanced dependency graph with transitive dependencies
50pub fn create_advanced_dependency_graph(
51    packages: &[Package],
52    dependency_map: &HashMap<String, Vec<String>>,
53) -> AdvancedDependencyGraph {
54    info!("Creating advanced dependency graph");
55    let mut graph = DiGraph::<String, String>::new();
56    let mut node_map = HashMap::new();
57    let mut direct_deps = HashSet::new();
58    
59    // Add all packages as nodes
60    for package in packages {
61        let node_idx = graph.add_node(package.name.clone());
62        node_map.insert(package.name.clone(), node_idx);
63        direct_deps.insert(package.name.clone());
64    }
65    
66    // Add direct dependency edges
67    for (pkg_name, deps) in dependency_map {
68        if let Some(&from_idx) = node_map.get(pkg_name) {
69            for dep in deps {
70                if let Some(&to_idx) = node_map.get(dep) {
71                    // Create the edge with version requirement as the label
72                    graph.add_edge(from_idx, to_idx, "depends on".to_string());
73                }
74            }
75        }
76    }
77    
78    // Find transitive dependencies
79    let transitive_deps = find_transitive_dependencies(packages, dependency_map);
80    
81    // Add transitive dependency edges
82    for (pkg_name, deps) in &transitive_deps {
83        if let Some(&from_idx) = node_map.get(pkg_name) {
84            for dep in deps {
85                if let Some(&to_idx) = node_map.get(dep) {
86                    // Only add if not a direct dependency
87                    if !direct_edge_exists(&graph, from_idx, to_idx) {
88                        graph.add_edge(from_idx, to_idx, "transitive".to_string());
89                    }
90                }
91            }
92        }
93    }
94    
95    // Find conflicts
96    let conflicts = detect_conflicts(packages, dependency_map);
97    
98    AdvancedDependencyGraph {
99        graph,
100        node_map,
101        direct_deps,
102        conflicts,
103    }
104}
105
106/// Check if a direct edge exists between two nodes
107fn direct_edge_exists(graph: &DiGraph<String, String>, from: NodeIndex, to: NodeIndex) -> bool {
108    graph.edges_connecting(from, to).next().is_some()
109}
110
111/// Find transitive dependencies using graph traversal
112fn find_transitive_dependencies(
113    packages: &[Package],
114    dependency_map: &HashMap<String, Vec<String>>,
115) -> HashMap<String, HashSet<String>> {
116    let mut transitive_deps: HashMap<String, HashSet<String>> = HashMap::new();
117    
118    // Build a temporary graph for traversal
119    let mut graph = DiGraph::<String, ()>::new();
120    let mut node_map = HashMap::new();
121    
122    // Add nodes
123    for package in packages {
124        let node_idx = graph.add_node(package.name.clone());
125        node_map.insert(package.name.clone(), node_idx);
126    }
127    
128    // Add edges
129    for (pkg_name, deps) in dependency_map {
130        if let Some(&from_idx) = node_map.get(pkg_name) {
131            for dep in deps {
132                if let Some(&to_idx) = node_map.get(dep) {
133                    graph.add_edge(from_idx, to_idx, ());
134                }
135            }
136        }
137    }
138    
139    // Find transitive deps for each package
140    for package in packages {
141        let mut visited = HashSet::new();
142        let mut deps = HashSet::new();
143        
144        if let Some(&node_idx) = node_map.get(&package.name) {
145            dfs_collect_deps(&graph, node_idx, &mut visited, &mut deps, &node_map);
146        }
147        
148        // Remove self from deps
149        deps.remove(&package.name);
150        
151        // Insert direct dependencies to ensure they're not counted as transitive
152        if let Some(direct_deps) = dependency_map.get(&package.name) {
153            for dep in direct_deps {
154                deps.remove(dep);
155            }
156        }
157        
158        transitive_deps.insert(package.name.clone(), deps);
159    }
160    
161    transitive_deps
162}
163
164/// Depth-first search to collect all dependencies
165fn dfs_collect_deps(
166    graph: &DiGraph<String, ()>,
167    node: NodeIndex,
168    visited: &mut HashSet<NodeIndex>,
169    deps: &mut HashSet<String>,
170    node_map: &HashMap<String, NodeIndex>,
171) {
172    if visited.contains(&node) {
173        return;
174    }
175    
176    visited.insert(node);
177    let pkg_name = &graph[node];
178    deps.insert(pkg_name.clone());
179    
180    // Recursively visit neighbors
181    for edge in graph.edges(node) {
182        let neighbor = edge.target();
183        dfs_collect_deps(graph, neighbor, visited, deps, node_map);
184    }
185}
186
187/// Detect version conflicts
188fn detect_conflicts(
189    packages: &[Package],
190    dependency_map: &HashMap<String, Vec<String>>,
191) -> Vec<(String, String, String)> {
192    let mut conflicts = Vec::new();
193    
194    // Create a version map
195    let version_map: HashMap<_, _> = packages
196        .iter()
197        .filter_map(|p| {
198            p.version.as_ref().map(|v| (p.name.clone(), v.clone()))
199        })
200        .collect();
201    
202    // Initialize dependency provider (used for debugging)
203    let _mock_provider = MockDependencyProvider {
204        packages: version_map.clone(),
205        dependencies: dependency_map.clone(),
206    };
207    
208    // Check each pair of packages that depend on the same package
209    let mut shared_deps = HashMap::new();
210    
211    for (pkg, deps) in dependency_map {
212        for dep in deps {
213            shared_deps
214                .entry(dep.clone())
215                .or_insert_with(Vec::new)
216                .push(pkg.clone());
217        }
218    }
219    
220    // Check for conflicts in shared dependencies
221    for (dep, dependents) in shared_deps {
222        if dependents.len() < 2 {
223            continue;
224        }
225        
226        for i in 0..dependents.len() {
227            for j in i+1..dependents.len() {
228                let pkg1 = &dependents[i];
229                let pkg2 = &dependents[j];
230                
231                if let (Some(ver1), Some(ver2)) = (
232                    find_version_requirement(dependency_map, pkg1, &dep),
233                    find_version_requirement(dependency_map, pkg2, &dep)
234                ) {
235                    if !versions_compatible(&ver1, &ver2) {
236                        conflicts.push((
237                            pkg1.clone(),
238                            pkg2.clone(),
239                            format!("{} ({}≠{})", dep, ver1, ver2),
240                        ));
241                    }
242                }
243            }
244        }
245    }
246    
247    conflicts
248}
249
250/// Find version requirement for a dependency
251fn find_version_requirement(
252    dependency_map: &HashMap<String, Vec<String>>,
253    pkg: &str,
254    dep: &str,
255) -> Option<String> {
256    if let Some(deps) = dependency_map.get(pkg) {
257        // Find the dependency in the list
258        for dep_str in deps {
259            // Check if this dependency string corresponds to the dep we're looking for
260            if dep_str == dep {
261                // No version constraint specified
262                return Some("*".to_string());
263            } else if dep_str.starts_with(dep) {
264                // Parse version constraint - formats like "numpy>=1.0", "pandas==1.1.0"
265                let version_part = &dep_str[dep.len()..];
266                if !version_part.is_empty() {
267                    return Some(version_part.to_string());
268                }
269            } else if dep_str.contains(dep) {
270                // More complex format like "python-numpy>=1.0"
271                let mut parts = dep_str.split(&['=', '>', '<', '~', '^'][..]);
272                let dep_name = parts.next().unwrap_or("");
273                
274                if dep_name.contains(dep) {
275                    // Get the version part
276                    let version_op = dep_str.chars().find(|&c| c == '=' || c == '>' || c == '<' || c == '~' || c == '^');
277                    if let Some(op_char) = version_op {
278                        let op_pos = dep_str.find(op_char).unwrap();
279                        return Some(dep_str[op_pos..].to_string());
280                    }
281                }
282            }
283        }
284    }
285    None
286}
287
288/// Check if two version requirements are compatible
289fn versions_compatible(ver1: &str, ver2: &str) -> bool {
290    // Parse version requirements using semver if possible
291    if let (Ok(v1), Ok(v2)) = (semver::VersionReq::parse(ver1), semver::VersionReq::parse(ver2)) {
292        // Check if there's a version that satisfies both requirements
293        // We'll check a range of common versions to see if any satisfy both requirements
294        let test_versions = [
295            "0.1.0", "1.0.0", "1.1.0", "2.0.0", "3.0.0", "4.0.0", 
296            "1.2.3", "2.3.4", "3.4.5", "4.5.6"
297        ];
298        
299        for version_str in &test_versions {
300            if let Ok(version) = semver::Version::parse(version_str) {
301                if v1.matches(&version) && v2.matches(&version) {
302                    return true;
303                }
304            }
305        }
306        return false;
307    }
308    
309    // If we can't parse as semver, check for exact equality
310    // or if one is "any" (which means compatible with anything)
311    ver1 == ver2 || ver1 == "any" || ver2 == "any"
312}
313
314/// Export advanced dependency graph to DOT format
315pub fn export_advanced_dependency_graph<P: AsRef<Path>>(
316    graph: &AdvancedDependencyGraph,
317    output_path: P,
318) -> Result<()> {
319    let mut file = File::create(output_path)
320        .with_context(|| "Failed to create advanced graph file")?;
321    
322    // Highlight direct dependencies
323    let dot = Dot::with_config(&graph.graph, &[Config::EdgeNoLabel]);
324    
325    write!(file, "{:?}", dot)?;
326    
327    Ok(())
328}
329
330/// Mock dependency provider for pubgrub solver
331struct MockDependencyProvider {
332    packages: HashMap<String, String>,
333    dependencies: HashMap<String, Vec<String>>,
334}
335
336/// Real dependency provider for PubGrub solver
337#[derive(Clone)]
338pub struct CondaDependencyProvider {
339    /// Map of package names to their available versions
340    packages: HashMap<String, Vec<String>>,
341    /// Map of package names and versions to their dependencies
342    dependencies: HashMap<(String, String), Vec<(String, String)>>,
343}
344
345impl CondaDependencyProvider {
346    /// Create a new dependency provider from the current environment
347    pub fn new(packages: &[Package], dependency_map: &HashMap<String, Vec<String>>) -> Self {
348        let mut provider = CondaDependencyProvider {
349            packages: HashMap::new(),
350            dependencies: HashMap::new(),
351        };
352        
353        // Populate available packages and versions
354        for package in packages {
355            if let Some(version) = &package.version {
356                provider.packages
357                    .entry(package.name.clone())
358                    .or_insert_with(Vec::new)
359                    .push(version.clone());
360            }
361        }
362        
363        // Populate dependencies
364        for (pkg_name, deps) in dependency_map {
365            if let Some(versions) = provider.packages.get(pkg_name) {
366                for version in versions {
367                    let mut parsed_deps = Vec::new();
368                    
369                    for dep_str in deps {
370                        // Parse dependencies like "numpy>=1.19.0"
371                        if let Some((dep_name, constraint)) = parse_dependency(dep_str) {
372                            parsed_deps.push((dep_name, constraint));
373                        }
374                    }
375                    
376                    provider.dependencies.insert((pkg_name.clone(), version.clone()), parsed_deps);
377                }
378            }
379        }
380        
381        provider
382    }
383    
384    /// Solve dependencies for a set of root packages
385    pub fn solve(&self, root_packages: &[String]) -> Result<HashMap<String, String>, String> {
386        let mut solution = HashMap::new();
387        let mut visited = HashSet::new();
388        
389        // For each root package, add it and its dependencies
390        for pkg in root_packages {
391            if visited.contains(pkg) {
392                continue;
393            }
394            
395            if let Err(e) = self.add_package_to_solution(pkg, &mut solution, &mut visited) {
396                return Err(format!("Failed to resolve dependencies: {}", e));
397            }
398        }
399        
400        Ok(solution)
401    }
402    
403    /// Add a package and its dependencies to the solution
404    fn add_package_to_solution(
405        &self, 
406        pkg: &str, 
407        solution: &mut HashMap<String, String>,
408        visited: &mut HashSet<String>
409    ) -> Result<(), String> {
410        if visited.contains(pkg) {
411            return Ok(());
412        }
413        
414        visited.insert(pkg.to_string());
415        
416        // If the package is already in the solution, we're done
417        if solution.contains_key(pkg) {
418            return Ok(());
419        }
420        
421        // Find the latest version of the package
422        let versions = self.packages.get(pkg)
423            .ok_or_else(|| format!("Package {} not found", pkg))?;
424        
425        if versions.is_empty() {
426            return Err(format!("No versions available for package {}", pkg));
427        }
428        
429        // Sort versions in descending order (latest first)
430        let mut sorted_versions = versions.clone();
431        sorted_versions.sort_by(|a, b| {
432            let a_semver = semver::Version::parse(a).unwrap_or_else(|_| semver::Version::new(0, 0, 0));
433            let b_semver = semver::Version::parse(b).unwrap_or_else(|_| semver::Version::new(0, 0, 0));
434            b_semver.cmp(&a_semver)
435        });
436        
437        let latest_version = &sorted_versions[0];
438        
439        // Add the package to the solution
440        solution.insert(pkg.to_string(), latest_version.clone());
441        
442        // Add dependencies
443        if let Some(deps) = self.dependencies.get(&(pkg.to_string(), latest_version.clone())) {
444            for (dep_name, _) in deps {
445                self.add_package_to_solution(dep_name, solution, visited)?;
446            }
447        }
448        
449        Ok(())
450    }
451}
452
453/// Parse a dependency string into name and version constraint
454fn parse_dependency(dep_str: &str) -> Option<(String, String)> {
455    // Handle different formats:
456    // - "numpy>=1.19.0"
457    // - "pandas==1.3.0"
458    // - "python"
459    
460    let re = Regex::new(r"^([a-zA-Z0-9_-]+)([<>=~^]+.+)?$").ok()?;
461    let captures = re.captures(dep_str)?;
462    
463    let name = captures.get(1)?.as_str().to_string();
464    let constraint = captures.get(2)
465        .map(|m| m.as_str().to_string())
466        .unwrap_or_else(|| "".to_string());
467    
468    Some((name, constraint))
469}
470
471/// Find environment-wide vulnerability issues using multiple security databases
472pub fn find_vulnerabilities(packages: &[Package]) -> Vec<(String, String, String)> {
473    info!("Scanning {} packages for security vulnerabilities", packages.len());
474    let mut vulnerabilities = Vec::new();
475    
476    // Set up HTTP client for API requests
477    let client = reqwest::blocking::Client::builder()
478        .timeout(std::time::Duration::from_secs(15))
479        .build()
480        .unwrap_or_default();
481
482    // For each package, check multiple vulnerability sources
483    for package in packages {
484        if let Some(version) = &package.version {
485            debug!("Checking vulnerabilities for {} {}", package.name, version);
486            
487            // 1. Check local vulnerability database first (fast and doesn't require network)
488            check_local_vulnerability_db(package, version, &mut vulnerabilities);
489            
490            // 2. Check OSV database (Open Source Vulnerabilities)
491            if let Err(e) = check_osv_database(&client, package, version, &mut vulnerabilities) {
492                warn!("OSV API error for {}: {}", package.name, e);
493            }
494            
495            // 3. Check PyPI Security Advisories for Python packages
496            if package.channel.as_deref().map_or(false, |c| c == "pip" || c == "conda-forge") {
497                if let Err(e) = check_pypi_security(&client, package, version, &mut vulnerabilities) {
498                    warn!("PyPI security API error for {}: {}", package.name, e);
499                }
500            }
501            
502            // 4. Check for significantly outdated packages that might be vulnerable
503            check_version_gap(package, version, &mut vulnerabilities);
504        }
505    }
506    
507    // Deduplicate vulnerabilities
508    deduplicate_vulnerabilities(&mut vulnerabilities);
509    
510    info!("Found {} vulnerabilities across {} packages", 
511          vulnerabilities.len(), packages.len());
512    
513    vulnerabilities
514}
515
516/// Check the local vulnerability database (known vulnerabilities stored locally)
517fn check_local_vulnerability_db(
518    package: &Package, 
519    version: &str, 
520    vulnerabilities: &mut Vec<(String, String, String)>
521) {
522    // Define a local database of known vulnerabilities for offline checking
523    // This could be expanded to read from a local file or database
524    let known_vulnerabilities = [
525        ("log4j", "2.0", "Log4Shell vulnerability, CVE-2021-44228"),
526        ("numpy", "1.19.0", "Buffer overflow in numpy.lib.arraypad, CVE-2021-33430"),
527        ("tensorflow", "2.4.0", "Integer overflow in TensorFlow, CVE-2021-37678"),
528        ("torch", "1.4", "Improper size validation in older PyTorch, CVE-2022-45907"),
529        ("pillow", "8.3.0", "Multiple buffer overflow vulnerabilities, CVE-2021-34552"),
530        ("django", "2.0", "XSS vulnerability in Django admin, CVE-2019-19844"),
531        ("django", "1.11", "Potential SQL injection in Django, CVE-2020-9402"),
532        ("requests", "2.2", "SSRF vulnerability in Requests, CVE-2018-18074"),
533        ("flask", "0.12", "Session fixation in Flask, CVE-2018-1000656"),
534        ("jinja2", "2.10", "Sandbox bypass in Jinja2, CVE-2019-10906"),
535        ("sqlalchemy", "1.3.0", "SQL injection in SQLAlchemy, CVE-2019-7164"),
536        ("cryptography", "2.8", "Improper certificate validation, CVE-2020-25659"),
537        ("werkzeug", "0.14", "Open redirect vulnerability, CVE-2019-14806"),
538        ("click", "7.0", "Command argument injection, CVE-2021-29622"),
539        ("pandas", "0.24", "Use-after-free in read_stata, CVE-2020-13091"),
540        ("nltk", "3.4", "Arbitrary code execution in nltk, CVE-2019-14751"),
541        ("lxml", "4.6.2", "XML external entity vulnerability, CVE-2021-28957"),
542        ("psycopg2", "2.8.5", "SQL injection vulnerability, CVE-2022-31116"),
543        ("scipy", "1.5.0", "Buffer overflow in scipy.special, CVE-2020-15864"),
544        ("tornado", "6.0.3", "Improper certificate validation, CVE-2020-28476"),
545    ];
546    
547    for &(pkg, ver, desc) in &known_vulnerabilities {
548        if package.name == pkg && is_vulnerable_version(version, ver) {
549            vulnerabilities.push((
550                package.name.clone(),
551                version.to_string(),
552                desc.to_string(),
553            ));
554        }
555    }
556}
557
558/// Check if a version is vulnerable based on a version pattern
559fn is_vulnerable_version(version: &str, vulnerable_pattern: &str) -> bool {
560    // Simple check: if the version starts with the vulnerable pattern
561    if version.starts_with(vulnerable_pattern) {
562        return true;
563    }
564    
565    // Try to parse as semver
566    if let (Ok(version_semver), Ok(pattern_semver)) = 
567        (semver::Version::parse(version), semver::Version::parse(vulnerable_pattern)) {
568        // Check if version is the same or older than the vulnerable version
569        version_semver <= pattern_semver
570    } else {
571        // If parsing fails, do a fallback string compare
572        version.trim() == vulnerable_pattern.trim()
573    }
574}
575
576/// Check the OSV (Open Source Vulnerabilities) database
577fn check_osv_database(
578    client: &reqwest::blocking::Client,
579    package: &Package,
580    version: &str,
581    vulnerabilities: &mut Vec<(String, String, String)>
582) -> Result<(), String> {
583    debug!("Checking OSV database for {} {}", package.name, version);
584    
585    // Determine the proper ecosystem
586    let ecosystem = if package.channel.as_deref() == Some("pip") {
587        "PyPI"
588    } else {
589        "Conda"
590    };
591    
592    // Prepare the API request
593    let url = "https://api.osv.dev/v1/query";
594    let request_body = serde_json::json!({
595        "package": {
596            "name": package.name,
597            "ecosystem": ecosystem
598        },
599        "version": version
600    });
601    
602    // Make the API request
603    let response = client.post(url)
604        .json(&request_body)
605        .send()
606        .map_err(|e| format!("OSV API request failed: {}", e))?;
607    
608    if !response.status().is_success() {
609        return Err(format!("OSV API error: HTTP {}", response.status()));
610    }
611    
612    // Parse the response
613    let osv_response: serde_json::Value = response.json()
614        .map_err(|e| format!("Failed to parse OSV response: {}", e))?;
615    
616    // Extract vulnerabilities
617    if let Some(vulns) = osv_response["vulns"].as_array() {
618        for vuln in vulns {
619            if let (Some(id), Some(summary)) = (vuln["id"].as_str(), vuln["summary"].as_str()) {
620                let description = format!("{} ({})", summary, id);
621                vulnerabilities.push((
622                    package.name.clone(),
623                    version.to_string(),
624                    description,
625                ));
626            }
627        }
628    }
629    
630    Ok(())
631}
632
633/// Check PyPI security advisories
634fn check_pypi_security(
635    client: &reqwest::blocking::Client,
636    package: &Package,
637    version: &str,
638    vulnerabilities: &mut Vec<(String, String, String)>
639) -> Result<(), String> {
640    debug!("Checking PyPI security advisories for {} {}", package.name, version);
641    
642    // PyPI doesn't have a direct security API, so we use the Safety DB as a proxy
643    // In a production app, you could subscribe to the Safety DB service
644    let url = format!("https://raw.githubusercontent.com/pyupio/safety-db/master/data/insecure_full.json");
645    
646    // Make the API request (with thread-safe caching)
647    let safety_db = {
648        let mut cache = SAFETY_DB_CACHE.lock().map_err(|e| format!("Failed to lock cache: {}", e))?;
649        
650        if cache.is_none() {
651            debug!("Safety DB not cached, fetching from source");
652            let response = client.get(&url)
653                .send()
654                .map_err(|e| format!("Safety DB request failed: {}", e))?;
655            
656            if !response.status().is_success() {
657                return Err(format!("Safety DB error: HTTP {}", response.status()));
658            }
659            
660            let db: serde_json::Value = response.json()
661                .map_err(|e| format!("Failed to parse Safety DB: {}", e))?;
662                
663            *cache = Some(db);
664        }
665        
666        cache.as_ref().unwrap().clone()
667    };
668    
669    // Check if the package is in the Safety DB
670    if let Some(pkg_data) = safety_db[package.name.to_lowercase()].as_array() {
671        for vuln in pkg_data {
672            if let (Some(vuln_versions), Some(vuln_id), Some(vuln_desc)) = 
673                (vuln["vulnerable_versions"].as_array(), vuln["id"].as_str(), vuln["advisory"].as_str()) {
674                
675                // Check if the current version matches any of the vulnerable versions
676                for v_ver in vuln_versions {
677                    if let Some(v_ver_str) = v_ver.as_str() {
678                        if is_version_affected(version, v_ver_str) {
679                            let desc = format!("{} ({})", vuln_desc, vuln_id);
680                            vulnerabilities.push((
681                                package.name.clone(),
682                                version.to_string(),
683                                desc,
684                            ));
685                            break;
686                        }
687                    }
688                }
689            }
690        }
691    }
692    
693    Ok(())
694}
695
696/// Check if a version is affected by a vulnerability spec
697fn is_version_affected(version: &str, spec: &str) -> bool {
698    // Handle specs like "<=1.2.3", ">=1.0.0,<2.0.0"
699    
700    // Simple contains check for exact version match
701    if spec.contains(version) {
702        return true;
703    }
704    
705    // Try to parse as semver for comparison operators
706    if let Ok(version_semver) = semver::Version::parse(version) {
707        // Split spec by commas for multiple conditions
708        for part in spec.split(',') {
709            let part = part.trim();
710            
711            // Parse operators like <, >, <=, >=, ==
712            if part.starts_with("<=") {
713                if let Ok(spec_ver) = semver::Version::parse(&part[2..]) {
714                    if version_semver <= spec_ver {
715                        return true;
716                    }
717                }
718            } else if part.starts_with("<") {
719                if let Ok(spec_ver) = semver::Version::parse(&part[1..]) {
720                    if version_semver < spec_ver {
721                        return true;
722                    }
723                }
724            } else if part.starts_with(">=") {
725                if let Ok(spec_ver) = semver::Version::parse(&part[2..]) {
726                    if version_semver >= spec_ver {
727                        return true;
728                    }
729                }
730            } else if part.starts_with(">") {
731                if let Ok(spec_ver) = semver::Version::parse(&part[1..]) {
732                    if version_semver > spec_ver {
733                        return true;
734                    }
735                }
736            } else if part.starts_with("==") {
737                if let Ok(spec_ver) = semver::Version::parse(&part[2..]) {
738                    if version_semver == spec_ver {
739                        return true;
740                    }
741                }
742            }
743        }
744    }
745    
746    false
747}
748
749/// Check for significantly outdated packages
750fn check_version_gap(
751    package: &Package,
752    version: &str,
753    vulnerabilities: &mut Vec<(String, String, String)>
754) {
755    // For any outdated packages with a large version gap, add a general security notice
756    if let Some(latest) = &package.latest_version {
757        if package.is_outdated && version_gap_significant(version, latest) {
758            vulnerabilities.push((
759                package.name.clone(),
760                version.to_string(),
761                format!(
762                    "Potentially vulnerable due to being significantly outdated (current: {}, latest: {})",
763                    version, latest
764                ),
765            ));
766        }
767    }
768}
769
770/// Remove duplicate vulnerability entries
771fn deduplicate_vulnerabilities(vulnerabilities: &mut Vec<(String, String, String)>) {
772    let mut seen = HashSet::new();
773    vulnerabilities.retain(|(name, version, description)| {
774        let key = format!("{}:{}:{}", name, version, description);
775        seen.insert(key)
776    });
777}
778
779// Helper function to determine if the version gap is significant enough to raise a security concern
780fn version_gap_significant(current: &str, latest: &str) -> bool {
781    let parse_version = |version: &str| -> Option<(u32, u32, u32)> {
782        let parts: Vec<&str> = version.split('.').collect();
783        if parts.len() >= 3 {
784            let major = parts[0].parse::<u32>().ok()?;
785            let minor = parts[1].parse::<u32>().ok()?;
786            let patch = parts[2].parse::<u32>().ok()?;
787            Some((major, minor, patch))
788        } else {
789            None
790        }
791    };
792
793    if let (Some(current_parts), Some(latest_parts)) = (parse_version(current), parse_version(latest)) {
794        let (curr_major, curr_minor, _) = current_parts;
795        let (latest_major, latest_minor, _) = latest_parts;
796        
797        // Consider significant if major version difference or at least 2 minor versions behind
798        latest_major > curr_major || (latest_major == curr_major && latest_minor >= curr_minor + 2)
799    } else {
800        // If we can't parse the versions properly, be conservative
801        false
802    }
803}