use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing::info;
use uuid::Uuid;
mod config;
mod db;
mod distributed;
mod logger;
mod resolver;
mod rules;
mod scanner;
mod utils;
#[derive(Parser)]
#[command(
name = "fatt",
author = "FATT Development Team",
version,
about = "Find All The Things - A high-performance, distributed security scanning tool",
long_about = "FATT (Find All The Things) is a high-performance, modular, asynchronous, and distributed security scanning CLI tool designed to rapidly identify sensitive or exposed files and directories across millions of domains."
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Scan {
#[arg(short, long, value_name = "FILE")]
input: String,
#[arg(short, long, value_name = "FILE", default_value = "rules.yaml")]
rules: String,
#[arg(short, long, value_name = "FILE", default_value = "results.sqlite")]
database: String,
#[arg(short, long, default_value = "100")]
concurrency: usize,
#[arg(short, long, default_value = "1000")]
batch_size: usize,
#[arg(long, default_value = "10")]
timeout: u64,
#[arg(short, long, default_value = "0")]
threads: usize,
#[arg(short, long)]
verbose: bool,
},
Rules {
#[command(subcommand)]
action: RulesCommands,
},
Results {
#[command(subcommand)]
action: ResultsCommands,
},
Dns {
#[command(subcommand)]
action: DnsCommands,
},
Worker {
#[command(subcommand)]
action: WorkerCommands,
},
}
#[derive(Subcommand)]
enum RulesCommands {
Add {
#[arg(short, long, value_name = "FILE")]
file: String,
},
Remove {
#[arg(short, long)]
name: String,
},
List {
#[arg(short, long, value_name = "FILE", default_value = "rules.yaml")]
file: String,
},
}
#[derive(Subcommand)]
enum ResultsCommands {
Export {
#[arg(short, long, value_name = "FILE")]
output: String,
#[arg(short, long, value_name = "FILE", default_value = "results.sqlite")]
database: String,
#[arg(short, long, default_value = "csv")]
format: String,
},
List {
#[arg(short, long, value_name = "FILE", default_value = "results.sqlite")]
database: String,
#[arg(short, long)]
domain: Option<String>,
#[arg(short, long)]
rule: Option<String>,
#[arg(short, long, default_value = "100")]
limit: usize,
},
}
#[derive(Subcommand)]
enum DnsCommands {
Flush,
Status,
}
#[derive(Subcommand)]
enum WorkerCommands {
Start {
#[arg(short, long, value_name = "HOST:PORT")]
master: String,
#[arg(short, long)]
id: Option<String>,
#[arg(short, long, default_value = "8080")]
port: u16,
},
Stop {
#[arg(short, long, default_value = "all")]
id: String,
},
Status,
}
fn main() -> Result<()> {
let args = Cli::parse();
logger::init_logger(false, None)?;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
match args.command {
Commands::Scan {
input,
rules,
database,
concurrency,
batch_size: _,
timeout,
threads: _,
verbose,
} => {
logger::set_verbosity(verbose);
let scan_config = config::ScanConfig {
input_file: input,
rules_file: rules,
concurrency,
verbosity: if verbose { 3 } else { 2 }, verbose,
distributed: false,
output_file: None,
db_path: database,
dns_timeout: 5, http_timeout: timeout,
connect_timeout: timeout,
dns_cache_size: 10000, quiet: false,
dns_only: false,
};
scanner::run_scan(scan_config).await
}
Commands::Rules { action } => match action {
RulesCommands::Add { file } => rules::add_rule(&file),
RulesCommands::Remove { name } => rules::remove_rule(&name),
RulesCommands::List { file } => rules::list_rules(&file),
},
Commands::Results { action } => match action {
ResultsCommands::Export {
output,
database,
format,
} => db::export_results(&database, &output, &format),
ResultsCommands::List {
database,
domain,
rule,
limit,
} => db::list_results(&database, domain.as_deref(), rule.as_deref(), limit),
},
Commands::Dns { action } => match action {
DnsCommands::Flush => resolver::flush_cache()
.await
.context("Failed to flush DNS cache"),
DnsCommands::Status => resolver::show_cache_status()
.await
.context("Failed to show DNS cache status"),
},
Commands::Worker { action } => match action {
WorkerCommands::Start { master, id, port } => {
let worker_id = id.unwrap_or_else(|| Uuid::new_v4().to_string());
info!("Starting worker with ID: {}", worker_id);
let worker_config = distributed::WorkerConfig {
worker_id,
master: format!("{}:{}", master, port),
concurrency: 10, };
distributed::start_worker(&worker_config)
.await
.context("Failed to start worker")
}
WorkerCommands::Stop { id } => distributed::stop_worker(&id)
.await
.context("Failed to stop worker"),
WorkerCommands::Status => distributed::worker_status()
.await
.context("Failed to get worker status"),
},
}
})?;
Ok(())
}