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}