syncable_cli/analyzer/vulnerability/checkers/
go.rs

1use std::path::Path;
2use std::process::Command;
3use log::{info, warn};
4use crate::analyzer::dependency_parser::DependencyInfo;
5use crate::analyzer::tool_management::ToolDetector;
6use crate::analyzer::vulnerability::{VulnerableDependency, VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity};
7use super::MutableLanguageVulnerabilityChecker;
8
9pub struct GoVulnerabilityChecker {
10    tool_detector: ToolDetector,
11}
12
13impl GoVulnerabilityChecker {
14    pub fn new() -> Self {
15        Self {
16            tool_detector: ToolDetector::new(),
17        }
18    }
19    
20    fn execute_govulncheck(
21        &mut self,
22        project_path: &Path,
23        dependencies: &[DependencyInfo],
24    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
25        // Check if govulncheck is available
26        let govulncheck_status = self.tool_detector.detect_tool("govulncheck");
27        if !govulncheck_status.available {
28            warn!("govulncheck not found, skipping Go vulnerability check. Install with: go install golang.org/x/vuln/cmd/govulncheck@latest");
29            return Ok(None);
30        }
31        
32        info!("Executing govulncheck in {}", project_path.display());
33        
34        // Execute govulncheck -json
35        let output = Command::new("govulncheck")
36            .args(&["-json", "./..."])
37            .current_dir(project_path)
38            .output()
39            .map_err(|e| VulnerabilityError::CommandError(
40                format!("Failed to run govulncheck: {}", e)
41            ))?;
42        
43        // govulncheck returns 0 even when vulnerabilities are found
44        // Non-zero exit code indicates an actual error
45        if !output.status.success() && output.stdout.is_empty() {
46            return Err(VulnerabilityError::CommandError(
47                format!("govulncheck failed with exit code {}: {}", 
48                    output.status.code().unwrap_or(-1),
49                    String::from_utf8_lossy(&output.stderr))
50            ));
51        }
52        
53        if output.stdout.is_empty() {
54            return Ok(None);
55        }
56        
57        // Parse govulncheck output
58        self.parse_govulncheck_output(&output.stdout, dependencies)
59    }
60    
61    fn parse_govulncheck_output(
62        &self,
63        output: &[u8],
64        dependencies: &[DependencyInfo],
65    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
66        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
67        
68        // Split output by lines and parse each JSON object
69        let output_str = String::from_utf8_lossy(output);
70        for line in output_str.lines() {
71            if line.trim().is_empty() {
72                continue;
73            }
74            
75            let audit_data: serde_json::Value = serde_json::from_str(line)
76                .map_err(|e| VulnerabilityError::ParseError(
77                    format!("Failed to parse govulncheck output line: {}", e)
78                ))?;
79            
80            // Govulncheck JSON structure parsing
81            if audit_data.get("finding").is_some() {
82                if let Some(finding) = audit_data.get("finding").and_then(|f| f.as_object()) {
83                    let package_name = finding.get("package").and_then(|p| p.as_str())
84                        .unwrap_or("").to_string();
85                    let module = finding.get("module").and_then(|m| m.as_str())
86                        .unwrap_or("").to_string();
87                    
88                    // Find matching dependency
89                    if let Some(dep) = dependencies.iter().find(|d| 
90                        d.name == package_name || d.name == module || 
91                        package_name.starts_with(&format!("{}/", d.name)) ||
92                        module.starts_with(&format!("{}/", d.name))) {
93                        
94                        let vuln_id = finding.get("osv").and_then(|o| o.as_str())
95                            .unwrap_or("unknown").to_string();
96                        let title = finding.get("summary").and_then(|s| s.as_str())
97                            .unwrap_or("Unknown vulnerability").to_string();
98                        let description = finding.get("details").and_then(|d| d.as_str())
99                            .unwrap_or("").to_string();
100                        let severity = VulnerabilitySeverity::Medium; // Govulncheck doesn't provide severity directly
101                        let fixed_version = finding.get("fixed_version").and_then(|v| v.as_str())
102                            .map(|s| s.to_string());
103                        
104                        let vuln_info = VulnerabilityInfo {
105                            id: vuln_id,
106                            vuln_type: "security".to_string(),  // Security vulnerability
107                            severity,
108                            title,
109                            description,
110                            cve: None, // Govulncheck uses OSV IDs
111                            ghsa: None, // Govulncheck uses OSV IDs
112                            affected_versions: "*".to_string(), // Govulncheck doesn't provide this directly
113                            patched_versions: fixed_version,
114                            published_date: None,
115                            references: Vec::new(), // Govulncheck doesn't provide references in this format
116                        };
117                        
118                        // Check if we already have this dependency
119                        if let Some(existing) = vulnerable_deps.iter_mut()
120                            .find(|vuln_dep| vuln_dep.name == dep.name)
121                        {
122                            // Avoid duplicate vulnerabilities
123                            if !existing.vulnerabilities.iter().any(|v| v.id == vuln_info.id) {
124                                existing.vulnerabilities.push(vuln_info);
125                            }
126                        } else {
127                            vulnerable_deps.push(VulnerableDependency {
128                                name: dep.name.clone(),
129                                version: dep.version.clone(),
130                                language: crate::analyzer::dependency_parser::Language::Go,
131                                vulnerabilities: vec![vuln_info],
132                            });
133                        }
134                    }
135                }
136            }
137        }
138        
139        if vulnerable_deps.is_empty() {
140            Ok(None)
141        } else {
142            Ok(Some(vulnerable_deps))
143        }
144    }
145}
146
147impl MutableLanguageVulnerabilityChecker for GoVulnerabilityChecker {
148    fn check_vulnerabilities(
149        &mut self,
150        dependencies: &[DependencyInfo],
151        project_path: &Path,
152    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
153        info!("Checking Go dependencies");
154        
155        match self.execute_govulncheck(project_path, dependencies) {
156            Ok(Some(vulns)) => Ok(vulns),
157            Ok(None) => Ok(vec![]),
158            Err(e) => Err(e),
159        }
160    }
161}