Skip to content

Commit f08df61

Browse files
authored
Merge pull request #159 from syncable-dev/develop
Develop
2 parents bfbbac6 + ba53512 commit f08df61

8 files changed

Lines changed: 466 additions & 320 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ path = "examples/check_vulnerabilities.rs"
8686

8787
[[example]]
8888
name = "security_analysis"
89-
path = "examples/security_analysis.rs"
89+
path = "examples/security_analysis.rs"

src/analyzer/frameworks/javascript.rs

Lines changed: 125 additions & 68 deletions
Large diffs are not rendered by default.

src/analyzer/frameworks/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ impl FrameworkDetectionUtils {
8484
let pattern_confidence = matches as f32 / total_patterns as f32;
8585
// Use additive approach instead of multiplicative to avoid extremely low scores
8686
// Base confidence provides a floor, pattern confidence provides the scaling
87-
let final_confidence = (rule.confidence * pattern_confidence + base_confidence * 0.1).min(1.0);
87+
// Cap dependency-based confidence at 0.95 to ensure file-based detection (1.0) takes precedence
88+
let final_confidence = (rule.confidence * pattern_confidence + base_confidence * 0.1).min(0.95);
8889

8990
// Debug logging for Tanstack Start detection
9091
if rule.name.contains("Tanstack") {
@@ -123,7 +124,9 @@ impl FrameworkDetectionUtils {
123124
dependency.contains(&pattern.replace('*', ""))
124125
}
125126
} else {
126-
dependency == pattern || dependency.contains(pattern)
127+
// For dependency detection, use exact matching to avoid false positives
128+
// Only match if the dependency is exactly the pattern or starts with the pattern followed by a version specifier
129+
dependency == pattern || dependency.starts_with(&(pattern.to_string() + "@")) || dependency.starts_with(&(pattern.to_string() + "/"))
127130
}
128131
}
129132

src/analyzer/tool_management/detector.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use log::{debug, info};
99
pub struct ToolStatus {
1010
pub available: bool,
1111
pub path: Option<PathBuf>,
12+
pub execution_path: Option<PathBuf>, // Path to use for execution
1213
pub version: Option<String>,
1314
pub installation_source: InstallationSource,
1415
pub last_checked: SystemTime,
@@ -162,6 +163,7 @@ impl ToolDetector {
162163
let not_found = ToolStatus {
163164
available: false,
164165
path: None,
166+
execution_path: None,
165167
version: None,
166168
installation_source: InstallationSource::NotFound,
167169
last_checked: SystemTime::now(),
@@ -183,6 +185,7 @@ impl ToolDetector {
183185
return ToolStatus {
184186
available: true,
185187
path: Some(path),
188+
execution_path: None, // Execute by name when in PATH
186189
version,
187190
installation_source: InstallationSource::SystemPath,
188191
last_checked: SystemTime::now(),
@@ -204,7 +207,8 @@ impl ToolDetector {
204207
tool_name, tool_path, version, source);
205208
return ToolStatus {
206209
available: true,
207-
path: Some(tool_path),
210+
path: Some(tool_path.clone()),
211+
execution_path: Some(tool_path), // Use full path for execution
208212
version: Some(version),
209213
installation_source: source,
210214
last_checked: SystemTime::now(),
@@ -222,6 +226,7 @@ impl ToolDetector {
222226
return ToolStatus {
223227
available: true,
224228
path: Some(tool_path_exe),
229+
execution_path: Some(tool_path_exe), // Use full path for execution
225230
version,
226231
installation_source: source,
227232
last_checked: SystemTime::now(),
@@ -236,6 +241,7 @@ impl ToolDetector {
236241
ToolStatus {
237242
available: false,
238243
path: None,
244+
execution_path: None,
239245
version: None,
240246
installation_source: InstallationSource::NotFound,
241247
last_checked: SystemTime::now(),

src/analyzer/tool_management/installers/go.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub fn install_govulncheck(
2323
if success {
2424
info!("✅ govulncheck installed successfully");
2525
installed_tools.insert("govulncheck".to_string(), true);
26-
tool_detector.clear_cache();
26+
tool_detector.clear_cache(); // Clear cache to force fresh detection
2727
info!("💡 Note: Make sure ~/go/bin is in your PATH to use govulncheck");
2828
} else {
2929
warn!("❌ Failed to install govulncheck");

src/analyzer/vulnerability/checkers/go.rs

Lines changed: 111 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,38 @@ impl GoVulnerabilityChecker {
3131

3232
info!("Executing govulncheck in {}", project_path.display());
3333

34-
// Execute govulncheck -json
35-
let output = Command::new("govulncheck")
34+
// Execute govulncheck using the full path if available
35+
let mut command = if let Some(exec_path) = &govulncheck_status.execution_path {
36+
// Use the full path when tool is not in PATH
37+
Command::new(exec_path)
38+
} else {
39+
// Use tool name directly when in PATH
40+
Command::new("govulncheck")
41+
};
42+
43+
let output = command
3644
.args(&["-json", "./..."])
3745
.current_dir(project_path)
3846
.output()
3947
.map_err(|e| VulnerabilityError::CommandError(
4048
format!("Failed to run govulncheck: {}", e)
4149
))?;
4250

51+
// Log debug information about the command output
52+
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+
// Log first few lines of stdout for debugging
62+
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+
4366
// govulncheck returns 0 even when vulnerabilities are found
4467
// Non-zero exit code indicates an actual error
4568
if !output.status.success() && output.stdout.is_empty() {
@@ -50,11 +73,12 @@ impl GoVulnerabilityChecker {
5073
));
5174
}
5275

76+
// Parse govulncheck output
5377
if output.stdout.is_empty() {
78+
info!("govulncheck returned empty output, no vulnerabilities found");
5479
return Ok(None);
5580
}
5681

57-
// Parse govulncheck output
5882
self.parse_govulncheck_output(&output.stdout, dependencies)
5983
}
6084

@@ -65,73 +89,98 @@ impl GoVulnerabilityChecker {
6589
) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
6690
let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
6791

68-
// Split output by lines and parse each JSON object
92+
// Convert output to string
6993
let output_str = String::from_utf8_lossy(output);
70-
for line in output_str.lines() {
71-
if line.trim().is_empty() {
94+
95+
// Check if output is empty or only whitespace
96+
if output_str.trim().is_empty() {
97+
info!("govulncheck output is empty, no vulnerabilities found");
98+
return Ok(None);
99+
}
100+
101+
// Govulncheck outputs a stream of JSON objects separated by newlines
102+
// Process each line and only parse lines that look like complete JSON objects
103+
for (line_num, line) in output_str.lines().enumerate() {
104+
let trimmed_line = line.trim();
105+
if trimmed_line.is_empty() {
72106
continue;
73107
}
74108

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-
))?;
109+
// Only try to parse lines that look like JSON objects (start with { and end with })
110+
if !trimmed_line.starts_with('{') || !trimmed_line.ends_with('}') {
111+
continue;
112+
}
79113

80-
// Govulncheck JSON structure parsing
81-
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-
// Find matching dependency
89-
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; // Govulncheck doesn't provide severity directly
101-
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(), // Security vulnerability
107-
severity,
108-
title,
109-
description,
110-
cve: None, // Govulncheck uses OSV IDs
111-
ghsa: None, // Govulncheck uses OSV IDs
112-
affected_versions: "*".to_string(), // Govulncheck doesn't provide this directly
113-
patched_versions: fixed_version,
114-
published_date: None,
115-
references: Vec::new(), // Govulncheck doesn't provide references in this format
116-
};
117-
118-
// Check if we already have this dependency
119-
if let Some(existing) = vulnerable_deps.iter_mut()
120-
.find(|vuln_dep| vuln_dep.name == dep.name)
121-
{
122-
// Avoid duplicate vulnerabilities
123-
if !existing.vulnerabilities.iter().any(|v| v.id == vuln_info.id) {
124-
existing.vulnerabilities.push(vuln_info);
114+
// Try to parse as JSON, but handle errors gracefully
115+
match serde_json::from_str::<serde_json::Value>(trimmed_line) {
116+
Ok(audit_data) => {
117+
// Govulncheck JSON structure parsing
118+
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+
// Find matching dependency
126+
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; // Govulncheck doesn't provide severity directly
138+
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(), // Security vulnerability
144+
severity,
145+
title,
146+
description,
147+
cve: None, // Govulncheck uses OSV IDs
148+
ghsa: None, // Govulncheck uses OSV IDs
149+
affected_versions: "*".to_string(), // Govulncheck doesn't provide this directly
150+
patched_versions: fixed_version,
151+
published_date: None,
152+
references: Vec::new(), // Govulncheck doesn't provide references in this format
153+
};
154+
155+
// Check if we already have this dependency
156+
if let Some(existing) = vulnerable_deps.iter_mut()
157+
.find(|vuln_dep| vuln_dep.name == dep.name)
158+
{
159+
// Avoid duplicate vulnerabilities
160+
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+
}
125171
}
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-
});
133172
}
134173
}
174+
},
175+
Err(e) => {
176+
// Log the error but continue processing other lines
177+
// Only log detailed errors for lines that look like they should be valid JSON
178+
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 with next line instead of failing completely
183+
continue;
135184
}
136185
}
137186
}

0 commit comments

Comments
 (0)