use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
use orchestrator_core::{
load_credentials_file, parse_scanner_list, Orchestrator, ScanMode, ScanRequest,
};
use tracing::info;
use url::Url;
#[derive(Parser)]
#[command(
name = "santhorchestrator",
version,
about = "Santh discovery orchestrator (loginflow → appmap → scanners)"
)]
struct Cli {
#[command(subcommand)]
command: Command,
#[arg(long, default_value = "info", global = true)]
log_level: String,
}
#[derive(Subcommand)]
enum Command {
Scan {
url: String,
#[arg(long)]
creds: Option<PathBuf>,
#[arg(long, default_value = "scald,bolascan")]
scanners: String,
#[arg(long)]
dry_run: bool,
#[arg(long)]
captchaforge: bool,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::new(&cli.log_level))
.init();
match cli.command {
Command::Scan {
url,
creds,
scanners,
dry_run,
captchaforge,
} => run_scan(url, creds, scanners, dry_run, captchaforge).await,
}
}
async fn run_scan(
url: String,
creds_path: Option<PathBuf>,
scanners_raw: String,
dry_run: bool,
use_captchaforge: bool,
) -> anyhow::Result<()> {
let target = Url::parse(&url).context("invalid target URL")?;
let scanner_ids = parse_scanner_list(&scanners_raw).map_err(anyhow::Error::msg)?;
let credentials = creds_path
.as_ref()
.map(|p| load_credentials_file(p))
.transpose()
.map_err(|e| anyhow::anyhow!("{e}"))?;
let orchestrator = Orchestrator::builder()
.build()
.map_err(|e| anyhow::anyhow!("{e}"))?;
let mode = if dry_run {
ScanMode::DryRun
} else {
ScanMode::RealTarget
};
let req = ScanRequest {
target,
credentials,
scanners: scanner_ids,
mode,
use_captchaforge,
};
info!("starting orchestrated scan");
let result = orchestrator.scan(req).await?;
info!(
phase = ?result.pipeline.phase,
endpoints = result.appmap.endpoints().len(),
findings = result.findings.len(),
"scan complete"
);
for (name, report) in &result.scanner_reports {
println!("scanner {name}: {} findings ({})", report.findings_count, report.detail);
}
if !result.findings.is_empty() {
let json = serde_json::to_string_pretty(&result.findings)?;
println!("{json}");
}
Ok(())
}