flintbase 0.3.1

Google / Firebase API key analyzer and APK secret scanner — tests keys against 20+ endpoints and extracts hardcoded credentials from Android apps
use indexmap::IndexMap;

use colored::Colorize;
use comfy_table::{modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Cell, Color, ContentArrangement, Table};

use crate::config::{FirebaseConfig, FirebaseProjectInfo, TestResult};

// ── Config panel ─────────────────────────────────────────────────────────────

pub fn print_config_info(config: &FirebaseConfig, app_name: Option<&str>) {
    let masked_key = if config.api_key.len() > 16 {
        format!("{}...{}", &config.api_key[..12], &config.api_key[config.api_key.len() - 4..])
    } else {
        config.api_key.clone()
    };

    println!();
    println!("╭─ {} ─────────────────────────────────────╮", "Firebase Configuration".bright_cyan().bold());
    if let Some(name) = app_name {
        println!("│  App:       {:<40}│", name);
    }
    println!("│  API Key:   {:<40}│", masked_key.yellow());
    println!("│  App ID:    {:<40}│", config.app_id.as_deref().unwrap_or("Not set").dimmed());
    println!("│  Project:   {:<40}│", config.project_id.as_deref().unwrap_or("Not set").green());
    println!("│  Sender ID: {:<40}│", config.gcm_sender_id.as_deref().unwrap_or("Not set").dimmed());
    println!("╰──────────────────────────────────────────────────────────╯");
}

// ── Results tables ───────────────────────────────────────────────────────────

pub fn print_report(
    _config: &FirebaseConfig,
    firebase_results: &IndexMap<String, TestResult>,
    google_results: &IndexMap<String, TestResult>,
    project_info: &FirebaseProjectInfo,
) {
    // Firebase results table
    println!("\n{}", "Firebase Services".bright_magenta().bold());
    let mut fb_table = Table::new();
    fb_table
        .load_preset(UTF8_FULL)
        .apply_modifier(UTF8_ROUND_CORNERS)
        .set_content_arrangement(ContentArrangement::Dynamic);

    fb_table.set_header(vec![
        Cell::new("Service").fg(Color::Magenta),
        Cell::new("Status"),
        Cell::new("Code"),
        Cell::new("Details"),
    ]);

    for (name, res) in firebase_results {
        let (status_text, status_color) = if res.success {
            ("WORKS", Color::Green)
        } else {
            ("DENIED", Color::Red)
        };

        let code: String = res
            .status_code
            .map(|c: u16| c.to_string())
            .unwrap_or_else(|| "".to_string());

        let detail = res
            .detail
            .as_deref()
            .or(res.error.as_deref())
            .unwrap_or("");
        let detail_truncated: String = detail.chars().take(55).collect::<String>();

        let detail_color = if detail.contains("CRITICAL") {
            Color::Red
        } else if detail.contains("WARNING") {
            Color::Yellow
        } else {
            Color::White
        };

        fb_table.add_row(vec![
            Cell::new(name).fg(Color::Cyan),
            Cell::new(status_text).fg(status_color),
            Cell::new(&code),
            Cell::new(&detail_truncated).fg(detail_color),
        ]);
    }
    println!("{fb_table}");

    // Google API results table
    println!("\n{}", "Google Cloud APIs".bright_magenta().bold());
    let mut g_table = Table::new();
    g_table
        .load_preset(UTF8_FULL)
        .apply_modifier(UTF8_ROUND_CORNERS)
        .set_content_arrangement(ContentArrangement::Dynamic);

    g_table.set_header(vec![
        Cell::new("Service").fg(Color::Magenta),
        Cell::new("Status"),
        Cell::new("Code"),
        Cell::new("Details"),
    ]);

    for (name, res) in google_results {
        let (status_text, status_color, detail) = if res.success {
            ("VALID", Color::Green, "Works".to_string())
        } else {
            let mut d = res.error.clone().unwrap_or_else(|| "Failed".to_string());
            if let Some(ref hint) = res.detail {
                d = format!("{}{}", d, hint);
            }
            ("FAIL", Color::Red, d)
        };

        let code: String = res
            .status_code
            .map(|c: u16| c.to_string())
            .unwrap_or_else(|| "".to_string());
        let detail_truncated: String = detail.chars().take(45).collect::<String>();

        g_table.add_row(vec![
            Cell::new(name).fg(Color::Cyan),
            Cell::new(status_text).fg(status_color),
            Cell::new(&code),
            Cell::new(&detail_truncated),
        ]);
    }
    println!("{g_table}");

    // Firebase analysis tree
    print_firebase_summary(project_info);

    // Summary
    println!("\n{}", "Summary:".bold());
    let fb_success = firebase_results.values().any(|r| r.success);
    let g_success = google_results.values().any(|r| r.success);

    if fb_success || g_success {
        println!(
            "{}",
            "✓ Key is VALID (accepted by at least one service)"
                .green()
                .bold()
        );
    } else {
        println!(
            "{}",
            "✗ Key appears INVALID or fully restricted".red().bold()
        );
    }

    println!("\n{}", "Key Insights:".yellow());
    println!(" • Firebase API keys IDENTIFY projects, not AUTHORIZE access");
    println!(" • App ID is needed for Installations API but not Identity Toolkit");
    println!(" • FCM server operations may require a different (server) key");
    println!(" • 403 = restriction, 401 = invalid key");
}

// ── Firebase project analysis tree ───────────────────────────────────────────

fn print_firebase_summary(info: &FirebaseProjectInfo) {
    println!();
    println!("{}", "Firebase Project Analysis".bright_cyan().bold());

    // Project Identity
    println!("  ├── {}", "Project Identity".bold());
    println!(
        "  │   ├── Project ID: {}",
        info.project_id
            .as_deref()
            .unwrap_or("Unknown")
            .yellow()
    );
    if let Some(ref pn) = info.project_number {
        println!("  │   ├── Project Number: {}", pn.dimmed());
    }
    if let Some(ref sb) = info.storage_bucket {
        println!("  │   └── Storage Bucket: {}", sb.dimmed());
    } else {
        println!("  │   └── (no additional identity info)");
    }

    // Authentication Configuration
    println!("  ├── {}", "Authentication Configuration".bold());
    if let Some(anon) = info.anonymous_auth_enabled {
        let status = if anon {
            "ENABLED".green().bold().to_string()
        } else {
            "Disabled".dimmed().to_string()
        };
        println!("  │   ├── Anonymous Auth: {}", status);
    }
    if let Some(email) = info.email_auth_enabled {
        let status = if email {
            "ENABLED".green().bold().to_string()
        } else {
            "Disabled".dimmed().to_string()
        };
        println!("  │   ├── Email/Password: {}", status);
    }
    if !info.enabled_providers.is_empty() {
        println!("  │   └── OAuth Providers:");
        let unique: Vec<&String> = {
            let mut seen = std::collections::HashSet::new();
            info.enabled_providers
                .iter()
                .filter(|p| seen.insert(p.as_str()))
                .collect()
        };
        for (i, p) in unique.iter().enumerate() {
            let prefix = if i == unique.len() - 1 {
                "└──"
            } else {
                "├──"
            };
            println!("{} {}", prefix, p.cyan());
        }
    } else {
        println!("  │   └── OAuth Providers: (none detected)");
    }

    // FCM Status
    println!("  ├── {}", "Firebase Cloud Messaging".bold());
    if let Some(fcm) = info.fcm_enabled {
        let status = if fcm {
            "ENABLED".green().bold().to_string()
        } else {
            "Disabled/Restricted".dimmed().to_string()
        };
        println!("  │   ├── Legacy API: {}", status);
    }
    if let Some(tok) = info.fcm_token_obtained {
        let status = if tok {
            "Works".green().to_string()
        } else {
            "Failed".dimmed().to_string()
        };
        println!("  │   ├── Token Registration: {}", status);
    }
    if let Some(topics) = info.fcm_topics_accessible {
        let status = if topics {
            "Accessible".green().to_string()
        } else {
            "Restricted".dimmed().to_string()
        };
        println!("  │   └── Topic Management: {}", status);
    } else {
        println!("  │   └── (no additional FCM info)");
    }

    // Security Findings
    println!("  └── {}", "Security Findings".bold());

    let mut critical: Vec<String> = Vec::new();
    let mut warnings: Vec<String> = Vec::new();

    if info.realtime_db_public_read == Some(true) {
        critical.push("Realtime Database PUBLICLY READABLE".to_string());
    }
    if info.storage_public == Some(true) {
        critical.push("Storage Bucket PUBLICLY LISTABLE".to_string());
    }
    if info.fcm_enabled == Some(true) {
        warnings.push("FCM enabled — push notifications possible".to_string());
    }
    if info.anonymous_auth_enabled == Some(true) {
        warnings.push("Anonymous auth — unrestricted account creation".to_string());
    }

    if !critical.is_empty() {
        println!("      ├── {}", "CRITICAL ISSUES".red().bold());
        for (i, issue) in critical.iter().enumerate() {
            let prefix = if i == critical.len() - 1 && warnings.is_empty() {
                "└──"
            } else {
                "├──"
            };
            println!("{} {}", prefix, issue.red());
        }
    }
    if !warnings.is_empty() {
        println!("      ├── {}", "Warnings".yellow().bold());
        for (i, w) in warnings.iter().enumerate() {
            let prefix = if i == warnings.len() - 1 {
                "└──"
            } else {
                "├──"
            };
            println!("{} {}", prefix, w.yellow());
        }
    }
    if critical.is_empty() && warnings.is_empty() {
        println!("      └── {}", "No critical issues detected".green());
    }
}

// ── Saved configs listing ────────────────────────────────────────────────────

pub fn print_saved_configs() {
    use crate::config::{config_file_path, load_saved_configs};

    let store = load_saved_configs();

    if store.configs.is_empty() {
        println!("\n{}", "No saved configurations found.".yellow().bold());
        println!();
        println!(
            "  Run {} to discover and save configs from an APK.",
            "flintbase scan <package> --save".bold()
        );
        println!(
            "  Configs are stored in: {}",
            config_file_path().display().to_string().dimmed()
        );
        return;
    }

    println!("\n{}", "Saved Configurations".bright_cyan().bold());
    println!(
        "{}\n",
        format!("({})", config_file_path().display()).dimmed()
    );

    for (key, cfg) in &store.configs {
        let bar_len = 58usize.saturating_sub(key.len());
        println!("╭─ --config {} {}", key.bold(), "".repeat(bar_len));
        println!("{}: {}", "Name".dimmed(), cfg.name);
        if let Some(ref pkg) = cfg.package {
            println!("{}:  {}", "Pkg".dimmed(), pkg);
        }
        println!(
            "{}:  {}...{}",
            "Key".dimmed(),
            &cfg.api_key[..12.min(cfg.api_key.len())],
            &cfg.api_key[cfg.api_key.len().saturating_sub(4)..]
        );
        if let Some(ref pid) = cfg.project_id {
            println!("{}: {}", "Project".dimmed(), pid.green());
        }
        if let Some(ref aid) = cfg.app_id {
            println!("{}: {}", "App ID".dimmed(), aid);
        }
        if let Some(ref sid) = cfg.gcm_sender_id {
            println!("{}: {}", "Sender".dimmed(), sid);
        }
        if let Some(ref db) = cfg.database_url {
            println!("{}:  {}", "DB".dimmed(), db);
        }
        if let Some(ref sb) = cfg.storage_bucket {
            println!("{}: {}", "Bucket".dimmed(), sb);
        }
        if let Some(ref ts) = cfg.saved_at {
            println!("{}: {}", "Saved".dimmed(), ts.dimmed());
        }
        println!("{}", "".repeat(60));
        println!();
    }

    println!(
        "{} {} to test a saved config.",
        "Tip:".bold(),
        "flintbase key --config <name>".bold()
    );
    println!(
        "     Edit {} to tweak values.",
        config_file_path().display().to_string().dimmed()
    );
}