parlov-core 0.7.0

Shared types, error types, and oracle class definitions for parlov.
Documentation
//! Observability-status types for endpoint-level scan diagnostics.
//!
//! These types answer "why did we get this verdict?" — distinguishing a hardened
//! endpoint (probed, no signal) from a blocked scan (auth gate fired before any
//! technique could observe the oracle layer).

use serde::{Deserialize, Serialize};

/// Orthogonal to `OracleVerdict`: describes whether techniques actually reached
/// the oracle layer and, if not, why.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ObservabilityStatus {
    /// At least one Positive or Contradictory event was observed.
    EvidenceObserved,
    /// Techniques reached the oracle layer but produced no differential signal.
    ProbedNoEvidence,
    /// ≥80% of expected observation opportunities were blocked by a scan-wide gate
    /// (Authorization or Method) before any technique reached the oracle layer.
    BlockedBeforeOracleLayer,
    /// Some techniques were blocked, some reached the oracle layer (20–80% blocked).
    PartiallyBlocked,
    /// Fewer than 3 expected observation opportunities — scan coverage too low to classify.
    Underpowered,
    /// Inapplicable outcomes dominated by `SurfaceMismatch` — wrong technique family
    /// for this surface.
    SurfaceMismatch,
}

/// Coarse categorisation of what caused a technique to be blocked.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BlockFamily {
    /// Auth gate (401/403/407/511/login-redirect) fired before technique.
    Authorization,
    /// Method gate (405 both sides) fired before resource lookup.
    Method,
    /// Parser/validator rejection before technique evaluated.
    Parser,
    /// Technique-local: applicability marker missing or mutation destroyed control.
    TechniqueLocal,
    /// Surface mismatch: wrong technique family for this response surface.
    Surface,
}

impl std::fmt::Display for ObservabilityStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::EvidenceObserved => write!(f, "EvidenceObserved"),
            Self::ProbedNoEvidence => write!(f, "ProbedNoEvidence"),
            Self::BlockedBeforeOracleLayer => write!(f, "BlockedBeforeOracleLayer"),
            Self::PartiallyBlocked => write!(f, "PartiallyBlocked"),
            Self::Underpowered => write!(f, "Underpowered"),
            Self::SurfaceMismatch => write!(f, "SurfaceMismatch"),
        }
    }
}

impl std::fmt::Display for BlockFamily {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Authorization => write!(f, "Authorization"),
            Self::Method => write!(f, "Method"),
            Self::Parser => write!(f, "Parser"),
            Self::TechniqueLocal => write!(f, "TechniqueLocal"),
            Self::Surface => write!(f, "Surface"),
        }
    }
}

/// Summary of what blocked observation opportunities during the scan.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockSummary {
    /// Total count of findings that could have observed the oracle layer.
    pub expected_observation_opportunities: usize,
    /// Count blocked by scan-wide gates (Authorization, Method).
    pub blocked_before_oracle_layer: usize,
    /// `blocked_before_oracle_layer / expected_observation_opportunities`
    pub blocked_fraction: f64,
    /// Family that caused the most blocks.
    pub dominant_block_family: BlockFamily,
    /// Operator-facing reason strings from blocked findings.
    pub dominant_block_reasons: Vec<String>,
    /// Suggested remediation action, if available.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub operator_action: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn observability_status_display() {
        assert_eq!(
            ObservabilityStatus::EvidenceObserved.to_string(),
            "EvidenceObserved"
        );
        assert_eq!(
            ObservabilityStatus::ProbedNoEvidence.to_string(),
            "ProbedNoEvidence"
        );
        assert_eq!(
            ObservabilityStatus::BlockedBeforeOracleLayer.to_string(),
            "BlockedBeforeOracleLayer"
        );
        assert_eq!(
            ObservabilityStatus::PartiallyBlocked.to_string(),
            "PartiallyBlocked"
        );
        assert_eq!(
            ObservabilityStatus::Underpowered.to_string(),
            "Underpowered"
        );
        assert_eq!(
            ObservabilityStatus::SurfaceMismatch.to_string(),
            "SurfaceMismatch"
        );
    }

    #[test]
    fn block_family_display() {
        assert_eq!(BlockFamily::Authorization.to_string(), "Authorization");
        assert_eq!(BlockFamily::Method.to_string(), "Method");
        assert_eq!(BlockFamily::Parser.to_string(), "Parser");
        assert_eq!(BlockFamily::TechniqueLocal.to_string(), "TechniqueLocal");
        assert_eq!(BlockFamily::Surface.to_string(), "Surface");
    }

    #[test]
    fn observability_status_roundtrip() {
        for status in [
            ObservabilityStatus::EvidenceObserved,
            ObservabilityStatus::ProbedNoEvidence,
            ObservabilityStatus::BlockedBeforeOracleLayer,
            ObservabilityStatus::PartiallyBlocked,
            ObservabilityStatus::Underpowered,
            ObservabilityStatus::SurfaceMismatch,
        ] {
            let json = serde_json::to_string(&status).expect("serialize");
            let back: ObservabilityStatus = serde_json::from_str(&json).expect("deserialize");
            assert_eq!(status, back);
        }
    }

    #[test]
    fn block_family_roundtrip() {
        for family in [
            BlockFamily::Authorization,
            BlockFamily::Method,
            BlockFamily::Parser,
            BlockFamily::TechniqueLocal,
            BlockFamily::Surface,
        ] {
            let json = serde_json::to_string(&family).expect("serialize");
            let back: BlockFamily = serde_json::from_str(&json).expect("deserialize");
            assert_eq!(family, back);
        }
    }
}