use http::StatusCode;
#[derive(Clone, Copy)]
pub(crate) struct PatternMatch {
pub base_confidence: u8,
pub base_impact: u8,
pub label: Option<&'static str>,
pub leaks: Option<&'static str>,
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),
}
}
const PATTERNS: &[((u16, u16), PatternMatch)] = &[
((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")),
((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")),
((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")),
((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")),
];
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)
}