use serde::{Deserialize, Serialize};
use crate::payload::AttackProvenance;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum VerifyVerdict {
Confirmed,
NotConfirmed,
Errored,
}
impl VerifyVerdict {
pub fn as_str(self) -> &'static str {
match self {
VerifyVerdict::Confirmed => "Confirmed",
VerifyVerdict::NotConfirmed => "NotConfirmed",
VerifyVerdict::Errored => "Errored",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Oracle {
OutputContains { marker: String },
SinkProbe {
sentinel_path: String,
#[serde(default)]
expect_contains: Option<String>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifyRun {
pub payload: Vec<u8>,
pub oracle_fired: bool,
pub exit_code: i32,
pub timed_out: bool,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub duration_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerifyResult {
pub finding_id: String,
pub verdict: VerifyVerdict,
pub oracle: Oracle,
pub vuln_run: VerifyRun,
pub benign_run: VerifyRun,
pub attack_provenance: AttackProvenance,
#[serde(default)]
pub replay_stable: Option<bool>,
#[serde(default)]
pub error_message: Option<String>,
}
impl VerifyResult {
pub fn from_runs(
finding_id: String,
oracle: Oracle,
vuln_run: VerifyRun,
benign_run: VerifyRun,
attack_provenance: AttackProvenance,
) -> Self {
let verdict = if vuln_run.oracle_fired && !benign_run.oracle_fired {
VerifyVerdict::Confirmed
} else {
VerifyVerdict::NotConfirmed
};
Self {
finding_id,
verdict,
oracle,
vuln_run,
benign_run,
attack_provenance,
replay_stable: None,
error_message: None,
}
}
pub fn errored(
finding_id: String,
oracle: Oracle,
vuln_run: VerifyRun,
benign_run: VerifyRun,
attack_provenance: AttackProvenance,
message: String,
) -> Self {
Self {
finding_id,
verdict: VerifyVerdict::Errored,
oracle,
vuln_run,
benign_run,
attack_provenance,
replay_stable: None,
error_message: Some(message),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(oracle_fired: bool) -> VerifyRun {
VerifyRun {
payload: b"x".to_vec(),
oracle_fired,
exit_code: 0,
timed_out: false,
stdout: Vec::new(),
stderr: Vec::new(),
duration_ms: 5,
}
}
#[test]
fn confirmed_iff_vuln_fires_and_benign_clean() {
let oracle = Oracle::OutputContains { marker: "X".into() };
let v = VerifyResult::from_runs(
"f".into(),
oracle.clone(),
run(true),
run(false),
AttackProvenance::Curated,
);
assert_eq!(v.verdict, VerifyVerdict::Confirmed);
let v = VerifyResult::from_runs(
"f".into(),
oracle.clone(),
run(false),
run(false),
AttackProvenance::Curated,
);
assert_eq!(v.verdict, VerifyVerdict::NotConfirmed);
let v = VerifyResult::from_runs(
"f".into(),
oracle.clone(),
run(true),
run(true),
AttackProvenance::Curated,
);
assert_eq!(v.verdict, VerifyVerdict::NotConfirmed, "benign trip ruins the differential");
let v = VerifyResult::from_runs(
"f".into(),
oracle,
run(false),
run(true),
AttackProvenance::Curated,
);
assert_eq!(v.verdict, VerifyVerdict::NotConfirmed);
}
#[test]
fn errored_carries_message_and_provenance() {
let oracle =
Oracle::SinkProbe { sentinel_path: ".nyx/sentinel".into(), expect_contains: None };
let v = VerifyResult::errored(
"f".into(),
oracle,
run(false),
run(false),
AttackProvenance::LlmSynthesised,
"harness setup failed".into(),
);
assert_eq!(v.verdict, VerifyVerdict::Errored);
assert_eq!(v.error_message.as_deref(), Some("harness setup failed"));
assert_eq!(v.attack_provenance, AttackProvenance::LlmSynthesised);
}
#[test]
fn verify_result_roundtrips_through_serde() {
let oracle = Oracle::OutputContains { marker: "leak".into() };
let v = VerifyResult::from_runs(
"fid".into(),
oracle,
run(true),
run(false),
AttackProvenance::LlmSynthesised,
);
let s = serde_json::to_string(&v).unwrap();
let back: VerifyResult = serde_json::from_str(&s).unwrap();
assert_eq!(back, v);
}
}