parlov 0.8.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
//! Finding-construction support for `scan_runner`.
//!
//! Owns `WireSurfaces`, `FindingOpts`, the `ProbeContext`/`ExchangeContext`
//! builders, and the central `make_finding` assembly function.

use bytes::Bytes;
use http::{HeaderMap, StatusCode};
use parlov_core::{OracleResult, ProbeDefinition};
use parlov_elicit::{ChainProvenance, StrategyMetadata};
use parlov_output::wire::{filter_security_headers, truncate_body_sample};
use parlov_output::{
    build_curl, BodySamplesBundle, ExchangeContext, HeadersBundle, ProbeContext, ReproInfo,
    ScanFinding,
};

/// Per-side request and response surfaces, threaded through `make_finding`.
pub(crate) struct WireSurfaces<'a> {
    pub(crate) baseline_def: &'a ProbeDefinition,
    pub(crate) probe_def: &'a ProbeDefinition,
    pub(crate) baseline_status: StatusCode,
    pub(crate) probe_status: StatusCode,
    pub(crate) baseline_resp_headers: &'a HeaderMap,
    pub(crate) probe_resp_headers: &'a HeaderMap,
    pub(crate) baseline_body: &'a Bytes,
    pub(crate) probe_body: &'a Bytes,
}

/// Knobs threaded into every `make_finding` call. `repro` controls curl
/// emission; `verbose` controls request/response header and body capture.
#[derive(Clone, Copy)]
pub(crate) struct FindingOpts {
    pub(crate) repro: bool,
    pub(crate) verbose: bool,
}

/// Builds a `ReproInfo` from a baseline/probe pair when `enabled`; otherwise `None`.
pub(crate) fn make_repro(
    enabled: bool,
    baseline: &ProbeDefinition,
    probe: &ProbeDefinition,
) -> Option<ReproInfo> {
    if !enabled {
        return None;
    }
    Some(ReproInfo {
        baseline_curl: build_curl(baseline),
        probe_curl: build_curl(probe),
    })
}

fn build_probe_context(surfaces: &WireSurfaces<'_>, method: &str, verbose: bool) -> ProbeContext {
    let headers = if verbose {
        Some(HeadersBundle {
            baseline: filter_security_headers(&surfaces.baseline_def.headers),
            probe: filter_security_headers(&surfaces.probe_def.headers),
        })
    } else {
        None
    };
    ProbeContext {
        baseline_url: surfaces.baseline_def.url.clone(),
        probe_url: surfaces.probe_def.url.clone(),
        method: method.to_owned(),
        headers,
    }
}

fn build_exchange_context(surfaces: &WireSurfaces<'_>, verbose: bool) -> ExchangeContext {
    let headers = if verbose {
        Some(HeadersBundle {
            baseline: filter_security_headers(surfaces.baseline_resp_headers),
            probe: filter_security_headers(surfaces.probe_resp_headers),
        })
    } else {
        None
    };
    let body_samples = if verbose {
        Some(BodySamplesBundle {
            baseline: truncate_body_sample(surfaces.baseline_body),
            probe: truncate_body_sample(surfaces.probe_body),
        })
    } else {
        None
    };
    ExchangeContext {
        baseline_status: surfaces.baseline_status.as_u16(),
        probe_status: surfaces.probe_status.as_u16(),
        headers,
        body_samples,
    }
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn make_finding(
    target: &str,
    meta: &StrategyMetadata,
    method: &str,
    result: &OracleResult,
    repro: Option<ReproInfo>,
    surfaces: &WireSurfaces<'_>,
    chain_provenance: Option<ChainProvenance>,
    opts: FindingOpts,
) -> ScanFinding {
    ScanFinding {
        target_url: target.to_owned(),
        strategy_id: meta.strategy_id.to_owned(),
        strategy_name: meta.strategy_name.to_owned(),
        method: method.to_owned(),
        result: result.clone(),
        repro,
        probe: build_probe_context(surfaces, method, opts.verbose),
        exchange: build_exchange_context(surfaces, opts.verbose),
        chain_provenance,
    }
}