parlov 0.5.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
//! Integration tests for cache-probing elicitation strategies.
//!
//! Area N: cache-probing strategies against the GET server's `/cp/` routes.
//! Each test spawns the server, generates a plan, finds the specific strategy
//! spec, executes it through the analysis pipeline, and asserts the verdict.

#![deny(clippy::all)]

use http::{HeaderMap, Method};
use parlov_analysis::existence::ExistenceAnalyzer;
use parlov_analysis::{Analyzer, SampleDecision};
use parlov_core::{DifferentialSet, OracleResult, OracleVerdict, Vector};
use parlov_elicit::{ProbePair, ProbeSpec, RiskLevel, ScanContext, generate_plan};
use parlov_probe::http::HttpProbe;
use parlov_probe::Probe;

mod fixtures {
    pub mod get_server;
}

// --- helpers ---

/// Adaptive existence-oracle loop for a `ProbePair` spec.
async fn collect_pair(pair: &ProbePair) -> OracleResult {
    let client = HttpProbe::new();
    let analyzer = ExistenceAnalyzer;
    let mut diff_set = DifferentialSet {
        baseline: Vec::new(),
        probe: Vec::new(),
        technique: pair.technique.clone(),
    };
    loop {
        let b = client.execute(&pair.baseline).await.expect("baseline failed");
        let p = client.execute(&pair.probe).await.expect("probe failed");
        diff_set.baseline.push(b);
        diff_set.probe.push(p);
        if let SampleDecision::Complete(result) = analyzer.evaluate(&diff_set) {
            return *result;
        }
    }
}

/// Build a `ScanContext` targeting a specific route on the given base URL.
fn make_ctx(base_url: &str, route: &str) -> ScanContext {
    ScanContext {
        target: format!("{base_url}{route}/{{id}}"),
        baseline_id: "42".to_owned(),
        probe_id: "9999".to_owned(),
        headers: HeaderMap::new(),
        max_risk: RiskLevel::Safe,
        known_duplicate: None,
        state_field: None,
        alt_credential: None,
        body_template: None,
    }
}

/// Find the first `Pair` spec matching strategy ID and method.
fn find_pair(plan: &[ProbeSpec], id: &str, method: &Method) -> Option<ProbePair> {
    plan.iter().find_map(|spec| {
        if let ProbeSpec::Pair(p) = spec {
            if p.metadata.strategy_id == id && p.baseline.method == *method {
                return Some(p.clone());
            }
        }
        None
    })
}

// --- plan generation tests ---

#[test]
fn safe_plan_includes_all_8_cache_probing_strategies() {
    let ctx = make_ctx("http://localhost:0", "/cp/conditional");
    let plan = generate_plan(&ctx);
    let cp_ids: Vec<&str> = plan
        .iter()
        .filter(|s| s.technique().vector == Vector::CacheProbing)
        .map(|s| s.technique().id)
        .collect();

    for expected in &[
        "cp-if-none-match",
        "cp-if-modified-since",
        "cp-if-match",
        "cp-if-unmodified-since",
        "cp-range",
        "cp-range-unsatisfiable",
        "cp-if-range",
        "cp-accept",
    ] {
        assert!(
            cp_ids.contains(expected),
            "cache-probing strategy '{expected}' missing from plan; got: {cp_ids:?}"
        );
    }
}

// --- /cp/conditional/{id} — conditional header strategies ---

#[tokio::test]
async fn cp_if_none_match_vs_conditional_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/conditional");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-if-none-match", &Method::GET)
        .expect("cp-if-none-match GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-if-none-match on /cp/conditional should be Confirmed; evidence: {:?}",
        result.signals
    );
}

#[tokio::test]
async fn cp_if_modified_since_vs_conditional_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/conditional");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-if-modified-since", &Method::GET)
        .expect("cp-if-modified-since GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-if-modified-since on /cp/conditional should be Confirmed; evidence: {:?}",
        result.signals
    );
}

#[tokio::test]
async fn cp_if_match_vs_conditional_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/conditional");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-if-match", &Method::GET)
        .expect("cp-if-match GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-if-match on /cp/conditional should be Confirmed; evidence: {:?}",
        result.signals
    );
}

#[tokio::test]
async fn cp_if_unmodified_since_vs_conditional_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/conditional");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-if-unmodified-since", &Method::GET)
        .expect("cp-if-unmodified-since GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-if-unmodified-since on /cp/conditional should be Confirmed; evidence: {:?}",
        result.signals
    );
}

// --- /cp/range/{id} — range strategies ---

#[tokio::test]
async fn cp_range_satisfiable_vs_range_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/range");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-range", &Method::GET)
        .expect("cp-range GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-range on /cp/range should be Confirmed; evidence: {:?}",
        result.signals
    );
}

#[tokio::test]
async fn cp_range_unsatisfiable_vs_range_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/range");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-range-unsatisfiable", &Method::GET)
        .expect("cp-range-unsatisfiable GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-range-unsatisfiable on /cp/range should be Confirmed; evidence: {:?}",
        result.signals
    );
}

#[tokio::test]
async fn cp_if_range_vs_range_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/range");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-if-range", &Method::GET)
        .expect("cp-if-range GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-if-range on /cp/range should be Confirmed; evidence: {:?}",
        result.signals
    );
}

// --- /cp/negotiated/{id} — content negotiation ---

#[tokio::test]
async fn cp_accept_vs_negotiated_confirmed() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/negotiated");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-accept", &Method::GET)
        .expect("cp-accept GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::Confirmed,
        "cp-accept on /cp/negotiated should be Confirmed; evidence: {:?}",
        result.signals
    );
}

// --- /cp/normalized/{id} — negative case (hardened server) ---

#[tokio::test]
async fn cp_if_none_match_vs_normalized_not_present() {
    let addr = fixtures::get_server::spawn().await;
    let ctx = make_ctx(&format!("http://{addr}"), "/cp/normalized");
    let plan = generate_plan(&ctx);

    let pair = find_pair(&plan, "cp-if-none-match", &Method::GET)
        .expect("cp-if-none-match GET spec must be in safe plan");
    let result = collect_pair(&pair).await;
    assert_eq!(
        result.verdict,
        OracleVerdict::NotPresent,
        "cp-if-none-match on /cp/normalized should be NotPresent; evidence: {:?}",
        result.signals
    );
}