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 using the full path if available
35        let mut command = if let Some(exec_path) = &govulncheck_status.execution_path {
36            // Use the full path when tool is not in PATH
37            Command::new(exec_path)
38        } else {
39            // Use tool name directly when in PATH
40            Command::new("govulncheck")
41        };
42
43        let output = command
44            .args(&["-json", "./..."])
45            .current_dir(project_path)
46            .output()
47            .map_err(|e| VulnerabilityError::CommandError(
48                format!("Failed to run govulncheck: {}", e)
49            ))?;
50        
51        // Log debug information about the command output
52        info!("govulncheck stdout length: {}, stderr length: {}", 
53              output.stdout.len(), output.stderr.len());
54        info!("govulncheck exit code: {:?}", output.status.code());
55        
56        if !output.stderr.is_empty() {
57            let stderr_str = String::from_utf8_lossy(&output.stderr);
58            info!("govulncheck stderr: {}", stderr_str);
59        }
60        
61        // Log first few lines of stdout for debugging
62        let stdout_str = String::from_utf8_lossy(&output.stdout);
63        let stdout_lines: Vec<&str> = stdout_str.lines().take(20).collect();
64        info!("govulncheck stdout first 20 lines: {:?}", stdout_lines);
65        
66        // govulncheck returns 0 even when vulnerabilities are found
67        // Non-zero exit code indicates an actual error
68        if !output.status.success() && output.stdout.is_empty() {
69            return Err(VulnerabilityError::CommandError(
70                format!("govulncheck failed with exit code {}: {}", 
71                    output.status.code().unwrap_or(-1),
72                    String::from_utf8_lossy(&output.stderr))
73            ));
74        }
75        
76        // Parse govulncheck output
77        if output.stdout.is_empty() {
78            info!("govulncheck returned empty output, no vulnerabilities found");
79            return Ok(None);
80        }
81        
82        self.parse_govulncheck_output(&output.stdout, dependencies)
83    }
84    
85    fn parse_govulncheck_output(
86        &self,
87        output: &[u8],
88        dependencies: &[DependencyInfo],
89    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
90        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
91        
92        // Convert output to string
93        let output_str = String::from_utf8_lossy(output);
94        
95        // Check if output is empty or only whitespace
96        if output_str.trim().is_empty() {
97            info!("govulncheck output is empty, no vulnerabilities found");
98            return Ok(None);
99        }
100        
101        // Govulncheck outputs a stream of JSON objects separated by newlines
102        // Process each line and only parse lines that look like complete JSON objects
103        for (line_num, line) in output_str.lines().enumerate() {
104            let trimmed_line = line.trim();
105            if trimmed_line.is_empty() {
106                continue;
107            }
108            
109            // Only try to parse lines that look like JSON objects (start with { and end with })
110            if !trimmed_line.starts_with('{') || !trimmed_line.ends_with('}') {
111                continue;
112            }
113            
114            // Try to parse as JSON, but handle errors gracefully
115            match serde_json::from_str::<serde_json::Value>(trimmed_line) {
116                Ok(audit_data) => {
117                    // Govulncheck JSON structure parsing
118                    if audit_data.get("finding").is_some() {
119                        if let Some(finding) = audit_data.get("finding").and_then(|f| f.as_object()) {
120                            let package_name = finding.get("package").and_then(|p| p.as_str())
121                                .unwrap_or("").to_string();
122                            let module = finding.get("module").and_then(|m| m.as_str())
123                                .unwrap_or("").to_string();
124                            
125                            // Find matching dependency
126                            if let Some(dep) = dependencies.iter().find(|d| 
127                                d.name == package_name || d.name == module || 
128                                package_name.starts_with(&format!("{}/", d.name)) ||
129                                module.starts_with(&format!("{}/", d.name))) {
130                                
131                                let vuln_id = finding.get("osv").and_then(|o| o.as_str())
132                                    .unwrap_or("unknown").to_string();
133                                let title = finding.get("summary").and_then(|s| s.as_str())
134                                    .unwrap_or("Unknown vulnerability").to_string();
135                                let description = finding.get("details").and_then(|d| d.as_str())
136                                    .unwrap_or("").to_string();
137                                let severity = VulnerabilitySeverity::Medium; // Govulncheck doesn't provide severity directly
138                                let fixed_version = finding.get("fixed_version").and_then(|v| v.as_str())
139                                    .map(|s| s.to_string());
140                                
141                                let vuln_info = VulnerabilityInfo {
142                                    id: vuln_id,
143                                    vuln_type: "security".to_string(),  // Security vulnerability
144                                    severity,
145                                    title,
146                                    description,
147                                    cve: None, // Govulncheck uses OSV IDs
148                                    ghsa: None, // Govulncheck uses OSV IDs
149                                    affected_versions: "*".to_string(), // Govulncheck doesn't provide this directly
150                                    patched_versions: fixed_version,
151                                    published_date: None,
152                                    references: Vec::new(), // Govulncheck doesn't provide references in this format
153                                };
154                                
155                                // Check if we already have this dependency
156                                if let Some(existing) = vulnerable_deps.iter_mut()
157                                    .find(|vuln_dep| vuln_dep.name == dep.name)
158                                {
159                                    // Avoid duplicate vulnerabilities
160                                    if !existing.vulnerabilities.iter().any(|v| v.id == vuln_info.id) {
161                                        existing.vulnerabilities.push(vuln_info);
162                                    }
163                                } else {
164                                    vulnerable_deps.push(VulnerableDependency {
165                                        name: dep.name.clone(),
166                                        version: dep.version.clone(),
167                                        language: crate::analyzer::dependency_parser::Language::Go,
168                                        vulnerabilities: vec![vuln_info],
169                                    });
170                                }
171                            }
172                        }
173                    }
174                },
175                Err(e) => {
176                    // Log the error but continue processing other lines
177                    // Only log detailed errors for lines that look like they should be valid JSON
178                    if trimmed_line.starts_with('{') && trimmed_line.ends_with('}') {
179                        warn!("Failed to parse govulncheck output line {}: {}. Line content: {}", 
180                              line_num + 1, e, trimmed_line);
181                    }
182                    // Continue with next line instead of failing completely
183                    continue;
184                }
185            }
186        }
187        
188        if vulnerable_deps.is_empty() {
189            Ok(None)
190        } else {
191            Ok(Some(vulnerable_deps))
192        }
193    }
194}
195
196impl MutableLanguageVulnerabilityChecker for GoVulnerabilityChecker {
197    fn check_vulnerabilities(
198        &mut self,
199        dependencies: &[DependencyInfo],
200        project_path: &Path,
201    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
202        info!("Checking Go dependencies");
203        
204        match self.execute_govulncheck(project_path, dependencies) {
205            Ok(Some(vulns)) => Ok(vulns),
206            Ok(None) => Ok(vec![]),
207            Err(e) => Err(e),
208        }
209    }
210}