parlov-core 0.7.0

Shared types, error types, and oracle class definitions for parlov.
Documentation
//! Probe generation metadata carried end-to-end through the pipeline.

use serde::{Deserialize, Serialize};

use crate::{OracleClass, ProbeDefinition, ResponseSurface};

/// Whether and how an `Authorization`-bearing credential was present on a request.
///
/// Tracked per-request so the auth-block classifier can distinguish "no credential"
/// from "credential present but rejected" — they correspond to different operator actions.
/// The credential value itself is never retained.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestAuthState {
    /// No `Authorization` header on the request.
    Absent,
    /// `Authorization` header present (value not retained — security policy).
    Present,
}

impl RequestAuthState {
    /// Infers auth state from the `Authorization` header in `req`.
    #[must_use]
    pub fn from_request(req: &ProbeDefinition) -> Self {
        if req.headers.contains_key(http::header::AUTHORIZATION) {
            Self::Present
        } else {
            Self::Absent
        }
    }
}

/// Graded confidence that a technique's applicability marker is observed in a response pair.
///
/// `Strong` (1.0) is full confidence the technique reached its oracle layer; `Weak` (0.3) is a
/// soft proxy signal; `Missing` (0.0) blocks the Contradictory outcome (downgrades to
/// `Inapplicable`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Applicability {
    /// Direct evidence the technique reached its oracle layer.
    Strong,
    /// Soft proxy signal — confidence is reduced but not blocked.
    Weak,
    /// No applicability evidence — Contradictory outcomes downgrade to `Inapplicable`.
    Missing,
}

impl Applicability {
    /// `Strong` → 1.0, `Weak` → 0.3, `Missing` → 0.0.
    #[must_use]
    pub fn confidence(self) -> f64 {
        match self {
            Self::Strong => 1.0,
            Self::Weak => 0.3,
            Self::Missing => 0.0,
        }
    }
}

/// Default applicability function — always `Strong`.
///
/// Techniques that don't need a specific applicability marker (e.g. `resource_id`,
/// `low_privilege` post-auth) point at this. The auth-gate / method-gate / parser-failure
/// upstream checks handle the phantom cases for these techniques.
#[must_use]
pub fn always_applicable(_baseline: &ResponseSurface, _probe: &ResponseSurface) -> Applicability {
    Applicability::Strong
}

/// Primary surface a technique's contradiction is evaluated on.
///
/// Used by the `surface_relevance` modifier to detect when a `SameStatus` contradiction is
/// mis-surfaced — i.e., the technique declares Status as its surface but the actual
/// differential is on the body or headers, meaning the Contradictory conclusion based on
/// status equality is wrong.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum SignalSurface {
    /// Status code is the primary differential.
    Status,
    /// Response body is the primary differential.
    Body,
    /// Response headers are the primary differential.
    Headers,
    /// Response timing is the primary differential.
    Timing,
    /// Multiple surfaces — no single surface dominates.
    Composite,
}

/// Why these probes were generated and what normative basis justifies the expected differential.
///
/// Set by a strategy at probe generation time, carried through execution unchanged, and consumed
/// by the analyzer for confidence calibration and evidence labeling.
///
/// Signal extraction is unconditional — the analyzer runs all extractors on every
/// `DifferentialSet`. Technique metadata is for attribution and confidence calibration, not for
/// gating which signals are extracted.
#[derive(Debug, Clone, Copy)]
pub struct Technique {
    /// Machine-readable identifier, e.g. `"if-none-match"` or `"get-200-404"`.
    pub id: &'static str,
    /// Human-readable name, e.g. `"If-None-Match conditional request"`.
    pub name: &'static str,
    /// Oracle class this technique probes.
    pub oracle_class: OracleClass,
    /// Elicitation method that produced this differential.
    pub vector: Vector,
    /// RFC mandate level for the expected behavior.
    pub strength: NormativeStrength,
    /// Weight applied when baseline and probe return the same status code (`SameStatus` path).
    ///
    /// Encodes how strongly the absence of a status differential implies response normalization.
    /// `Some(w)` produces `StrategyOutcome::Contradictory(result, w)`.
    /// `None` produces `StrategyOutcome::NoSignal`. Vectors where same-status carries
    /// no evidential meaning (`CacheProbing`, `ErrorMessageGranularity`, `RedirectDiff`) use `None`.
    pub normalization_weight: Option<f32>,
    /// Weight applied when a differential is observed in the opposite direction to the oracle
    /// hypothesis (e.g. probe rate-limited when baseline was not, or probe-only rate-limit headers).
    ///
    /// `None` means the strategy has no inverted-signal semantics.
    pub inverted_signal_weight: Option<f32>,
    /// True when method-level rejection (405) is oracle-relevant for this technique.
    ///
    /// Generally false — only techniques specifically probing method handling set this.
    pub method_relevant: bool,
    /// True when parser/validator errors (400/422) are oracle-relevant.
    ///
    /// Set by techniques like `uniqueness`, `state_transition`, `content_type` where the
    /// parser response IS the oracle. False for techniques whose layer is downstream of parsing
    /// (cache validators, content negotiation, etc.) — for those, same `400/422` means the
    /// request was rejected before the oracle-bearing code path.
    pub parser_relevant: bool,
    /// Returns confidence that the technique's applicability marker is observed in the
    /// response pair.
    ///
    /// Default for techniques with no specific marker is [`always_applicable`]. Techniques whose
    /// signal depends on a specific response feature (e.g. `If-None-Match` requires `ETag`)
    /// implement this to return `Missing` when the marker is absent — blocking phantom
    /// Contradictory outcomes from techniques whose mutation never reached the oracle layer.
    pub applicability: fn(&ResponseSurface, &ResponseSurface) -> Applicability,
    /// Primary surface this technique's contradiction is evaluated on.
    ///
    /// The `surface_relevance` modifier consults this to detect mis-surfaced contradictions —
    /// when `SameStatus` fires on a `Status`-surface technique but the body or headers show a
    /// significant differential, the contradiction is mis-surfaced and downgrades to Inapplicable.
    pub contradiction_surface: SignalSurface,
}

/// Detection method being used to produce the differential.
///
/// Strategies declare their vector. The analyzer uses it to select which signal extractors to run.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Vector {
    /// Differential produced by comparing status codes across baseline/probe inputs.
    StatusCodeDiff,
    /// Differential produced by manipulating cache-related headers (e.g. `If-None-Match`).
    CacheProbing,
    /// Differential produced by comparing error message body content across baseline/probe inputs.
    ErrorMessageGranularity,
    /// Differential produced by comparing redirect behavior (3xx vs non-3xx) and Location headers.
    RedirectDiff,
}

#[cfg(test)]
#[path = "technique_tests.rs"]
mod tests;

/// How strongly the RFC mandates the expected server behavior.
///
/// Directly affects confidence calibration: a `Must`-level differential is stronger evidence than
/// a `May`-level one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum NormativeStrength {
    /// RFC MUST: server is required to behave this way.
    Must,
    /// RFC MUST NOT: violation of this is definitive evidence.
    MustNot,
    /// RFC SHOULD: server is expected but not required to comply.
    Should,
    /// RFC MAY: server is permitted but not expected to exhibit this behavior.
    May,
}

impl std::fmt::Display for Vector {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::StatusCodeDiff => write!(f, "StatusCodeDiff"),
            Self::CacheProbing => write!(f, "CacheProbing"),
            Self::ErrorMessageGranularity => write!(f, "ErrorMessageGranularity"),
            Self::RedirectDiff => write!(f, "RedirectDiff"),
        }
    }
}

impl std::fmt::Display for NormativeStrength {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Must => write!(f, "Must"),
            Self::MustNot => write!(f, "MustNot"),
            Self::Should => write!(f, "Should"),
            Self::May => write!(f, "May"),
        }
    }
}