parlov 0.3.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
//! Existence oracle pipeline: URL substitution, adaptive probe collection, analysis, output.
//!
//! Called from `main` when the `existence` subcommand is dispatched. Receives parsed
//! `ExistenceArgs`, drives an adaptive sampling loop collecting baseline/probe pairs until
//! the analyzer reaches a verdict, then prints table output to stdout.

use bytes::Bytes;
use http::{HeaderMap, Method};
use parlov_analysis::existence::ExistenceAnalyzer;
use parlov_analysis::{Analyzer, SampleDecision};
use parlov_core::{Error, OracleResult, ProbeDefinition, ProbeSet};
use parlov_output::render_table;
use parlov_probe::http::HttpProbe;
use parlov_probe::Probe;
use uuid::Uuid;

use crate::cli::ExistenceArgs;
use crate::util::parse_headers;

/// Runs the existence oracle pipeline for the given CLI arguments.
///
/// Drives an adaptive sampling loop: collects one baseline/probe pair per iteration,
/// calls `evaluate` after each, and stops when the analyzer returns `Complete`.
/// Same-status pairs short-circuit after 1 sample; differentials collect up to 3.
pub async fn run(args: ExistenceArgs) -> Result<(), Error> {
    let probe_id = args
        .probe_id
        .unwrap_or_else(|| Uuid::new_v4().to_string());

    let method = parse_method(&args.method)?;
    let headers = parse_headers(&args.headers)?;
    let body_template = args.body.as_deref();

    let baseline_def =
        build_probe_def(&args.target, &args.baseline_id, &method, &headers, body_template);
    let probe_def = build_probe_def(&args.target, &probe_id, &method, &headers, body_template);

    let result = collect_until_verdict(&baseline_def, &probe_def).await?;
    println!("{}", render_table(&result));
    Ok(())
}

/// Builds a `ProbeDefinition` by substituting `{id}` in the target URL and optional body.
fn build_probe_def(
    target: &str,
    id: &str,
    method: &Method,
    headers: &HeaderMap,
    body_template: Option<&str>,
) -> ProbeDefinition {
    ProbeDefinition {
        url: target.replace("{id}", id),
        method: method.clone(),
        headers: headers.clone(),
        body: substitute_body(body_template, id),
    }
}

/// Adaptive sampling loop: collects pairs until the analyzer reaches a verdict.
///
/// Each iteration executes one baseline + one probe request, appends both surfaces
/// to the growing `ProbeSet`, and asks the analyzer to evaluate. Returns the final
/// `OracleResult` once `evaluate` returns `Complete`.
pub(crate) async fn collect_until_verdict(
    baseline_def: &ProbeDefinition,
    probe_def: &ProbeDefinition,
) -> Result<OracleResult, Error> {
    let client = HttpProbe::new();
    let analyzer = ExistenceAnalyzer;
    let mut probe_set = ProbeSet {
        baseline: Vec::new(),
        probe: Vec::new(),
    };

    loop {
        let (b_surface, p_surface) = tokio::try_join!(
            client.execute(baseline_def),
            client.execute(probe_def),
        )?;

        probe_set.baseline.push(b_surface);
        probe_set.probe.push(p_surface);

        if let SampleDecision::Complete(result) = analyzer.evaluate(&probe_set) {
            return Ok(result);
        }
    }
}

fn parse_method(method_str: &str) -> Result<Method, Error> {
    method_str
        .parse::<Method>()
        .map_err(|e| Error::Http(format!("invalid HTTP method '{method_str}': {e}")))
}

/// Substitutes `{id}` in a body template, returning `None` when no template is provided.
fn substitute_body(template: Option<&str>, id: &str) -> Option<Bytes> {
    template.map(|t| Bytes::from(t.replace("{id}", id)))
}