parlov-core 0.7.0

Shared types, error types, and oracle class definitions for parlov.
Documentation
use serde::{Deserialize, Serialize};

/// One piece of evidence — the atoms that classifiers compose into verdicts.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Signal {
    /// Which category of differential this signal represents.
    pub kind: SignalKind,
    /// Human-readable description of the observation, e.g. `"304 vs 404"`.
    pub evidence: String,
    /// RFC section grounding the expected behavior, e.g. `"RFC 9110 \u{00a7}13.1.2"`.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub rfc_basis: Option<String>,
}

/// Categories of observable differential signals.
///
/// Each variant maps to a distinct signal extractor in `parlov-analysis`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum SignalKind {
    /// status codes differ between baseline and probe
    StatusCodeDiff,
    /// header present in one set, absent in the other
    HeaderPresence,
    /// header values differ across sets
    HeaderValue,
    /// Response body content differs between baseline and probe.
    BodyDiff,
    /// Response timing distributions differ (statistical significance required).
    TimingDiff,
    /// A response header leaks additional metadata, e.g. `Content-Range` reveals resource size.
    MetadataLeak,
    /// Body differential is fully explained by the request URL identifier being echoed back
    /// into the response (e.g. error pages that include the requested path). Treated as
    /// evidence *against* a real existence oracle — same response shape modulo input echo.
    InputReflection,
}

/// Classification of leak impact, independent of confidence.
///
/// Determines severity when gated by confidence threshold. Computed from the peak leak type
/// among validated signals — existence-only is `Low`, metadata disclosure is `Medium`, sensitive
/// metadata (exact sizes, internal state) is `High`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum ImpactClass {
    /// Existence confirmed, no metadata disclosed.
    Low,
    /// Metadata disclosed (validators, auth scheme, timing).
    Medium,
    /// Sensitive metadata disclosed (exact size, internal state).
    High,
}

impl std::fmt::Display for SignalKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::StatusCodeDiff => write!(f, "StatusCodeDiff"),
            Self::HeaderPresence => write!(f, "HeaderPresence"),
            Self::HeaderValue => write!(f, "HeaderValue"),
            Self::BodyDiff => write!(f, "BodyDiff"),
            Self::TimingDiff => write!(f, "TimingDiff"),
            Self::MetadataLeak => write!(f, "MetadataLeak"),
            Self::InputReflection => write!(f, "InputReflection"),
        }
    }
}

impl std::fmt::Display for ImpactClass {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Low => write!(f, "Low"),
            Self::Medium => write!(f, "Medium"),
            Self::High => write!(f, "High"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{ImpactClass, SignalKind};

    #[test]
    fn signal_kind_display() {
        assert_eq!(format!("{}", SignalKind::StatusCodeDiff), "StatusCodeDiff");
        assert_eq!(format!("{}", SignalKind::HeaderPresence), "HeaderPresence");
        assert_eq!(format!("{}", SignalKind::HeaderValue), "HeaderValue");
        assert_eq!(format!("{}", SignalKind::BodyDiff), "BodyDiff");
        assert_eq!(format!("{}", SignalKind::TimingDiff), "TimingDiff");
        assert_eq!(format!("{}", SignalKind::MetadataLeak), "MetadataLeak");
        assert_eq!(
            format!("{}", SignalKind::InputReflection),
            "InputReflection"
        );
    }

    #[test]
    fn impact_class_display() {
        assert_eq!(format!("{}", ImpactClass::Low), "Low");
        assert_eq!(format!("{}", ImpactClass::Medium), "Medium");
        assert_eq!(format!("{}", ImpactClass::High), "High");
    }
}