dumpling 0.1.0

A fast JavaScript runtime and bundler in Rust
Documentation
use std::path::PathBuf;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::error::{Result, DumplingError};
use crate::package_manager::PackageJson;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
    pub id: String,
    pub title: String,
    pub description: String,
    pub severity: String, // "low", "moderate", "high", "critical"
    pub url: String,
    pub package: String,
    pub versions: Vec<String>, // Affected versions
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VulnerabilityReport {
    pub vulnerabilities: Vec<Vulnerability>,
    pub summary: VulnerabilitySummary,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VulnerabilitySummary {
    pub total: usize,
    pub critical: usize,
    pub high: usize,
    pub moderate: usize,
    pub low: usize,
}

pub struct SecurityScanner {
    // In a real implementation, this would use an external vulnerability database
    // For now, we'll use a mock implementation
    mock_vulnerabilities: Vec<Vulnerability>,
}

impl SecurityScanner {
    pub fn new() -> Self {
        let mock_vulnerabilities = vec![
            Vulnerability {
                id: "CVE-2021-23337".to_string(),
                title: "Prototype Pollution".to_string(),
                description: "The package lodash before 4.17.21 is vulnerable to Prototype Pollution.".to_string(),
                severity: "high".to_string(),
                url: "https://nvd.nist.gov/vuln/detail/CVE-2021-23337".to_string(),
                package: "lodash".to_string(),
                versions: vec!["<4.17.21".to_string()],
            },
            Vulnerability {
                id: "CVE-2022-25883".to_string(),
                title: "Regular Expression Denial of Service".to_string(),
                description: "The package minimist before 1.2.6 is vulnerable to Regular Expression Denial of Service (ReDoS).".to_string(),
                severity: "moderate".to_string(),
                url: "https://nvd.nist.gov/vuln/detail/CVE-2022-25883".to_string(),
                package: "minimist".to_string(),
                versions: vec!["<1.2.6".to_string()],
            },
            Vulnerability {
                id: "CVE-2021-23424".to_string(),
                title: "Cross-site Scripting".to_string(),
                description: "The package axios before 0.21.1 is vulnerable to Cross-site Scripting (XSS).".to_string(),
                severity: "moderate".to_string(),
                url: "https://nvd.nist.gov/vuln/detail/CVE-2021-23424".to_string(),
                package: "axios".to_string(),
                versions: vec!["<0.21.1".to_string()],
            },
        ];

        Self { mock_vulnerabilities }
    }

    /// Scan for vulnerabilities in the dependencies of a project
    pub async fn scan(&self, root: &PathBuf) -> Result<VulnerabilityReport> {
        let package_json_path = root.join("package.json");
        if !package_json_path.exists() {
            return Err(DumplingError::FileNotFound("package.json not found".to_string()));
        }

        let package = PackageJson::load(&package_json_path).await?;
        let dependencies = package.dependencies();
        let mut vulnerabilities = Vec::new();

        // Check each dependency against the vulnerability database
        for (name, version) in dependencies {
            for vuln in &self.mock_vulnerabilities {
                if vuln.package == name && self.is_version_affected(version.as_str(), &vuln.versions) {
                    vulnerabilities.push(vuln.clone());
                }
            }
        }

        // Calculate summary
        let summary = self.calculate_summary(&vulnerabilities);

        Ok(VulnerabilityReport {
            vulnerabilities,
            summary,
        })
    }

    /// Check if a version is affected by the vulnerabilities
    fn is_version_affected(&self, version: &str, affected_versions: &[String]) -> bool {
        // Simplified version comparison
        // In a real implementation, this would use proper semver comparison
        for affected in affected_versions {
            if affected.starts_with('<') {
                let threshold = affected.trim_start_matches('<');
                if self.compare_versions(version, threshold) < 0 {
                    return true;
                }
            }
        }
        false
    }

    /// Simple version comparison
    fn compare_versions(&self, a: &str, b: &str) -> i32 {
        // Very simplified version comparison
        let a_parts: Vec<&str> = a.split('.').collect();
        let b_parts: Vec<&str> = b.split('.').collect();
        
        let min_len = a_parts.len().min(b_parts.len());
        for i in 0..min_len {
            if let (Ok(a_num), Ok(b_num)) = (a_parts[i].parse::<u32>(), b_parts[i].parse::<u32>()) {
                if a_num > b_num {
                    return 1;
                } else if a_num < b_num {
                    return -1;
                }
            }
        }
        
        if a_parts.len() > b_parts.len() {
            1
        } else if a_parts.len() < b_parts.len() {
            -1
        } else {
            0
        }
    }

    /// Calculate summary statistics for vulnerabilities
    fn calculate_summary(&self, vulnerabilities: &[Vulnerability]) -> VulnerabilitySummary {
        let mut summary = VulnerabilitySummary {
            total: vulnerabilities.len(),
            critical: 0,
            high: 0,
            moderate: 0,
            low: 0,
        };

        for vuln in vulnerabilities {
            match vuln.severity.as_str() {
                "critical" => summary.critical += 1,
                "high" => summary.high += 1,
                "moderate" => summary.moderate += 1,
                "low" => summary.low += 1,
                _ => {}
            }
        }

        summary
    }

    /// Generate a security report in JSON format
    pub fn generate_report_json(&self, report: &VulnerabilityReport) -> Result<String> {
        serde_json::to_string_pretty(report)
            .map_err(|e| DumplingError::Build(format!("Failed to generate report: {}", e)))
    }

    /// Generate a security report in markdown format
    pub fn generate_report_markdown(&self, report: &VulnerabilityReport) -> String {
        let mut output = String::new();
        
        output.push_str("# Security Vulnerability Report\n\n");
        output.push_str(&format!("## Summary\n\n"));
        output.push_str(&format!("- Total vulnerabilities: {}\n", report.summary.total));
        output.push_str(&format!("- Critical: {}\n", report.summary.critical));
        output.push_str(&format!("- High: {}\n", report.summary.high));
        output.push_str(&format!("- Moderate: {}\n", report.summary.moderate));
        output.push_str(&format!("- Low: {}\n", report.summary.low));
        output.push_str("\n## Vulnerabilities\n\n");
        
        if report.vulnerabilities.is_empty() {
            output.push_str("No vulnerabilities found! 🎉\n");
        } else {
            for vuln in &report.vulnerabilities {
                output.push_str(&format!("### {}\n\n", vuln.title));
                output.push_str(&format!("- **Package**: {}\n", vuln.package));
                output.push_str(&format!("- **Severity**: {}\n", vuln.severity));
                output.push_str(&format!("- **ID**: {}\n", vuln.id));
                output.push_str(&format!("- **Affected versions**: {}\n", vuln.versions.join(", ")));
                output.push_str(&format!("- **Description**: {}\n", vuln.description));
                output.push_str(&format!("- **More info**: [{}]({})\n", vuln.id, vuln.url));
                output.push_str("\n---\n\n");
            }
        }
        
        output
    }

    /// Check license compliance
    pub async fn check_licenses(&self, root: &PathBuf) -> Result<LicenseReport> {
        let package_json_path = root.join("package.json");
        if !package_json_path.exists() {
            return Err(DumplingError::FileNotFound("package.json not found".to_string()));
        }

        let package = PackageJson::load(&package_json_path).await?;
        let dependencies = package.dependencies();
        let mut licenses = HashMap::new();

        // Mock license checking - in a real implementation, this would check node_modules
        for name in dependencies.keys() {
            let license = match name.as_str() {
                "lodash" => "MIT",
                "axios" => "MIT",
                "react" => "MIT",
                "vue" => "MIT",
                "angular" => "MIT",
                "express" => "MIT",
                "licensecheck" => "GPL-3.0",
                _ => "Unknown",
            };
            licenses.insert(name.clone(), license.to_string());
        }

        Ok(LicenseReport { licenses })
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LicenseReport {
    pub licenses: HashMap<String, String>,
}

/// Audit project for security vulnerabilities and license compliance
pub async fn audit(format: String, check_licenses: bool) -> Result<()> {
    let current_dir = std::env::current_dir()?;
    let scanner = SecurityScanner::new();
    
    // Scan for vulnerabilities
    let report = scanner.scan(&current_dir).await?;
    
    // Output vulnerability report
    match format.as_str() {
        "json" => {
            let json = scanner.generate_report_json(&report)?;
            println!("{}", json);
        }
        "markdown" | _ => {
            let markdown = scanner.generate_report_markdown(&report);
            println!("{}", markdown);
        }
    }
    
    // Check licenses if requested
    if check_licenses {
        let license_report = scanner.check_licenses(&current_dir).await?;
        let markdown = license_report.to_markdown();
        println!("{}", markdown);
    }
    
    // Exit with non-zero code if vulnerabilities were found
    if !report.vulnerabilities.is_empty() {
        std::process::exit(1);
    }
    
    Ok(())
}

impl LicenseReport {
    /// Check for problematic licenses
    pub fn has_problematic_licenses(&self) -> bool {
        for license in self.licenses.values() {
            if self.is_problematic_license(license) {
                return true;
            }
        }
        false
    }

    /// Determine if a license might be problematic for commercial use
    fn is_problematic_license(&self, license: &str) -> bool {
        // Simplified check for GPL and other copyleft licenses
        license.to_lowercase().contains("gpl") || 
        license.to_lowercase().contains("agpl") ||
        license.to_lowercase().contains("lgpl")
    }

    /// Generate a license report in markdown format
    pub fn to_markdown(&self) -> String {
        let mut output = String::new();
        
        output.push_str("# License Compliance Report\n\n");
        
        if self.licenses.is_empty() {
            output.push_str("No dependencies found.\n");
        } else {
            output.push_str("## Dependencies and Licenses\n\n");
            
            let mut problematic = Vec::new();
            
            for (name, license) in &self.licenses {
                if self.is_problematic_license(license) {
                    problematic.push((name, license));
                } else {
                    output.push_str(&format!("- {}: {}\n", name, license));
                }
            }
            
            if !problematic.is_empty() {
                output.push_str("\n## ⚠️  Potentially Problematic Licenses\n\n");
                output.push_str("The following dependencies use copyleft licenses that may require special consideration:\n\n");
                for (name, license) in problematic {
                    output.push_str(&format!("- {}: **{}**\n", name, license));
                }
                output.push_str("\nPlease consult with your legal team regarding these licenses.\n");
            }
        }
        
        output
    }
}