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 output = Command::new("govulncheck")
36 .args(&["-json", "./..."])
37 .current_dir(project_path)
38 .output()
39 .map_err(|e| VulnerabilityError::CommandError(
40 format!("Failed to run govulncheck: {}", e)
41 ))?;
42
43 if !output.status.success() && output.stdout.is_empty() {
46 return Err(VulnerabilityError::CommandError(
47 format!("govulncheck failed with exit code {}: {}",
48 output.status.code().unwrap_or(-1),
49 String::from_utf8_lossy(&output.stderr))
50 ));
51 }
52
53 if output.stdout.is_empty() {
54 return Ok(None);
55 }
56
57 self.parse_govulncheck_output(&output.stdout, dependencies)
59 }
60
61 fn parse_govulncheck_output(
62 &self,
63 output: &[u8],
64 dependencies: &[DependencyInfo],
65 ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
66 let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
67
68 let output_str = String::from_utf8_lossy(output);
70 for line in output_str.lines() {
71 if line.trim().is_empty() {
72 continue;
73 }
74
75 let audit_data: serde_json::Value = serde_json::from_str(line)
76 .map_err(|e| VulnerabilityError::ParseError(
77 format!("Failed to parse govulncheck output line: {}", e)
78 ))?;
79
80 if audit_data.get("finding").is_some() {
82 if let Some(finding) = audit_data.get("finding").and_then(|f| f.as_object()) {
83 let package_name = finding.get("package").and_then(|p| p.as_str())
84 .unwrap_or("").to_string();
85 let module = finding.get("module").and_then(|m| m.as_str())
86 .unwrap_or("").to_string();
87
88 if let Some(dep) = dependencies.iter().find(|d|
90 d.name == package_name || d.name == module ||
91 package_name.starts_with(&format!("{}/", d.name)) ||
92 module.starts_with(&format!("{}/", d.name))) {
93
94 let vuln_id = finding.get("osv").and_then(|o| o.as_str())
95 .unwrap_or("unknown").to_string();
96 let title = finding.get("summary").and_then(|s| s.as_str())
97 .unwrap_or("Unknown vulnerability").to_string();
98 let description = finding.get("details").and_then(|d| d.as_str())
99 .unwrap_or("").to_string();
100 let severity = VulnerabilitySeverity::Medium; let fixed_version = finding.get("fixed_version").and_then(|v| v.as_str())
102 .map(|s| s.to_string());
103
104 let vuln_info = VulnerabilityInfo {
105 id: vuln_id,
106 vuln_type: "security".to_string(), severity,
108 title,
109 description,
110 cve: None, ghsa: None, affected_versions: "*".to_string(), patched_versions: fixed_version,
114 published_date: None,
115 references: Vec::new(), };
117
118 if let Some(existing) = vulnerable_deps.iter_mut()
120 .find(|vuln_dep| vuln_dep.name == dep.name)
121 {
122 if !existing.vulnerabilities.iter().any(|v| v.id == vuln_info.id) {
124 existing.vulnerabilities.push(vuln_info);
125 }
126 } else {
127 vulnerable_deps.push(VulnerableDependency {
128 name: dep.name.clone(),
129 version: dep.version.clone(),
130 language: crate::analyzer::dependency_parser::Language::Go,
131 vulnerabilities: vec![vuln_info],
132 });
133 }
134 }
135 }
136 }
137 }
138
139 if vulnerable_deps.is_empty() {
140 Ok(None)
141 } else {
142 Ok(Some(vulnerable_deps))
143 }
144 }
145}
146
147impl MutableLanguageVulnerabilityChecker for GoVulnerabilityChecker {
148 fn check_vulnerabilities(
149 &mut self,
150 dependencies: &[DependencyInfo],
151 project_path: &Path,
152 ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
153 info!("Checking Go dependencies");
154
155 match self.execute_govulncheck(project_path, dependencies) {
156 Ok(Some(vulns)) => Ok(vulns),
157 Ok(None) => Ok(vec![]),
158 Err(e) => Err(e),
159 }
160 }
161}