use super::*;
use parlov_core::{OracleClass, OracleResult, OracleVerdict, Severity, Signal, SignalKind};
use crate::context::test_helpers::{exchange_ctx, probe_ctx};
use crate::ScanFinding;
fn confirmed_result() -> 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: "403 (baseline) vs 404 (probe)".into(),
rfc_basis: None,
}],
technique_id: Some("get-403-404".into()),
vector: Some(parlov_core::Vector::StatusCodeDiff),
normative_strength: Some(parlov_core::NormativeStrength::Should),
label: Some("Authorization-based differential".into()),
leaks: Some("Resource existence confirmed".into()),
rfc_basis: Some("RFC 9110 \u{00a7}15.5.4".into()),
}
}
fn not_present_result() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: None,
confidence: 0,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: SignalKind::StatusCodeDiff,
evidence: "404 (baseline) vs 404 (probe)".into(),
rfc_basis: None,
}],
technique_id: Some("get-404-404".into()),
vector: Some(parlov_core::Vector::StatusCodeDiff),
normative_strength: None,
label: None,
leaks: None,
rfc_basis: None,
}
}
fn scan_finding(verdict: OracleVerdict, severity: Option<Severity>) -> ScanFinding {
ScanFinding {
target_url: "https://api.example.com/users/1".to_owned(),
strategy_id: "existence-get-200-404".to_owned(),
strategy_name: "GET 200/404 existence".to_owned(),
method: "GET".to_owned(),
result: OracleResult {
class: OracleClass::Existence,
verdict,
severity,
confidence: 85,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: SignalKind::StatusCodeDiff,
evidence: "403 (baseline) vs 404 (probe)".into(),
rfc_basis: None,
}],
technique_id: Some("get-200-404".into()),
vector: Some(parlov_core::Vector::StatusCodeDiff),
normative_strength: None,
label: None,
leaks: None,
rfc_basis: None,
},
repro: None,
probe: probe_ctx(
"GET",
"https://api.example.com/users/1",
"https://api.example.com/users/9999",
),
exchange: exchange_ctx(403, 404),
chain_provenance: None,
}
}
#[test]
fn sarif_confirmed_produces_error_level() {
let r = confirmed_result();
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(v["runs"][0]["results"][0]["level"], "error");
}
#[test]
fn sarif_not_present_produces_empty_results() {
let r = not_present_result();
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert!(v["runs"][0]["results"]
.as_array()
.expect("results")
.is_empty());
}
#[test]
fn sarif_rule_id_is_technique_id() {
let r = confirmed_result();
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(v["runs"][0]["results"][0]["ruleId"], "get-403-404");
assert_eq!(
v["runs"][0]["tool"]["driver"]["rules"][0]["id"],
"get-403-404"
);
}
#[test]
fn sarif_has_fingerprints() {
let r = confirmed_result();
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
let fp = &v["runs"][0]["results"][0]["fingerprints"]["oracleFingerprint/v1"];
assert_eq!(fp.as_str().expect("string").len(), 12);
}
#[test]
fn sarif_has_related_locations_for_signals() {
let r = confirmed_result();
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
let rl = v["runs"][0]["results"][0]["relatedLocations"]
.as_array()
.expect("array");
assert_eq!(rl.len(), 1);
assert!(rl[0]["message"]["text"]
.as_str()
.expect("text")
.contains("StatusCodeDiff"));
}
#[test]
fn sarif_message_uses_leaks_when_present() {
let r = confirmed_result();
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(
v["runs"][0]["results"][0]["message"]["text"],
"Resource existence confirmed"
);
}
#[test]
fn sarif_message_falls_back_to_primary_evidence() {
let mut r = confirmed_result();
r.leaks = None;
let json =
render_sarif("https://api.example.com/users/1", &r, "s1", "GET").expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(
v["runs"][0]["results"][0]["message"]["text"],
"403 (baseline) vs 404 (probe)"
);
}
#[test]
fn scan_sarif_filters_not_present() {
let findings = vec![
scan_finding(OracleVerdict::Confirmed, Some(Severity::High)),
scan_finding(OracleVerdict::NotPresent, None),
];
let json =
render_scan_sarif("https://api.example.com/users/1", &findings).expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
let results = v["runs"][0]["results"].as_array().expect("results");
assert_eq!(results.len(), 1);
}
#[test]
fn scan_sarif_has_run_properties() {
let findings = vec![scan_finding(OracleVerdict::Confirmed, Some(Severity::High))];
let json =
render_scan_sarif("https://api.example.com/users/1", &findings).expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(
v["runs"][0]["properties"]["target_url"],
"https://api.example.com/users/1"
);
}
#[test]
fn scan_sarif_valid_json_with_version() {
let findings = vec![scan_finding(OracleVerdict::Confirmed, Some(Severity::High))];
let json =
render_scan_sarif("https://api.example.com/users/1", &findings).expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(v["version"], "2.1.0");
}
fn confirmed_endpoint_verdict() -> parlov_core::EndpointVerdict {
use parlov_core::{
EndpointStopReason, EndpointVerdict, ObservabilityStatus, OracleClass, OracleVerdict,
Severity,
};
EndpointVerdict {
oracle_class: OracleClass::Existence,
posterior_probability: 0.93,
verdict: OracleVerdict::Confirmed,
severity: Some(Severity::High),
strategies_run: 12,
strategies_total: 26,
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 endpoint_sarif_has_endpoint_verdict_in_run_properties() {
let findings = vec![scan_finding(OracleVerdict::Confirmed, Some(Severity::High))];
let verdict = confirmed_endpoint_verdict();
let json =
render_endpoint_verdict_sarif("https://api.example.com/users/1", &verdict, &findings)
.expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(v["runs"][0]["properties"]["endpoint_verdict"], "Confirmed");
}
#[test]
fn endpoint_sarif_has_posterior_probability_in_run_properties() {
let findings = vec![scan_finding(OracleVerdict::Confirmed, Some(Severity::High))];
let verdict = confirmed_endpoint_verdict();
let json =
render_endpoint_verdict_sarif("https://api.example.com/users/1", &verdict, &findings)
.expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
let p = v["runs"][0]["properties"]["posterior_probability"]
.as_f64()
.expect("f64");
assert!((p - 0.93).abs() < 1e-9);
}
#[test]
fn endpoint_sarif_has_strategies_run_and_total_in_run_properties() {
let findings = vec![scan_finding(OracleVerdict::Confirmed, Some(Severity::High))];
let verdict = confirmed_endpoint_verdict();
let json =
render_endpoint_verdict_sarif("https://api.example.com/users/1", &verdict, &findings)
.expect("render failed");
let v: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert_eq!(v["runs"][0]["properties"]["strategies_run"], 12);
assert_eq!(v["runs"][0]["properties"]["strategies_total"], 26);
}