nyx/tests/pointer_disabled_bit_identity.rs
Eli Peter 82f18184b1
Prerelease cleanup (#46)
* feat: Add const_bound_vars tracking to prevent false positives in ownership checks

* feat: Introduce field interner and typed bounded vars for enhanced type tracking

* feat: Add typed_call_receivers and typed_bounded_dto_fields for enhanced type tracking

* feat: Centralize method name extraction with bare_method_name helper

* feat: Implement Phase-6 hierarchy fan-out for runtime virtual dispatch

* feat: Enhance C++ taint tracking with additional container operations and inline method resolution

* feat: Introduce field-sensitive points-to analysis for enhanced resource tracking

* feat: Implement Pointer-Phase 6 subscript handling for enhanced container analysis

* test: Add comprehensive tests for JavaScript control flow constructs and lattice operations

* docs: Update advanced analysis documentation with field-sensitive points-to and hierarchy fan-out details

* test: Add comprehensive tests for lattice algebra laws and SSA edge cases

* feat: Add destructured session user handling and safe user ID access patterns

* feat: Implement row-population reverse-walk for enhanced authorization checks

* feat: Enhance authorization checks with local alias chain for self-actor types

* feat: Introduce ActiveRecord query safety checks and enhance snippet extraction

* feat: Implement chained method call inner-gate rebinding for SSRF prevention

* feat: Add observability and error modules, enhance debug functionality, and implement theme context

* feat: Remove Auth Analysis page and update navigation to redirect to Explorer

* feat: Optimize SSA lowering by sharing results between taint engine and artifact extractor

* feat: Optimize SSA lowering by sharing results between taint engine and artifact extractor

* feat: Reset path-safe-suppressed spans before lowering to maintain analysis integrity

* fix(ssa): ungate debug_assert_bfs_ordering for release-tests build

The helper at src/ssa/lower.rs was gated `#[cfg(debug_assertions)]` while
the unit test at the bottom of the file was gated only `#[cfg(test)]`.
Since `cfg(test)` is set in release builds with `--tests` but
`cfg(debug_assertions)` is not, `cargo build --release --tests` failed
with E0425. Removing the gate fixes the build; the body is `debug_assert!`
only, so the helper is free in release. Also drop the gate at the call
site to avoid a `dead_code` warning when the lib is built without
`--tests`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(closure-capture): flip JS/TS fixtures to required-finding

The JS and TS closure-capture fixtures pinned the old broken behaviour
via `forbidden_findings: [{ "id_prefix": "taint-" }]`. The engine now
correctly traces taint through the closure boundary (env source captured
by an arrow function, sunk via `child_process.exec` inside the body), so
the formerly-forbidden finding is a true positive.

Match the Python sibling's shape — `required_findings` with
`id_prefix` + `min_count` plus a small `noise_budget` — and rewrite the
companion READMEs and the phase8_fragility_tests doc-comments from
"known gap" to "regression guard".

Verified:
- cargo test --release --test phase8_fragility_tests → 8/8 pass
- cargo test --release --lib bfs_assertion → pass
- corpus benchmark F1 = 0.9976 (TP=205, FP=1, FN=0) — unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: Add OWASP mapping and baseline mutation hooks for enhanced security analysis

* feat: Introduce health module and enhance health score computation with calibration tests

* feat: Add expectations configuration and cleanup .gitignore for log files

* feat: Implement theme selection and enhance settings panel for triage sync

* feat: Suppress false positives for strcpy calls with literal sources in AST

* feat: Update analyse_function_ssa to return body CFG for accurate analysis

* feat: Add bug report and feature request templates for improved issue tracking

* feat: removed dev scripts

* feat: update README.md for clarity and consistency in fixture descriptions

* feat: removed dev docs

* feat: clean up error handling and UI elements for improved user experience

* feat: adjust button sizes in HeaderBar for better UI consistency

* feat: enhance taint analysis with additional context for sanitizer and taint findings

* cargo fmt

* prettier

* refactor: simplify conditional checks and improve code readability in AST and screenshot capture scripts

* feat: add script to frame PNG screenshots with brand gradient

* feat: add fuzzing support with new targets and CI workflows

* refactor: streamline match expressions and improve formatting in CLI and output handling

* feat: enhance configuration display with detailed output options

* feat: stage demo configuration for improved CLI screenshot output

* feat: expose merge_configs function for user-configurable settings

* refactor: simplify code structure and improve readability in config handling

* refactor: improve descriptions for vulnerability patterns in various languages

* feat: update MIT License section with additional usage details and copyright information

* feat: update screenshots

* refactor: update build process and paths for frontend assets

* feat: add cross-file taint fuzzing target and supporting dictionary

* refactor: clean up formatting and comments in fuzz configuration and example files

* refactor: remove outdated comments and clean up CI configuration files

* chore: update changelog dates and improve formatting in documentation

* refactor: update Cargo.toml and CI configuration for improved packaging and build process

* refactor: enhance quote-stripping logic to prevent panics and add regression tests

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:58:38 -04:00

205 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! A1 / A4: env-var-toggle bit-identity gates for the
//! `NYX_POINTER_ANALYSIS` flag.
//!
//! These tests guard the strict-additive contract that the pointer
//! analysis module promises: when off (`NYX_POINTER_ANALYSIS=0` or
//! unset), the engine must produce a finding set bit-identical to the
//! pre-pointer baseline. When on (`=1`), the finding set must be a
//! superset that DROPS no genuine findings.
//!
//! Both modes are exercised in the same test process via a serial
//! mutex around env-var manipulation — cargo runs tests in parallel
//! and an unprotected env-var write would leak between threads.
//!
//! A4 baseline snapshot: when the env variable
//! `UPDATE_SNAPSHOTS=1` is set, the disabled-mode finding set is
//! written to `tests/snapshots/pointer_disabled_baseline.json`.
//! Otherwise the test verifies the disabled-mode set matches the
//! checked-in snapshot. This guards against silent finding-set
//! drift across unrelated engine changes.
mod common;
use common::scan_fixture_dir;
use nyx_scanner::utils::config::AnalysisMode;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::Mutex;
/// Process-wide guard: env-var writes from one test thread would race
/// with reads from another. Every test in this file claims this
/// guard before touching `NYX_POINTER_ANALYSIS`.
static ENV_VAR_GUARD: Mutex<()> = Mutex::new(());
const ENV_VAR: &str = "NYX_POINTER_ANALYSIS";
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(name)
}
/// Fixture mix curated for the strict-additive guard. Picks shapes
/// the pointer module actively touches:
///
/// * `container_taint_js` — JS container ops (push/shift/pop) flow
/// through the W2 / W4 ELEM cells when pointer is on.
/// * `container_taint_py` — Python container shapes mirror the JS path
/// for non-method `__getitem__` / `__setitem__` (W5; deferred but
/// the existing method-shape ops are still exercised).
/// * `cross_file_py_object_field` — field-flow shapes that exercise
/// the W1 / W3 cross-call resolver with field-name keys.
///
/// Picked deliberately small: every additional fixture multiplies the
/// runtime by ~1×, and these three already span container element
/// flow + field flow + cross-call propagation.
const CURATED_FIXTURES: &[&str] = &[
"container_taint_js",
"container_taint_py",
"cross_file_py_object_field",
];
/// One scan, one (path, line, col, id) tuple per finding. Stripped
/// of all derived fields (rank, evidence, message, etc.) so the
/// comparison is robust to incidental ranking / formatting changes
/// while still anchoring on the structural identity of each finding.
type FindingId = (String, usize, usize, String);
fn collect_finding_ids(fixture: &str) -> BTreeSet<FindingId> {
let dir = fixture_path(fixture);
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
let manifest_dir = env!("CARGO_MANIFEST_DIR");
diags
.into_iter()
.map(|d| {
let rel = d
.path
.strip_prefix(manifest_dir)
.map(|s| s.trim_start_matches('/').to_string())
.unwrap_or(d.path);
(rel, d.line, d.col, d.id)
})
.collect()
}
/// Run a closure with `NYX_POINTER_ANALYSIS=value` set, restoring the
/// prior environment afterwards. The guard is held across the
/// closure so concurrent tests don't race. SAFETY: cargo's test
/// harness runs each test on its own thread; Rust's std `set_var` is
/// thread-unsafe in principle, but with the process-wide guard no
/// concurrent reader can observe a torn write.
fn with_env<F, R>(value: &str, f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = ENV_VAR_GUARD.lock().unwrap_or_else(|e| e.into_inner());
let prior = std::env::var(ENV_VAR).ok();
// SAFETY: see function-level note.
unsafe { std::env::set_var(ENV_VAR, value) };
let r = f();
// Restore prior environment.
match prior {
Some(v) => unsafe { std::env::set_var(ENV_VAR, v) },
None => unsafe { std::env::remove_var(ENV_VAR) },
}
r
}
/// A1: scanning each curated fixture under `NYX_POINTER_ANALYSIS=0`
/// and `=1` produces the same set of `(path, line, col, id)` tuples.
///
/// Strict-additive contract: pointer analysis must only suppress FPs
/// (or surface new findings via fixtures we haven't included here);
/// it must not change the structural identity of any existing
/// finding. The current curated fixtures exercise shapes the
/// pointer module touches but where existing engine analyses already
/// produce all the findings — so the equality check is the right
/// shape today. When pointer-on starts adding NEW findings to these
/// fixtures, the test should be updated to assert
/// `enabled.is_superset(disabled)`.
#[test]
fn pointer_toggle_preserves_finding_set() {
for &fixture in CURATED_FIXTURES {
let disabled = with_env("0", || collect_finding_ids(fixture));
let enabled = with_env("1", || collect_finding_ids(fixture));
assert_eq!(
disabled,
enabled,
"NYX_POINTER_ANALYSIS toggle must preserve the finding \
set on fixture {fixture:?}. off-only: {:#?}\non-only: {:#?}",
disabled.difference(&enabled).collect::<Vec<_>>(),
enabled.difference(&disabled).collect::<Vec<_>>(),
);
}
}
/// A4: bit-identity baseline. Captures the current pointer-disabled
/// finding set on the curated fixtures and pins it to a checked-in
/// snapshot. Refresh with:
///
/// ```bash
/// UPDATE_SNAPSHOTS=1 cargo test --test pointer_disabled_bit_identity \
/// pointer_disabled_finding_set_matches_baseline
/// ```
///
/// The snapshot lives next to the test, not under `tests/snapshots/`,
/// so a checkout-only-files-changed diff highlights this baseline
/// alongside its test.
#[test]
fn pointer_disabled_finding_set_matches_baseline() {
let snapshot_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("snapshots")
.join("pointer_disabled_baseline.json");
// Collect the disabled-mode finding set across the curated mix.
let mut current: Vec<(String, Vec<FindingId>)> = CURATED_FIXTURES
.iter()
.map(|f| {
let ids = with_env("0", || collect_finding_ids(f));
(f.to_string(), ids.into_iter().collect())
})
.collect();
// Deterministic ordering.
current.sort_by(|a, b| a.0.cmp(&b.0));
if std::env::var("UPDATE_SNAPSHOTS").as_deref() == Ok("1") {
// Write snapshot.
if let Some(parent) = snapshot_path.parent() {
std::fs::create_dir_all(parent).expect("failed to create snapshots dir");
}
let json = serde_json::to_string_pretty(&current).expect("failed to serialize finding set");
std::fs::write(&snapshot_path, &json).expect("failed to write snapshot");
eprintln!("Snapshot written: {}", snapshot_path.display());
return;
}
let snapshot_text = match std::fs::read_to_string(&snapshot_path) {
Ok(s) => s,
Err(_) => {
// First run / missing snapshot — write it and skip the
// diff check. Subsequent runs will assert against this
// captured value.
if let Some(parent) = snapshot_path.parent() {
std::fs::create_dir_all(parent).expect("failed to create snapshots dir");
}
let json =
serde_json::to_string_pretty(&current).expect("failed to serialize finding set");
std::fs::write(&snapshot_path, &json).expect("failed to write snapshot");
eprintln!(
"Initial snapshot written to {}; re-run to verify.",
snapshot_path.display()
);
return;
}
};
let baseline: Vec<(String, Vec<FindingId>)> =
serde_json::from_str(&snapshot_text).expect("failed to parse baseline JSON");
assert_eq!(
baseline, current,
"pointer-disabled baseline drift detected — \
re-run with UPDATE_SNAPSHOTS=1 if intentional",
);
}