parlov-output 0.8.0

Output formatters for parlov: SARIF, terminal table, and raw JSON.
Documentation
//! Detail-row helpers for `render_scan_table` / `render_endpoint_verdict_table`.
//!
//! Probe summary, chain-provenance, repro, and verbose request/response rows
//! are appended under each finding by these functions. Suppression rules:
//! probe and repro rows are hidden for `NotPresent` verdicts to keep the
//! summary view free of noise.

use std::collections::BTreeMap;

use comfy_table::{Cell, Table};
use parlov_core::OracleVerdict;

use crate::ScanFinding;

/// Two `Reproduce` rows when `repro` is set and the verdict is non-`NotPresent`.
pub(crate) fn add_repro_rows(table: &mut Table, finding: &ScanFinding) {
    let Some(repro) = &finding.repro else { return };
    if finding.result.verdict == OracleVerdict::NotPresent {
        return;
    }
    table.add_row(vec![
        Cell::new("Reproduce baseline:"),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(&repro.baseline_curl),
    ]);
    table.add_row(vec![
        Cell::new("Reproduce probe:"),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(&repro.probe_curl),
    ]);
}

/// Per-finding probe-summary row showing method, baseline/probe URLs, and
/// observed status codes. Suppressed for `NotPresent`.
pub(crate) fn add_probe_row(table: &mut Table, finding: &ScanFinding) {
    if finding.result.verdict == OracleVerdict::NotPresent {
        return;
    }
    let summary = format!(
        "Probe: {} {} -> {} | {} -> {}",
        finding.probe.method,
        finding.probe.baseline_url,
        finding.exchange.baseline_status,
        finding.probe.probe_url,
        finding.exchange.probe_status,
    );
    table.add_row(vec![
        Cell::new(summary),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
    ]);
}

/// `Chain:` row when the finding has chain provenance.
pub(crate) fn add_chain_row(table: &mut Table, finding: &ScanFinding) {
    let Some(prov) = &finding.chain_provenance else {
        return;
    };
    let summary = format!(
        "Chain: derived from {}={}",
        prov.producer_kind, prov.producer_value,
    );
    table.add_row(vec![
        Cell::new(summary),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
    ]);
}

/// Verbose request/response header rows + body sample rows.
///
/// All four blocks are independently `Some`-gated so partial bundles still
/// render whatever they have.
pub(crate) fn add_verbose_rows(table: &mut Table, finding: &ScanFinding) {
    if let Some(req_hdrs) = finding.probe.headers.as_ref() {
        push_header_section(table, "Request headers (baseline)", &req_hdrs.baseline);
        push_header_section(table, "Request headers (probe)", &req_hdrs.probe);
    }
    if let Some(resp_hdrs) = finding.exchange.headers.as_ref() {
        push_header_section(table, "Response headers (baseline)", &resp_hdrs.baseline);
        push_header_section(table, "Response headers (probe)", &resp_hdrs.probe);
    }
    if let Some(samples) = finding.exchange.body_samples.as_ref() {
        table.add_row(vec![
            Cell::new("Body (baseline)"),
            Cell::new(""),
            Cell::new(""),
            Cell::new(""),
            Cell::new(""),
            Cell::new(&samples.baseline),
        ]);
        table.add_row(vec![
            Cell::new("Body (probe)"),
            Cell::new(""),
            Cell::new(""),
            Cell::new(""),
            Cell::new(""),
            Cell::new(&samples.probe),
        ]);
    }
}

fn push_header_section(table: &mut Table, label: &str, headers: &BTreeMap<String, String>) {
    if headers.is_empty() {
        return;
    }
    let body: String = headers
        .iter()
        .map(|(k, v)| format!("{k}: {v}"))
        .collect::<Vec<_>>()
        .join("\n");
    table.add_row(vec![
        Cell::new(label),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(""),
        Cell::new(body),
    ]);
}