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