use parlov_core::{
finding_id, ImpactClass, NormativeStrength, OracleResult, ScoringReason, Signal, Vector,
};
use serde::Serialize;
use crate::{ChainProvenance, ExchangeContext, ProbeContext, ReproInfo, ScanFinding};
pub(crate) const SCHEMA_VERSION: &str = "1.2.0";
#[derive(Serialize)]
pub(crate) struct SingleFindingOutput<'a> {
schema_version: &'static str,
target_url: &'a str,
finding: FindingOutput<'a>,
}
#[derive(Serialize)]
pub(crate) struct ScanOutput<'a> {
schema_version: &'static str,
target_url: &'a str,
findings: Vec<FindingOutput<'a>>,
}
#[derive(Serialize)]
pub(crate) struct FindingOutput<'a> {
finding_id: String,
strategy: StrategyOutput<'a>,
result: ResultOutput,
technique: TechniqueOutput<'a>,
matched_pattern: MatchedPatternOutput<'a>,
evidence: EvidenceOutput<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
repro: Option<&'a ReproInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
probe: Option<&'a ProbeContext>,
#[serde(skip_serializing_if = "Option::is_none")]
exchange: Option<&'a ExchangeContext>,
#[serde(skip_serializing_if = "Option::is_none")]
chain_provenance: Option<&'a ChainProvenance>,
}
#[derive(Serialize)]
struct StrategyOutput<'a> {
id: &'a str,
name: &'a str,
method: &'a str,
}
#[derive(Serialize)]
struct ResultOutput {
oracle_class: String,
verdict: String,
confidence: u8,
severity: String,
#[serde(skip_serializing_if = "Option::is_none")]
impact_class: Option<String>,
}
#[derive(Serialize)]
struct TechniqueOutput<'a> {
id: &'a str,
vector: String,
normative_strength: String,
}
#[derive(Serialize)]
struct MatchedPatternOutput<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
label: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
leaks: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
rfc_basis: Option<&'a str>,
}
#[derive(Serialize)]
struct EvidenceOutput<'a> {
reasons: &'a [ScoringReason],
signals: &'a [Signal],
}
pub fn render_json(
target_url: &str,
result: &OracleResult,
strategy_id: &str,
strategy_name: &str,
method: &str,
) -> Result<String, serde_json::Error> {
let finding = build_finding(
target_url,
result,
strategy_id,
strategy_name,
method,
None,
None,
None,
None,
);
let output = SingleFindingOutput {
schema_version: SCHEMA_VERSION,
target_url,
finding,
};
serde_json::to_string_pretty(&output)
}
pub fn render_scan_json(
target_url: &str,
findings: &[ScanFinding],
) -> Result<String, serde_json::Error> {
let items: Vec<FindingOutput> = findings
.iter()
.map(|f| {
build_finding(
target_url,
&f.result,
&f.strategy_id,
&f.strategy_name,
&f.method,
f.repro.as_ref(),
Some(&f.probe),
Some(&f.exchange),
f.chain_provenance.as_ref(),
)
})
.collect();
let output = ScanOutput {
schema_version: SCHEMA_VERSION,
target_url,
findings: items,
};
serde_json::to_string_pretty(&output)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_finding<'a>(
target_url: &str,
result: &'a OracleResult,
strategy_id: &'a str,
strategy_name: &'a str,
method: &'a str,
repro: Option<&'a ReproInfo>,
probe: Option<&'a ProbeContext>,
exchange: Option<&'a ExchangeContext>,
chain_provenance: Option<&'a ChainProvenance>,
) -> FindingOutput<'a> {
let oracle_class = result.class.to_string();
let technique_id = result.technique_id.as_deref().unwrap_or("unknown");
let fid = finding_id(technique_id, target_url, &oracle_class, method, strategy_id);
FindingOutput {
finding_id: fid,
strategy: StrategyOutput {
id: strategy_id,
name: strategy_name,
method,
},
result: build_result_output(result, &oracle_class),
technique: build_technique_output(result),
matched_pattern: build_matched_pattern(result),
evidence: EvidenceOutput {
reasons: &result.reasons,
signals: &result.signals,
},
repro,
probe,
exchange,
chain_provenance,
}
}
fn build_result_output(result: &OracleResult, oracle_class: &str) -> ResultOutput {
ResultOutput {
oracle_class: oracle_class.to_owned(),
verdict: result.verdict.to_string(),
confidence: result.confidence,
severity: format_severity(result.severity.as_ref()),
impact_class: result.impact_class.map(format_impact_class),
}
}
fn build_technique_output(result: &OracleResult) -> TechniqueOutput<'_> {
TechniqueOutput {
id: result.technique_id.as_deref().unwrap_or("unknown"),
vector: result
.vector
.map_or_else(|| "Unknown".to_owned(), format_vector),
normative_strength: result
.normative_strength
.map_or_else(|| "Unknown".to_owned(), format_normative_strength),
}
}
fn build_matched_pattern(result: &OracleResult) -> MatchedPatternOutput<'_> {
MatchedPatternOutput {
label: result.label.as_deref(),
leaks: result.leaks.as_deref(),
rfc_basis: result.rfc_basis.as_deref(),
}
}
pub(crate) fn format_severity(severity: Option<&parlov_core::Severity>) -> String {
match severity {
Some(s) => s.to_string(),
None => "None".to_owned(),
}
}
fn format_impact_class(ic: ImpactClass) -> String {
ic.to_string()
}
fn format_vector(v: Vector) -> String {
v.to_string()
}
fn format_normative_strength(ns: NormativeStrength) -> String {
ns.to_string()
}
pub use crate::json_endpoint::render_endpoint_verdict_json;
#[cfg(test)]
#[path = "json_tests.rs"]
mod tests;