parlov-output 0.8.0

Output formatters for parlov: SARIF, terminal table, and raw JSON.
Documentation
//! Output-format tests verifying that `ReproInfo` propagates through
//! JSON, SARIF, and table renderers when set, and is absent when unset.

use parlov_core::{
    EndpointStopReason, EndpointVerdict, OracleClass, OracleResult, OracleVerdict, Severity,
    Signal, SignalKind, Vector,
};

use crate::context::test_helpers::{exchange_ctx, probe_ctx};
use crate::{
    render_endpoint_verdict_sarif, render_scan_json, render_scan_sarif, render_scan_table,
    ReproInfo, ScanFinding,
};

const BASELINE_CURL: &str = "curl -X GET 'http://x/y' \\\n  -H 'authorization: Bearer t'";
const PROBE_CURL: &str = "curl -X GET 'http://x/zzz' \\\n  -H 'authorization: Bearer t'";

fn confirmed() -> OracleResult {
    OracleResult {
        class: OracleClass::Existence,
        verdict: OracleVerdict::Confirmed,
        severity: Some(Severity::High),
        confidence: 85,
        impact_class: None,
        reasons: vec![],
        signals: vec![Signal {
            kind: SignalKind::StatusCodeDiff,
            evidence: "200 vs 404".into(),
            rfc_basis: None,
        }],
        technique_id: Some("get-200-404".into()),
        vector: Some(Vector::StatusCodeDiff),
        normative_strength: None,
        label: None,
        leaks: None,
        rfc_basis: None,
    }
}

fn finding(verdict: OracleVerdict, with_repro: bool) -> ScanFinding {
    let mut result = confirmed();
    result.verdict = verdict;
    ScanFinding {
        target_url: "http://x/y".to_owned(),
        strategy_id: "s1".to_owned(),
        strategy_name: "n1".to_owned(),
        method: "GET".to_owned(),
        result,
        repro: if with_repro {
            Some(ReproInfo {
                baseline_curl: BASELINE_CURL.to_owned(),
                probe_curl: PROBE_CURL.to_owned(),
            })
        } else {
            None
        },
        probe: probe_ctx("GET", "http://x/y", "http://x/zzz"),
        exchange: exchange_ctx(200, 404),
        chain_provenance: None,
    }
}

fn endpoint_verdict() -> EndpointVerdict {
    use parlov_core::ObservabilityStatus;
    EndpointVerdict {
        oracle_class: OracleClass::Existence,
        posterior_probability: 0.92,
        verdict: OracleVerdict::Confirmed,
        severity: Some(Severity::High),
        strategies_run: 1,
        strategies_total: 1,
        stop_reason: Some(EndpointStopReason::EarlyAccept),
        first_threshold_crossed_by: None,
        final_confirming_strategy: None,
        contributing_findings: vec![],
        observability_status: ObservabilityStatus::EvidenceObserved,
        block_summary: None,
    }
}

#[test]
fn repro_renders_in_json_when_flag_set() {
    let findings = vec![finding(OracleVerdict::Confirmed, true)];
    let json = render_scan_json("http://x/y", &findings).expect("render");
    let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
    assert_eq!(
        v["findings"][0]["repro"]["baseline_curl"], BASELINE_CURL,
        "baseline_curl missing or mismatched: {json}"
    );
    assert_eq!(v["findings"][0]["repro"]["probe_curl"], PROBE_CURL);
}

#[test]
fn repro_absent_in_json_when_flag_unset() {
    let findings = vec![finding(OracleVerdict::Confirmed, false)];
    let json = render_scan_json("http://x/y", &findings).expect("render");
    let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
    assert!(
        v["findings"][0]["repro"].is_null(),
        "repro must be omitted: {json}"
    );
}

#[test]
fn repro_renders_in_sarif_properties_when_flag_set() {
    let findings = vec![finding(OracleVerdict::Confirmed, true)];
    let json = render_scan_sarif("http://x/y", &findings).expect("render");
    let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
    let result_props = &v["runs"][0]["results"][0]["properties"];
    assert_eq!(result_props["repro"]["baseline_curl"], BASELINE_CURL);
    assert_eq!(result_props["repro"]["probe_curl"], PROBE_CURL);
}

#[test]
fn repro_renders_in_endpoint_sarif_properties_when_flag_set() {
    let findings = vec![finding(OracleVerdict::Confirmed, true)];
    let verdict = endpoint_verdict();
    let json = render_endpoint_verdict_sarif("http://x/y", &verdict, &findings).expect("render");
    let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
    assert_eq!(
        v["runs"][0]["results"][0]["properties"]["repro"]["baseline_curl"],
        BASELINE_CURL
    );
}

#[test]
fn repro_absent_in_sarif_when_flag_unset() {
    let findings = vec![finding(OracleVerdict::Confirmed, false)];
    let json = render_scan_sarif("http://x/y", &findings).expect("render");
    let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
    assert!(v["runs"][0]["results"][0]["properties"]["repro"].is_null());
}

#[test]
fn repro_renders_in_table_when_flag_set_and_verdict_present() {
    let findings = vec![finding(OracleVerdict::Confirmed, true)];
    let table = render_scan_table(&findings);
    assert!(
        table.contains("Reproduce baseline:"),
        "missing baseline row:\n{table}"
    );
    assert!(
        table.contains("Reproduce probe:"),
        "missing probe row:\n{table}"
    );
}

#[test]
fn repro_suppressed_in_table_when_verdict_not_present() {
    let findings = vec![finding(OracleVerdict::NotPresent, true)];
    let table = render_scan_table(&findings);
    assert!(
        !table.contains("Reproduce baseline:"),
        "should suppress on NotPresent:\n{table}"
    );
}

#[test]
fn repro_absent_in_table_when_flag_unset() {
    let findings = vec![finding(OracleVerdict::Confirmed, false)];
    let table = render_scan_table(&findings);
    assert!(!table.contains("Reproduce"));
}