parlov-analysis 0.3.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Pattern table for existence oracle status-code differentials.
//!
//! Each entry maps a (baseline, probe) status pair to a verdict, severity,
//! human-readable label, leaks description, and RFC basis. Patterns are split
//! by severity tier to stay within function size limits.

use http::StatusCode;
use parlov_core::{OracleVerdict, Severity};

/// All fields produced by matching a (baseline, probe) status pair.
pub(crate) struct PatternMatch {
    pub verdict: OracleVerdict,
    pub severity: Option<Severity>,
    pub label: Option<&'static str>,
    pub leaks: Option<&'static str>,
    pub rfc_basis: Option<&'static str>,
}

/// Matches a (baseline, probe) status pair against known oracle patterns.
///
/// Returns `NotPresent` with no metadata for identical codes, `Likely/Low`
/// with no metadata for unrecognised differentials, and a fully populated
/// `PatternMatch` for all catalogued patterns.
pub(crate) fn lookup(baseline: StatusCode, probe: StatusCode) -> PatternMatch {
    if baseline == probe {
        return PatternMatch {
            verdict: OracleVerdict::NotPresent,
            severity: None,
            label: None,
            leaks: None,
            rfc_basis: None,
        };
    }
    confirmed_high_patterns(baseline, probe)
        .or_else(|| confirmed_medium_patterns(baseline, probe))
        .or_else(|| likely_medium_patterns(baseline, probe))
        .unwrap_or(PatternMatch {
            verdict: OracleVerdict::Likely,
            severity: Some(Severity::Low),
            label: None,
            leaks: None,
            rfc_basis: None,
        })
}

fn confirmed_high_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    let m = |label, leaks, rfc| PatternMatch {
        verdict: OracleVerdict::Confirmed,
        severity: Some(Severity::High),
        label: Some(label),
        leaks: Some(leaks),
        rfc_basis: Some(rfc),
    };
    Some(match (b, p) {
        (S::FORBIDDEN, S::NOT_FOUND) => m(
            "Authorization-based differential",
            "Resource existence confirmed to low-privilege callers",
            "RFC 9110 §15.5.4",
        ),
        (S::OK, S::NOT_FOUND) => m(
            "Direct access differential",
            "Resource existence confirmed. Response body may contain full representation (IDOR)",
            "RFC 9110 §15.3.1",
        ),
        (S::UNAUTHORIZED, S::NOT_FOUND) => m(
            "Authentication-based differential",
            "Resource existence confirmed. WWW-Authenticate header leaks auth scheme",
            "RFC 9110 §15.5.2",
        ),
        (S::CONFLICT, S::CREATED | S::OK | S::SEE_OTHER | S::ACCEPTED) => m(
            "Conflict-based creation differential",
            "Resource existence confirmed via uniqueness constraint violation",
            "RFC 9110 §15.5.10",
        ),
        (S::UNPROCESSABLE_ENTITY, S::NOT_FOUND) => m(
            "Validation-path differential",
            "Resource existence confirmed. Validation errors may leak schema",
            "RFC 9110 §15.5.21",
        ),
        (S::UNPROCESSABLE_ENTITY, S::CREATED) => m(
            "Validation-path differential",
            "Resource existence confirmed. Server creates nonexistent resources",
            "RFC 9110 §9.3.4",
        ),
        (S::PARTIAL_CONTENT, S::NOT_FOUND) => m(
            "Range-request differential",
            "Resource existence confirmed. Content-Range header may leak resource size",
            "RFC 9110 §15.3.7",
        ),
        (S::NOT_MODIFIED, S::NOT_FOUND) => m(
            "Conditional-request differential",
            "Resource existence confirmed via cache validation",
            "RFC 9110 §15.4.5",
        ),
        (S::NOT_ACCEPTABLE, S::NOT_FOUND) => m(
            "Content-negotiation differential",
            "Resource existence confirmed. Server resolved resource before negotiation",
            "RFC 9110 §15.5.7",
        ),
        (S::PRECONDITION_FAILED, S::NOT_FOUND) => m(
            "Precondition-failed differential",
            "Resource existence confirmed via conditional request evaluation",
            "RFC 9110 §13.1.1",
        ),
        (S::UNSUPPORTED_MEDIA_TYPE, S::NOT_FOUND) => m(
            "Media-type differential",
            "Resource existence confirmed. Server resolved resource before content-type check",
            "RFC 9110 §15.5.16",
        ),
        (S::CONFLICT, S::NOT_FOUND) => m(
            "State-conflict differential",
            "Resource existence confirmed via state constraint violation",
            "RFC 9110 §15.5.10",
        ),
        (S::CONFLICT, S::NO_CONTENT) => m(
            "Conflict-based differential",
            "Resource existence confirmed via state conflict against no-content success",
            "RFC 9110 §15.5.10",
        ),
        _ => return None,
    })
}

fn confirmed_medium_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    let m = |label, leaks, rfc| PatternMatch {
        verdict: OracleVerdict::Confirmed,
        severity: Some(Severity::Medium),
        label: Some(label),
        leaks: Some(leaks),
        rfc_basis: Some(rfc),
    };
    Some(match (b, p) {
        (S::GONE, S::NOT_FOUND) => m(
            "Tombstone differential",
            "Prior resource existence confirmed via tombstone record",
            "RFC 9110 §15.5.11",
        ),
        (S::INTERNAL_SERVER_ERROR, S::NOT_FOUND) => m(
            "Crash-path differential",
            "Resource existence confirmed. Server error may leak internals",
            "RFC 9110 §15.6.1",
        ),
        (S::NO_CONTENT, S::NOT_FOUND) => m(
            "No-content differential",
            "Resource existence confirmed with no response body",
            "RFC 9110 §9.3.2",
        ),
        (S::METHOD_NOT_ALLOWED, S::NOT_FOUND) => m(
            "Method-restriction differential",
            "Resource existence confirmed. Allow header leaks supported methods",
            "RFC 9110 §15.5.6",
        ),
        (S::MOVED_PERMANENTLY, S::NOT_FOUND) => m(
            "Redirect-based differential",
            "Resource existence confirmed via canonical path redirect",
            "RFC 9110 §15.4.2",
        ),
        (S::RANGE_NOT_SATISFIABLE, S::NOT_FOUND) => m(
            "Range-not-satisfiable differential",
            "Resource existence confirmed. Content-Range may leak resource size",
            "RFC 9110 §15.5.17",
        ),
        (S::PAYLOAD_TOO_LARGE, S::NOT_FOUND) => m(
            "Payload-size differential",
            "Resource existence confirmed via per-resource size limit",
            "RFC 9110 §15.5.14",
        ),
        (S::LENGTH_REQUIRED, S::NOT_FOUND) => m(
            "Length-required differential",
            "Resource existence confirmed. Server resolved resource before length check",
            "RFC 9110 §15.5.12",
        ),
        (S::ACCEPTED, S::NOT_FOUND) => m(
            "Async-acceptance differential",
            "Resource existence confirmed via async processing acceptance",
            "RFC 9110 §15.3.3",
        ),
        _ => return None,
    })
}

fn likely_medium_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    let m = |label, leaks, rfc| PatternMatch {
        verdict: OracleVerdict::Likely,
        severity: Some(Severity::Medium),
        label: Some(label),
        leaks: Some(leaks),
        rfc_basis: Some(rfc),
    };
    Some(match (b, p) {
        (S::PAYMENT_REQUIRED, S::NOT_FOUND) => m(
            "Payment-gate differential",
            "Resource existence confirmed behind paywall",
            "RFC 9110 §15.5.3",
        ),
        (S::BAD_REQUEST, S::CREATED) => m(
            "Client-error creation differential",
            "Resource may exist — server reached validation before creation",
            "RFC 9110 §15.5.1",
        ),
        (S::BAD_REQUEST, S::OK) => m(
            "Client-error differential",
            "Resource may exist — server reached validation layer",
            "RFC 9110 §15.5.1",
        ),
        (S::TOO_MANY_REQUESTS, S::NOT_FOUND) => m(
            "Rate-limit-based differential",
            "Resource existence confirmed via per-resource rate limiting",
            "RFC 6585 §4",
        ),
        _ => return None,
    })
}