Skip to main content

parlov_core/
signal.rs

1use serde::{Deserialize, Serialize};
2
3/// One piece of evidence — the atoms that classifiers compose into verdicts.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6pub struct Signal {
7    /// Which category of differential this signal represents.
8    pub kind: SignalKind,
9    /// Human-readable description of the observation, e.g. `"304 vs 404"`.
10    pub evidence: String,
11    /// RFC section grounding the expected behavior, e.g. `"RFC 9110 \u{00a7}13.1.2"`.
12    #[serde(skip_serializing_if = "Option::is_none", default)]
13    pub rfc_basis: Option<String>,
14}
15
16/// Categories of observable differential signals.
17///
18/// Each variant maps to a distinct signal extractor in `parlov-analysis`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21pub enum SignalKind {
22    /// status codes differ between baseline and probe
23    StatusCodeDiff,
24    /// header present in one set, absent in the other
25    HeaderPresence,
26    /// header values differ across sets
27    HeaderValue,
28    /// Response body content differs between baseline and probe.
29    BodyDiff,
30    /// Response timing distributions differ (statistical significance required).
31    TimingDiff,
32    /// A response header leaks additional metadata, e.g. `Content-Range` reveals resource size.
33    MetadataLeak,
34    /// Body differential is fully explained by the request URL identifier being echoed back
35    /// into the response (e.g. error pages that include the requested path). Treated as
36    /// evidence *against* a real existence oracle — same response shape modulo input echo.
37    InputReflection,
38}
39
40/// Classification of leak impact, independent of confidence.
41///
42/// Determines severity when gated by confidence threshold. Computed from the peak leak type
43/// among validated signals — existence-only is `Low`, metadata disclosure is `Medium`, sensitive
44/// metadata (exact sizes, internal state) is `High`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47pub enum ImpactClass {
48    /// Existence confirmed, no metadata disclosed.
49    Low,
50    /// Metadata disclosed (validators, auth scheme, timing).
51    Medium,
52    /// Sensitive metadata disclosed (exact size, internal state).
53    High,
54}
55
56impl std::fmt::Display for SignalKind {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Self::StatusCodeDiff => write!(f, "StatusCodeDiff"),
60            Self::HeaderPresence => write!(f, "HeaderPresence"),
61            Self::HeaderValue => write!(f, "HeaderValue"),
62            Self::BodyDiff => write!(f, "BodyDiff"),
63            Self::TimingDiff => write!(f, "TimingDiff"),
64            Self::MetadataLeak => write!(f, "MetadataLeak"),
65            Self::InputReflection => write!(f, "InputReflection"),
66        }
67    }
68}
69
70impl std::fmt::Display for ImpactClass {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            Self::Low => write!(f, "Low"),
74            Self::Medium => write!(f, "Medium"),
75            Self::High => write!(f, "High"),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::{ImpactClass, SignalKind};
83
84    #[test]
85    fn signal_kind_display() {
86        assert_eq!(format!("{}", SignalKind::StatusCodeDiff), "StatusCodeDiff");
87        assert_eq!(format!("{}", SignalKind::HeaderPresence), "HeaderPresence");
88        assert_eq!(format!("{}", SignalKind::HeaderValue), "HeaderValue");
89        assert_eq!(format!("{}", SignalKind::BodyDiff), "BodyDiff");
90        assert_eq!(format!("{}", SignalKind::TimingDiff), "TimingDiff");
91        assert_eq!(format!("{}", SignalKind::MetadataLeak), "MetadataLeak");
92        assert_eq!(
93            format!("{}", SignalKind::InputReflection),
94            "InputReflection"
95        );
96    }
97
98    #[test]
99    fn impact_class_display() {
100        assert_eq!(format!("{}", ImpactClass::Low), "Low");
101        assert_eq!(format!("{}", ImpactClass::Medium), "Medium");
102        assert_eq!(format!("{}", ImpactClass::High), "High");
103    }
104}