parlov-analysis 0.7.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.
#[derive(Clone, Copy)]
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,
};

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),
    }
}

/// Flat table of all known (baseline, probe) status-code differentials, keyed by raw `u16` pairs.
///
/// Ordered by descending confidence tier: strong, upper-moderate, lower-moderate, weak.
/// Multi-target arms from the original match are expanded to individual entries.
const PATTERNS: &[((u16, u16), PatternMatch)] = &[
    // --- Strong ---
    ((200, 404), pm(92, 50,
        "Direct access differential",
        "Resource existence confirmed. Response body may contain full representation (IDOR)",
        "RFC 9110 \u{00a7}15.3.1")),
    ((206, 404), pm(88, 55,
        "Range-request differential",
        "Resource existence confirmed. Content-Range header may leak resource size",
        "RFC 9110 \u{00a7}15.3.7")),
    ((409, 201), pm(86, 45,
        "Conflict-based creation differential",
        "Resource existence confirmed via uniqueness constraint violation",
        "RFC 9110 \u{00a7}15.5.10")),
    ((409, 200), pm(86, 45,
        "Conflict-based creation differential",
        "Resource existence confirmed via uniqueness constraint violation",
        "RFC 9110 \u{00a7}15.5.10")),
    ((409, 303), pm(86, 45,
        "Conflict-based creation differential",
        "Resource existence confirmed via uniqueness constraint violation",
        "RFC 9110 \u{00a7}15.5.10")),
    ((409, 202), pm(86, 45,
        "Conflict-based creation differential",
        "Resource existence confirmed via uniqueness constraint violation",
        "RFC 9110 \u{00a7}15.5.10")),
    ((403, 404), pm(85, 40,
        "Authorization-based differential",
        "Resource existence confirmed to low-privilege callers",
        "RFC 9110 \u{00a7}15.5.4")),
    ((401, 404), pm(85, 40,
        "Authentication-based differential",
        "Resource existence confirmed. WWW-Authenticate header leaks auth scheme",
        "RFC 9110 \u{00a7}15.5.2")),
    // --- Upper moderate ---
    ((304, 404), pm(84, 40,
        "Conditional-request differential",
        "Resource existence confirmed via cache validation",
        "RFC 9110 \u{00a7}15.4.5")),
    ((422, 404), pm(83, 40,
        "Validation-path differential",
        "Resource existence confirmed. Validation errors may leak schema",
        "RFC 9110 \u{00a7}15.5.21")),
    ((422, 201), pm(83, 40,
        "Validation-path differential",
        "Resource existence confirmed. Server creates nonexistent resources",
        "RFC 9110 \u{00a7}9.3.4")),
    ((412, 404), pm(83, 40,
        "Precondition-failed differential",
        "Resource existence confirmed via conditional request evaluation",
        "RFC 9110 \u{00a7}13.1.1")),
    ((406, 404), pm(82, 35,
        "Content-negotiation differential",
        "Resource existence confirmed. Server resolved resource before negotiation",
        "RFC 9110 \u{00a7}15.5.7")),
    ((415, 404), pm(82, 35,
        "Media-type differential",
        "Resource existence confirmed. Server resolved resource before content-type check",
        "RFC 9110 \u{00a7}15.5.16")),
    ((409, 404), pm(82, 40,
        "State-conflict differential",
        "Resource existence confirmed via state constraint violation",
        "RFC 9110 \u{00a7}15.5.10")),
    ((409, 204), pm(82, 40,
        "Conflict-based differential",
        "Resource existence confirmed via state conflict against no-content success",
        "RFC 9110 \u{00a7}15.5.10")),
    ((416, 404), pm(82, 50,
        "Range-not-satisfiable differential",
        "Resource existence confirmed. Content-Range may leak resource size",
        "RFC 9110 \u{00a7}15.5.17")),
    // --- Lower moderate ---
    ((410, 404), pm(80, 30,
        "Tombstone differential",
        "Prior resource existence confirmed via tombstone record",
        "RFC 9110 \u{00a7}15.5.11")),
    ((204, 404), pm(82, 35,
        "No-content differential",
        "Resource existence confirmed with no response body",
        "RFC 9110 \u{00a7}9.3.2")),
    ((405, 404), pm(82, 35,
        "Method-restriction differential",
        "Resource existence confirmed. Allow header leaks supported methods",
        "RFC 9110 \u{00a7}15.5.6")),
    ((301, 404), pm(80, 30,
        "Redirect-based differential",
        "Resource existence confirmed via canonical path redirect",
        "RFC 9110 \u{00a7}15.4.2")),
    ((413, 404), pm(80, 30,
        "Payload-size differential",
        "Resource existence confirmed via per-resource size limit",
        "RFC 9110 \u{00a7}15.5.14")),
    ((411, 404), pm(80, 30,
        "Length-required differential",
        "Resource existence confirmed. Server resolved resource before length check",
        "RFC 9110 \u{00a7}15.5.12")),
    ((202, 404), pm(80, 30,
        "Async-acceptance differential",
        "Resource existence confirmed via async processing acceptance",
        "RFC 9110 \u{00a7}15.3.3")),
    ((500, 404), pm(80, 35,
        "Crash-path differential",
        "Resource existence confirmed. Server error may leak internals",
        "RFC 9110 \u{00a7}15.6.1")),
    ((302, 404), pm(80, 30,
        "Temporary-redirect differential",
        "Resource existence confirmed via temporary redirect. Location header may leak temporary URI",
        "RFC 9110 \u{00a7}15.4.3")),
    ((307, 404), pm(80, 30,
        "Method-preserving temporary-redirect differential",
        "Resource existence confirmed via method-preserving temporary redirect",
        "RFC 9110 \u{00a7}15.4.8")),
    ((308, 404), pm(80, 30,
        "Method-preserving permanent-redirect differential",
        "Resource existence confirmed via method-preserving permanent redirect. Location header may leak canonical URI",
        "RFC 9110 \u{00a7}15.4.9")),
    ((303, 404), pm(80, 30,
        "Post-mutation redirect differential",
        "Resource existence confirmed via post-mutation redirect. Location header may leak result resource URI",
        "RFC 9110 \u{00a7}15.4.4")),
    // --- Weak ---
    ((402, 404), pm(65, 25,
        "Payment-gate differential",
        "Resource existence confirmed behind paywall",
        "RFC 9110 \u{00a7}15.5.3")),
    ((400, 201), pm(65, 25,
        "Client-error creation differential",
        "Resource may exist \u{2014} server reached validation before creation",
        "RFC 9110 \u{00a7}15.5.1")),
    ((400, 200), pm(65, 25,
        "Client-error differential",
        "Resource may exist \u{2014} server reached validation layer",
        "RFC 9110 \u{00a7}15.5.1")),
    ((429, 404), pm(65, 25,
        "Rate-limit-based differential",
        "Resource existence confirmed via per-resource rate limiting",
        "RFC 6585 \u{00a7}4")),
    ((300, 404), pm(65, 25,
        "Multiple-choices differential",
        "Resource existence confirmed via content negotiation. Response body may list alternative representations",
        "RFC 9110 \u{00a7}15.4.1")),
];

/// Looks up the pattern match for a (baseline, probe) status pair.
///
/// Returns `NOT_PRESENT` when both sides match, `UNRECOGNISED` for unknown differentials,
/// or the matching `PatternMatch` from the const table.
pub(crate) fn lookup(baseline: StatusCode, probe: StatusCode) -> PatternMatch {
    if baseline == probe {
        return NOT_PRESENT;
    }
    let key = (baseline.as_u16(), probe.as_u16());
    PATTERNS
        .iter()
        .find(|(k, _)| *k == key)
        .map_or(UNRECOGNISED, |(_, v)| *v)
}