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 std::path::Path;
use std::process::Command;

use colored::Colorize;

/// Run NoseyParker to scan a directory for secrets.
///
/// Creates a datastore at `datastore_path` and scans `target_dir`.
pub fn scan_directory(target_dir: &Path, datastore_path: &Path) -> anyhow::Result<()> {
    println!(
        "  {} Scanning for secrets with noseyparker...",
        "".cyan(),
    );

    // If the datastore already exists, remove it for a fresh scan
    if datastore_path.exists() {
        std::fs::remove_dir_all(datastore_path)?;
    }

    let output = Command::new("noseyparker")
        .arg("scan")
        .arg("-d")
        .arg(datastore_path)
        .arg(target_dir)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        // noseyparker returns non-zero if it finds secrets (in some versions)
        // Only fail if stderr indicates a real error
        if stderr.contains("Error") || stderr.contains("error:") {
            anyhow::bail!("noseyparker scan failed: {}", stderr.trim());
        }
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    // Print scan summary from stderr (noseyparker writes progress there)
    for line in stderr.lines() {
        if line.contains("findings") || line.contains("matches") || line.contains("Found") {
            println!("  {} {}", "".cyan(), line.trim());
        }
    }
    if !stdout.trim().is_empty() {
        for line in stdout.lines().take(5) {
            println!("    {}", line.dimmed());
        }
    }

    println!(
        "  {} Scan complete. Datastore: {}",
        "".green(),
        datastore_path.display()
    );

    Ok(())
}

/// Generate a NoseyParker report from an existing datastore.
///
/// `format` should be one of: "human", "json", "jsonl", "sarif"
pub fn generate_report(
    datastore_path: &Path,
    format: &str,
    output_file: Option<&Path>,
) -> anyhow::Result<Option<String>> {
    println!(
        "  {} Generating {} report...",
        "".cyan(),
        format.bold()
    );

    let mut cmd = Command::new("noseyparker");
    cmd.arg("report")
        .arg("-d")
        .arg(datastore_path)
        .arg("-f")
        .arg(format);

    if let Some(out_path) = output_file {
        cmd.arg("-o").arg(out_path);
    }

    let output = cmd.output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("noseyparker report failed: {}", stderr.trim());
    }

    if output_file.is_some() {
        println!(
            "  {} Report written to {}",
            "".green(),
            output_file.unwrap().display()
        );
        Ok(None)
    } else {
        let report = String::from_utf8_lossy(&output.stdout).to_string();
        Ok(Some(report))
    }
}

/// Generate a JSON report and return the raw JSON string (for parsing).
pub fn generate_json_report(datastore_path: &Path) -> anyhow::Result<String> {
    let mut cmd = Command::new("noseyparker");
    cmd.arg("report")
        .arg("-d")
        .arg(datastore_path)
        .arg("-f")
        .arg("json")
        .arg("--max-matches=0");  // no limit — get all matches

    let output = cmd.output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("noseyparker report (json) failed: {}", stderr.trim());
    }

    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}