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
pub mod decompile;
pub mod download;

use std::path::{Path, PathBuf};

use colored::Colorize;

use crate::scanner;

/// Workspace layout for a single APK analysis.
pub struct ApkWorkspace {
    pub base_dir: PathBuf,
    pub apk_dir: PathBuf,
    pub decompiled_dir: PathBuf,
    pub datastore_dir: PathBuf,
    pub report_dir: PathBuf,
}

impl ApkWorkspace {
    pub fn new(base_dir: &Path, package: &str) -> Self {
        let pkg_dir = base_dir.join(package);
        Self {
            base_dir: pkg_dir.clone(),
            apk_dir: pkg_dir.join("apk"),
            decompiled_dir: pkg_dir.join("decompiled"),
            datastore_dir: pkg_dir.join("reports").join("datastore.np"),
            report_dir: pkg_dir.join("reports"),
        }
    }

    pub fn create_dirs(&self) -> anyhow::Result<()> {
        std::fs::create_dir_all(&self.apk_dir)?;
        std::fs::create_dir_all(&self.decompiled_dir)?;
        std::fs::create_dir_all(&self.report_dir)?;
        Ok(())
    }
}

/// Run the full APK analysis pipeline for a single package.
///
/// 1. Download APK via apkeep (with fallback sources)
/// 2. Decompile with jadx (tolerant of partial errors)
/// 3. Scan with noseyparker
/// 4. Generate report
pub fn run_apk_pipeline(
    package: &str,
    output_base: &Path,
    report_format: &str,
    verbose: bool,
) -> anyhow::Result<()> {
    let ws = ApkWorkspace::new(output_base, package);
    ws.create_dirs()?;

    println!(
        "\n{} {}",
        "Processing:".bright_magenta().bold(),
        package.bold()
    );

    // Step 1: Download APK
    println!("\n  {}", "[1/3] Downloading APK".bold());
    let apk_path = download::download_apk(package, &ws.apk_dir, verbose)?;
    println!(
        "  {} APK saved: {}",
        "".green(),
        apk_path.display()
    );

    // Step 2: Decompile
    println!("\n  {}", "[2/3] Decompiling APK".bold());
    let decompiled_dir = decompile::decompile_apk(&apk_path, &ws.decompiled_dir, verbose)?;

    // Step 3: Scan for secrets
    println!("\n  {}", "[3/3] Scanning for secrets".bold());
    scanner::scan_directory(&decompiled_dir, &ws.datastore_dir)?;

    // Generate report
    let report_file = ws.report_dir.join(format!("secrets.{}", match report_format {
        "json" => "json",
        "jsonl" => "jsonl",
        "sarif" => "sarif",
        _ => "txt",
    }));

    let report = scanner::generate_report(&ws.datastore_dir, report_format, Some(&report_file))?;

    // If format is human and we got stdout output, print it
    if report_format == "human" {
        if let Some(text) = report {
            println!("\n{}", "Secret Scan Results".bright_magenta().bold());
            println!("{}", "".repeat(60));
            println!("{}", text);
        } else {
            // Read and display the report file
            if let Ok(content) = std::fs::read_to_string(&report_file) {
                if content.trim().is_empty() {
                    println!(
                        "\n  {} {}",
                        "".green(),
                        "No secrets detected.".green().bold()
                    );
                } else {
                    println!("\n{}", "Secret Scan Results".bright_magenta().bold());
                    println!("{}", "".repeat(60));
                    println!("{}", content);
                }
            }
        }
    }

    // Summary
    println!("\n{}", "".repeat(60));
    println!("{} {}", "Workspace:".bold(), ws.base_dir.display());
    println!("  APK:        {}", ws.apk_dir.display());
    println!("  Decompiled: {}", ws.decompiled_dir.display());
    println!("  Reports:    {}", ws.report_dir.display());

    Ok(())
}

/// Run the APK pipeline for all packages parsed from input.
pub fn run_apk_command(
    input: &str,
    output_dir: &Path,
    report_format: &str,
    verbose: bool,
) -> anyhow::Result<()> {
    // Pre-flight check
    crate::preflight::ensure_apk_tools_available().map_err(|e| anyhow::anyhow!(e))?;

    println!(
        "\n{}",
        "flintBase APK Analysis Pipeline".bright_cyan().bold()
    );
    println!("{}", "".repeat(60));

    // Parse input to package names
    println!("\n{}", "Resolving packages...".bold());
    let packages = download::parse_store_input(input)?;

    if packages.is_empty() {
        anyhow::bail!("No packages found to process.");
    }

    println!(
        "\n  {} package(s) to analyze: {}",
        packages.len(),
        packages.join(", ").bold()
    );

    let mut successes = 0;
    let mut failures = Vec::new();

    for package in &packages {
        match run_apk_pipeline(package, output_dir, report_format, verbose) {
            Ok(()) => successes += 1,
            Err(e) => {
                eprintln!(
                    "\n  {} Failed to process {}: {}",
                    "".red(),
                    package,
                    e
                );
                failures.push((package.clone(), e.to_string()));
            }
        }
    }

    // Final summary
    println!("\n{}", "".repeat(60));
    println!("{}", "Pipeline Summary".bright_cyan().bold());
    println!(
        "  Succeeded: {}",
        format!("{}/{}", successes, packages.len()).green()
    );
    if !failures.is_empty() {
        println!(
            "  Failed:    {}",
            format!("{}/{}", failures.len(), packages.len()).red()
        );
        for (pkg, err) in &failures {
            let first_line = err.lines().next().unwrap_or(err);
            println!("{}{}", pkg, first_line.dimmed());
        }
    }

    Ok(())
}