mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
* feat: Enhance data exfiltration detection with source sensitivity gating for cookies and headers * feat: Implement cross-file data exfiltration detection with parameter-specific gate filters * feat: Add calibration tests and refine DATA_EXFIL severity scoring logic * feat: Introduce per-detector configuration for data exfiltration suppression * feat: Enhance DATA_EXFIL findings with destination field tracking in diagnostics and SARIF output * feat: Add tainted body and URL handling for data exfiltration detection * feat: Add integration tests and fixtures for DATA_EXFIL and SSRF detection in Go * feat: Add Java integration tests and fixtures for DATA_EXFIL detection across multiple HTTP clients * feat: Add synthetic externals handling for closure-captured variables in SSA * feat: Implement closure-based suppression for resource leak findings * feat: Add regression guards for shell-injection and taint propagation in for-of destructure patterns * feat: Implement constructor cap narrowing for data exfiltration detection in HTTP request builders * feat: Add gated sinks for data exfiltration detection in C and C++ using curl_easy_setopt * feat: Implement DATA_EXFIL cap parity for backwards analysis and add integration tests * feat: Add data exfiltration sinks for various languages and enhance documentation * refactor: Simplify formatting and improve readability in various files * refactor: Improve readability by simplifying conditional statements and adding clippy linting * docs: Update CHANGELOG and comments for data exfiltration features and configuration * docs: Clarify configuration instructions for data exfiltration trusted destinations * docs: Enhance comments for evidence routing logic in data exfiltration
143 lines
5.1 KiB
Rust
143 lines
5.1 KiB
Rust
//! Demand-driven backwards taint analysis integration tests.
|
|
//!
|
|
//! These tests exercise the full scan pipeline with the
|
|
//! `backwards_analysis` switch flipped on (via the `NYX_BACKWARDS`
|
|
//! environment variable that `analysis_options::current()` consults when
|
|
//! no runtime has been installed).
|
|
//!
|
|
//! The four fixture-backed sub-cases live on a single `#[test]` so the
|
|
//! env-var flip is serialised in-process (no `serial_test` dev-dep
|
|
//! needed). Inside the test we iterate the fixtures in-order, toggling
|
|
//! the env var before each sub-case so each exercises the intended
|
|
//! backwards on/off configuration.
|
|
//!
|
|
//! Assertions:
|
|
//! * Forward findings stay byte-stable in count / id (no regression).
|
|
//! * With backwards ON, a matching forward finding picks up a
|
|
//! `backwards-confirmed` cutoff note on `evidence.symbolic`.
|
|
//! * With backwards OFF, no forward finding carries such a note
|
|
//! (regression guard: the switch is honoured).
|
|
//! * Source-free fixtures emit no backwards-only standalones.
|
|
|
|
#![allow(clippy::expect_fun_call)]
|
|
|
|
mod common;
|
|
|
|
use common::{scan_fixture_dir, validate_expectations};
|
|
use nyx_scanner::commands::scan::Diag;
|
|
use nyx_scanner::utils::config::AnalysisMode;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn fixture_path(name: &str) -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
fn has_backwards_note(diag: &Diag, note: &str) -> bool {
|
|
diag.evidence
|
|
.as_ref()
|
|
.and_then(|e| e.symbolic.as_ref())
|
|
.is_some_and(|sv| sv.cutoff_notes.iter().any(|n| n == note))
|
|
}
|
|
|
|
fn count_backwards_confirmed(diags: &[Diag]) -> usize {
|
|
diags
|
|
.iter()
|
|
.filter(|d| has_backwards_note(d, "backwards-confirmed"))
|
|
.count()
|
|
}
|
|
|
|
fn set_backwards(enabled: bool) {
|
|
// SAFETY: this test runs as the only `#[test]` function in the
|
|
// binary, so the process has only the ambient test-harness threads
|
|
// at this point. We mutate a process-wide env var between
|
|
// sub-cases.
|
|
unsafe {
|
|
if enabled {
|
|
std::env::set_var("NYX_BACKWARDS", "1");
|
|
} else {
|
|
std::env::remove_var("NYX_BACKWARDS");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn demand_driven_suite() {
|
|
// ── 1. reach_source: backwards ON confirms the forward finding.
|
|
set_backwards(true);
|
|
let dir = fixture_path("demand_driven_reach_source");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
let confirmed = count_backwards_confirmed(&diags);
|
|
assert!(
|
|
confirmed >= 1,
|
|
"reach_source: expected ≥1 backwards-confirmed finding; got diags: {}",
|
|
diags
|
|
.iter()
|
|
.map(|d| format!("{}:{}", d.id, d.line))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
);
|
|
|
|
// ── 2. prove_infeasible: first cut keeps the forward finding.
|
|
set_backwards(true);
|
|
let dir = fixture_path("demand_driven_prove_infeasible");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
|
|
// ── 3. catch_new_fn: first cut reports forward; no reverse-edge yet.
|
|
set_backwards(true);
|
|
let dir = fixture_path("demand_driven_catch_new_fn");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
|
|
// ── 4. no_source: no findings emitted in either direction.
|
|
set_backwards(true);
|
|
let dir = fixture_path("demand_driven_no_source");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
assert_eq!(
|
|
count_backwards_confirmed(&diags),
|
|
0,
|
|
"no_source: no backwards-confirmed notes on a source-free fixture"
|
|
);
|
|
|
|
// ── 5. data_exfil cap parity: the backwards engine must
|
|
// round-trip `Cap::DATA_EXFIL` exactly like SQL/CMD/SSRF.
|
|
// The forward engine fires `taint-data-exfiltration`
|
|
// on a cookie → fetch-body flow; backwards must reach
|
|
// the request.cookies source and confirm.
|
|
set_backwards(true);
|
|
let dir = fixture_path("demand_driven_data_exfil");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
let exfil_confirmed = diags
|
|
.iter()
|
|
.filter(|d| {
|
|
d.id.starts_with("taint-data-exfiltration")
|
|
&& has_backwards_note(d, "backwards-confirmed")
|
|
})
|
|
.count();
|
|
assert!(
|
|
exfil_confirmed >= 1,
|
|
"data_exfil: expected ≥1 backwards-confirmed taint-data-exfiltration finding; got diags: {}",
|
|
diags
|
|
.iter()
|
|
.map(|d| format!("{}:{}", d.id, d.line))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
);
|
|
|
|
// ── 6. backwards OFF is a strict no-op: no confirmed notes.
|
|
set_backwards(false);
|
|
let dir = fixture_path("demand_driven_reach_source");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
assert_eq!(
|
|
count_backwards_confirmed(&diags),
|
|
0,
|
|
"backwards OFF must not emit any backwards-confirmed annotations"
|
|
);
|
|
}
|