pub mod decompile;
pub mod download;
use std::path::{Path, PathBuf};
use colored::Colorize;
use crate::scanner;
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(())
}
}
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()
);
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()
);
println!("\n {}", "[2/3] Decompiling APK".bold());
let decompiled_dir = decompile::decompile_apk(&apk_path, &ws.decompiled_dir, verbose)?;
println!("\n {}", "[3/3] Scanning for secrets".bold());
scanner::scan_directory(&decompiled_dir, &ws.datastore_dir)?;
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 report_format == "human" {
if let Some(text) = report {
println!("\n{}", "Secret Scan Results".bright_magenta().bold());
println!("{}", "─".repeat(60));
println!("{}", text);
} else {
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);
}
}
}
}
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(())
}
pub fn run_apk_command(
input: &str,
output_dir: &Path,
report_format: &str,
verbose: bool,
) -> anyhow::Result<()> {
crate::preflight::ensure_apk_tools_available().map_err(|e| anyhow::anyhow!(e))?;
println!(
"\n{}",
"flintBase APK Analysis Pipeline".bright_cyan().bold()
);
println!("{}", "═".repeat(60));
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()));
}
}
}
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(())
}