parlov 0.8.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
//! Mutable scan-pipeline state threaded through `run_plan_specs` phases.
//!
//! Bundles findings, the Bayesian evidence accumulator, and the early-stop decision
//! into a single value so `run` and `run_plan_specs` share a coherent view of
//! pipeline progress without passing half a dozen separate `&mut` arguments.

use parlov_analysis::{EvidenceAccumulator, StopDecision};
use parlov_core::StrategyOutcome;
use parlov_output::ScanFinding;

/// Accumulated state for a single scan run.
pub(crate) struct ScanPipelineState {
    pub findings: Vec<(ScanFinding, StrategyOutcome)>,
    pub accumulator: EvidenceAccumulator,
    pub stop_decision: Option<StopDecision>,
    pub strategies_run: usize,
    pub strategies_total: usize,
    /// First strategy to cross the confirm threshold; exhaustive mode only.
    pub first_threshold_crossed_by: Option<String>,
}

impl ScanPipelineState {
    /// Creates a new state with a neutral prior and the given strategy budget.
    pub fn new(total: usize) -> Self {
        Self {
            findings: Vec::new(),
            accumulator: EvidenceAccumulator::new(),
            stop_decision: None,
            strategies_run: 0,
            strategies_total: total,
            first_threshold_crossed_by: None,
        }
    }

    /// Returns an iterator over only the `ScanFinding` values, discarding companions.
    pub fn findings_only(&self) -> impl Iterator<Item = &ScanFinding> {
        self.findings.iter().map(|(f, _)| f)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use parlov_core::{NormativeStrength, OracleClass, OracleResult, OracleVerdict, Vector};
    use parlov_output::ScanFinding;

    fn make_oracle_result() -> OracleResult {
        OracleResult {
            class: OracleClass::Existence,
            verdict: OracleVerdict::NotPresent,
            severity: None,
            confidence: 0,
            impact_class: None,
            reasons: vec![],
            signals: vec![],
            technique_id: None,
            vector: Some(Vector::StatusCodeDiff),
            normative_strength: Some(NormativeStrength::Must),
            label: None,
            leaks: None,
            rfc_basis: None,
        }
    }

    fn make_finding(strategy_id: &str) -> ScanFinding {
        use parlov_output::{ExchangeContext, ProbeContext};
        ScanFinding {
            target_url: "https://example.com/{id}".to_owned(),
            strategy_id: strategy_id.to_owned(),
            strategy_name: "Test Strategy".to_owned(),
            method: "GET".to_owned(),
            result: make_oracle_result(),
            repro: None,
            probe: ProbeContext {
                baseline_url: "https://example.com/1".to_owned(),
                probe_url: "https://example.com/9999".to_owned(),
                method: "GET".to_owned(),
                headers: None,
            },
            exchange: ExchangeContext {
                baseline_status: 200,
                probe_status: 404,
                headers: None,
                body_samples: None,
            },
            chain_provenance: None,
        }
    }

    #[test]
    fn pipeline_state_new_initialises_correctly() {
        let state = ScanPipelineState::new(10);
        assert!(state.findings.is_empty());
        assert!(state.stop_decision.is_none());
        assert_eq!(state.strategies_total, 10);
        assert_eq!(state.strategies_run, 0);
    }

    #[test]
    fn findings_only_discards_outcomes() {
        let mut state = ScanPipelineState::new(2);
        let outcome = StrategyOutcome::NoSignal(make_oracle_result());
        state
            .findings
            .push((make_finding("strategy-a"), outcome.clone()));
        state.findings.push((make_finding("strategy-b"), outcome));
        let only: Vec<&ScanFinding> = state.findings_only().collect();
        assert_eq!(only.len(), 2);
        assert_eq!(only[0].strategy_id, "strategy-a");
        assert_eq!(only[1].strategy_id, "strategy-b");
    }

    #[test]
    fn findings_only_returns_references_not_clones() {
        let mut state = ScanPipelineState::new(1);
        let outcome = StrategyOutcome::NoSignal(make_oracle_result());
        state.findings.push((make_finding("strategy-x"), outcome));
        // iterator yields references into state.findings — collect as slice-compatible Vec
        let collected: Vec<&ScanFinding> = state.findings_only().collect();
        assert!(std::ptr::eq(collected[0], &raw const state.findings[0].0));
    }
}