use http::StatusCode;
use parlov_core::{OracleVerdict, Severity};
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>,
}
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,
})
}