parlov 0.8.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
use http::{HeaderMap, HeaderName, HeaderValue};
use parlov_core::{
    always_applicable, NormativeStrength, OracleClass, OracleResult, OracleVerdict, SignalSurface,
    StrategyOutcome, Vector,
};

fn make_technique(oracle_class: OracleClass, vector: Vector) -> parlov_core::Technique {
    parlov_core::Technique {
        id: "test-tech",
        name: "Test technique",
        oracle_class,
        vector,
        strength: NormativeStrength::Must,
        normalization_weight: None,
        inverted_signal_weight: None,
        method_relevant: false,
        parser_relevant: false,
        applicability: always_applicable,
        contradiction_surface: SignalSurface::Status,
    }
}

fn inner_result(outcome: &StrategyOutcome) -> &OracleResult {
    match outcome {
        StrategyOutcome::Positive(r)
        | StrategyOutcome::NoSignal(r)
        | StrategyOutcome::Contradictory(r, _) => r,
        StrategyOutcome::Inapplicable(_) => panic!("unexpected Inapplicable in test"),
    }
}

fn make_header_map(pairs: &[(&str, &str)]) -> HeaderMap {
    let mut map = HeaderMap::new();
    for &(name, value) in pairs {
        map.insert(
            HeaderName::from_bytes(name.as_bytes()).expect("valid header name"),
            HeaderValue::from_str(value).expect("valid header value"),
        );
    }
    map
}

fn make_result(verdict: OracleVerdict) -> OracleResult {
    OracleResult {
        class: OracleClass::Existence,
        verdict,
        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,
    }
}

#[test]
fn into_outcome_confirmed_maps_to_positive() {
    use parlov_core::StrategyOutcome;
    let result = make_result(OracleVerdict::Confirmed);
    let outcome = result.into_outcome();
    assert!(matches!(outcome, StrategyOutcome::Positive(_)));
}

#[test]
fn into_outcome_not_present_maps_to_no_signal() {
    use parlov_core::StrategyOutcome;
    let result = make_result(OracleVerdict::NotPresent);
    let outcome = result.into_outcome();
    assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
}

#[test]
fn into_outcome_likely_maps_to_no_signal() {
    use parlov_core::StrategyOutcome;
    let result = make_result(OracleVerdict::Likely);
    let outcome = result.into_outcome();
    assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
}

#[test]
fn burst_result_produces_status_code_diff_signal() {
    use parlov_core::SignalKind;
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let outcome = parlov_analysis::burst_result(
        5,
        0,
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    let r = inner_result(&outcome);
    assert_eq!(r.signals.len(), 1);
    assert_eq!(r.signals[0].kind, SignalKind::StatusCodeDiff);
}

#[test]
fn burst_result_confirmed_has_nonzero_confidence() {
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let outcome = parlov_analysis::burst_result(
        1,
        0,
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    assert!(matches!(outcome, StrategyOutcome::Positive(_)));
    let r = inner_result(&outcome);
    assert_eq!(r.verdict, OracleVerdict::Confirmed);
    assert_eq!(r.confidence, 80);
}

#[test]
fn burst_result_not_present_has_zero_confidence() {
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let outcome = parlov_analysis::burst_result(
        0,
        0,
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
    let r = inner_result(&outcome);
    assert_eq!(r.verdict, OracleVerdict::NotPresent);
    assert_eq!(r.confidence, 0);
}

#[test]
fn header_diff_result_produces_header_presence_signal() {
    use parlov_core::SignalKind;
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let baseline = make_header_map(&[("x-ratelimit-remaining", "99")]);
    let probe = HeaderMap::new();
    let outcome = parlov_analysis::header_diff_result(
        &baseline,
        &probe,
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    let r = inner_result(&outcome);
    assert_eq!(r.signals.len(), 1);
    assert_eq!(r.signals[0].kind, SignalKind::HeaderPresence);
}

#[test]
fn header_diff_result_confirmed_has_nonzero_confidence() {
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let baseline = make_header_map(&[("x-ratelimit-limit", "100")]);
    let probe = HeaderMap::new();
    let outcome = parlov_analysis::header_diff_result(
        &baseline,
        &probe,
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    assert!(matches!(outcome, StrategyOutcome::Positive(_)));
    let r = inner_result(&outcome);
    assert_eq!(r.verdict, OracleVerdict::Confirmed);
    assert_eq!(r.confidence, 80);
}

#[test]
fn header_diff_result_not_present_has_zero_confidence() {
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let outcome = parlov_analysis::header_diff_result(
        &HeaderMap::new(),
        &HeaderMap::new(),
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
    let r = inner_result(&outcome);
    assert_eq!(r.verdict, OracleVerdict::NotPresent);
    assert_eq!(r.confidence, 0);
}

#[test]
fn burst_result_uses_oracle_class_from_technique() {
    let technique = make_technique(OracleClass::Existence, Vector::StatusCodeDiff);
    let outcome = parlov_analysis::burst_result(
        5,
        0,
        &technique,
        parlov_analysis::ModifierResult {
            modifiers: parlov_analysis::EvidenceModifiers::default(),
            block_reason: None,
        },
    );
    assert_eq!(inner_result(&outcome).class, OracleClass::Existence);
}