Skip to main content

parlov_output/
context.rs

1//! Per-finding wire context: what was sent and what came back.
2//!
3//! `ProbeContext` and `ExchangeContext` are always-present blocks on every
4//! `ScanFinding`. The optional sub-bundles (`HeadersBundle`, `BodySamplesBundle`)
5//! are populated only when the scan was run with `--verbose`. None of these
6//! values are ever redacted — they reproduce exactly what crossed the wire.
7
8use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11
12/// Always-present block: what was actually sent on the wire.
13///
14/// `headers` is populated only when `--verbose` is set.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17pub struct ProbeContext {
18    /// Final URL of the baseline request after `{id}` substitution.
19    pub baseline_url: String,
20    /// Final URL of the probe request after `{id}` substitution.
21    pub probe_url: String,
22    /// HTTP method, e.g. `"GET"`.
23    pub method: String,
24    /// Filtered to security-relevant request headers, populated only by
25    /// `--verbose`. NOT redacted — secrets appear verbatim.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub headers: Option<HeadersBundle>,
28}
29
30/// Always-present block: response status codes; `--verbose` adds headers and
31/// body samples.
32#[derive(Clone, Debug, Serialize, Deserialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34pub struct ExchangeContext {
35    /// HTTP status code observed on the baseline response.
36    pub baseline_status: u16,
37    /// HTTP status code observed on the probe response.
38    pub probe_status: u16,
39    /// Filtered to security-relevant response headers, populated only by `--verbose`.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub headers: Option<HeadersBundle>,
42    /// UTF-8-safe body samples (256-byte cap), populated only by `--verbose`.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub body_samples: Option<BodySamplesBundle>,
45}
46
47/// Per-side header sets, filtered to a security-relevant allowlist.
48///
49/// NOT redacted. `Authorization`, `Cookie`, and any other secrets present on
50/// the allowlist are emitted verbatim — `--verbose` is opt-in for verification
51/// and matches the no-redaction stance of `--repro`.
52#[derive(Clone, Debug, Serialize, Deserialize)]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54pub struct HeadersBundle {
55    /// Header name → value for the baseline request/response.
56    pub baseline: BTreeMap<String, String>,
57    /// Header name → value for the probe request/response.
58    pub probe: BTreeMap<String, String>,
59}
60
61/// Per-side body samples, UTF-8-safe truncated to 256 bytes with a
62/// `(truncated, total Nb)` marker when over cap, or `<N bytes, non-text>`
63/// when not valid UTF-8.
64#[derive(Clone, Debug, Serialize, Deserialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66pub struct BodySamplesBundle {
67    /// Body sample of the baseline response.
68    pub baseline: String,
69    /// Body sample of the probe response.
70    pub probe: String,
71}
72
73#[cfg(test)]
74pub(crate) mod test_helpers {
75    use super::{ExchangeContext, ProbeContext};
76
77    /// Test helper: minimal `ProbeContext` with empty URLs and `GET`.
78    pub(crate) fn probe_ctx(method: &str, baseline: &str, probe: &str) -> ProbeContext {
79        ProbeContext {
80            baseline_url: baseline.to_owned(),
81            probe_url: probe.to_owned(),
82            method: method.to_owned(),
83            headers: None,
84        }
85    }
86
87    /// Test helper: minimal `ExchangeContext` with given status codes.
88    pub(crate) fn exchange_ctx(baseline_status: u16, probe_status: u16) -> ExchangeContext {
89        ExchangeContext {
90            baseline_status,
91            probe_status,
92            headers: None,
93            body_samples: None,
94        }
95    }
96}