parlov_core/technique.rs
1//! Probe generation metadata carried end-to-end through the pipeline.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{OracleClass, ProbeDefinition, ResponseSurface};
6
7/// Whether and how an `Authorization`-bearing credential was present on a request.
8///
9/// Tracked per-request so the auth-block classifier can distinguish "no credential"
10/// from "credential present but rejected" — they correspond to different operator actions.
11/// The credential value itself is never retained.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RequestAuthState {
14 /// No `Authorization` header on the request.
15 Absent,
16 /// `Authorization` header present (value not retained — security policy).
17 Present,
18}
19
20impl RequestAuthState {
21 /// Infers auth state from the `Authorization` header in `req`.
22 #[must_use]
23 pub fn from_request(req: &ProbeDefinition) -> Self {
24 if req.headers.contains_key(http::header::AUTHORIZATION) {
25 Self::Present
26 } else {
27 Self::Absent
28 }
29 }
30}
31
32/// Graded confidence that a technique's applicability marker is observed in a response pair.
33///
34/// `Strong` (1.0) is full confidence the technique reached its oracle layer; `Weak` (0.3) is a
35/// soft proxy signal; `Missing` (0.0) blocks the Contradictory outcome (downgrades to
36/// `Inapplicable`).
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Applicability {
39 /// Direct evidence the technique reached its oracle layer.
40 Strong,
41 /// Soft proxy signal — confidence is reduced but not blocked.
42 Weak,
43 /// No applicability evidence — Contradictory outcomes downgrade to `Inapplicable`.
44 Missing,
45}
46
47impl Applicability {
48 /// `Strong` → 1.0, `Weak` → 0.3, `Missing` → 0.0.
49 #[must_use]
50 pub fn confidence(self) -> f64 {
51 match self {
52 Self::Strong => 1.0,
53 Self::Weak => 0.3,
54 Self::Missing => 0.0,
55 }
56 }
57}
58
59/// Default applicability function — always `Strong`.
60///
61/// Techniques that don't need a specific applicability marker (e.g. `resource_id`,
62/// `low_privilege` post-auth) point at this. The auth-gate / method-gate / parser-failure
63/// upstream checks handle the phantom cases for these techniques.
64#[must_use]
65pub fn always_applicable(_baseline: &ResponseSurface, _probe: &ResponseSurface) -> Applicability {
66 Applicability::Strong
67}
68
69/// Primary surface a technique's contradiction is evaluated on.
70///
71/// Used by the `surface_relevance` modifier to detect when a `SameStatus` contradiction is
72/// mis-surfaced — i.e., the technique declares Status as its surface but the actual
73/// differential is on the body or headers, meaning the Contradictory conclusion based on
74/// status equality is wrong.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77pub enum SignalSurface {
78 /// Status code is the primary differential.
79 Status,
80 /// Response body is the primary differential.
81 Body,
82 /// Response headers are the primary differential.
83 Headers,
84 /// Response timing is the primary differential.
85 Timing,
86 /// Multiple surfaces — no single surface dominates.
87 Composite,
88}
89
90/// Why these probes were generated and what normative basis justifies the expected differential.
91///
92/// Set by a strategy at probe generation time, carried through execution unchanged, and consumed
93/// by the analyzer for confidence calibration and evidence labeling.
94///
95/// Signal extraction is unconditional — the analyzer runs all extractors on every
96/// `DifferentialSet`. Technique metadata is for attribution and confidence calibration, not for
97/// gating which signals are extracted.
98#[derive(Debug, Clone, Copy)]
99pub struct Technique {
100 /// Machine-readable identifier, e.g. `"if-none-match"` or `"get-200-404"`.
101 pub id: &'static str,
102 /// Human-readable name, e.g. `"If-None-Match conditional request"`.
103 pub name: &'static str,
104 /// Oracle class this technique probes.
105 pub oracle_class: OracleClass,
106 /// Elicitation method that produced this differential.
107 pub vector: Vector,
108 /// RFC mandate level for the expected behavior.
109 pub strength: NormativeStrength,
110 /// Weight applied when baseline and probe return the same status code (`SameStatus` path).
111 ///
112 /// Encodes how strongly the absence of a status differential implies response normalization.
113 /// `Some(w)` produces `StrategyOutcome::Contradictory(result, w)`.
114 /// `None` produces `StrategyOutcome::NoSignal`. Vectors where same-status carries
115 /// no evidential meaning (`CacheProbing`, `ErrorMessageGranularity`, `RedirectDiff`) use `None`.
116 pub normalization_weight: Option<f32>,
117 /// Weight applied when a differential is observed in the opposite direction to the oracle
118 /// hypothesis (e.g. probe rate-limited when baseline was not, or probe-only rate-limit headers).
119 ///
120 /// `None` means the strategy has no inverted-signal semantics.
121 pub inverted_signal_weight: Option<f32>,
122 /// True when method-level rejection (405) is oracle-relevant for this technique.
123 ///
124 /// Generally false — only techniques specifically probing method handling set this.
125 pub method_relevant: bool,
126 /// True when parser/validator errors (400/422) are oracle-relevant.
127 ///
128 /// Set by techniques like `uniqueness`, `state_transition`, `content_type` where the
129 /// parser response IS the oracle. False for techniques whose layer is downstream of parsing
130 /// (cache validators, content negotiation, etc.) — for those, same `400/422` means the
131 /// request was rejected before the oracle-bearing code path.
132 pub parser_relevant: bool,
133 /// Returns confidence that the technique's applicability marker is observed in the
134 /// response pair.
135 ///
136 /// Default for techniques with no specific marker is [`always_applicable`]. Techniques whose
137 /// signal depends on a specific response feature (e.g. `If-None-Match` requires `ETag`)
138 /// implement this to return `Missing` when the marker is absent — blocking phantom
139 /// Contradictory outcomes from techniques whose mutation never reached the oracle layer.
140 pub applicability: fn(&ResponseSurface, &ResponseSurface) -> Applicability,
141 /// Primary surface this technique's contradiction is evaluated on.
142 ///
143 /// The `surface_relevance` modifier consults this to detect mis-surfaced contradictions —
144 /// when `SameStatus` fires on a `Status`-surface technique but the body or headers show a
145 /// significant differential, the contradiction is mis-surfaced and downgrades to Inapplicable.
146 pub contradiction_surface: SignalSurface,
147}
148
149/// Detection method being used to produce the differential.
150///
151/// Strategies declare their vector. The analyzer uses it to select which signal extractors to run.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
153#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
154pub enum Vector {
155 /// Differential produced by comparing status codes across baseline/probe inputs.
156 StatusCodeDiff,
157 /// Differential produced by manipulating cache-related headers (e.g. `If-None-Match`).
158 CacheProbing,
159 /// Differential produced by comparing error message body content across baseline/probe inputs.
160 ErrorMessageGranularity,
161 /// Differential produced by comparing redirect behavior (3xx vs non-3xx) and Location headers.
162 RedirectDiff,
163}
164
165#[cfg(test)]
166#[path = "technique_tests.rs"]
167mod tests;
168
169/// How strongly the RFC mandates the expected server behavior.
170///
171/// Directly affects confidence calibration: a `Must`-level differential is stronger evidence than
172/// a `May`-level one.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
175pub enum NormativeStrength {
176 /// RFC MUST: server is required to behave this way.
177 Must,
178 /// RFC MUST NOT: violation of this is definitive evidence.
179 MustNot,
180 /// RFC SHOULD: server is expected but not required to comply.
181 Should,
182 /// RFC MAY: server is permitted but not expected to exhibit this behavior.
183 May,
184}
185
186impl std::fmt::Display for Vector {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::StatusCodeDiff => write!(f, "StatusCodeDiff"),
190 Self::CacheProbing => write!(f, "CacheProbing"),
191 Self::ErrorMessageGranularity => write!(f, "ErrorMessageGranularity"),
192 Self::RedirectDiff => write!(f, "RedirectDiff"),
193 }
194 }
195}
196
197impl std::fmt::Display for NormativeStrength {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 Self::Must => write!(f, "Must"),
201 Self::MustNot => write!(f, "MustNot"),
202 Self::Should => write!(f, "Should"),
203 Self::May => write!(f, "May"),
204 }
205 }
206}