Skip to content

v0.4.0

Choose a tag to compare

@elicpeter elicpeter released this 26 Feb 02:34
· 53 commits to master since this release
c4ce08b

Added

  • Low-noise prioritization system — post-analysis pipeline that reduces noise from high-frequency LOW/Quality findings without hiding security signal. Three-stage process: category filtering, rollup grouping, and LOW budgets.
    • FindingCategory enum (Security, Reliability, Quality) — every Diag now carries a category field. AST pattern findings derive their category from PatternCategory metadata (CodeQualityQuality, all others → Security). Taint, CFG, and state findings are always Security.
    • Category filtering — Quality-category findings (e.g. rs.quality.unwrap, rs.quality.expect) are excluded by default. Use --include-quality to include them.
    • Rollup grouping — eligible HIGH-frequency rules (rs.quality.unwrap, rs.quality.expect, rs.quality.panic_macro) are grouped by (file, rule) into a single rollup finding with occurrence count and example locations. Canonical location is the first sorted occurrence. Example count controlled by --rollup-examples (default 5).
    • LOW budgets — three configurable limits enforce noise caps: --max-low (default 20, total), --max-low-per-file (default 1), --max-low-per-rule (default 10). Rollups count as one finding for all budgets. High/Medium findings are never dropped.
    • --all CLI flag — disables all prioritization (no category filtering, no rollups, no budgets).
    • --show-instances <RULE> — bypasses rollup for a specific rule, expanding all individual occurrences.
    • Console suppression footer — when findings are suppressed, a footer displays the count and active filter values with adjustment hints.
    • rollup field on Diag — optional RollupData with count and occurrences (example Locations). Serializes to JSON automatically; omitted when not a rollup.
    • SARIF rollup supportcategory in result properties, rollup count in properties.rollup.count, example locations in relatedLocations.
    • max_results severity stability — when max_results truncation is needed, High findings are kept first, then Medium, then Low. Low findings never displace higher-severity ones.
    • New config fields in [output]: include_quality, show_all, max_low, max_low_per_file, max_low_per_rule, rollup_examples.
    • 14 new unit tests covering category filtering, rollup grouping/examples/canonical, LOW budgets (per-file/per-rule/total), High/Medium immunity, rollup-counts-as-one, show_instances bypass, JSON serialization, and determinism.
  • Pattern-level confidence for AST rules — each AST pattern in src/patterns/ now carries an explicit confidence: Confidence field (High, Medium, or Low). Confidence is set at the pattern definition site and flows directly into emitted Diags, replacing the old heuristic that inferred AST confidence from severity alone. compute_confidence() is retained as a fallback for detectors that don't set confidence (taint, state, legacy).
    • Tier A patterns with High/Medium severity → Confidence::High (deterministic structural match).
    • Tier A patterns with Low severity → Confidence::Medium (quality/crypto signals).
    • Tier B patterns (heuristic-guarded) → Confidence::Medium.
    • Example: rs.quality.expect now produces Confidence: High regardless of its Low severity.
  • Inline per-finding suppressions — suppress specific findings directly in source code using nyx:ignore comments. Two directive forms: nyx:ignore <RULE_ID> (same line) and nyx:ignore-next-line <RULE_ID> (next line). Supports comma-separated IDs, wildcard suffixes (rs.quality.*), and automatic canonicalization of taint rule IDs (parenthetical suffixes stripped). Comment detection covers all 10 languages with string/raw-string/template-literal guards to avoid false positives.
    • --show-suppressed CLI flag — reveal suppressed findings in output, dimmed with [SUPPRESSED] tag. Summary shows "N issues (M suppressed)". In JSON/SARIF mode, suppressed findings include "suppressed": true and "suppression": {...} metadata fields.
    • suppressed and suppression fields on Diag — conditionally serialized; JSON output is unchanged when no suppressions are active.
    • Suppressed findings are excluded from --fail-on exit-code checks and severity counts.
    • New module src/suppress/mod.rs with 22 unit tests covering all comment styles, string guards, wildcard matching, canonicalization, CRLF, and edge cases.
  • --min-score <N> CLI flag and output.min_score config option — filter out findings whose attack-surface rank score falls below the given threshold. Applied after ranking and severity filtering, before max_results truncation. Has no effect when --no-rank is used. CLI value overrides config.
  • Attack surface ranking — deterministic post-analysis scoring layer that prioritizes findings by exploitability. Each Diag receives an f64 score computed from five components: severity base (High=60, Medium=30, Low=10), analysis kind bonus (taint +10 > state +8 > cfg +3/5 > ast 0), evidence strength (+1 per item, +2–6 for source-kind priority), state rule type bonus (+1–6), and a path-validation penalty (−5 for guarded paths). Findings are sorted by descending score before truncation so max_results keeps the most important results. Tie-breaking is deterministic by severity, rule ID, file path, line, column, and message hash.
    • rank_score and rank_reason fields on Diag — optional fields with #[serde(skip_serializing_if = "Option::is_none")]; JSON output is unchanged when ranking is disabled.
    • --no-rank CLI flag — disables attack-surface ranking (enabled by default).
    • output.attack_surface_ranking config key — boolean (default true) to control ranking via config file.
    • Console score display — dim Score: N appended to each finding's header line when ranking is enabled.
    • New module src/rank.rscompute_attack_rank(), rank_diags(), and sort_key() functions. Scoring uses only in-memory data; no extra file I/O or graph recomputation.
    • 10 new unit tests: ordering correctness (high taint > medium file-io, must-leak > may-leak, taint > cfg-only, state rules, AST lowest at same severity), determinism (input-order-independent), path-validation penalty, and JSON serialization (rank fields omitted when None, present when set).
  • State-model dataflow analysis — new src/state/ module implementing a forward worklist dataflow engine over the existing CFG. Tracks per-variable resource lifecycle (UNINIT, OPEN, CLOSED, MOVED) via bitset lattice and per-path authentication level (Unauthed, Authed, Admin) as a composable product domain. Detects:
    • Use-after-close (state-use-after-close, High) — variable read/written after its resource handle was closed.
    • Double-close (state-double-close, Medium) — resource handle closed more than once.
    • Must-leak (state-resource-leak, High) — resource acquired but never closed on any exit path.
    • May-leak (state-resource-leak-possible, Medium) — resource open on some but not all exit paths (branch-aware via lattice join).
    • Unauthenticated access (state-unauthed-access, High) — sensitive sink reached without a preceding auth/admin check.
  • State analysis architecture — six-module design:
    • lattice.rsLattice trait (bot, join, leq) for generic fixed-point computation.
    • domain.rsResourceLifecycle (bitflag), ResourceDomainState, AuthLevel, AuthDomainState, ProductState with lattice impls.
    • symbol.rsSymbolInterner that builds a string-interning table from CFG node defines/uses; SymbolId newtype.
    • transfer.rsDefaultTransfer function: maps CFG node kinds (Call, Assignment, If, Return) to state transitions using the existing ResourcePair definitions from cfg_analysis::rules. Emits TransferEvent for illegal transitions.
    • engine.rs — two-phase forward worklist solver: Phase 1 iterates to a fixed point (no events collected to avoid spurious reports from intermediate states); Phase 2 re-applies transfer once over converged states to collect events. Bounded by MAX_TRACKED_VARS (64) with guarded degradation.
    • facts.rs — post-analysis pass: extracts StateFindings from transfer events (use-after-close, double-close) and exit-node state inspection (must-leak, may-leak, unauthed access).
  • scanner.enable_state_analysis config option — opt-in boolean (default false) in ScannerConfig and default-nyx.conf. Requires CFG mode (full or taint).
  • Diag.message field — optional human-readable message on diagnostic output. State findings carry variable-specific context (e.g. "variable f used after close"). Surfaced in console output (dimmed line below the finding), JSON, and SARIF (message.text prefers per-finding message over generic rule description).
  • State finding dedup — when state analysis produces findings on a line, overlapping cfg-resource-leak and cfg-auth-gap findings on the same line are suppressed (state analysis is more precise).
  • SARIF rule descriptions for all five state rule IDs.
  • 21 integration tests (tests/state_tests.rs) with 19 C fixture files covering: use-after-close, double-close, resource leak, clean usage, opt-in gating, may-leak vs must-leak branch semantics, early return, nested branches, both-branches-close, loop convergence, loop use-after-close, handle overwrite, reopen-after-close, multiple handles, conservative join masking, chain operations, malloc/free pairs, straight-line double-close, and message field population.
  • 30+ unit tests across state modules: lattice properties, lifecycle join/leq, domain merging, auth-level join, product state composition, may/must leak semantics, symbol interning, and transfer event generation.
  • --severity <EXPR> filter — replaces --high-only with a flexible severity expression supporting single levels (HIGH), comma lists (HIGH,MEDIUM), and thresholds (>=MEDIUM). Parsing is case-insensitive with whitespace tolerance. SeverityFilter type with parse() and matches() in patterns/mod.rs.
  • --mode <full|ast|cfg|taint> — replaces --ast-only and --cfg-only with a single canonical analysis mode flag. Enforces mutual exclusivity via clap ValueEnum.
  • --index <auto|off|rebuild> — replaces --no-index and --rebuild-index with a single flag (default auto).
  • --fail-on <SEVERITY> — CI ergonomics: exit code 1 if any emitted finding meets or exceeds the threshold severity. Example: --fail-on HIGH.
  • --quiet — CLI flag to suppress all human-readable status output (equivalent to output.quiet = true in config).
  • --keep-nonprod-severity — renamed from --include-nonprod for clarity; old name kept as hidden alias.
  • OutputFormat enum--format now uses clap ValueEnum with typed Console, Json, Sarif variants (default Console). No more empty-string default.
  • 10 new unit tests: SeverityFilter parsing (single, comma list, threshold, case-insensitive, whitespace, empty rejection, invalid level rejection), Severity::from_str rejection of unknown values, and severity_filter_applied_at_output_stage integration test verifying that downgraded findings are correctly filtered.
  • AST pattern overhaul -- all 10 language pattern files (src/patterns/*.rs) rewritten with consistent conventions, structured metadata, and validated tree-sitter queries.
    • Pattern schema extensions -- PatternTier (A = structural, B = heuristic-guarded), PatternCategory (13 vulnerability classes), and Hash on Severity. Module-level docs explain conventions and how to add new patterns.
    • Namespaced IDs -- all pattern IDs follow <lang>.<category>.<specific> format (e.g. java.deser.readobject, py.cmdi.os_system, js.xss.document_write).
    • New vulnerability coverage -- 30+ new patterns across languages: Python deserialization (pickle.loads, yaml.load, shelve.open), Python command injection (os.system, os.popen), Python weak crypto (hashlib.md5/sha1), Java reflection (Method.invoke), Java weak digest (MessageDigest.getInstance("MD5")), Java XSS (getWriter().println), Go TLS misconfiguration (InsecureSkipVerify: true), Go SQL concat, Go hardcoded secrets, Go gob deserialization, PHP assert() code exec, PHP include $var path traversal, PHP weak crypto (md5/sha1/rand), C/C++ popen(), C/C++ format-string with variable first arg, C++ const_cast, Ruby Digest::MD5.
    • Query fixes -- fixed 11 broken tree-sitter queries: Java object_creation_expression used wrong type node (identifiertype_identifier), C++ reinterpret_cast/const_cast used non-existent node types (→ template_function match), Ruby backtick used shell_command (→ subshell), Python SQL used binary_expression (→ binary_operator), TypeScript as any used inaccessible field (→ positional child), PHP patterns missing argument wrapper nodes, Rust unsafe fn regex used unsupported \b.
    • No-duplicate rule -- patterns that overlap with taint sinks use distinct ID namespaces and are documented; dedup in ast.rs prevents duplicate findings at the same location.
    • Severity recalibration -- unwrap/expect/panic!/todo! moved to Low (filtered by default min_severity). Security patterns remain High/Medium.
  • Pattern test suite (tests/pattern_tests.rs, 26 tests) -- sanity checks (unique IDs, query compilation, non-empty descriptions, naming convention, severity distribution), positive fixture tests (10 languages), and negative fixture tests (10 languages verifying no false positives on safe code).
  • Pattern test fixtures -- positive and negative fixture files for all 10 languages under tests/fixtures/patterns/<lang>/.
  • Real world test suite — comprehensive fixture-based test suite (tests/real_world_tests.rs) with ~180 test fixtures across all 10 supported languages (C, C++, Go, Java, JavaScript, PHP, Python, Ruby, Rust, TypeScript). Each fixture has an .expect.json file declaring expected findings (with must_match for hard requirements and soft expectations for aspirational coverage). Fixtures are organized by analysis type (taint/, state/, cfg/, mixed/) under tests/fixtures/real_world/<lang>/. A single parameterized test runner validates all fixtures in both full and ast modes, with verbose output via NYX_TEST_VERBOSE=1.

Changed

  • Console header line now includes confidence — the finding header shows score and confidence together as a parenthesized suffix: (Score: 36, Confidence: Medium). The previous standalone Confidence: ... body line is removed. All four combinations are handled (both, score-only, confidence-only, neither).
  • Confidence display uses Title CaseConfidence::Display now renders as Low, Medium, High (previously lowercase).
  • Breaking: Config and data directory changed from dev.ecpeter23.nyx to nyx (e.g. ~/Library/Application Support/nyx/ on macOS). Existing config files (nyx.conf, nyx.local) and SQLite indexes at the old path will not be picked up automatically — copy them to the new location or re-run nyx scan to regenerate.
  • Improved diagnostic output formatting — overhauled console renderer for a professional, security-tool-grade look:
    • Severity is now the strongest visual anchor: HIGH (bold red with ✖), MEDIUM (bold orange ⚠), LOW (muted blue-gray ●). Fewer colors, clearer hierarchy.
    • File paths rendered dim blue (never brighter than severity).
    • Taint flow messages now use arrow between shortened source/sink instead of backtick-wrapped text.
    • Evidence values (Source, Sink) no longer wrapped in backticks — cleaner rendering with no risk of broken backtick spans across wrapped lines.
  • Fixed taint expression rendering — multi-line sink/source call chains are now normalised before display:
    • Whitespace collapsed (foo() .bar()foo().bar()).
    • Newlines joined into single-line canonical form.
    • Spacing artefacts between ) and . in method chains cleaned up.
    • Long chains truncated with ellipsis.
  • Added terminal_size dependency for terminal-width-aware line wrapping.
  • Monotone forward dataflow taint analysis — replaced the BFS taint engine in taint/mod.rs with a proper worklist-based forward dataflow analysis where termination is guaranteed by lattice finiteness. The generic Transfer<S: Lattice> trait in state/engine.rs now powers both the resource lifecycle/auth analysis and taint analysis.
    • TaintState lattice (taint/domain.rs) — bounded abstract state with per-variable VarTaint (Cap bitflags + multi-origin tracking via SmallVec<[TaintOrigin; 2]>), dual validation bitsets (validated_must for intersection/all-paths, validated_may for union/any-path), and monotone PredicateSummary for contradiction pruning. Variables stored in sorted SmallVec keyed by SymbolId for O(n) merge-join. Lattice height bounded at ~8700 (7-bit Cap × 64 vars + validation bits + predicate bits).
    • TaintTransfer (taint/transfer.rs) — implements Transfer<TaintState> with identical taint logic to the old BFS (source → propagation → sanitization → sink check). Callee resolution unchanged (local → global same-lang → interop edges). Emits TaintEvent::SinkReached events during Phase 2 of the engine.
    • JS/TS two-level solve — prevents cross-function taint leakage (the main source of state explosion in the old BFS) while preserving global-to-function flows. Level 1 solves top-level code; Level 2 solves each function seeded with read-only top-level taint via global_seed.
    • Monotone predicate tracking — path-sensitivity predicates moved from per-BFS-item PathState (which duplicated state exponentially) to monotone PredicateSummary in the lattice. Contradiction pruning uses known_true & known_false bit intersection (NullCheck/EmptyCheck/ErrorCheck only), which is both more precise and guaranteed monotone.
    • Multi-origin tracking — each tainted variable tracks up to 4 TaintOrigin (node + SourceKind), enabling multiple findings when distinct sources flow to the same sink.
    • Guaranteed termination — no more MAX_BFS_ITERATIONS/MAX_SEEN_STATES safety nets needed (though a 100K worklist iteration budget remains as defense-in-depth). Convergence follows from finite lattice height × finite CFG edges.
    • analyse_file() signature unchangedFinding struct, Diag conversion, and all callers are unaffected.
  • Generic dataflow engine (state/engine.rs) — run_forward() and DataflowResult are now generic over any S: Lattice + T: Transfer<S>. DefaultTransfer (resource lifecycle) implements Transfer<ProductState>; TaintTransfer implements Transfer<TaintState>. Per-domain iteration budget and on_budget_exceeded hooks added.
  • path_state.rs simplified — removed PathState, Predicate, MAX_PATH_PREDICATES, state_hash(), priority() structs/methods. Kept PredicateKind enum and classify_condition() function (used by the new transfer for predicate classification).
  • Removed BFS infrastructuretaint_hash(), BFS Item struct, pred predecessor map, two-tier seen-state map, and all bail-out constants (MAX_BFS_ITERATIONS=200K, MAX_SEEN_STATES=100K, PATH_SENSITIVITY_NODE_LIMIT=500, PATH_SENSITIVITY_QUEUE_LIMIT=10K, MAX_PATH_VARIANTS_PER_KEY=4) are no longer needed and have been removed.
  • Severity filtering applied at output stage--severity (and legacy --high-only) filtering is now applied ONCE in scan::handle() after all severity normalization (nonprod downgrades, dedup, truncation). Previously --high-only only filtered AST patterns during analysis; taint and CFG findings bypassed the filter entirely.
  • --format default is console — previously defaulted to empty string, requiring fallback logic.
  • All status/progress output goes to stderr — "Checking...", "Finished in...", config notes, and progress bars now use eprintln!/stderr exclusively. JSON and SARIF output is stdout-only.
  • Severity::from_str returns Err for unknown values — previously returned Ok(Severity::Low) for any unrecognized input.
  • Deprecated CLI flags preserved as hidden aliases--high-only, --no-index, --rebuild-index, --ast-only, --cfg-only, and --include-nonprod are hidden from help but still functional, mapping to their canonical replacements.
  • Path-sensitive taint analysis -- the BFS taint engine now carries a PathState (bounded set of branch predicates) alongside the taint map. When the BFS traverses a True or False edge from an If node, it records a Predicate with the condition's variables, kind, and polarity. This enables two new capabilities:
    • Infeasible path pruning -- paths with contradictory predicates (e.g. if x.is_none() { return; } if x.is_none() { sink }) are detected and pruned, eliminating false positives on code guarded by redundant null/empty/error checks. Contradiction detection is conservative: only whitelisted kinds (NullCheck, EmptyCheck, ErrorCheck) with single-variable predicates are pruned.
    • Validation guard annotation -- when all tainted variables reaching a sink are guarded by a ValidationCall predicate (e.g. if validate(&x) { sink } or if !validate(&x) { return; } sink), the finding is annotated with path_validated: true and guard_kind: ValidationCall. This metadata is surfaced in JSON and console output without changing severity.
  • Condition metadata on CFG nodes -- NodeInfo now carries condition_text, condition_vars, and condition_negated for If nodes, extracted during CFG construction. Negation detection handles !expr, not expr, and Ruby unless. Classification of condition text into PredicateKind (NullCheck, EmptyCheck, ErrorCheck, ValidationCall, SanitizerCall, Comparison, Unknown) is conservative: call-based kinds require ( in the text and a matching callee token.
  • path_validated and guard_kind fields on Diag -- taint findings carry path-sensitivity metadata in JSON output (fields omitted when not set) and console output (suffix line Path guard: ValidationCall when present). Finding IDs are unchanged for dedup stability.
  • smallvec dependency -- used for inline-allocated predicate storage in PathState (avoids heap allocation for the common case of ≤4 predicates per path).
  • Interprocedural call graph -- a whole-program CallGraph (petgraph::DiGraph<FuncKey, CallEdge>) is now built between Pass 1 and Pass 2 of every taint-enabled scan. Each function definition is a node; resolved callee relationships are edges. The graph is constructed from the merged GlobalSummaries and is available in both the filesystem and indexed scan paths.
  • Three-valued callee resolution -- CalleeResolution enum distinguishes Resolved(FuncKey), NotFound, and Ambiguous(Vec<FuncKey>). Ambiguous callees (same name in multiple namespaces, caller in a third namespace) are tracked separately from missing callees for diagnostics.
  • Shared resolution helper -- GlobalSummaries::resolve_callee_key() centralizes same-language callee resolution with arity-aware filtering and namespace disambiguation. Both the call graph builder and the taint engine now use the same resolution logic.
  • Callee-name normalization -- normalize_callee_name() extracts the last segment from qualified callee text ("env::var""var", "obj.method""method") before resolution. The raw call-site text is preserved on graph edges for diagnostics.
  • SCC / topological analysis -- CallGraphAnalysis computes strongly connected components via Tarjan's algorithm and exposes a callee-first (leaves-first) topological ordering of SCC indices, ready for future bottom-up taint propagation.
  • Call graph tracing -- tracing::info! log with node count, edge count, unresolved-not-found count, unresolved-ambiguous count, and SCC count is emitted after every call graph build.
  • 8 new path-sensitivity integration tests: early-return validation guard, failed-validation branch, contradictory null-check pruning, if/else validation annotation, sanitize-one-branch regression, path-state budget graceful degradation, unknown-predicate non-pruning, multi-var non-pruning.
  • 35 new unit tests in taint::path_state: classify_condition variants, PathState push/truncation, contradiction detection (whitelisted kinds, single-var only), has_validation_for semantics, state_hash determinism, priority ordering.
  • 11 new unit tests: callee normalization, same-name-different-namespaces resolution, cross-language isolation, arity separation, recursive SCC detection, not-found vs ambiguous diagnostics, diamond topo ordering, interop edge resolution, namespace normalization consistency, and raw call-site preservation.
  • Edge-aware taint traversal -- analyse_file() now uses cfg.edges(node) instead of cfg.neighbors(node), inspecting EdgeKind on each edge. This is required for predicate recording but also makes the taint engine aware of the CFG's branch structure for the first time.
  • Two-tier seen-state deduplication -- the BFS seen-state map changed from HashSet<(NodeIndex, u64)> to a HashMap keyed by (NodeIndex, taint_hash) mapping to a bounded list of (path_hash, priority) pairs. At most MAX_PATH_VARIANTS_PER_KEY (4) path variants are tracked per taint state, with deterministic eviction preferring non-truncated states with fewer predicates.
  • Finding deduplication -- taint findings are now deduplicated by (sink, source) pair after analysis, preferring findings with path_validated = true (most informative metadata).
  • taint::Finding struct -- added path_validated: bool and guard_kind: Option<PredicateKind> fields. Code that constructs Finding directly must include these fields.
  • Diag struct -- added path_validated: bool and guard_kind: Option<String> fields. Both use #[serde(skip_serializing_if)] to omit from JSON when not set.
  • taint::resolve_callee() refactored -- the global resolution step now delegates to GlobalSummaries::resolve_callee_key() and applies normalize_callee_name() before lookup, unifying resolution logic with the call graph builder.
  • Label rules expanded across 8 languages:
    • Go — added r.URL.Query, r.URL.Query.Get, Request.FormValue, Request.URL sources; filepath.Clean/filepath.Base sanitizers; fmt.Fprintf/fmt.Sprintf/fmt.Printf format-string sinks; os.Open/os.OpenFile/os.Create/ioutil.ReadFile/os.ReadFile FILE_IO sinks; template.HTML HTML sink; db.QueryRow/db.Prepare SQL sinks.
    • PHP — sources now match both $_GET and _GET (without $ prefix, matching collect_idents stripping); added $_FILES/_FILES, $_SERVER/_SERVER, $_ENV/_ENV sources; eval/assert shell sinks; include/include_once/require/require_once FILE_IO sinks; unserialize sink; move_uploaded_file/copy/file_put_contents/fwrite FILE_IO sinks; basename FILE_IO sanitizer; query SQL sink.
    • Java — added readObject/readLine sources; ProcessBuilder shell sink; Class.forName reflection sink; println/print/write HTML sinks.
    • Python — added send_file/send_from_directory FILE_IO sinks; os.path.realpath FILE_IO sanitizer; open changed from source to FILE_IO sink (fixes source/sink conflict for path traversal detection).
    • Rubyparams source detection now works via subscript handling.
    • Rust — added fs::read_to_string/fs::write/fs::read/File::open/File::create as FILE_IO sinks; fs::read_to_string removed from sources (was source/sink conflict).
    • C/C++ — added fopen/open as FILE_IO sinks.
  • Ruby rb.cmdi.system_interp pattern broadened — no longer requires string interpolation in arguments; now matches any system/exec call, promoted from Tier B to Tier A.
  • C++ cpp.cmdi.popen pattern addedpopen() command execution detection for C++, using the language-namespaced ID (the C pattern retains c.cmdi.popen).
  • Test config enables state analysistest_config() now sets enable_state_analysis = true.

Fixed

  • Taint source kind misclassified as "unknown" for non-call sources — source-bearing nodes with CallWrapper or Assignment kind (e.g. userInput = req.query.data) had their callee field set to None because the CFG builder only populated callee for StmtKind::Call nodes. This caused infer_source_kind() to receive an empty string, failing to match any keyword pattern and defaulting to SourceKind::Unknown. Fixed by also setting callee when a label (Source/Sink/Sanitizer) is detected, so the extracted member text (e.g. "req.query") flows through to source kind inference. Affects severity classification and diagnostic output for property-access sources across all languages.
  • Full KINDS map audit across all 10 languages — 89 missing tree-sitter node types added to KINDS maps so the CFG builder no longer silently drops code inside switch/case, try/catch/finally, class bodies, closures/lambdas, and other container nodes. Previously, any node not in a language's KINDS map hit the build_sub fallback which created a terminal Seq node without recursing into children, effectively making all wrapped code invisible to analysis.
    • C (+3): switch_statement, case_statement, labeled_statement
    • C++ (+7, 1 fix): switch_statement, case_statement, labeled_statement, throw_statement (Return), try_statement, catch_clause, lambda_expression; critical fix: namespace_definition changed from Trivia to Block (all function definitions inside namespaces were silently dropped)
    • Java (+11): do_statement (While), throw_statement (Return), switch_expression, switch_block, switch_block_statement_group, try_statement, catch_clause, finally_clause, lambda_expression, constructor_body, static_initializer
    • JavaScript (+11): switch_statement, switch_body, switch_case, switch_default, try_statement, catch_clause, finally_clause, class_declaration, class (expression), class_body, export_statement
    • TypeScript (+13): all JS switch/try/class entries plus abstract_class_declaration, export_statement, enum_declaration (Trivia)
    • PHP (+11): do_statement (While), throw_expression (Return), switch_statement, switch_block, case_statement, default_statement, try_statement, catch_clause, finally_clause, colon_block, class_declaration
    • Python (+7): try_statement, except_clause, finally_clause, class_definition, decorated_definition, match_statement, case_clause
    • Ruby (+11): until (While), begin, rescue, ensure, case, when, class, module, singleton_method (Function), do, block
    • Go (+10): expression_switch_statement, type_switch_statement, expression_case, type_case, default_case, select_statement, communication_case, go_statement, defer_statement, func_literal (Function)
    • Rust (+5, 1 removal): closure_expression, async_block, impl_item, trait_item, declaration_list; removed dead loop_statement entry (node doesn't exist in tree-sitter-rust 0.24.0)
  • Removed unused Kind::LoopBody enum variant from labels/mod.rs (no arm in build_sub, last reference was the dead Rust loop_statement entry)
  • CFG: else_clause not recursed into for C/C++ — tree-sitter's C and C++ grammars wrap else bodies in an else_clause node. This node was missing from both languages' KINDS maps, so the CFG builder's fallback arm treated it as a terminal Seq node without descending into children. All statements inside else blocks (e.g. fclose(f)) were silently dropped from the CFG, causing false-positive resource leak and incorrect branch analysis. Fixed by mapping "else_clause" => Kind::Block in src/labels/c.rs and src/labels/cpp.rs.
  • CFG: else_clause missing from Rust, JavaScript, TypeScript, Python, PHP KINDS maps — same bug class as C/C++: tree-sitter wraps else bodies in an else_clause node that was not in KINDS, silently dropping all code inside else blocks from the CFG. Fixed by mapping "else_clause" => Kind::Block in all five languages. Also added "elif_clause" => Kind::Block (Python), "else_if_clause" => Kind::Block (PHP), and "elsif" => Kind::If (Ruby) to handle chained elif/elsif nodes.
  • Rust KINDS using wrong tree-sitter node names — tree-sitter-rust uses _expression suffixes (not _statement) for while, for, and return nodes. The existing while_statement, for_statement, and return_statement entries were dead code (0 grammar matches). Added while_expression, for_expression, and return_expression mappings.
  • Rust match_expression, match_block, match_arm, unsafe_block missing from KINDS — these wrapper nodes were not mapped, causing all code inside match arms and unsafe blocks to be silently dropped from the CFG. Mapped to Kind::Block for sequential traversal.
  • TypeScript missing throw_statement and do_statementthrow was mapped in JavaScript but not TypeScript; do_statement (do-while loops) was missing from both JS and TS. Added "throw_statement" => Kind::Return and "do_statement" => Kind::While to both languages.
  • Python raise_statement and with_statement missing from KINDSraise terminates the current path (mapped to Kind::Return); with wraps code in a context manager (mapped to Kind::Block). Both were silently dropping enclosed code.
  • Dead KINDS entries removed"for_of_statement" in TypeScript (0 grammar matches; TS inherits for_in_statement from JS) and "method_call" in Ruby (0 grammar matches; Ruby only has call).
  • --high-only emitting Low/Medium taint and CFG findings — severity filter was only applied to AST pattern queries during analysis. Taint findings (whose severity derives from SourceKind) and CFG structural findings passed through unfiltered. The filter is now applied at the final output stage after all severity normalization, ensuring --severity HIGH never emits downgraded Medium/Low findings.
  • JSON/SARIF output contaminated with status messages on stdout — status messages ("Checking...", "Finished in...") used println! and appeared in stdout alongside machine output. Now all status goes to stderr.
  • CFG: False edge to then-block exits in no-else if statements -- previously, if (cond) { body } without an else block created a False edge from the condition node directly to the then-block's exit nodes. This made the false path appear to traverse the then-block, causing incorrect predicate polarity in path-sensitive analysis and duplicate taint findings with contradictory metadata. The CFG now creates a synthetic pass-through Seq node for the false path with an explicit False edge from the condition, correctly modeling "skip the then-block." This also fixes the frontier: previously, the no-else non-terminating case duplicated then_exits in the frontier (then_exits ++ then_exits.clone()); it now correctly produces then_exits ∪ [pass_through].
  • Taint BFS non-termination on large JS files — the BFS taint engine in taint/mod.rs had no global iteration bound. The seen-state deduplication keyed on (node, taint_hash), so every distinct taint map at a CFG node was treated as a novel state. In files with loops and many tainted variables (e.g. a 2,200-line JS file with 18+ top-level variables tainted via window.location.search), each loop iteration produced a slightly different taint map, causing the BFS to revisit loop bodies indefinitely. Both --no-index and --rebuild-index scans hung near completion (progress showed e.g. 87/88 files). Fixed by adding two hard bounds: MAX_BFS_ITERATIONS (200,000 queue pops) and MAX_SEEN_STATES (100,000 unique (node, taint_hash) entries in the seen-state map). When either limit is reached the analysis bails out gracefully and returns all findings collected so far. A tracing::warn! is emitted on iteration-limit bail-out. Normal files are unaffected (typical BFS uses <1,000 iterations).
  • Rust if let / while let taint propagation — the CFG builder now extracts pattern bindings from let_condition nodes as variable definitions in def_use(), and classifies the value expression (e.g. env::var("CMD")) for source/sink labels in push_node(). Previously, if let Ok(cmd) = env::var("CMD") { Command::new("sh").arg(&cmd) } produced no taint finding because cmd was never recognized as a tainted definition. Now correctly detects taint flow through if let and while let bindings.
  • C++ popen pattern ID collision — renamed c.cmdi.popen to cpp.cmdi.popen in C++ patterns to fix a cross-language duplicate ID that caused all_pattern_ids_are_globally_unique test failure.
  • State analysis early-return leak duplicationextract_findings in state/facts.rs now skips early-return nodes when checking for resource leaks, only inspecting the synthesized function exit node. Previously, early-return nodes with path-specific state (OPEN only) emitted state-resource-leak alongside the correct state-resource-leak-possible from the merged exit state.
  • Severity filter bugmin_severity comparison in ast.rs was inverted (<= instead of >), causing all AST patterns at the minimum severity level to be silently dropped. With the default min_severity = Low, all Low-severity patterns (.unwrap(), .expect(), panic!, todo!, mem::forget, Go crypto patterns, narrow casts) were never reported. Fixed 29 test cases.
  • Nested function analysis — CFG builder now recurses into function expressions passed as call arguments (e.g., Express app.get('/path', function(req, res) { ... }), Sinatra get '/path' do...end). Added collect_nested_function_nodes() to discover Kind::Function nodes inside CallWrapper/CallFn AST subtrees. Also added function_expression to JS/TS KINDS maps, and do_block/block as Kind::Function in Ruby for Sinatra/Rails blocks. Anonymous functions now get unique names (<anon@{offset}>) to prevent scope collisions in JS two-level taint solve.
  • Chained method call classificationclassify() now normalizes chained calls like r.URL.Query().Get by stripping internal () between . segments, producing r.URL.Query.Get. Suffix matching is attempted against both the original head and the normalized form, fixing Go HTTP handler source detection and similar patterns.
  • Subscript access source detectionfirst_member_label and first_member_text now handle subscript_expression, subscript, and element_reference nodes, enabling source classification for PHP $_GET['cmd'], Ruby params[:cmd], and Python os.environ['KEY'].
  • Return-statement call extractionKind::Return added to the node types that extract inner call identifiers via first_call_ident, fixing cases like return send_file(path) where the sink was not classified.
  • Nested call classification — new find_classifiable_inner_call() tries all nested calls when the outermost one doesn't classify, fixing str(eval(expr)) where eval is a sink wrapped in a non-sink call.
  • Java new expression text extraction — added type field fallback in push_node and first_call_ident for CallFn nodes, fixing new ProcessBuilder(...) not matching as a sink.
  • Function body lookup for anonymous functionsKind::Function handler now falls back to finding a Kind::Block child when child_by_field_name("body") returns None, supporting JS/TS anonymous function expressions and Ruby blocks.
  • Function-level resource leak detectionextract_findings in state/facts.rs now inspects per-function Return nodes for leaked resources, not just the file-level Exit node. Previously, variables from one function could be overwritten by same-named variables in subsequent functions, masking leaks.
  • Use-after-free for memory functions — added strcpy, strncpy, memcpy, memmove, memset, memcmp, strcmp, strncmp, strlen, sprintf, snprintf to RESOURCE_USE_PATTERNS in state analysis, enabling use-after-free detection for common C/C++ string and memory functions.