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
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 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 let mut command = if let Some(exec_path) = &govulncheck_status.execution_path {
40 Command::new(exec_path)
42 } else {
43 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 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 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 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 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 let output_str = String::from_utf8_lossy(output);
101
102 if output_str.trim().is_empty() {
104 info!("govulncheck output is empty, no vulnerabilities found");
105 return Ok(None);
106 }
107
108 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 if !trimmed_line.starts_with('{') || !trimmed_line.ends_with('}') {
118 continue;
119 }
120
121 match serde_json::from_str::<serde_json::Value>(trimmed_line) {
123 Ok(audit_data) => {
124 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 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; 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(), severity,
171 title,
172 description,
173 cve: None, ghsa: None, affected_versions: "*".to_string(), patched_versions: fixed_version,
177 published_date: None,
178 references: Vec::new(), };
180
181 if let Some(existing) = vulnerable_deps
183 .iter_mut()
184 .find(|vuln_dep| vuln_dep.name == dep.name)
185 {
186 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 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;
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}