Skip to main content

parlov_analysis/existence/
analyzer.rs

1//! `ExistenceAnalyzer` — delegates classification to the scoring pipeline in `classifier`.
2
3use parlov_core::{DifferentialSet, OracleClass, OracleResult, OracleVerdict, ProbeExchange};
4
5use crate::signals;
6use crate::{Analyzer, SampleDecision};
7
8use super::classifier::classify;
9
10/// Analyzes a `DifferentialSet` for existence oracle signals via status-code differential.
11///
12/// Requires three samples when a differential is detected to confirm stability before
13/// classifying. Short-circuits on same-status pairs (`NotPresent`) after the first sample.
14/// Runs ALL signal extractors unconditionally and delegates scoring to `classifier::classify`.
15pub struct ExistenceAnalyzer;
16
17impl Analyzer for ExistenceAnalyzer {
18    fn evaluate(&self, data: &DifferentialSet) -> SampleDecision {
19        let b0 = data.baseline[0].response.status;
20        let p0 = data.probe[0].response.status;
21
22        if b0 == p0 {
23            return SampleDecision::Complete(Box::new(build_result(data)));
24        }
25
26        if data.baseline.len() < 3 {
27            return SampleDecision::NeedMore;
28        }
29
30        let stable = is_consistent(&data.baseline) && is_consistent(&data.probe);
31        if stable {
32            SampleDecision::Complete(Box::new(build_result(data)))
33        } else {
34            SampleDecision::Complete(Box::new(unstable_result(data)))
35        }
36    }
37
38    fn oracle_class(&self) -> OracleClass {
39        OracleClass::Existence
40    }
41}
42
43/// Builds the full `OracleResult` from a stable `DifferentialSet`.
44fn build_result(data: &DifferentialSet) -> OracleResult {
45    let b0 = data.baseline[0].response.status;
46    let p0 = data.probe[0].response.status;
47
48    let signals = extract_all_signals(data);
49    classify(b0, p0, signals, &data.technique)
50}
51
52/// Runs all signal extractors unconditionally on every `DifferentialSet`.
53fn extract_all_signals(data: &DifferentialSet) -> Vec<parlov_core::Signal> {
54    let mut out = Vec::new();
55    out.extend(signals::status_code::extract(data));
56    out.extend(signals::header::extract(data));
57    out.extend(signals::metadata::extract(data));
58    out.extend(signals::body::extract(data));
59    out
60}
61
62/// Returns `true` when every exchange in `exchanges` shares the same status as the first.
63fn is_consistent(exchanges: &[ProbeExchange]) -> bool {
64    exchanges
65        .iter()
66        .all(|e| e.response.status == exchanges[0].response.status)
67}
68
69fn unstable_result(data: &DifferentialSet) -> OracleResult {
70    let baseline_stable = is_consistent(&data.baseline);
71    let probe_stable = is_consistent(&data.probe);
72
73    let which = match (baseline_stable, probe_stable) {
74        (false, false) => "baseline and probe sides",
75        (false, true) => "baseline side",
76        (true, false) => "probe side",
77        (true, true) => unreachable!("unstable_result called when both sides are stable"),
78    };
79
80    OracleResult {
81        class: OracleClass::Existence,
82        verdict: OracleVerdict::NotPresent,
83        severity: None,
84        confidence: 0,
85        impact_class: None,
86        reasons: vec![],
87        signals: vec![parlov_core::Signal {
88            kind: parlov_core::SignalKind::StatusCodeDiff,
89            evidence: format!("unstable: {which}"),
90            rfc_basis: None,
91        }],
92        technique_id: Some(data.technique.id.to_string()),
93        vector: Some(data.technique.vector),
94        normative_strength: Some(data.technique.strength),
95        label: None,
96        leaks: None,
97        rfc_basis: None,
98    }
99}
100
101#[cfg(test)]
102#[path = "analyzer_tests.rs"]
103mod tests;