Skip to main content

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}