parlov-output 0.8.0

Output formatters for parlov: SARIF, terminal table, and raw JSON.
Documentation
//! Per-finding wire context: what was sent and what came back.
//!
//! `ProbeContext` and `ExchangeContext` are always-present blocks on every
//! `ScanFinding`. The optional sub-bundles (`HeadersBundle`, `BodySamplesBundle`)
//! are populated only when the scan was run with `--verbose`. None of these
//! values are ever redacted — they reproduce exactly what crossed the wire.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

/// Always-present block: what was actually sent on the wire.
///
/// `headers` is populated only when `--verbose` is set.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ProbeContext {
    /// Final URL of the baseline request after `{id}` substitution.
    pub baseline_url: String,
    /// Final URL of the probe request after `{id}` substitution.
    pub probe_url: String,
    /// HTTP method, e.g. `"GET"`.
    pub method: String,
    /// Filtered to security-relevant request headers, populated only by
    /// `--verbose`. NOT redacted — secrets appear verbatim.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub headers: Option<HeadersBundle>,
}

/// Always-present block: response status codes; `--verbose` adds headers and
/// body samples.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ExchangeContext {
    /// HTTP status code observed on the baseline response.
    pub baseline_status: u16,
    /// HTTP status code observed on the probe response.
    pub probe_status: u16,
    /// Filtered to security-relevant response headers, populated only by `--verbose`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub headers: Option<HeadersBundle>,
    /// UTF-8-safe body samples (256-byte cap), populated only by `--verbose`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub body_samples: Option<BodySamplesBundle>,
}

/// Per-side header sets, filtered to a security-relevant allowlist.
///
/// NOT redacted. `Authorization`, `Cookie`, and any other secrets present on
/// the allowlist are emitted verbatim — `--verbose` is opt-in for verification
/// and matches the no-redaction stance of `--repro`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct HeadersBundle {
    /// Header name → value for the baseline request/response.
    pub baseline: BTreeMap<String, String>,
    /// Header name → value for the probe request/response.
    pub probe: BTreeMap<String, String>,
}

/// Per-side body samples, UTF-8-safe truncated to 256 bytes with a
/// `(truncated, total Nb)` marker when over cap, or `<N bytes, non-text>`
/// when not valid UTF-8.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct BodySamplesBundle {
    /// Body sample of the baseline response.
    pub baseline: String,
    /// Body sample of the probe response.
    pub probe: String,
}

#[cfg(test)]
pub(crate) mod test_helpers {
    use super::{ExchangeContext, ProbeContext};

    /// Test helper: minimal `ProbeContext` with empty URLs and `GET`.
    pub(crate) fn probe_ctx(method: &str, baseline: &str, probe: &str) -> ProbeContext {
        ProbeContext {
            baseline_url: baseline.to_owned(),
            probe_url: probe.to_owned(),
            method: method.to_owned(),
            headers: None,
        }
    }

    /// Test helper: minimal `ExchangeContext` with given status codes.
    pub(crate) fn exchange_ctx(baseline_status: u16, probe_status: u16) -> ExchangeContext {
        ExchangeContext {
            baseline_status,
            probe_status,
            headers: None,
            body_samples: None,
        }
    }
}