syncable_cli/analyzer/vulnerability/checkers/
go.rs1use 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 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 let mut command = if let Some(exec_path) = &govulncheck_status.execution_path {
39 Command::new(exec_path)
41 } else {
42 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 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 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 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 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 let output_str = String::from_utf8_lossy(output);
100
101 if output_str.trim().is_empty() {
103 info!("govulncheck output is empty, no vulnerabilities found");
104 return Ok(None);
105 }
106
107 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 if !trimmed_line.starts_with('{') || !trimmed_line.ends_with('}') {
117 continue;
118 }
119
120 match serde_json::from_str::<serde_json::Value>(trimmed_line) {
122 Ok(audit_data) => {
123 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 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; 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(), severity,
168 title,
169 description,
170 cve: None, ghsa: None, affected_versions: "*".to_string(), patched_versions: fixed_version,
174 published_date: None,
175 references: Vec::new(), };
177
178 if let Some(existing) = vulnerable_deps
180 .iter_mut()
181 .find(|vuln_dep| vuln_dep.name == dep.name)
182 {
183 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 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;
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}