nyx/tests/sarif_dynamic_verdict_tests.rs

276 lines
9 KiB
Rust

//! SARIF output tests for the dynamic verification vendor extension (§5.4).
//!
//! Acceptance criterion: SARIF output contains both
//! `partialFingerprints.dynamic_verdict_status` and
//! `properties.nyx_dynamic_verdict` for every `VerifyStatus` variant, and
//! both keys are absent when no dynamic verdict is attached.
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::evidence::{
AttemptSummary, Evidence, InconclusiveReason, UnsupportedReason, VerifyResult, VerifyStatus,
};
use nyx_scanner::output::build_sarif;
use nyx_scanner::patterns::{FindingCategory, Severity};
use std::path::Path;
fn base_diag() -> Diag {
Diag {
path: "/scan_root/src/main.rs".into(),
line: 10,
col: 5,
severity: Severity::High,
id: "taint-unsanitised-flow".into(),
category: FindingCategory::Security,
path_validated: false,
guard_kind: None,
message: None,
labels: vec![],
confidence: None,
evidence: None,
rank_score: None,
rank_reason: None,
suppressed: false,
suppression: None,
triage_state: "open".to_string(),
triage_note: String::new(),
rollup: None,
finding_id: "deadbeef01234567".into(),
alternative_finding_ids: Vec::new(),
stable_hash: 0,
}
}
fn diag_with_verdict(verdict: VerifyResult) -> Diag {
let mut d = base_diag();
d.evidence = Some(Evidence {
dynamic_verdict: Some(verdict),
..Default::default()
});
d
}
// ── Helpers ──────────────────────────────────────────────────────────────────
fn sarif_result(diag: Diag) -> serde_json::Value {
let sarif = build_sarif(&[diag], Path::new("/scan_root"));
sarif["runs"][0]["results"][0].clone()
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[test]
fn sarif_confirmed_verdict_sets_partial_fingerprint() {
let verdict = VerifyResult {
finding_id: "deadbeef01234567".into(),
status: VerifyStatus::Confirmed,
triggered_payload: Some("sqli-tautology".into()),
reason: None,
inconclusive_reason: None,
detail: None,
attempts: vec![AttemptSummary {
payload_label: "sqli-tautology".into(),
exit_code: Some(0),
timed_out: false,
triggered: true,
sink_hit: true,
}],
toolchain_match: Some("exact".into()),
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
assert_eq!(
result["partialFingerprints"]["dynamic_verdict_status"], "Confirmed",
"partialFingerprints.dynamic_verdict_status must be 'Confirmed'"
);
assert!(
result["properties"]["nyx_dynamic_verdict"].is_object(),
"properties.nyx_dynamic_verdict must be an object: {}",
result["properties"]["nyx_dynamic_verdict"]
);
assert_eq!(
result["properties"]["nyx_dynamic_verdict"]["status"], "Confirmed",
"nyx_dynamic_verdict.status must be 'Confirmed'"
);
}
#[test]
fn sarif_not_confirmed_verdict_sets_partial_fingerprint() {
let verdict = VerifyResult {
finding_id: "deadbeef01234567".into(),
status: VerifyStatus::NotConfirmed,
triggered_payload: None,
reason: None,
inconclusive_reason: None,
detail: None,
attempts: vec![],
toolchain_match: Some("exact".into()),
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
assert_eq!(
result["partialFingerprints"]["dynamic_verdict_status"], "NotConfirmed",
"partialFingerprints.dynamic_verdict_status must be 'NotConfirmed'"
);
assert!(
result["properties"]["nyx_dynamic_verdict"].is_object(),
"properties.nyx_dynamic_verdict must be an object"
);
}
#[test]
fn sarif_unsupported_verdict_sets_partial_fingerprint() {
let verdict = VerifyResult {
finding_id: "deadbeef01234567".into(),
status: VerifyStatus::Unsupported,
triggered_payload: None,
reason: Some(UnsupportedReason::NoPayloadsForCap),
inconclusive_reason: None,
detail: None,
attempts: vec![],
toolchain_match: None,
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
assert_eq!(
result["partialFingerprints"]["dynamic_verdict_status"], "Unsupported",
"partialFingerprints.dynamic_verdict_status must be 'Unsupported'"
);
assert!(
result["properties"]["nyx_dynamic_verdict"].is_object(),
"properties.nyx_dynamic_verdict must be an object"
);
assert_eq!(
result["properties"]["nyx_dynamic_verdict"]["reason"], "NoPayloadsForCap",
"nyx_dynamic_verdict must carry the unsupported reason"
);
}
#[test]
fn sarif_inconclusive_verdict_sets_partial_fingerprint() {
let verdict = VerifyResult {
finding_id: "deadbeef01234567".into(),
status: VerifyStatus::Inconclusive,
triggered_payload: None,
reason: None,
inconclusive_reason: Some(InconclusiveReason::BuildFailed),
detail: Some("build failed after 3 attempts".into()),
attempts: vec![],
toolchain_match: None,
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
assert_eq!(
result["partialFingerprints"]["dynamic_verdict_status"], "Inconclusive",
"partialFingerprints.dynamic_verdict_status must be 'Inconclusive'"
);
assert!(
result["properties"]["nyx_dynamic_verdict"].is_object(),
"properties.nyx_dynamic_verdict must be an object"
);
assert_eq!(
result["properties"]["nyx_dynamic_verdict"]["inconclusive_reason"], "BuildFailed",
"nyx_dynamic_verdict must carry the inconclusive reason"
);
}
#[test]
fn sarif_no_dynamic_verdict_omits_both_keys() {
let diag = base_diag();
let result = sarif_result(diag);
assert!(
result["partialFingerprints"].is_null()
|| result["partialFingerprints"] == serde_json::Value::Null,
"partialFingerprints must be absent when no dynamic verdict: {}",
result["partialFingerprints"]
);
assert!(
result["properties"]["nyx_dynamic_verdict"].is_null()
|| result["properties"]["nyx_dynamic_verdict"] == serde_json::Value::Null,
"properties.nyx_dynamic_verdict must be absent when no dynamic verdict"
);
}
#[test]
fn sarif_confirmed_verdict_nyx_dynamic_verdict_contains_triggered_payload() {
let verdict = VerifyResult {
finding_id: "deadbeef01234567".into(),
status: VerifyStatus::Confirmed,
triggered_payload: Some("cmd-injection-semicolon".into()),
reason: None,
inconclusive_reason: None,
detail: None,
attempts: vec![],
toolchain_match: Some("exact".into()),
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
assert_eq!(
result["properties"]["nyx_dynamic_verdict"]["triggered_payload"], "cmd-injection-semicolon",
"triggered_payload must appear in nyx_dynamic_verdict"
);
}
#[test]
fn sarif_all_statuses_produce_partial_fingerprint() {
let statuses = [
(VerifyStatus::Confirmed, "Confirmed"),
(VerifyStatus::PartiallyConfirmed, "PartiallyConfirmed"),
(VerifyStatus::NotConfirmed, "NotConfirmed"),
(VerifyStatus::Unsupported, "Unsupported"),
(VerifyStatus::Inconclusive, "Inconclusive"),
];
for (status, expected_str) in statuses {
let verdict = VerifyResult {
finding_id: "deadbeef01234567".into(),
status,
triggered_payload: None,
reason: None,
inconclusive_reason: None,
detail: None,
attempts: vec![],
toolchain_match: None,
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
};
let result = sarif_result(diag_with_verdict(verdict));
assert_eq!(
result["partialFingerprints"]["dynamic_verdict_status"], expected_str,
"status {expected_str}: partialFingerprints.dynamic_verdict_status mismatch"
);
assert!(
result["properties"]["nyx_dynamic_verdict"].is_object(),
"status {expected_str}: properties.nyx_dynamic_verdict must be an object"
);
}
}