mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
276 lines
9 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|