Skip to main content

parlov_core/
observability.rs

1//! Observability-status types for endpoint-level scan diagnostics.
2//!
3//! These types answer "why did we get this verdict?" — distinguishing a hardened
4//! endpoint (probed, no signal) from a blocked scan (auth gate fired before any
5//! technique could observe the oracle layer).
6
7use serde::{Deserialize, Serialize};
8
9/// Orthogonal to `OracleVerdict`: describes whether techniques actually reached
10/// the oracle layer and, if not, why.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum ObservabilityStatus {
13    /// At least one Positive or Contradictory event was observed.
14    EvidenceObserved,
15    /// Techniques reached the oracle layer but produced no differential signal.
16    ProbedNoEvidence,
17    /// ≥80% of expected observation opportunities were blocked by a scan-wide gate
18    /// (Authorization or Method) before any technique reached the oracle layer.
19    BlockedBeforeOracleLayer,
20    /// Some techniques were blocked, some reached the oracle layer (20–80% blocked).
21    PartiallyBlocked,
22    /// Fewer than 3 expected observation opportunities — scan coverage too low to classify.
23    Underpowered,
24    /// Inapplicable outcomes dominated by `SurfaceMismatch` — wrong technique family
25    /// for this surface.
26    SurfaceMismatch,
27}
28
29/// Coarse categorisation of what caused a technique to be blocked.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum BlockFamily {
32    /// Auth gate (401/403/407/511/login-redirect) fired before technique.
33    Authorization,
34    /// Method gate (405 both sides) fired before resource lookup.
35    Method,
36    /// Parser/validator rejection before technique evaluated.
37    Parser,
38    /// Technique-local: applicability marker missing or mutation destroyed control.
39    TechniqueLocal,
40    /// Surface mismatch: wrong technique family for this response surface.
41    Surface,
42}
43
44impl std::fmt::Display for ObservabilityStatus {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::EvidenceObserved => write!(f, "EvidenceObserved"),
48            Self::ProbedNoEvidence => write!(f, "ProbedNoEvidence"),
49            Self::BlockedBeforeOracleLayer => write!(f, "BlockedBeforeOracleLayer"),
50            Self::PartiallyBlocked => write!(f, "PartiallyBlocked"),
51            Self::Underpowered => write!(f, "Underpowered"),
52            Self::SurfaceMismatch => write!(f, "SurfaceMismatch"),
53        }
54    }
55}
56
57impl std::fmt::Display for BlockFamily {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::Authorization => write!(f, "Authorization"),
61            Self::Method => write!(f, "Method"),
62            Self::Parser => write!(f, "Parser"),
63            Self::TechniqueLocal => write!(f, "TechniqueLocal"),
64            Self::Surface => write!(f, "Surface"),
65        }
66    }
67}
68
69/// Summary of what blocked observation opportunities during the scan.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BlockSummary {
72    /// Total count of findings that could have observed the oracle layer.
73    pub expected_observation_opportunities: usize,
74    /// Count blocked by scan-wide gates (Authorization, Method).
75    pub blocked_before_oracle_layer: usize,
76    /// `blocked_before_oracle_layer / expected_observation_opportunities`
77    pub blocked_fraction: f64,
78    /// Family that caused the most blocks.
79    pub dominant_block_family: BlockFamily,
80    /// Operator-facing reason strings from blocked findings.
81    pub dominant_block_reasons: Vec<String>,
82    /// Suggested remediation action, if available.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub operator_action: Option<String>,
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn observability_status_display() {
93        assert_eq!(
94            ObservabilityStatus::EvidenceObserved.to_string(),
95            "EvidenceObserved"
96        );
97        assert_eq!(
98            ObservabilityStatus::ProbedNoEvidence.to_string(),
99            "ProbedNoEvidence"
100        );
101        assert_eq!(
102            ObservabilityStatus::BlockedBeforeOracleLayer.to_string(),
103            "BlockedBeforeOracleLayer"
104        );
105        assert_eq!(
106            ObservabilityStatus::PartiallyBlocked.to_string(),
107            "PartiallyBlocked"
108        );
109        assert_eq!(
110            ObservabilityStatus::Underpowered.to_string(),
111            "Underpowered"
112        );
113        assert_eq!(
114            ObservabilityStatus::SurfaceMismatch.to_string(),
115            "SurfaceMismatch"
116        );
117    }
118
119    #[test]
120    fn block_family_display() {
121        assert_eq!(BlockFamily::Authorization.to_string(), "Authorization");
122        assert_eq!(BlockFamily::Method.to_string(), "Method");
123        assert_eq!(BlockFamily::Parser.to_string(), "Parser");
124        assert_eq!(BlockFamily::TechniqueLocal.to_string(), "TechniqueLocal");
125        assert_eq!(BlockFamily::Surface.to_string(), "Surface");
126    }
127
128    #[test]
129    fn observability_status_roundtrip() {
130        for status in [
131            ObservabilityStatus::EvidenceObserved,
132            ObservabilityStatus::ProbedNoEvidence,
133            ObservabilityStatus::BlockedBeforeOracleLayer,
134            ObservabilityStatus::PartiallyBlocked,
135            ObservabilityStatus::Underpowered,
136            ObservabilityStatus::SurfaceMismatch,
137        ] {
138            let json = serde_json::to_string(&status).expect("serialize");
139            let back: ObservabilityStatus = serde_json::from_str(&json).expect("deserialize");
140            assert_eq!(status, back);
141        }
142    }
143
144    #[test]
145    fn block_family_roundtrip() {
146        for family in [
147            BlockFamily::Authorization,
148            BlockFamily::Method,
149            BlockFamily::Parser,
150            BlockFamily::TechniqueLocal,
151            BlockFamily::Surface,
152        ] {
153            let json = serde_json::to_string(&family).expect("serialize");
154            let back: BlockFamily = serde_json::from_str(&json).expect("deserialize");
155            assert_eq!(family, back);
156        }
157    }
158}