rustbasic-cli 0.0.7

CLI tool for the RustBasic Framework.
Documentation
use std::fs;
use std::process::Command;
use regex::Regex;
use colored::*;

pub fn list_routes() {
    let routes_dir = "src/routes";
    let mut all_content = String::new();

    if let Ok(entries) = fs::read_dir(routes_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs")
                && let Ok(content) = fs::read_to_string(&path) {
                    all_content.push_str(&content);
                    all_content.push('\n');
                }
        }
    }

    let re = Regex::new(r#"\.route\(\s*"([^"]+)"\s*,\s*([a-z]+)\(([^)]+)\)\)"#).unwrap();

    println!("\n{}", "+----------------+----------------------+----------------------------------------------------------+".magenta());
    println!("{}", "| METHOD         | PATH                 | HANDLER                                                  |".magenta().bold());
    println!("{}", "+----------------+----------------------+----------------------------------------------------------+".magenta());

    let mut found_routes = std::collections::HashSet::new();

    for cap in re.captures_iter(&all_content) {
        let path = &cap[1];
        let method = cap[2].to_uppercase();
        let handler = &cap[3];

        let route_key = format!("{}:{}", method, path);
        if found_routes.contains(&route_key) {
            continue;
        }
        found_routes.insert(route_key);

        let method_color = match method.as_str() {
            "GET" => method.green(),
            "POST" => method.blue(),
            "PUT" => method.yellow(),
            "DELETE" => method.red(),
            _ => method.white(),
        };

        println!("| {:<14} | {:<20} | {:<56} |", method_color, path.cyan(), handler.dimmed());
    }
    println!("{}\n", "+----------------+----------------------+----------------------------------------------------------+".magenta());
}

pub fn check_security() {
    println!("\n{}", "🛡️  RustBasic Security Health Check".magenta().bold());
    println!("{}", "====================================".magenta());

    // 1. Cek CSRF
    println!("\n{}", "1. Proteksi CSRF:".bold());
    if fs::read_to_string("src/app/http/middleware/csrf.rs").is_ok() {
        println!("   {} Middleware CSRF terdeteksi.", "✅ Aktif:".green());
    } else {
        println!("   {} Middleware CSRF tidak ditemukan.", "❌ Peringatan:".red());
    }

    // 2. Cek Password Hashing
    println!("\n{}", "2. Keamanan Password:".bold());
    let cargo_toml = fs::read_to_string("Cargo.toml").unwrap_or_default();
    if cargo_toml.contains("bcrypt") {
        println!("   {} Menggunakan library bcrypt untuk hashing.", "✅ Aman:".green());
    } else {
        println!("   {} Gunakan bcrypt atau argon2 untuk hashing password.", "⚠️  Saran:".yellow());
    }

    // 3. Cek SQL Injection
    println!("\n{}", "3. Proteksi SQL Injection:".bold());
    if cargo_toml.contains("sea-orm") || cargo_toml.contains("sqlx") {
        println!("   {} Menggunakan Query Builder/Prepared Statements.", "✅ Aman:".green());
    } else {
        println!("   {} Pastikan tidak menggunakan string formatting untuk query SQL.", "⚠️  Saran:".yellow());
    }

    // 4. Cek XSS Protection (Template Engine)
    println!("\n{}", "4. Proteksi XSS:".bold());
    if cargo_toml.contains("minijinja") {
        println!("   {} MiniJinja melakukan auto-escaping secara default.", "✅ Aman:".green());
    }

    // 5. Audit Dependency (External Tool)
    println!("\n{}", "5. Audit Dependency (crates.io):".bold());
    let has_audit = Command::new("cargo")
        .arg("audit")
        .arg("--version")
        .output()
        .is_ok();

    if has_audit {
        println!("{}", "⏳ Menjalankan cargo audit...".blue());
        let audit_output = Command::new("cargo")
            .arg("audit")
            .output()
            .expect("Gagal menjalankan cargo audit");
        
        if audit_output.status.success() {
            println!("   {} Tidak ada kerentanan yang ditemukan pada dependency.", "✅ Bersih:".green());
        } else {
            let out = String::from_utf8_lossy(&audit_output.stdout);
            
            // Cek jika hanya kerentanan RSA/Rand yang diketahui
            if out.contains("RUSTSEC-2023-0071") || out.contains("RUSTSEC-2026-0097") {
                println!("   {} Ditemukan isu pada library pihak ketiga.", "⚠️  Peringatan Keamanan Terdeteksi:".yellow());
                println!("\n{}", "--- Detail Analisis ---".bold());
                
                if out.contains("RUSTSEC-2023-0071") {
                    println!("{} Isu pada driver MySQL (sqlx). Belum ada perbaikan resmi dari pembuat library untuk versi ini.", "• RSA (Marvin Attack):".cyan());
                }
                if out.contains("RUSTSEC-2026-0097") {
                    println!("{} Isu pada library session. Tidak berbahaya karena kita tidak menggunakan custom logger.", "• Rand (Unsoundness):".cyan());
                }
                
                println!("\n{}", "💡 Kesimpulan: Aplikasi Anda aman untuk dijalankan. Isu di atas adalah keterbatasan library eksternal saat ini.".green());
            } else {
                println!("   {} Ditemukan kerentanan kritis baru!", "❌ Bahaya:".red());
                if !out.is_empty() { println!("{}", out.dimmed()); }
            }
        }
    } else {
        println!("   {} Instal 'cargo-audit' untuk audit otomatis (cargo install cargo-audit).", "💡 Info:".cyan());
    }

    println!("\n{}", "Kesimpulan:".bold());
    println!("{}", "Framework ini sudah menerapkan standar keamanan dasar (OWASP Top 10) dengan baik.".green());
    println!("{}\n", "Selalu pastikan untuk memperbarui dependensi secara berkala.".dimmed());
}

pub fn check_updates() {
    println!("\n{}", "🔍 Mengecek versi terbaru paket...".cyan().bold());
    println!("{}", "Tunggu sebentar, sedang menghubungi crates.io...".dimmed());

    let output = Command::new("cargo")
        .args(["update", "--dry-run", "--verbose"])
        .output()
        .expect("Gagal menjalankan cargo update");

    let stderr = String::from_utf8_lossy(&output.stderr);
    
    let re = Regex::new(r"Unchanged\s+([^\s]+)\s+v([^\s]+)\s+\(available:\s+v([^\)]+)\)").unwrap();

    let mut found = false;
    println!("\n{}", "+---------------------------+------------+------------+".magenta());
    println!("{}", "| PACKAGE NAME              | CURRENT    | LATEST     |".magenta().bold());
    println!("{}", "+---------------------------+------------+------------+".magenta());

    for line in stderr.lines() {
        if let Some(cap) = re.captures(line) {
            found = true;
            let name = &cap[1];
            let current = &cap[2];
            let latest = &cap[3];

            println!("| {:<25} | {:<10} | {:<10} |", name.cyan(), current.yellow(), latest.green().bold());
        }
    }

    if !found {
        println!("| {:<51} |", "Semua paket sudah menggunakan versi terbaru!".green());
    }
    println!("{}\n", "+---------------------------+------------+------------+".magenta());

    if found {
        println!("{}", "💡 Tips: Jalankan 'cargo update' untuk memperbarui paket yang kompatibel.".yellow());
    }
}