parlov-analysis 0.5.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 base confidence, base impact,
//! human-readable label, leaks description, and RFC basis.

use http::StatusCode;

/// All fields produced by matching a (baseline, probe) status pair.
pub(crate) struct PatternMatch {
    /// Base confidence score (0-100) from the status code differential alone.
    pub base_confidence: u8,
    /// Base impact score (0-100) from the status code differential alone.
    pub base_impact: u8,
    /// Human-readable name for the detected pattern.
    pub label: Option<&'static str>,
    /// What information the oracle leaks.
    pub leaks: Option<&'static str>,
    /// RFC section grounding the behavior.
    pub rfc_basis: Option<&'static str>,
}

const NOT_PRESENT: PatternMatch = PatternMatch {
    base_confidence: 0, base_impact: 0, label: None, leaks: None, rfc_basis: None,
};

const UNRECOGNISED: PatternMatch = PatternMatch {
    base_confidence: 40, base_impact: 15, label: None, leaks: None, rfc_basis: None,
};

/// Builds a populated `PatternMatch` from all fields.
const fn pm(
    c: u8, i: u8, l: &'static str, lk: &'static str, r: &'static str,
) -> PatternMatch {
    PatternMatch {
        base_confidence: c, base_impact: i, label: Some(l), leaks: Some(lk), rfc_basis: Some(r),
    }
}

/// Matches a (baseline, probe) status pair against known oracle patterns.
pub(crate) fn lookup(baseline: StatusCode, probe: StatusCode) -> PatternMatch {
    if baseline == probe { return NOT_PRESENT; }
    strong_patterns(baseline, probe)
        .or_else(|| upper_moderate(baseline, probe))
        .or_else(|| lower_moderate(baseline, probe))
        .or_else(|| weak_patterns(baseline, probe))
        .unwrap_or(UNRECOGNISED)
}

/// Patterns with strong evidence (`base_confidence` >= 85).
fn strong_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    Some(match (b, p) {
        (S::OK, S::NOT_FOUND) => pm(92, 50,
            "Direct access differential",
            "Resource existence confirmed. Response body may contain full representation (IDOR)",
            "RFC 9110 \u{00a7}15.3.1"),
        (S::PARTIAL_CONTENT, S::NOT_FOUND) => pm(88, 55,
            "Range-request differential",
            "Resource existence confirmed. Content-Range header may leak resource size",
            "RFC 9110 \u{00a7}15.3.7"),
        (S::CONFLICT, S::CREATED | S::OK | S::SEE_OTHER | S::ACCEPTED) => pm(86, 45,
            "Conflict-based creation differential",
            "Resource existence confirmed via uniqueness constraint violation",
            "RFC 9110 \u{00a7}15.5.10"),
        (S::FORBIDDEN, S::NOT_FOUND) => pm(85, 40,
            "Authorization-based differential",
            "Resource existence confirmed to low-privilege callers",
            "RFC 9110 \u{00a7}15.5.4"),
        (S::UNAUTHORIZED, S::NOT_FOUND) => pm(85, 40,
            "Authentication-based differential",
            "Resource existence confirmed. WWW-Authenticate header leaks auth scheme",
            "RFC 9110 \u{00a7}15.5.2"),
        _ => return None,
    })
}

/// Moderate patterns, upper tier (`base_confidence` 82-84).
fn upper_moderate(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    Some(match (b, p) {
        (S::NOT_MODIFIED, S::NOT_FOUND) => pm(84, 40,
            "Conditional-request differential",
            "Resource existence confirmed via cache validation",
            "RFC 9110 \u{00a7}15.4.5"),
        (S::UNPROCESSABLE_ENTITY, S::NOT_FOUND) => pm(83, 40,
            "Validation-path differential",
            "Resource existence confirmed. Validation errors may leak schema",
            "RFC 9110 \u{00a7}15.5.21"),
        (S::UNPROCESSABLE_ENTITY, S::CREATED) => pm(83, 40,
            "Validation-path differential",
            "Resource existence confirmed. Server creates nonexistent resources",
            "RFC 9110 \u{00a7}9.3.4"),
        (S::PRECONDITION_FAILED, S::NOT_FOUND) => pm(83, 40,
            "Precondition-failed differential",
            "Resource existence confirmed via conditional request evaluation",
            "RFC 9110 \u{00a7}13.1.1"),
        (S::NOT_ACCEPTABLE, S::NOT_FOUND) => pm(82, 35,
            "Content-negotiation differential",
            "Resource existence confirmed. Server resolved resource before negotiation",
            "RFC 9110 \u{00a7}15.5.7"),
        (S::UNSUPPORTED_MEDIA_TYPE, S::NOT_FOUND) => pm(82, 35,
            "Media-type differential",
            "Resource existence confirmed. Server resolved resource before content-type check",
            "RFC 9110 \u{00a7}15.5.16"),
        (S::CONFLICT, S::NOT_FOUND) => pm(82, 40,
            "State-conflict differential",
            "Resource existence confirmed via state constraint violation",
            "RFC 9110 \u{00a7}15.5.10"),
        (S::CONFLICT, S::NO_CONTENT) => pm(82, 40,
            "Conflict-based differential",
            "Resource existence confirmed via state conflict against no-content success",
            "RFC 9110 \u{00a7}15.5.10"),
        (S::RANGE_NOT_SATISFIABLE, S::NOT_FOUND) => pm(82, 50,
            "Range-not-satisfiable differential",
            "Resource existence confirmed. Content-Range may leak resource size",
            "RFC 9110 \u{00a7}15.5.17"),
        _ => return None,
    })
}

/// Moderate patterns, lower tier (`base_confidence` 80-82).
fn lower_moderate(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    Some(match (b, p) {
        (S::GONE, S::NOT_FOUND) => pm(80, 30,
            "Tombstone differential",
            "Prior resource existence confirmed via tombstone record",
            "RFC 9110 \u{00a7}15.5.11"),
        (S::NO_CONTENT, S::NOT_FOUND) => pm(82, 35,
            "No-content differential",
            "Resource existence confirmed with no response body",
            "RFC 9110 \u{00a7}9.3.2"),
        (S::METHOD_NOT_ALLOWED, S::NOT_FOUND) => pm(82, 35,
            "Method-restriction differential",
            "Resource existence confirmed. Allow header leaks supported methods",
            "RFC 9110 \u{00a7}15.5.6"),
        (S::MOVED_PERMANENTLY, S::NOT_FOUND) => pm(80, 30,
            "Redirect-based differential",
            "Resource existence confirmed via canonical path redirect",
            "RFC 9110 \u{00a7}15.4.2"),
        (S::PAYLOAD_TOO_LARGE, S::NOT_FOUND) => pm(80, 30,
            "Payload-size differential",
            "Resource existence confirmed via per-resource size limit",
            "RFC 9110 \u{00a7}15.5.14"),
        (S::LENGTH_REQUIRED, S::NOT_FOUND) => pm(80, 30,
            "Length-required differential",
            "Resource existence confirmed. Server resolved resource before length check",
            "RFC 9110 \u{00a7}15.5.12"),
        (S::ACCEPTED, S::NOT_FOUND) => pm(80, 30,
            "Async-acceptance differential",
            "Resource existence confirmed via async processing acceptance",
            "RFC 9110 \u{00a7}15.3.3"),
        (S::INTERNAL_SERVER_ERROR, S::NOT_FOUND) => pm(80, 35,
            "Crash-path differential",
            "Resource existence confirmed. Server error may leak internals",
            "RFC 9110 \u{00a7}15.6.1"),
        _ => return None,
    })
}

/// Patterns with weaker evidence (`base_confidence` < 80).
fn weak_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
    use StatusCode as S;
    Some(match (b, p) {
        (S::PAYMENT_REQUIRED, S::NOT_FOUND) => pm(65, 25,
            "Payment-gate differential",
            "Resource existence confirmed behind paywall",
            "RFC 9110 \u{00a7}15.5.3"),
        (S::BAD_REQUEST, S::CREATED) => pm(65, 25,
            "Client-error creation differential",
            "Resource may exist \u{2014} server reached validation before creation",
            "RFC 9110 \u{00a7}15.5.1"),
        (S::BAD_REQUEST, S::OK) => pm(65, 25,
            "Client-error differential",
            "Resource may exist \u{2014} server reached validation layer",
            "RFC 9110 \u{00a7}15.5.1"),
        (S::TOO_MANY_REQUESTS, S::NOT_FOUND) => pm(65, 25,
            "Rate-limit-based differential",
            "Resource existence confirmed via per-resource rate limiting",
            "RFC 6585 \u{00a7}4"),
        _ => return None,
    })
}