mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
* 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>
971 lines
36 KiB
Rust
971 lines
36 KiB
Rust
mod common;
|
|
|
|
use common::{assert_no_findings, scan_fixture_dir, validate_expectations};
|
|
use nyx_scanner::utils::config::AnalysisMode;
|
|
use std::collections::HashSet;
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture_path(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
// ── Per-fixture tests ──────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn rust_web_app() {
|
|
let dir = fixture_path("rust_web_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn rust_framework_rules() {
|
|
let dir = fixture_path("rust_framework_rules");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn rust_module_path_resolution() {
|
|
// Two modules define `pub fn validate(&str) -> String` with the same arity.
|
|
// `main.rs` has `use crate::auth::token::validate;` and calls `validate(&cmd)`.
|
|
// A correct use-map driven resolver must target `auth::token::validate`
|
|
// (pass-through sanitizer) and NOT `auth::session::validate` (shell sink);
|
|
// the expectations forbid any taint finding on main.rs.
|
|
let dir = fixture_path("rust_module_path_resolution");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn express_app() {
|
|
let dir = fixture_path("express_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn koa_app() {
|
|
let dir = fixture_path("koa_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn fastify_app() {
|
|
let dir = fixture_path("fastify_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_integration() {
|
|
let dir = fixture_path("auth_analysis_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_frameworks_integration() {
|
|
let dir = fixture_path("auth_analysis_frameworks_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_noise_frameworks() {
|
|
let dir = fixture_path("auth_analysis_noise_frameworks");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_python_frameworks_integration() {
|
|
let dir = fixture_path("auth_analysis_python_frameworks_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_ruby_frameworks_integration() {
|
|
let dir = fixture_path("auth_analysis_ruby_frameworks_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_go_java_frameworks_integration() {
|
|
let dir = fixture_path("auth_analysis_go_java_frameworks_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_rust_frameworks_integration() {
|
|
let dir = fixture_path("auth_analysis_rust_frameworks_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_admin_multilang_integration() {
|
|
let dir = fixture_path("auth_analysis_admin_multilang_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_analysis_ownership_multilang_integration() {
|
|
let dir = fixture_path("auth_analysis_ownership_multilang_integration");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn flask_app() {
|
|
let dir = fixture_path("flask_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn go_server() {
|
|
let dir = fixture_path("go_server");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn c_utils() {
|
|
let dir = fixture_path("c_utils");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn java_service() {
|
|
let dir = fixture_path("java_service");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_project() {
|
|
let dir = fixture_path("mixed_project");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_taint() {
|
|
let dir = fixture_path("cross_file_taint");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_ssa_propagation() {
|
|
let dir = fixture_path("cross_file_ssa_propagation");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_ssa_source() {
|
|
let dir = fixture_path("cross_file_ssa_source");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_ssa_sanitizer() {
|
|
let dir = fixture_path("cross_file_ssa_sanitizer");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── Cross-file param sink precision ───────────────────────────────────────
|
|
|
|
#[test]
|
|
fn cross_file_param_sink_precision() {
|
|
let dir = fixture_path("cross_file_param_sink_precision");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_mixed_cap_sink() {
|
|
let dir = fixture_path("cross_file_mixed_cap_sink");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Two different sinks on the same line (SQL + SHELL) must produce two
|
|
/// distinct taint findings. Regression guard for the dedup fix where
|
|
/// the grouping key includes sink capability bits, so `sink_sql(x);
|
|
/// sink_shell(x);` no longer collapses into a single finding.
|
|
#[test]
|
|
fn dedup_same_line_different_sinks() {
|
|
let dir = fixture_path("dedup_same_line_different_sinks");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
|
|
// Inspect the specific line where the two sinks live. Both findings
|
|
// must exist, and must carry different resolved sink cap bits.
|
|
let taint_on_target_line: Vec<&nyx_scanner::commands::scan::Diag> = diags
|
|
.iter()
|
|
.filter(|d| d.id.starts_with("taint-unsanitised-flow") && d.line == 10)
|
|
.collect();
|
|
assert!(
|
|
taint_on_target_line.len() >= 2,
|
|
"expected at least 2 taint findings on line 10 (dedup must not collapse \
|
|
different sinks), got {}: {:#?}",
|
|
taint_on_target_line.len(),
|
|
taint_on_target_line
|
|
.iter()
|
|
.map(|d| format!(
|
|
"{}:{} [caps={}]",
|
|
d.path,
|
|
d.line,
|
|
d.evidence.as_ref().map(|e| e.sink_caps).unwrap_or(0)
|
|
))
|
|
.collect::<Vec<_>>()
|
|
);
|
|
let caps: HashSet<u16> = taint_on_target_line
|
|
.iter()
|
|
.map(|d| d.evidence.as_ref().map(|e| e.sink_caps).unwrap_or(0))
|
|
.collect();
|
|
assert!(
|
|
caps.len() >= 2,
|
|
"expected findings on line 10 to carry distinct sink_caps, got {:?}",
|
|
caps
|
|
);
|
|
}
|
|
|
|
// ── Multi-arg validator target narrowing ────────────────────────────────
|
|
|
|
/// `validate(x, 100)` must narrow validation to `x`, so the tainted
|
|
/// `x` flowing to `os.system(x)` on the true branch is correctly silenced.
|
|
/// Regression guard for the existing target-extraction path.
|
|
#[test]
|
|
fn predicate_multi_arg_validator_tainted() {
|
|
let dir = fixture_path("predicate_multi_arg_validator_tainted");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// `validate(limit, x)` validates `limit`, not `x`. Tainted `x`
|
|
/// still flows to `os.system(x)` and the finding must fire. Regression guard
|
|
/// against upstream code marking every `condition_var` as validated when
|
|
/// target extraction narrows to a non-tainted var.
|
|
#[test]
|
|
fn predicate_multi_arg_validator_wrong() {
|
|
let dir = fixture_path("predicate_multi_arg_validator_wrong");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── Gated-sink dynamic activation conservatism ────────────────────────────
|
|
|
|
/// `setAttribute(attr, val)` with a dynamic first arg returns the
|
|
/// ALL_ARGS_PAYLOAD sentinel, so sink scanning expands to every positional
|
|
/// arg — a tainted attribute name is itself a vulnerability path. Expects
|
|
/// at least two findings (one per call where either arg is tainted).
|
|
#[test]
|
|
fn gated_sink_dynamic_activation() {
|
|
let dir = fixture_path("gated_sink_dynamic_activation");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── SCC SSA summary refinement ────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn cross_file_scc_ssa() {
|
|
let dir = fixture_path("cross_file_scc_ssa");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_scc_convergence() {
|
|
let dir = fixture_path("cross_file_scc_convergence");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_symex_body() {
|
|
let dir = fixture_path("cross_file_symex_body");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_symex_js() {
|
|
let dir = fixture_path("cross_file_symex_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── New multi-file fixtures ────────────────────────────────────────────────
|
|
|
|
// --- True positives ---------------------------------------------------------
|
|
|
|
/// Go: HTTP handler in handler.go passes r.FormValue("cmd") to runCommand()
|
|
/// defined in executor.go, which calls exec.Command — shell execution sink.
|
|
#[test]
|
|
fn cross_file_go_handler_exec() {
|
|
let dir = fixture_path("cross_file_go_handler_exec");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Java: UserController.java reads getParameter("name") and passes it to
|
|
/// UserRepository.findByName(), which concatenates it into executeQuery().
|
|
/// Cross-file taint propagates via param_to_sink in the resolved summary.
|
|
#[test]
|
|
fn cross_file_java_sqli() {
|
|
let dir = fixture_path("cross_file_java_sqli");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// TypeScript: router.ts reads req.query.url and forwards it to
|
|
/// fetchRemote() in httpClient.ts, which passes it to fetch() — SSRF.
|
|
#[test]
|
|
fn cross_file_ts_ssrf() {
|
|
let dir = fixture_path("cross_file_ts_ssrf");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JavaScript: source.js exports getInput(data); app.js destructures it under
|
|
/// the alias fetchUserCmd and passes req.query.cmd through it to execSync.
|
|
/// Import alias resolution maps fetchUserCmd → getInput for cross-file taint.
|
|
#[test]
|
|
fn cross_file_js_aliased_import() {
|
|
let dir = fixture_path("cross_file_js_aliased_import");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JavaScript: req.body.returnTo (inline source member expression in call arg)
|
|
/// flows through cross-file safeRedirect() passthrough to res.redirect() sink.
|
|
/// Exercises source node pre-emission for source member expressions nested
|
|
/// directly inside sink call arguments.
|
|
#[test]
|
|
fn cross_file_js_redirect() {
|
|
let dir = fixture_path("cross_file_js_redirect");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JavaScript: req.query.q flows through cross-file globalSearch() which
|
|
/// concatenates the param into raw SQL and passes it to db.query().
|
|
/// Tests cross-file param_to_sink propagation for SQL injection.
|
|
#[test]
|
|
fn cross_file_js_sqli() {
|
|
let dir = fixture_path("cross_file_js_sqli");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Python: 3-file chain — os.environ in input_reader.py → passthrough in
|
|
/// transform.py → subprocess.call in executor.py. Taint must survive two
|
|
/// inter-file hops with no sanitisation.
|
|
#[test]
|
|
fn cross_file_py_nested_chain() {
|
|
let dir = fixture_path("cross_file_py_nested_chain");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Python: object attribute carries taint across files — JobRequest.cmd is
|
|
/// populated from os.environ in models.py; handler.py reads req.cmd and
|
|
/// passes it to subprocess.call.
|
|
#[test]
|
|
fn cross_file_py_object_field() {
|
|
let dir = fixture_path("cross_file_py_object_field");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// --- True negatives ---------------------------------------------------------
|
|
|
|
/// Python: shlex.quote (SHELL_ESCAPE sanitiser) is defined in shell_utils.py
|
|
/// and called from handler.py before subprocess.call — no finding expected.
|
|
#[test]
|
|
fn cross_file_py_shlex_sanitizer() {
|
|
let dir = fixture_path("cross_file_py_shlex_sanitizer");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JavaScript: xss() HTML sanitiser defined in security.js is applied before
|
|
/// document.write in app.js — no taint-unsanitised-flow expected.
|
|
#[test]
|
|
fn cross_file_js_html_sanitized() {
|
|
let dir = fixture_path("cross_file_js_html_sanitized");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Python: constants.py returns a hardcoded string literal; runner.py uses it
|
|
/// in subprocess.call — no taint source exists, so no finding expected.
|
|
#[test]
|
|
fn cross_file_py_const_passthrough() {
|
|
let dir = fixture_path("cross_file_py_const_passthrough");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Go: validation.go converts r.FormValue("id") with strconv.Atoi (Cap::all
|
|
/// sanitiser) before handler.go calls db.QueryRow — no SQL taint expected.
|
|
#[test]
|
|
fn cross_file_go_int_validated() {
|
|
let dir = fixture_path("cross_file_go_int_validated");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// --- Near-miss cases --------------------------------------------------------
|
|
|
|
/// Python near miss — TRUE POSITIVE:
|
|
/// html_guard.py applies html.escape (HTML_ESCAPE cap) before a SQL
|
|
/// concatenation in app.py. The HTML sanitiser does not cover SQL_QUERY
|
|
/// capability, so the flow is still vulnerable — Nyx should detect it.
|
|
/// Tests that the engine does not over-sanitise with the wrong cap type.
|
|
#[test]
|
|
fn cross_file_near_miss_wrong_sanitizer() {
|
|
let dir = fixture_path("cross_file_near_miss_wrong_sanitizer");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JavaScript near miss — TRUE NEGATIVE:
|
|
/// session.js stores user input in `lastUser` but getDefaultQuery() returns
|
|
/// the constant `defaultQuery`. app.js passes the result to pool.query().
|
|
/// A coarse analysis might falsely flag this; a precise one should not.
|
|
/// Tests that the engine does not conflate distinct module-level variables.
|
|
#[test]
|
|
fn cross_file_near_miss_field_isolation() {
|
|
let dir = fixture_path("cross_file_near_miss_field_isolation");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Same-file identity collision — ADVERSARIAL.
|
|
/// `runTask` is defined as a free function (shell-exec sink) AND as a
|
|
/// method on multiple classes in the same file with conflicting
|
|
/// security behaviours. A bare `runTask(tainted)` top-level call MUST
|
|
/// resolve to the free function (its summary carries a SHELL_ESCAPE
|
|
/// sink) — the pre-fix resolver returned Ambiguous for this call and
|
|
/// silently dropped the finding. Regression guard for the bare-call
|
|
/// free-function preference (resolve_callee step 5.5).
|
|
#[test]
|
|
fn same_name_collisions_js() {
|
|
let dir = fixture_path("same_name_collisions_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── New sink coverage fixtures ────────────────────────────────────────────
|
|
|
|
/// JS: execAsync wraps child_process.exec; user input flows through the
|
|
/// wrapper to the inner exec call — SHELL_ESCAPE finding expected.
|
|
#[test]
|
|
fn exec_async_wrapper() {
|
|
let dir = fixture_path("exec_async_wrapper");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JS: res.download(path.join(root, req.query.path)) — path traversal
|
|
/// via Express res.download FILE_IO sink.
|
|
#[test]
|
|
fn path_traversal_download() {
|
|
let dir = fixture_path("path_traversal_download");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JS: md5(password) and crypto.createHash("sha1") — weak hash patterns.
|
|
#[test]
|
|
fn weak_hash_password() {
|
|
let dir = fixture_path("weak_hash_password");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// JS: hardcoded secret/password in object literal.
|
|
#[test]
|
|
fn hardcoded_secret() {
|
|
let dir = fixture_path("hardcoded_secret");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── Cross-cutting tests ───────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn ast_only_mode_excludes_taint() {
|
|
let dir = fixture_path("rust_web_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Ast);
|
|
|
|
assert_no_findings(&diags, "taint-");
|
|
assert_no_findings(&diags, "cfg-");
|
|
}
|
|
|
|
#[test]
|
|
fn taint_only_mode_excludes_ast() {
|
|
let dir = fixture_path("rust_web_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Taint);
|
|
|
|
// Taint mode should not produce AST-only pattern findings
|
|
assert_no_findings(&diags, "rs.quality.unwrap");
|
|
assert_no_findings(&diags, "rs.quality.expect");
|
|
}
|
|
|
|
#[test]
|
|
fn dedup_no_double_report() {
|
|
let dir = fixture_path("rust_web_app");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
|
|
// The same (path, line, col, rule_id) tuple should never appear twice.
|
|
// Different rule IDs at the same location are fine (e.g., taint + cfg-auth-gap).
|
|
let mut seen: HashSet<(String, usize, usize, String)> = HashSet::new();
|
|
let mut exact_dupes = Vec::new();
|
|
for d in &diags {
|
|
let key = (d.path.clone(), d.line, d.col, d.id.clone());
|
|
if !seen.insert(key) {
|
|
exact_dupes.push(format!("{}:{}:{} {}", d.path, d.line, d.col, d.id));
|
|
}
|
|
}
|
|
assert!(
|
|
exact_dupes.is_empty(),
|
|
"Exact duplicate findings (same location + rule ID) found ({}):\n {}",
|
|
exact_dupes.len(),
|
|
exact_dupes.join("\n ")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_project_multi_language() {
|
|
let dir = fixture_path("mixed_project");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
|
|
// Findings should span at least 2 different file extensions
|
|
let extensions: HashSet<&str> = diags
|
|
.iter()
|
|
.filter_map(|d| {
|
|
std::path::Path::new(&d.path)
|
|
.extension()
|
|
.and_then(|e| e.to_str())
|
|
})
|
|
.collect();
|
|
|
|
assert!(
|
|
extensions.len() >= 2,
|
|
"Expected findings from >= 2 language file extensions, got: {:?}",
|
|
extensions
|
|
);
|
|
|
|
// Total findings >= 3 across languages
|
|
assert!(
|
|
diags.len() >= 3,
|
|
"Expected >= 3 total findings in mixed project, got {}",
|
|
diags.len()
|
|
);
|
|
}
|
|
|
|
/// JS: throw in error-check branch should be recognized as a terminator,
|
|
/// suppressing cfg-error-fallthrough false positives.
|
|
#[test]
|
|
fn error_throw_terminates() {
|
|
let dir = fixture_path("error_throw_terminates");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── Binary smoke test ──────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn binary_json_output() {
|
|
let fixture = fixture_path("rust_web_app");
|
|
#[allow(deprecated)]
|
|
let cmd = assert_cmd::Command::cargo_bin("nyx")
|
|
.expect("nyx binary should exist")
|
|
.arg("scan")
|
|
.arg(fixture.to_str().unwrap())
|
|
.arg("--no-index")
|
|
.arg("--format")
|
|
.arg("json")
|
|
.output()
|
|
.expect("failed to execute nyx binary");
|
|
|
|
assert!(
|
|
cmd.status.success(),
|
|
"nyx scan exited with non-zero status: {:?}\nstderr: {}",
|
|
cmd.status,
|
|
String::from_utf8_lossy(&cmd.stderr)
|
|
);
|
|
|
|
let stdout = String::from_utf8_lossy(&cmd.stdout);
|
|
// Find the JSON array in stdout (config notes and "Finished" surround it)
|
|
let json_start = stdout.find('[').expect("Expected JSON array in stdout");
|
|
let json_end = stdout.rfind(']').expect("Expected closing bracket in JSON") + 1;
|
|
let json_str = &stdout[json_start..json_end];
|
|
let parsed: Vec<serde_json::Value> =
|
|
serde_json::from_str(json_str).expect("stdout should contain valid JSON array");
|
|
|
|
assert!(
|
|
!parsed.is_empty(),
|
|
"Expected at least 1 finding in JSON output"
|
|
);
|
|
}
|
|
|
|
// ── EJS / config / debug endpoint fixtures ──────────────────────────────────
|
|
|
|
/// EJS template: detects unescaped `<%- query %>` and `<%- resultHtml %>`
|
|
/// but not `<%- include(...) %>` or `<%= safe %>`.
|
|
#[test]
|
|
fn ejs_xss() {
|
|
let dir = fixture_path("ejs_xss");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Express session config: detects httpOnly: false, secure: false,
|
|
/// sameSite: "none", and hardcoded secret.
|
|
#[test]
|
|
fn insecure_session_config() {
|
|
let dir = fixture_path("insecure_session_config");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Debug endpoint: process.env → res.json() should be caught by taint.
|
|
#[test]
|
|
fn debug_endpoint() {
|
|
let dir = fixture_path("debug_endpoint");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Internal path-prefix redirects should be suppressed; open redirects should fire.
|
|
#[test]
|
|
fn internal_redirect_taint() {
|
|
let dir = fixture_path("internal_redirect_taint");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Route registration methods (router.get/post) and session lifecycle should
|
|
/// not propagate taint or generate findings.
|
|
#[test]
|
|
fn route_registration_noise() {
|
|
let dir = fixture_path("route_registration_noise");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
#[test]
|
|
fn route_registration_noise_frameworks() {
|
|
let dir = fixture_path("route_registration_noise_frameworks");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Dynamic HTTP module dispatch: lib = require("http"), lib.request(url)
|
|
/// should be resolved as SSRF sink via module alias tracking.
|
|
#[test]
|
|
fn dynamic_dispatch_ssrf() {
|
|
let dir = fixture_path("dynamic_dispatch_ssrf");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Cross-file info leak: service returns process.env data (source-independent
|
|
/// taint), caller passes to res.json() sink.
|
|
#[test]
|
|
fn cross_file_info_leak() {
|
|
let dir = fixture_path("cross_file_info_leak");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Python `subprocess.run(cmd, shell=True)` where `cmd` is user-controlled —
|
|
/// the multi-kwarg SHELL_ESCAPE gate activates. Validates end-to-end wiring
|
|
/// of `CallMeta.kwargs` through `classify_gated_sink`'s `dangerous_kwargs`
|
|
/// path (presence-aware shell=True → dangerous).
|
|
#[test]
|
|
fn python_subprocess_shell_true_tainted() {
|
|
let dir = fixture_path("python_subprocess_shell_true");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Python `subprocess.run([cmd], shell=False)` — shell kwarg present but not
|
|
/// dangerous. The gate must not fire and no taint flow should be reported.
|
|
#[test]
|
|
fn python_subprocess_shell_false_safe() {
|
|
let dir = fixture_path("python_subprocess_shell_false_safe");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// Python `subprocess.run([cmd])` — no shell kwarg (default shell=False).
|
|
/// The gate must not fire and no taint flow should be reported.
|
|
#[test]
|
|
fn python_subprocess_shell_default_safe() {
|
|
let dir = fixture_path("python_subprocess_shell_default_safe");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
// ── FP guard fixtures ─────────────────────────────────────────────────────
|
|
//
|
|
// Each fixture below is a small source file exercising a pattern where
|
|
// the analyser must NOT emit a taint-unsanitised-flow (with the single
|
|
// exception of `fp_guard_call_site_specialization_py`, which requires
|
|
// one finding only on the tainted call-site). The fixtures are grouped
|
|
// into five categories so a single regression cannot silently erase a
|
|
// whole category's coverage.
|
|
|
|
/// FP guard — sanitizer edge case: hand-rolled HTML escape covers
|
|
/// document.write sink.
|
|
#[test]
|
|
fn fp_guard_sanitizer_html_escape_js() {
|
|
let dir = fixture_path("fp_guards/sanitizer_html_escape_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — sanitizer edge case: shlex.quote with shell metacharacters.
|
|
#[test]
|
|
fn fp_guard_sanitizer_shlex_quote_py() {
|
|
let dir = fixture_path("fp_guards/sanitizer_shlex_quote_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — sanitizer edge case: encodeURIComponent on a URL argument.
|
|
#[test]
|
|
fn fp_guard_sanitizer_url_encode_js() {
|
|
let dir = fixture_path("fp_guards/sanitizer_url_encode_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — sanitizer edge case: multi-step chain (`.strip()` then
|
|
/// `shlex.quote`) preserves the final SHELL_ESCAPE cap.
|
|
#[test]
|
|
fn fp_guard_sanitizer_multi_step_py() {
|
|
let dir = fixture_path("fp_guards/sanitizer_multi_step_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — type-driven suppression: `int()` parse of env port
|
|
/// before `socket.bind`.
|
|
#[test]
|
|
fn fp_guard_types_int_port_py() {
|
|
let dir = fixture_path("fp_guards/types_int_port_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — type-driven suppression: `int()` parse guarantees SQL
|
|
/// concat is decimal-only.
|
|
#[test]
|
|
fn fp_guard_types_int_id_sql_py() {
|
|
let dir = fixture_path("fp_guards/types_int_id_sql_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — type-driven suppression: Go `strconv.Atoi` covers
|
|
/// Cap::all on the resulting int.
|
|
#[test]
|
|
fn fp_guard_types_parse_int_go() {
|
|
let dir = fixture_path("fp_guards/types_parse_int_go");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — type-driven suppression: bool comparison never reaches
|
|
/// a string-context sink.
|
|
#[test]
|
|
fn fp_guard_types_bool_flag_py() {
|
|
let dir = fixture_path("fp_guards/types_bool_flag_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — struct-field isolation: JS object `safeField` used at
|
|
/// sink, tainted `unsafeField` unused.
|
|
#[test]
|
|
fn fp_guard_fields_object_isolation_js() {
|
|
let dir = fixture_path("fp_guards/fields_object_isolation_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — struct-field isolation: Python class attributes — only
|
|
/// the hardcoded attribute flows to the sink.
|
|
#[test]
|
|
fn fp_guard_fields_class_attr_py() {
|
|
let dir = fixture_path("fp_guards/fields_class_attr_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — struct-field isolation: Python dict keys — only the
|
|
/// constant key flows to the sink.
|
|
#[test]
|
|
fn fp_guard_fields_dict_key_py() {
|
|
let dir = fixture_path("fp_guards/fields_dict_key_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — struct-field isolation: nested JS objects — sibling path
|
|
/// isolation at `cfg.auth.*`.
|
|
#[test]
|
|
fn fp_guard_fields_nested_object_js() {
|
|
let dir = fixture_path("fp_guards/fields_nested_object_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — cross-call-site specialization: same callee, two callers
|
|
/// (one tainted, one constant). Required finding only from the
|
|
/// tainted caller.
|
|
#[test]
|
|
fn fp_guard_call_site_specialization_py() {
|
|
let dir = fixture_path("fp_guards/call_site_specialization_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — cross-call-site specialization: JS helper called with a
|
|
/// literal SQL string must not inherit taint.
|
|
#[test]
|
|
fn fp_guard_call_site_specialization_js() {
|
|
let dir = fixture_path("fp_guards/call_site_specialization_js");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — cross-call-site specialization: helper called with a
|
|
/// shlex.quote-sanitised value, inline analysis sees SHELL_ESCAPE cap.
|
|
#[test]
|
|
fn fp_guard_call_site_sanitized_caller_py() {
|
|
let dir = fixture_path("fp_guards/call_site_sanitized_caller_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — cross-call-site specialization: polymorphic caller
|
|
/// (int branch and constant branch) — neither carries a payload.
|
|
#[test]
|
|
fn fp_guard_call_site_polymorphic_py() {
|
|
let dir = fixture_path("fp_guards/call_site_polymorphic_py");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — framework-safe pattern: Rails `sanitize` before render.
|
|
#[test]
|
|
fn fp_guard_framework_rails_sanitize() {
|
|
let dir = fixture_path("fp_guards/framework_rails_sanitize");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — framework-safe pattern: Flask + MarkupSafe `escape`.
|
|
#[test]
|
|
fn fp_guard_framework_flask_escape() {
|
|
let dir = fixture_path("fp_guards/framework_flask_escape");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — framework-safe pattern: Express `res.json` with a
|
|
/// constant payload is not an XSS sink.
|
|
#[test]
|
|
fn fp_guard_framework_express_res_json() {
|
|
let dir = fixture_path("fp_guards/framework_express_res_json");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — framework-safe pattern: JDBC PreparedStatement.setString
|
|
/// covers SQL_QUERY on the bound parameter.
|
|
#[test]
|
|
fn fp_guard_framework_prepared_stmt_java() {
|
|
let dir = fixture_path("fp_guards/framework_prepared_stmt_java");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — JPA parameterised execute chain
|
|
/// (`em.createQuery(LITERAL).setParameter(...).executeUpdate()`).
|
|
/// Pinned from a 150-finding cluster in keycloak's
|
|
/// `JpaEventStoreProvider.java`. The engine walks the receiver chain
|
|
/// from the zero-arg `.executeUpdate()` / `.executeQuery()` sink down
|
|
/// to the SQL-binding call (`createQuery` / `createNativeQuery`) and
|
|
/// synthesises a same-node `Sanitizer(SQL_QUERY)` when arg 0 is a
|
|
/// `string_literal`.
|
|
#[test]
|
|
fn fp_guard_framework_jpa_parameterised_execute() {
|
|
let dir = fixture_path("fp_guards/framework_jpa_parameterised_execute");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — composer / PSR-4 autoloader closure includes a parameter.
|
|
/// Pinned from a 32-finding cluster in nextcloud's vendored
|
|
/// `composer/composer/ClassLoader.php` plus three further methods
|
|
/// (Router::requireRouteFile, Installer::includeAppScript,
|
|
/// Template/Base::load). The pattern rule fires syntactically on
|
|
/// `include $var`; without taint context it over-fires when `$var` is a
|
|
/// formal parameter of the immediately enclosing function/closure with
|
|
/// no intervening reassignment.
|
|
#[test]
|
|
fn fp_guard_php_include_param_passthrough() {
|
|
let dir = fixture_path("fp_guards/php_include_param_passthrough");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — `unserialize($x, ['allowed_classes' => …])` PHP 7+
|
|
/// structural mitigation against object injection. Pinned from
|
|
/// nextcloud's profiler / DAV custom-properties / queue-bus call sites
|
|
/// where `allowed_classes` is set to `false`, an array literal, or a
|
|
/// class constant referring to an explicit allow-list.
|
|
#[test]
|
|
fn fp_guard_php_unserialize_allowed_classes() {
|
|
let dir = fixture_path("fp_guards/php_unserialize_allowed_classes");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|
|
|
|
/// FP guard — C/C++ buffer-overflow pattern rules
|
|
/// (`c.memory.strcpy`, `strcat`, `sprintf`) over-fire when the source /
|
|
/// format-string argument is a literal whose contributed length is
|
|
/// statically bounded. Pinned from a 938-finding cluster across postgres
|
|
/// (`pg_prewarm/autoprewarm.c::apw_start_leader_worker`,
|
|
/// `formatting.c::DCH_a_m` ternary-of-literals, `datetime.c::EncodeDateTime`
|
|
/// `%.*s`/numeric-only sprintf). Layer D suppression in
|
|
/// `src/ast.rs::is_c_buffer_call_literal_safe`.
|
|
#[test]
|
|
fn fp_guard_c_buffer_literal_src() {
|
|
let dir = fixture_path("fp_guards/c_buffer_literal_src");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
validate_expectations(&diags, &dir);
|
|
}
|