mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
166 lines
6.2 KiB
Rust
166 lines
6.2 KiB
Rust
|
|
//! Phase 07 — differential confirmation rule (`differential::evaluate`).
|
||
|
|
//!
|
||
|
|
//! These tests pin the pure-function behaviour of the differential rule
|
||
|
|
//! (§4.1): given the (vulnerable, benign-control) oracle firing booleans
|
||
|
|
//! produce the right verdict. Each case has a matching paragraph in the
|
||
|
|
//! plan's acceptance criteria.
|
||
|
|
//!
|
||
|
|
//! The harness here does *not* spawn a sandbox — it exercises the rule
|
||
|
|
//! independently of payload corpus, sandbox availability, or per-language
|
||
|
|
//! toolchains. Integration coverage that runs both payloads end-to-end
|
||
|
|
//! lives in `tests/{python,rust}_fixtures.rs` and the golden harness from
|
||
|
|
//! Phase 05.
|
||
|
|
|
||
|
|
#![cfg(feature = "dynamic")]
|
||
|
|
|
||
|
|
use nyx_scanner::dynamic::differential::{build_outcome, evaluate};
|
||
|
|
use nyx_scanner::dynamic::probe::{ProbeArg, ProbeKind, ProbeWitness, SinkProbe};
|
||
|
|
use nyx_scanner::evidence::DifferentialVerdict;
|
||
|
|
|
||
|
|
// ── Rule table ──────────────────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// | vuln fires | benign fires | verdict |
|
||
|
|
// |------------|--------------|-------------------------------|
|
||
|
|
// | true | true | OracleCollisionSuspected (a) |
|
||
|
|
// | true | false | Confirmed (b) |
|
||
|
|
// | false | false | NotConfirmed (c) |
|
||
|
|
// | false | true | ReversedDifferential (d) |
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn case_a_both_fire_is_oracle_collision() {
|
||
|
|
assert_eq!(
|
||
|
|
evaluate(true, true),
|
||
|
|
DifferentialVerdict::OracleCollisionSuspected,
|
||
|
|
"both vulnerable and benign firing must downgrade to OracleCollisionSuspected"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn case_b_only_vuln_fires_is_confirmed() {
|
||
|
|
assert_eq!(
|
||
|
|
evaluate(true, false),
|
||
|
|
DifferentialVerdict::Confirmed,
|
||
|
|
"vuln fires + benign silent is the canonical Confirmed shape"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn case_c_neither_fires_is_not_confirmed() {
|
||
|
|
assert_eq!(
|
||
|
|
evaluate(false, false),
|
||
|
|
DifferentialVerdict::NotConfirmed,
|
||
|
|
"zero firings is plain NotConfirmed (nothing to triage)"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn case_d_only_benign_fires_is_reversed_differential() {
|
||
|
|
assert_eq!(
|
||
|
|
evaluate(false, true),
|
||
|
|
DifferentialVerdict::ReversedDifferential,
|
||
|
|
"only-benign-fires surfaces a misconfigured corpus, never a real Confirmed"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── build_outcome plumbing ───────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// `build_outcome` is what the runner actually calls — it stamps the
|
||
|
|
// verdict and converts native [`SinkProbe`] records into the serde-stable
|
||
|
|
// shape stored on `VerifyResult`. These tests pin the conversion.
|
||
|
|
|
||
|
|
fn sample_probe(callee: &str, arg: &str, label: &str) -> SinkProbe {
|
||
|
|
SinkProbe {
|
||
|
|
sink_callee: callee.into(),
|
||
|
|
args: vec![ProbeArg::String(arg.into())],
|
||
|
|
captured_at_ns: 1,
|
||
|
|
payload_id: label.into(),
|
||
|
|
kind: ProbeKind::Normal,
|
||
|
|
witness: ProbeWitness::empty(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn build_outcome_confirmed_carries_both_traces() {
|
||
|
|
let vuln = vec![sample_probe(
|
||
|
|
"os.system",
|
||
|
|
"; echo NYX_PWN_CMDI",
|
||
|
|
"cmdi-echo-marker",
|
||
|
|
)];
|
||
|
|
let benign = vec![sample_probe("os.system", "benign_safe_cmdi", "cmdi-benign")];
|
||
|
|
let outcome = build_outcome(
|
||
|
|
"cmdi-echo-marker",
|
||
|
|
true,
|
||
|
|
&vuln,
|
||
|
|
"cmdi-benign",
|
||
|
|
false,
|
||
|
|
&benign,
|
||
|
|
);
|
||
|
|
assert_eq!(outcome.verdict, DifferentialVerdict::Confirmed);
|
||
|
|
assert_eq!(outcome.vuln_label, "cmdi-echo-marker");
|
||
|
|
assert_eq!(outcome.benign_label, "cmdi-benign");
|
||
|
|
assert_eq!(outcome.vuln_probes.len(), 1);
|
||
|
|
assert_eq!(outcome.benign_probes.len(), 1);
|
||
|
|
assert_eq!(outcome.vuln_probes[0].sink_callee, "os.system");
|
||
|
|
assert_eq!(outcome.vuln_probes[0].payload_id, "cmdi-echo-marker");
|
||
|
|
assert_eq!(outcome.benign_probes[0].payload_id, "cmdi-benign");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn build_outcome_oracle_collision_keeps_both_traces() {
|
||
|
|
let vuln = vec![sample_probe("os.system", "a", "v")];
|
||
|
|
let benign = vec![sample_probe("os.system", "b", "b")];
|
||
|
|
let outcome = build_outcome("v", true, &vuln, "b", true, &benign);
|
||
|
|
assert_eq!(
|
||
|
|
outcome.verdict,
|
||
|
|
DifferentialVerdict::OracleCollisionSuspected
|
||
|
|
);
|
||
|
|
assert_eq!(outcome.vuln_probes.len(), 1);
|
||
|
|
assert_eq!(outcome.benign_probes.len(), 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn build_outcome_not_confirmed_records_empty_traces() {
|
||
|
|
let outcome = build_outcome("v", false, &[], "b", false, &[]);
|
||
|
|
assert_eq!(outcome.verdict, DifferentialVerdict::NotConfirmed);
|
||
|
|
assert!(outcome.vuln_probes.is_empty());
|
||
|
|
assert!(outcome.benign_probes.is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn build_outcome_reversed_records_benign_only_trace() {
|
||
|
|
let benign = vec![sample_probe("os.system", "x", "b")];
|
||
|
|
let outcome = build_outcome("v", false, &[], "b", true, &benign);
|
||
|
|
assert_eq!(outcome.verdict, DifferentialVerdict::ReversedDifferential);
|
||
|
|
assert!(outcome.vuln_probes.is_empty());
|
||
|
|
assert_eq!(outcome.benign_probes.len(), 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Serde stability ──────────────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// `VerifyResult.differential` is part of the public verdict JSON shape
|
||
|
|
// (consumed by SARIF emitters, the React frontend, and the verdict cache).
|
||
|
|
// Pin the wire format.
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn differential_outcome_serialises_as_pascal_case_verdict() {
|
||
|
|
let outcome = build_outcome("v", true, &[], "b", false, &[]);
|
||
|
|
let json = serde_json::to_value(&outcome).expect("serialise");
|
||
|
|
assert_eq!(json["verdict"], "Confirmed");
|
||
|
|
assert_eq!(json["vuln_label"], "v");
|
||
|
|
assert_eq!(json["benign_label"], "b");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn differential_verdict_round_trips_through_json() {
|
||
|
|
for v in [
|
||
|
|
DifferentialVerdict::Confirmed,
|
||
|
|
DifferentialVerdict::OracleCollisionSuspected,
|
||
|
|
DifferentialVerdict::NotConfirmed,
|
||
|
|
DifferentialVerdict::ReversedDifferential,
|
||
|
|
] {
|
||
|
|
let json = serde_json::to_string(&v).unwrap();
|
||
|
|
let back: DifferentialVerdict = serde_json::from_str(&json).unwrap();
|
||
|
|
assert_eq!(v, back);
|
||
|
|
}
|
||
|
|
}
|