syncable-cli 0.37.1

A Rust-based CLI that analyzes code repositories and generates Infrastructure as Code configurations
Documentation
use super::MutableLanguageVulnerabilityChecker;
use crate::analyzer::dependency_parser::DependencyInfo;
use crate::analyzer::tool_management::ToolDetector;
use crate::analyzer::vulnerability::{
    VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity, VulnerableDependency,
};
use log::{info, warn};
use std::path::Path;
use std::process::Command;

#[derive(Default)]
pub struct GoVulnerabilityChecker {
    tool_detector: ToolDetector,
}

impl GoVulnerabilityChecker {
    pub fn new() -> Self {
        Self::default()
    }

    fn execute_govulncheck(
        &mut self,
        project_path: &Path,
        dependencies: &[DependencyInfo],
    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
        // Check if govulncheck is available
        let govulncheck_status = self.tool_detector.detect_tool("govulncheck");
        if !govulncheck_status.available {
            warn!(
                "govulncheck not found, skipping Go vulnerability check. Install with: go install golang.org/x/vuln/cmd/govulncheck@latest"
            );
            return Ok(None);
        }

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

        // Execute govulncheck using the full path if available
        let mut command = if let Some(exec_path) = &govulncheck_status.execution_path {
            // Use the full path when tool is not in PATH
            Command::new(exec_path)
        } else {
            // Use tool name directly when in PATH
            Command::new("govulncheck")
        };

        let output = command
            .args(["-json", "./..."])
            .current_dir(project_path)
            .output()
            .map_err(|e| {
                VulnerabilityError::CommandError(format!("Failed to run govulncheck: {}", e))
            })?;

        // Log debug information about the command output
        info!(
            "govulncheck stdout length: {}, stderr length: {}",
            output.stdout.len(),
            output.stderr.len()
        );
        info!("govulncheck exit code: {:?}", output.status.code());

        if !output.stderr.is_empty() {
            let stderr_str = String::from_utf8_lossy(&output.stderr);
            info!("govulncheck stderr: {}", stderr_str);
        }

        // Log first few lines of stdout for debugging
        let stdout_str = String::from_utf8_lossy(&output.stdout);
        let stdout_lines: Vec<&str> = stdout_str.lines().take(20).collect();
        info!("govulncheck stdout first 20 lines: {:?}", stdout_lines);

        // govulncheck returns 0 even when vulnerabilities are found
        // Non-zero exit code indicates an actual error
        if !output.status.success() && output.stdout.is_empty() {
            return Err(VulnerabilityError::CommandError(format!(
                "govulncheck failed with exit code {}: {}",
                output.status.code().unwrap_or(-1),
                String::from_utf8_lossy(&output.stderr)
            )));
        }

        // Parse govulncheck output
        if output.stdout.is_empty() {
            info!("govulncheck returned empty output, no vulnerabilities found");
            return Ok(None);
        }

        self.parse_govulncheck_output(&output.stdout, dependencies)
    }

    fn parse_govulncheck_output(
        &self,
        output: &[u8],
        dependencies: &[DependencyInfo],
    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();

        // Convert output to string
        let output_str = String::from_utf8_lossy(output);

        // Check if output is empty or only whitespace
        if output_str.trim().is_empty() {
            info!("govulncheck output is empty, no vulnerabilities found");
            return Ok(None);
        }

        // Govulncheck outputs a stream of JSON objects separated by newlines
        // Process each line and only parse lines that look like complete JSON objects
        for (line_num, line) in output_str.lines().enumerate() {
            let trimmed_line = line.trim();
            if trimmed_line.is_empty() {
                continue;
            }

            // Only try to parse lines that look like JSON objects (start with { and end with })
            if !trimmed_line.starts_with('{') || !trimmed_line.ends_with('}') {
                continue;
            }

            // Try to parse as JSON, but handle errors gracefully
            match serde_json::from_str::<serde_json::Value>(trimmed_line) {
                Ok(audit_data) => {
                    // Govulncheck JSON structure parsing
                    if let Some(finding) = audit_data.get("finding").and_then(|f| f.as_object()) {
                        let package_name = finding
                            .get("package")
                            .and_then(|p| p.as_str())
                            .unwrap_or("")
                            .to_string();
                        let module = finding
                            .get("module")
                            .and_then(|m| m.as_str())
                            .unwrap_or("")
                            .to_string();

                        // Find matching dependency
                        if let Some(dep) = dependencies.iter().find(|d| {
                            d.name == package_name
                                || d.name == module
                                || package_name.starts_with(&format!("{}/", d.name))
                                || module.starts_with(&format!("{}/", d.name))
                        }) {
                            let vuln_id = finding
                                .get("osv")
                                .and_then(|o| o.as_str())
                                .unwrap_or("unknown")
                                .to_string();
                            let title = finding
                                .get("summary")
                                .and_then(|s| s.as_str())
                                .unwrap_or("Unknown vulnerability")
                                .to_string();
                            let description = finding
                                .get("details")
                                .and_then(|d| d.as_str())
                                .unwrap_or("")
                                .to_string();
                            let severity = VulnerabilitySeverity::Medium; // Govulncheck doesn't provide severity directly
                            let fixed_version = finding
                                .get("fixed_version")
                                .and_then(|v| v.as_str())
                                .map(|s| s.to_string());

                            let vuln_info = VulnerabilityInfo {
                                id: vuln_id,
                                vuln_type: "security".to_string(), // Security vulnerability
                                severity,
                                title,
                                description,
                                cve: None,  // Govulncheck uses OSV IDs
                                ghsa: None, // Govulncheck uses OSV IDs
                                affected_versions: "*".to_string(), // Govulncheck doesn't provide this directly
                                patched_versions: fixed_version,
                                published_date: None,
                                references: Vec::new(), // Govulncheck doesn't provide references in this format
                            };

                            // Check if we already have this dependency
                            if let Some(existing) = vulnerable_deps
                                .iter_mut()
                                .find(|vuln_dep| vuln_dep.name == dep.name)
                            {
                                // Avoid duplicate vulnerabilities
                                if !existing
                                    .vulnerabilities
                                    .iter()
                                    .any(|v| v.id == vuln_info.id)
                                {
                                    existing.vulnerabilities.push(vuln_info);
                                }
                            } else {
                                vulnerable_deps.push(VulnerableDependency {
                                    name: dep.name.clone(),
                                    version: dep.version.clone(),
                                    language: crate::analyzer::dependency_parser::Language::Go,
                                    vulnerabilities: vec![vuln_info],
                                    source_dir: None,
                                });
                            }
                        }
                    }
                }
                Err(e) => {
                    // Log the error but continue processing other lines
                    // Only log detailed errors for lines that look like they should be valid JSON
                    if trimmed_line.starts_with('{') && trimmed_line.ends_with('}') {
                        warn!(
                            "Failed to parse govulncheck output line {}: {}. Line content: {}",
                            line_num + 1,
                            e,
                            trimmed_line
                        );
                    }
                    // Continue with next line instead of failing completely
                    continue;
                }
            }
        }

        if vulnerable_deps.is_empty() {
            Ok(None)
        } else {
            Ok(Some(vulnerable_deps))
        }
    }
}

impl MutableLanguageVulnerabilityChecker for GoVulnerabilityChecker {
    fn check_vulnerabilities(
        &mut self,
        dependencies: &[DependencyInfo],
        project_path: &Path,
    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
        info!("Checking Go dependencies");

        match self.execute_govulncheck(project_path, dependencies) {
            Ok(Some(vulns)) => Ok(vulns),
            Ok(None) => Ok(vec![]),
            Err(e) => Err(e),
        }
    }
}