mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
239 lines
7.9 KiB
Rust
239 lines
7.9 KiB
Rust
//! Snapshot-style tests for the `[DYN: ...]` annotation in console output.
|
|
//!
|
|
//! Each `VerifyStatus` variant must produce the correct dim annotation line
|
|
//! beneath the finding block when `evidence.dynamic_verdict` is set.
|
|
|
|
use nyx_scanner::commands::scan::Diag;
|
|
use nyx_scanner::evidence::{
|
|
AttemptSummary, Evidence, InconclusiveReason, UnsupportedReason, VerifyResult, VerifyStatus,
|
|
};
|
|
use nyx_scanner::fmt::render_console;
|
|
use nyx_scanner::patterns::{FindingCategory, Severity};
|
|
|
|
// ── Helper ───────────────────────────────────────────────────────────────────
|
|
|
|
fn strip_ansi(s: &str) -> String {
|
|
let mut out = String::new();
|
|
let mut in_escape = false;
|
|
for ch in s.chars() {
|
|
if ch == '\x1b' {
|
|
in_escape = true;
|
|
} else if in_escape {
|
|
if ch == 'm' {
|
|
in_escape = false;
|
|
}
|
|
} else {
|
|
out.push(ch);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn base_diag() -> Diag {
|
|
Diag {
|
|
path: "src/main.rs".into(),
|
|
line: 42,
|
|
col: 5,
|
|
severity: Severity::High,
|
|
id: "taint-unsanitised-flow".into(),
|
|
category: FindingCategory::Security,
|
|
path_validated: false,
|
|
guard_kind: None,
|
|
message: Some("unsanitised input flows to exec".into()),
|
|
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: String::new(),
|
|
alternative_finding_ids: Vec::new(),
|
|
stable_hash: 0,
|
|
}
|
|
}
|
|
|
|
fn diag_with_verdict(status: VerifyStatus) -> Diag {
|
|
let verdict = match status {
|
|
VerifyStatus::Confirmed => VerifyResult {
|
|
finding_id: "abc123".into(),
|
|
status,
|
|
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,
|
|
},
|
|
VerifyStatus::PartiallyConfirmed => VerifyResult {
|
|
finding_id: "abc123".into(),
|
|
status,
|
|
triggered_payload: None,
|
|
reason: None,
|
|
inconclusive_reason: None,
|
|
detail: Some(
|
|
"sink-reachability probe fired but the oracle marker was not observed; exploit chain did not complete".into(),
|
|
),
|
|
attempts: vec![AttemptSummary {
|
|
payload_label: "sqli-tautology".into(),
|
|
exit_code: Some(0),
|
|
timed_out: false,
|
|
triggered: false,
|
|
sink_hit: true,
|
|
}],
|
|
toolchain_match: Some("exact".into()),
|
|
differential: None,
|
|
replay_stable: None,
|
|
wrong: None,
|
|
hardening_outcome: None,
|
|
},
|
|
VerifyStatus::NotConfirmed => VerifyResult {
|
|
finding_id: "abc123".into(),
|
|
status,
|
|
triggered_payload: None,
|
|
reason: None,
|
|
inconclusive_reason: None,
|
|
detail: None,
|
|
attempts: vec![AttemptSummary {
|
|
payload_label: "sqli-tautology".into(),
|
|
exit_code: Some(0),
|
|
timed_out: false,
|
|
triggered: false,
|
|
sink_hit: false,
|
|
}],
|
|
toolchain_match: Some("exact".into()),
|
|
differential: None,
|
|
replay_stable: None,
|
|
wrong: None,
|
|
hardening_outcome: None,
|
|
},
|
|
VerifyStatus::Unsupported => VerifyResult {
|
|
finding_id: "abc123".into(),
|
|
status,
|
|
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,
|
|
},
|
|
VerifyStatus::Inconclusive => VerifyResult {
|
|
finding_id: "abc123".into(),
|
|
status,
|
|
triggered_payload: None,
|
|
reason: None,
|
|
inconclusive_reason: Some(InconclusiveReason::BuildFailed),
|
|
detail: Some("build failed after 3 attempts: linker error".into()),
|
|
attempts: vec![],
|
|
toolchain_match: None,
|
|
differential: None,
|
|
replay_stable: None,
|
|
wrong: None,
|
|
hardening_outcome: None,
|
|
},
|
|
};
|
|
|
|
let mut d = base_diag();
|
|
d.evidence = Some(Evidence {
|
|
dynamic_verdict: Some(verdict),
|
|
..Default::default()
|
|
});
|
|
d
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn console_confirmed_shows_payload_id() {
|
|
let diag = diag_with_verdict(VerifyStatus::Confirmed);
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
stripped.contains("[DYN: confirmed via sqli-tautology]"),
|
|
"expected DYN confirmed annotation, got:\n{stripped}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_not_confirmed_shows_annotation() {
|
|
let diag = diag_with_verdict(VerifyStatus::NotConfirmed);
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
stripped.contains("[DYN: not confirmed]"),
|
|
"expected DYN not-confirmed annotation, got:\n{stripped}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_partially_confirmed_shows_sink_reached() {
|
|
let diag = diag_with_verdict(VerifyStatus::PartiallyConfirmed);
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
stripped.contains("[DYN: partially confirmed (sink reached)]"),
|
|
"expected DYN partially-confirmed annotation, got:\n{stripped}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_unsupported_shows_reason() {
|
|
let diag = diag_with_verdict(VerifyStatus::Unsupported);
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
stripped.contains("[DYN: unsupported (no payloads for cap)]"),
|
|
"expected DYN unsupported annotation, got:\n{stripped}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_inconclusive_shows_reason() {
|
|
let diag = diag_with_verdict(VerifyStatus::Inconclusive);
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
stripped.contains("[DYN: inconclusive (build failed)]"),
|
|
"expected DYN inconclusive annotation, got:\n{stripped}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_no_annotation_when_no_dynamic_verdict() {
|
|
let diag = base_diag();
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
!stripped.contains("[DYN:"),
|
|
"expected no DYN annotation when evidence is None:\n{stripped}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_no_annotation_when_evidence_has_no_verdict() {
|
|
let mut diag = base_diag();
|
|
diag.evidence = Some(Evidence::default());
|
|
let output = render_console(&[diag], "proj", None, &[]);
|
|
let stripped = strip_ansi(&output);
|
|
assert!(
|
|
!stripped.contains("[DYN:"),
|
|
"expected no DYN annotation when dynamic_verdict is None:\n{stripped}"
|
|
);
|
|
}
|