syncable_cli/analyzer/vulnerability/checkers/
go.rs1use 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 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 let mut command = if let Some(exec_path) = &govulncheck_status.execution_path {
36 Command::new(exec_path)
38 } else {
39 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 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 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 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 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 let output_str = String::from_utf8_lossy(output);
94
95 if output_str.trim().is_empty() {
97 info!("govulncheck output is empty, no vulnerabilities found");
98 return Ok(None);
99 }
100
101 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 if !trimmed_line.starts_with('{') || !trimmed_line.ends_with('}') {
111 continue;
112 }
113
114 match serde_json::from_str::<serde_json::Value>(trimmed_line) {
116 Ok(audit_data) => {
117 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 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; 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(), severity,
145 title,
146 description,
147 cve: None, ghsa: None, affected_versions: "*".to_string(), patched_versions: fixed_version,
151 published_date: None,
152 references: Vec::new(), };
154
155 if let Some(existing) = vulnerable_deps.iter_mut()
157 .find(|vuln_dep| vuln_dep.name == dep.name)
158 {
159 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 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;
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}