use parlov_analysis::StopRule;
use parlov_core::{EndpointVerdict, Error};
use parlov_elicit::{default_chain_registry, generate_dag_chained_plan};
use parlov_output::{
render_endpoint_verdict_json, render_endpoint_verdict_sarif, render_endpoint_verdict_table,
ScanFinding,
};
use parlov_probe::http::HttpProbe;
use crate::cli::{OutputFormat, ScanArgs};
use crate::pipeline_state::ScanPipelineState;
use crate::scan_context::build_scan_context;
use crate::scan_exec::{rate_limit_gate, run_plan_specs, RunOpts};
use crate::strategy_filter::apply_strategy_filters;
use crate::vector_filter::{apply_vector_filters, parse_vector_flag, VectorFilter};
use crate::verdict_builder::build_endpoint_verdict;
pub(crate) const CONFIRM_LOG_ODDS_THRESHOLD: f64 = 1.386_294_361_119_890_6_f64;
pub async fn run(args: ScanArgs, format: OutputFormat) -> Result<(), Error> {
let vector_filters = parse_vector_flags(&args.vectors)?;
validate_vector_risk_exclusivity(&args, &vector_filters)?;
validate_strategy_exclusivity(&args, &vector_filters)?;
let ctx = build_scan_context(&args, &vector_filters)?;
let plan = build_filtered_plan(&ctx, &vector_filters, &args.strategies);
let total_specs = plan.len();
let mut state = ScanPipelineState::new(total_specs);
let stop_rule = StopRule::new();
let probe = HttpProbe::new();
tracing::debug!(strategies_total = state.strategies_total, "starting scan");
let opts = RunOpts {
exhaustive: args.exhaustive,
confirm_threshold: CONFIRM_LOG_ODDS_THRESHOLD,
repro: args.repro,
verbose: args.verbose,
};
let exchanges = run_plan_specs(&plan, &args.target, &mut state, &stop_rule, &probe, opts).await;
let phase2_allowed =
(state.stop_decision.is_none() || args.exhaustive) && !rate_limit_gate(&exchanges);
if phase2_allowed {
let registry = default_chain_registry();
let chained = generate_dag_chained_plan(&ctx, &exchanges, ®istry);
state.strategies_total += chained.len();
run_plan_specs(&chained, &args.target, &mut state, &stop_rule, &probe, opts).await;
}
let verdict = build_endpoint_verdict(&state);
let findings: Vec<ScanFinding> = state.findings_only().cloned().collect();
let output = format_scan_results(format, &args.target, &verdict, &findings)?;
println!("{output}");
Ok(())
}
fn format_scan_results(
format: OutputFormat,
target: &str,
verdict: &EndpointVerdict,
findings: &[ScanFinding],
) -> Result<String, Error> {
match format {
OutputFormat::Table => Ok(render_endpoint_verdict_table(verdict, findings)),
OutputFormat::Json => {
render_endpoint_verdict_json(target, verdict, findings).map_err(Error::Serialization)
}
OutputFormat::Sarif => {
render_endpoint_verdict_sarif(target, verdict, findings).map_err(Error::Serialization)
}
}
}
fn parse_vector_flags(raw: &[String]) -> Result<Vec<VectorFilter>, Error> {
raw.iter().map(|s| parse_vector_flag(s)).collect()
}
fn validate_vector_risk_exclusivity(
args: &ScanArgs,
filters: &[VectorFilter],
) -> Result<(), Error> {
if !filters.is_empty() && args.risk != "safe" {
return Err(Error::Cli(
"--vector and --risk are mutually exclusive".to_owned(),
));
}
Ok(())
}
fn validate_strategy_exclusivity(args: &ScanArgs, filters: &[VectorFilter]) -> Result<(), Error> {
if args.strategies.is_empty() {
return Ok(());
}
if args.risk != "safe" {
return Err(Error::Cli(
"--strategy and --risk are mutually exclusive".to_owned(),
));
}
if !filters.is_empty() {
return Err(Error::Cli(
"--strategy and --vector are mutually exclusive".to_owned(),
));
}
Ok(())
}
fn build_filtered_plan(
ctx: &parlov_elicit::ScanContext,
filters: &[VectorFilter],
strategy_ids: &[String],
) -> Vec<parlov_elicit::ProbeSpec> {
let plan = parlov_elicit::generate_plan(ctx);
if !strategy_ids.is_empty() {
return apply_strategy_filters(plan, strategy_ids);
}
if filters.is_empty() {
plan
} else {
apply_vector_filters(plan, filters)
}
}
#[cfg(test)]
#[path = "scan_tests/mod.rs"]
mod tests;