parlov-output 0.8.0

Output formatters for parlov: SARIF, terminal table, and raw JSON.
Documentation
//! SARIF v2.1.0 output rendering for parlov oracle results.
//!
//! Uses `serde-sarif` typed structs. Rule IDs are technique-based,
//! signals map to `relatedLocations`, and deterministic fingerprints use `finding_id`.

use parlov_core::{EndpointVerdict, OracleResult};

use crate::sarif_builder::{
    build_rule, build_sarif_document, build_sarif_result, build_verdict_run_properties,
    deduplicate_rules, findings_to_results, ResultContext,
};
use crate::ScanFinding;

/// Renders a single `OracleResult` as a SARIF v2.1.0 JSON string.
///
/// `NotPresent` verdicts produce an empty `results` array.
///
/// # Errors
///
/// Returns `Err` if serialization fails (cannot occur for well-formed inputs).
pub fn render_sarif(
    target_url: &str,
    result: &OracleResult,
    strategy_id: &str,
    method: &str,
) -> Result<String, serde_json::Error> {
    let rules = deduplicate_rules(vec![build_rule(result)]);
    let ctx = ResultContext {
        target_url,
        result,
        strategy_id,
        method,
    };
    let results: Vec<_> = build_sarif_result(&ctx).into_iter().collect();
    let doc = build_sarif_document(rules, results, None);
    serde_json::to_string_pretty(&doc)
}

/// Renders a slice of `ScanFinding`s as SARIF v2.1.0.
///
/// `NotPresent` findings are excluded. Strategy metadata is included in `run.properties`.
///
/// # Errors
///
/// Returns `Err` if serialization fails (cannot occur for well-formed inputs).
pub fn render_scan_sarif(
    target_url: &str,
    findings: &[ScanFinding],
) -> Result<String, serde_json::Error> {
    use serde_sarif::sarif::PropertyBag;
    use std::collections::BTreeMap;

    let rules = deduplicate_rules(findings.iter().map(|f| build_rule(&f.result)).collect());
    let results = findings_to_results(target_url, findings, None);

    let mut props = BTreeMap::new();
    props.insert("target_url".to_owned(), serde_json::json!(target_url));
    let run_props = PropertyBag::builder().additional_properties(props).build();

    let doc = build_sarif_document(rules, results, Some(run_props));
    serde_json::to_string_pretty(&doc)
}

/// Renders scan findings with endpoint-level verdict as SARIF v2.1.0.
///
/// `NotPresent` per-finding results are excluded when the endpoint verdict is also `NotPresent`.
/// When the endpoint verdict is `Confirmed` or `Likely`, all non-`NotPresent` findings are
/// included. Endpoint verdict fields are added to `runs[0].properties`.
///
/// # Errors
///
/// Returns `Err` if serialization fails (cannot occur for well-formed inputs).
pub fn render_endpoint_verdict_sarif(
    target_url: &str,
    verdict: &EndpointVerdict,
    findings: &[ScanFinding],
) -> Result<String, serde_json::Error> {
    let rules = deduplicate_rules(findings.iter().map(|f| build_rule(&f.result)).collect());
    let results = findings_to_results(target_url, findings, Some(verdict));
    let run_props = build_verdict_run_properties(target_url, verdict);
    let doc = build_sarif_document(rules, results, Some(run_props));
    serde_json::to_string_pretty(&doc)
}

#[cfg(test)]
#[path = "sarif_tests.rs"]
mod tests;