syncable_cli/analyzer/vulnerability/checkers/
go.rs

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