#Changelog
All notable changes to Nyx are documented here. The format is based on Keep a Changelog and the project follows Semantic Versioning. For where Nyx is going, see the Roadmap.
#[0.8.0] - 2026-08-13
The dynamic-verification release. An attack-surface map, a sandboxed dynamic verifier, a framework adapter registry that grounds both, the per-language build infrastructure that makes per-finding verification affordable at corpus scale, and the first real-corpus acceptance gates.
Alongside it, a static-engine accuracy and throughput campaign. Measured against the previous engine on an identical 669-case corpus, false positives drop from 43 to 0 and false negatives from 20 to 1 (rule-level F1 0.906 to 0.998), while a full scan of a 492-file Go service runs 1.35x faster and reports more findings than before.
The attack-surface map and chain composer turn the flat finding list into a route-to-sink graph. The dynamic verifier re-runs every Medium-or-higher finding against a payload corpus and stamps a Confirmed / PartiallyConfirmed / NotConfirmed / Inconclusive / Unsupported verdict on each. The adapter registry (130+ entries across 8 languages) covers HTTP, message-broker, scheduled-job, GraphQL, WebSocket, middleware, and migration entry points. Per-language build pools and copy-on-write workdirs hold the with-verify wall-clock to within 1.5x of a static-only scan.
#Attack-surface map
nyx surfacesubcommand. Prints the project's entry points, datastores, external services, and dangerous local sinks as text, JSON, Graphvizdot, or rendered SVG. Loads the persistedSurfaceMapfrom the most recent indexed scan when available, or rebuilds inline from source.--buildforces a full pass-1 + call-graph walk so DataStore / ExternalService / DangerousLocal nodes populate on an unscanned project.- Surface page in
nyx serve. NewSurfacePagerenders the same graph in the browser UI, with ELK layout, sidebar navigation, and a wide-canvas SVG viewer. Persists alongside the index so the frontend reloads without a rescan. - Chain findings.
ChainFindingrecords connect a route entry point to a downstream sink via the call graph + surface map. The composer scores(impact × evidence)per chain, queues the top-N for composite reverification, and wires the result intofindings.json/ SARIF / the dashboard. Chains rank above isolated findings. - Per-finding exposure. A finding reachable from an externally-facing route now carries the worst-case route that drives it, surfaced as an
Exposurerecord (route, method, framework, auth state, and whether the reach is direct or transitive through the call graph). Findings with no reaching entry point, and all findings when the project has no detected entry points, carry no record, so an absentExposuremeans "not connected", not "safe". Unauthenticated routes win over auth-gated ones and direct file matches win over transitive ones. The annotation shows up as anExposure:evidence line in console output and onfindings.json, SARIFproperties.exposure, and the server finding view. Ranking adds a bonus for it, so a finding reachable from an unauthenticated route sorts above an otherwise-equal internal one. - Entry-point risk scoring.
nyx surfaceopens with a risk-sorted "Top risk entry-points" banner and tags each route with alow/medium/high/criticaltier. The score is explainable: the worst reachable sink class dominates, writing a store outranks reading it, talking to an external service and mutating HTTP methods add, and missing auth multiplies the whole exposure. The/api/surfaceresponse carries the sameentry_risksarray so the browser UI renders the ranking without re-deriving it. - Function-level reachability with typed edges. Reachability now matches a destination to an entry point when the owning function is on the call-graph frontier, not merely when they share a file, so two unrelated handlers in one file no longer both claim a co-located
eval(). Edges are typed by destination:ReadsFrom/WritesTofor a datastore (split by the access direction inferred from the call verb, so a route that writes SQL reads differently from one that only queries),TalksTofor an external service,Reachesfor a dangerous local sink. The pass falls back to the same-file heuristic when the handler seed cannot be resolved in the call graph, or when a destination loaded from an older persisted map predates the owning-function field. - Richer surface nodes.
DangerousLocalcarries a decoded sink-class label (code-exec,deserialize,ssti, ...) and a real sink span instead of a raw cap bitfield at line 0.DataStoreandExternalServicecarry the qualified name of the owning function. The dangerous-local sink set widened from four classes to ten, adding LDAP injection, XPath injection, header injection, open redirect, XXE, and prototype pollution, and datastore / external detection gained cap-driven fallbacks (SQL_QUERY/FILE_IOandSSRF/DATA_EXFIL) so a custom DAO wrapper or proxy helper still surfaces when no named driver matched. - Entry-point and auth recall. Handlers the framework probes miss but pass-1 already tagged as entry points are synthesised into the surface map so the entry set is a superset of what the taint engine treats as adversary-driven.
auth_requiredupgrades when a handler's own body calls a known auth guard, complementing the router-level decorator / annotation / middleware detection. - Coverage telemetry. A fresh
nyx surfacebuild prints a coverage line (files seen, files in a supported language, files parsed, files with routes, plus unparsed / unreadable counts) so a small map can be told apart from "the probes did not understand this project". A loaded persisted map reports node and edge counts and points at--buildfor a source rebuild.
#Framework adapter registry
src/dynamic/framework/ ships a FrameworkAdapter trait with concrete adapters across 8 languages (116 entries today, growing per release). Each adapter binds a route / handler / consumer pattern to a FrameworkBinding so the surface map and dynamic verifier can locate entry points without re-walking the AST.
- HTTP routers. Flask, Django, FastAPI, Starlette (Python); Express, Koa, NestJS, Fastify (JS/TS); Spring, Quarkus, Micronaut, Jakarta Servlet (Java); Gin, Echo, Fiber, Chi (Go); Axum, Actix, Rocket, Warp (Rust); Rails, Sinatra, Hanami (Ruby); Laravel, Symfony, CodeIgniter (PHP).
- New
EntryKindvariants.ClassMethod,MessageHandler,ScheduledJob,GraphQLResolver,WebSocket,Middleware,Migrationjoin the existingRouteHandler/Functionset so the surface map shows non-HTTP entry surfaces. - Message broker handlers. Kafka, AWS SQS, Google Pub/Sub, NATS, and RabbitMQ consumers across Python, Node, Java, and Go.
- Scheduled jobs. Celery (Python), Sidekiq (Ruby), Quartz (Java), plain cron expression recognition.
- GraphQL resolvers. Apollo, Relay, gqlgen, Juniper, Graphene.
- WebSocket handlers. ws, Socket.IO, ActionCable, Django Channels.
- Middleware + migrations. Express, Laravel, Spring, Django, Rails middleware; Django, Flask, Laravel, Rails, Prisma, Sequelize migration scripts.
- Sanitizer-aware adapter strengthening. Every XXE, header-injection, open-redirect, SSTI, LDAP, XPath, deserialization, crypto, and data-exfiltration adapter rejects bindings when the surrounding source visibly hardens the parser (
disallow-doctype-decl,resolve_entities=False,libxml_disable_entity_loader), routes the value through a known encoder (LdapEncoder.filterEncode,escape_filter_chars,ldap_escape), swaps a weak primitive for a CSPRNG (secrets.token_bytes,crypto.randomBytes,SecureRandom), or validates the destination host through an allowlist. Cuts adapter FPs without losing the genuinely dangerous calls.
#Dynamic verification
nyx scan --verify. Every finding withConfidence >= Mediumis re-executed inside a sandboxed harness against a curated payload corpus. The verdict (Confirmed/NotConfirmed/Inconclusive/Unsupported) lands onEvidence.dynamic_verdictand shows up in console output, JSON, SARIF, and the dashboard via a newVerdictBadgecomponent on the finding detail page.- Backends. In-process on Linux with
Standard/Stricthardening (namespace unshare, chroot, RLIMIT cap, seccomp filter), in-process on macOS viasandbox-execwith a profile-per-policy wrap, Docker with a published image-builder catalogue, and a Firecracker trait stub for future microVM execution. The Docker backend ships native binary support for Rust and Go so harnesses no longer need to drag a toolchain into every image. - Language coverage. Per-language harness emitters for Python, JS/TS, Go, Java, PHP, Ruby, Rust, C, and C++. Stub harness intercepts SQL, HTTP, Redis, and filesystem boundaries so the verdict reflects the sink, not the network. The
JSON_PARSE,UNAUTHORIZED_ID, andDATA_EXFILcap dispatchers are wired into every emitter that ships these caps (Python, JS, TS, Go, Java, PHP, Ruby, Rust), so the verdict pipeline closes the loop on each cap end-to-end rather than per-language piecemeal. - Abstract-interpretation and symex sanitizer suppression. Symbolic execution and the interval/string abstract domain are now consulted at verdict time, so a payload that the static engine would call dangerous but symex can prove never reaches the sink lands as NotConfirmed.
- Guard-aware verdicts. When a known input-validation or output-sanitization middleware sits in front of a Confirmed sink (Spring
@PreAuthorize, Expresshelmet, Nest@UseGuards, Django@permission_classes, and the per-language registry insrc/dynamic/framework/auth_markers.rs), the verdict demotes toConfirmedWithKnownGuardand the guard names land ondifferential.known_guards. Authentication-only filters do not trigger the demotion since they do not mitigate injection. - Repro bundles. Every verified finding writes a hermetic bundle to
~/.cache/nyx/dynamic/repro/<spec_hash>/withreproduce.sh,expected/{verdict.json,outcome.json,trace.jsonl}, and adocker_pull.shwhen the toolchain is pinned intools/image-builder/images.toml.--verboseflushes the per-stepVerifyTraceto stderr for live triage. - Real-engine harness paths. LDAP injection routes through an embedded LDAPv3 BER server, exercised from Java via JNDI
InitialDirContextand from Python and PHP via pure-stdlib BER clients. XPath injection runs against the live parser in each language: Javajavax.xml.xpath, PHPDOMXPath, JSxpathnpm, Pythonlxml.Cap::CRYPTOlands aWeakKeyprobe across Python, Go, Java, PHP, and Rust that flags sub-2^16 keys produced by non-CSPRNG sources. A newHeaderSmuggledInWireoracle predicate catches CRLF smuggling on hand-rolled raw-socket HTTP servers (Pythonhttp.server, Nodenet, Ruststd::net::TcpListener) where framework-level CRLF strip cannot intervene. - Differential rule v2 and partial confirmations. A finding confirms when any vulnerable payload in the set fires and every paired benign control stays clean, replacing the strict pair-wise rule so a single missing control no longer downgrades a confirmable finding. A new
PartiallyConfirmedverdict marks findings where the sink is reached but the exploit chain does not complete (no marker written, no callback observed), so engine work can ratchet without the tool overstating what it proved. - Spec derivation v2. Every derivation strategy now runs and is scored on flow-step depth, framework binding, cross-file source resolution via
GlobalSummaries, and payload availability; the highest-scoring candidate wins and the runner-up ranking lands in the trace so engine gaps stay visible. Cross-file seeding walks the call graph (max depth 5) until aSourcestep or framework binding is found. NewEntryKindadapters auto-recover the entry surface from framework decorators and annotations.
#Performance
- Per-language build pools. A warm
javacdaemon compiles batched harness sources in one long-lived JVM (Track O headline, Phase 22); Node, PHP, Ruby, Go, Rust, C, and C++ reuse shared module / package / object caches; Python layers a read-only venv perrequirements_hashwith a warmed bytecode cache. Target per-finding harness build: P50 ≤ 200ms hot, ≤ 1.5s cold. Pools self-skip when a toolchain is absent so toolchain-less CI rows stay green. - Copy-on-write workdirs. Per-finding workdir setup uses
clonefileon macOS andreflink/copy_file_rangeon Linux instead of copying every harness file, cutting setup cost to single-digit milliseconds. - Cap-routed concurrency lanes. The verifier worker pool splits into per-cap lanes (
SSRF: 8,DESERIALIZE: 2,CRYPTO: 1, and so on) so a slow harness for one cap cannot head-of-line block fast ones. - Ship-gate budgets. Gate 3 holds the with-verify / static-only wall-clock ratio at ≤ 1.5x on
benches/fixtures/; Gate 6 holds the Java OWASP Benchmark--verifyrun at ≤ 15 min on CI / ≤ 10 min on the dev reference machine.
#Determinism, policy, telemetry
- YAML policy deny list.
src/policy.rsis consulted before harness build. Network egress, filesystem writes outside the sandbox root, and process spawns can be denied per-rule; deny decisions land in the trace, redacted via the shared scrubber. - Seeded RNG.
dynamic::rand::SpecRngis seeded from eachHarnessSpechash so two runs of the same spec produce identical payloads.scripts/check_no_unseeded_rand.shaudits the tree for unseededrandusage on every CI run. VerifyTraceobservability. Every per-step decision (probe selection, payload mutation, oracle check, deny verdict) writes to the trace stream and the repro bundle.- Schema-versioned telemetry.
events.jsonlcarriesschema_version,nyx_version,corpus_version,kind, andtson every envelope. PII and secret scrubbing runs on every persisted artefact viasrc/utils/redact.rs. NYX_NO_TELEMETRY=1disables event persistence outright.
#CVE corpus and ground truth
- New
Capcorpora. Vulnerable + patched fixtures landed for the seven new cap classes (LDAP injection, XPath injection, header injection, open redirect, SSTI, XXE, prototype pollution) plus deserialization, crypto, JSON parsing, unauthorized-id, and data exfiltration. Every cap now carries at least one positive / negative / adversarial / unsupported fixture quad per supported language. - OWASP Benchmark v1.2 importer.
tests/eval_corpus/owasp_gt_convert.pyconverts the OWASP Java Benchmark expected-results manifest into Nyx ground truth and lands a 16k-lineowasp_benchmark_v1.2.jsonfor evaluation. - NIST SARD importer.
tests/eval_corpus/sard_gt_convert.pyconverts SARD test cases into the same format so cross-dataset recall numbers stay comparable. - Evaluation corpus tooling.
tests/eval_corpus/run_full.shruns the Nyx benchmark, OWASP Benchmark, and NIST SARD evaluation sets and writestests/eval_corpus/results.json.tests/eval_corpus/report.pyandtabulate.pyproduce the per-cap and per-language summary used to track coverage and accuracy. - Real-corpus acceptance gates.
scripts/m7_ship_gate.shadds Gate 6 (Java OWASP Benchmark v1.2), Gate 7 (NodeGoat + Juice Shop), and Gate 8 (RailsGoat, DVWA, DVPWA, gosec, RustSec). Each row enforces the per-(cap, lang)budget intests/eval_corpus/budget.tomland publishes per-cap precision / recall / confirmed-rate against a committed ground truth. The corpora are not vendored; each row self-skips unless itsNYX_<NAME>_CORPUSpoints at a checkout. - Per-spec cryptographic canary. Every oracle marker is now derived from
BLAKE3(spec_hash || run_nonce)rather than a fixed literal, so markers are unique per finding, collision-resistant against ambient harness output, and never leak to the host. A compile-time audit rejects any new ad-hoc canary.
#Engine
- DB fast-fail preflight.
Indexer::initreads the first 16 bytes of any candidate SQLite file and rejects anything without the standardSQLite format 3\0magic. Stops a misnamed JSON / text file from corrupting the index path with a SQLite error halfway through migration. - Symbolic-execution coverage. Symex now recognises a wider set of string operations (
substr,replace,to_lower,to_upper,trim,strlen) per the value/transfer pipeline, and the abstract-interpretation framework reasons about interval and prefix/suffix string facts during the dynamic verdict pass.
#Fixed (engine correctness)
- CFG construction. Python
if/elif/elif/elsechains no longer drop every alternative past the first, so a sink in a secondelifor a trailingelseis analysed (same fix covers PHPelse_if). C-stylefor (init; cond; incr)loops now lower the initializer and increment, so taint introduced in the loop header (for (cmd = getenv(...); ...)) reaches the body. Aswitchdefaultis no longer unconditionally hoisted to the chain tail, preserving fall-through order in C/C++/JS/TS/PHP/Java. Source/sink calls inside short-circuit&&/||operands of anif/whilecondition are now classified instead of dropped. - SSA lowering of exception handlers. Catch blocks with internal control flow (an
if/ loop / nestedtryinside the handler) no longer lose every instruction past the catch entry: the orphan subtree is renamed through a virtual-root dominator tree, so sinks reached only inside acatchare seen. Catch-side variable reads now resolve to the most entry-dominating reaching definition (the pre-tryvalue) rather than a post-join reassignment. A genuine positional argument equal to a chained-call receiver root (a.b.m(p, a)) is preserved instead of being stripped as the implicit chain root. - Taint soundness. Sink suppression now gates on
validated_must(validated on every path) instead ofvalidated_may(any path), closing a false-negative where a single validated branch silenced a sink.is_noreturn_callno longer matches receiver-qualified.exit()/.abort()/ method calls, sotransaction.abort()stops wiping taint state. The SSRF same-origin check rejects protocol-relative//hostprefixes (an open-redirect / SSRF bypass that a bare/-prefix check accepted). Inline-return taint unions the derived and parameter-passthrough channels for mixed-return helpers (if (c) return src(); return x;). The inline-analysis cache is keyed to exclude callback-bound arguments, so a function-valued argument no longer poisons sibling call sites that pass a different callback. - Taint precision.
String.valueOf(String/Object)is no longer tagged a safe-string producer (it is an identity passthrough, soString.valueOf(req.getParameter(...))was silently suppressed). Cross-parameter sanitizers no longer bleed onto sibling arguments (f(a, b){ return a + escape(b) }sanitises onlyb), and a cross-file sanitizer resolved through the coarse summary tier still applies its strip. Relative-URL and host-allowlist cap clearing is alias-aware. Substring-rejection andindexOf() === -1idioms are no longer misread as allowlist validation, and dotted multi-argument validators no longer over-validate unrelated targets. - Interprocedural resolution. SCC / topo file batching and reachability key files by their package-qualified namespace, matching the call-graph nodes and SSA summary tier so cross-package callers resolve. Directly self-recursive functions now get SCC fixed-point treatment. Call resolution tolerates under-application (a call supplying fewer arguments than a callee with default / optional parameters) while still degrading to
Ambiguousrather than a wrong pick. A failed SCC iteration no longer overwrites a file's cached diagnostics with an empty set. JS/TS module resolution appends extensions to dotted specifiers (./user.service→user.service.ts) and swaps a.jsimport to a.tsfile (NodeNext / ESM). - JS/TS two-level solve. Pass-2 top-level (global) taint now reaches nested closures two or more scopes deep, and the pass-2 dirty-skip no longer drops a nested body that transitively consumes a changed global through a parent-local.
- Scan pipeline and index.
replace_all_for_filedeletes stale SSA summary / body rows unconditionally, so an incremental rescan cannot leave orphaned rows. Cached findings recompute their category instead of being stampedSecurity, so structural warnings keep their real class. The indexed build persists auth summaries and cross-package imports, logs-and-skips an unreadable file instead of aborting the whole build, and keysFuncSummaryentries to match the SSA tier so an indexed scan and a full scan agree. - Language coverage (recall). KINDS maps were completed so previously-dropped bodies are walked: Java interface / enum / record /
synchronizedblocks, Rust inlinemod { ... }items, Go labeled-statement bodies, and Ruby lambda / brace-block bodies. Go variadic and Python*args/**kwargsparameters are seeded with correct arity. C/C++scanf/fscanf/sscanf/readregister their output buffers as taint sources. TypeScript gated sinks dropped from the JS mirror were restored (_.template,http.get/https.get,setValue/dotProp.set/jp.set). The weak-hash and HTTP-URL AST patterns match single- and double-quoted string literals across JS, TS, and Ruby. - Symbolic execution. Interprocedural parameter seeding fixed an off-by-one for method calls and now seeds the receiver / self parameter; the cross-file depth guard increments on descent; and a path cut short by the global step budget records
Inconclusiveinstead ofConfirmed. - Abstract interpretation and pointer analysis. Interval division handles the
i64::MIN / -1overflow (degrading to unbounded instead of a falsely-narrow range) and multiplication computes overflow ini128.AbstractState::leqchecks entries present only in the other state, restoring a sound partial order. The pointer fixpoint re-projects container-element field reads after the receiver's points-to set converges. - CFG-level analyses. Error-fallthrough termination stops at the
ifjoin point; a guard's constant-operand test refuses aSource-labelled call result; guard / sanitizer matchers require a leaf-name boundary (soinvalidateno longer matches thevalidateguard andunquoteno longer matchesquote); resource ownership-transfer requires a real->field =assignment rather than any->in a span; post-dominators are computed once per resource pass; and the web-entrypoint heuristic confirms web parameters against the candidate handler only, so an unrelatedreqparameter elsewhere in the file no longer promotes batch / CLI functions to web entry points.
#Cross-file authorization
- Caller-scope authorization lifting. A private helper that is never itself a route handler now inherits route-level authorization from the units that call it. Pass 1 records one caller-scope edge per (caller, callee) pair; pass 2 appends synthetic route-level checks to a callee only when every observed caller is authorized, so one unauthorized path anywhere refuses the lift. Removes the dominant missing-ownership-check false positive on layered
services/code without weakening the check itself. - Cross-file authorization on the indexed scan path. Caller-scope edges and FastAPI router facts are persisted per file and reloaded in pass 2, so the lift applies under
--index auto(the CLI default) and not only under--index off. Both fact sets previously existed in memory only, which meant a default scan of a layered project reported the ownership false positives the lift is designed to remove: on the cross-file auth fixtures,--index autoreturned up to twice the findings of--index off. The loader replays persisted edges through the same fold the non-indexed path uses, and the accumulator is rebuilt from raw per-file edges on every scan rather than stored folded, becauseall_authorizedis a conjunction and a stored conjunction cannot be corrected when a contributing file is edited or deleted. Indexed-vs-non-indexed parity for these fixtures is now covered by cold and warm cases intests/indexed_parity_tests.rs. Requires a one-time re-index: the cache schema version moves to 5. - Framework fact suppliers. Three extractors feed the mechanism without putting framework knowledge in its core: gitea-style
web.Routerclosure groups (ownership middleware is recognised structurally, by a permission-field read paired with an error emit, rather than by a name allowlist), grapheneMeta.permissionsmutation classes, and FastAPIinclude_router, which is now a cycle-guarded transitive graph instead of a single-hop lookup.
#Static engine precision and recall
- Divergent guards. A guard whose failing branch aborts is dominating again.
LangAnalysisRules::terminatorswas populated only from user config, so out of the box no language had any never-returning call at all and the aborting branch kept a fall-through edge into the join, where thevalidated_mustintersection erased the guard's narrowing. PHPdie/exit, C/C++exit/abort/err, Rustpanic!/unreachable!/process::exit, Pythonsys.exit, Goos.Exit/log.Fatal, JavaSystem.exit, JSprocess.exit, and Rubyexit/abortare now built-in terminators, with user config unioned on top. PHP'sexit_statementis additionally mapped as a control-flow sink, sinceexitparses as its own node rather than as a call. Restores suppression of every "validate, else abort" shape, including the dash-prefix hostname gate in the patched CVE-2017-1000117 fixture. - Rust match guards.
let x = match v { g if <guard> => …, _ => return }no longer reportsxas unvalidated. When every arm other than the guarded one diverges, the guard-false gate is dropped from the statement frontier instead of being handed to the join as a predecessor on which the binding was never validated. execv*executable path. Theexecv/execve/execvp/execvpegates listed only the argv vector as a payload position, on the reasoning that argument 0 is not shell-parsed. There is no shell inexecv*, so no metacharacter is needed: attacker control of argument 0 is direct arbitrary-program execution.execvp(getenv("PROG_PATH"), NULL)was silent in C and C++ and now reports. The existing argv-array refinement that keeps the CVE-2017-1000117 env-config shape (execvp(args[0], args)) quiet now resolves an element read back to its container, so it still applies with argument 0 in scope.- Structural and taint engines agree on what a source is. The structural pass treated every
Source-labelled node as non-constant without mirroring the taint engine's network-fetch exemption, sofile_get_contents("https://api.example.com/health")followed byechoreported an unguarded sink on data that provably came from a compile-time-pinned endpoint. Both engines now consult one predicate. String.valueOf(x.length()). Restored, in argument-aware form, the confinement that a blanketString.valueOfsafe-string arm used to provide before it was removed as unsound. The zero-argument numeric-length accessors are classified instead of the wrapper, soString.valueOf(rendered.length())is confined whileString.valueOf(userInput)still flows.- TypeScript typed parameters. An id-shaped parameter carrying any type annotation was dropped from
unit.params, and since TypeScript annotates essentially every parameter, single-parameter helpers ended up with no user-input evidence at all and could never report a missing ownership check. The drop is now gated on the annotation naming a payload-incompatible primitive (number/bigint/boolean), matching the Go arm, sotargetUserId: stringis user input again whileuserId: numberstays a caller-passed scope key. - Must-leaks are security findings.
state-resource-leakis emitted only when a resource is acquired on every path and reaches no release call, out-parameter, or field escape; anything weaker is already downgraded tostate-resource-leak-possibleat the emit site. It is now categorisedSecurity(CWE-401 / CWE-404 resource exhaustion) rather thanReliability. The may-analyses (state-resource-leak-possible,cfg-resource-leak,cfg-error-fallthrough) stayReliability. Because a leak fixture's only finding is a leak, the blanket demotion had removed must-leaks from the security stream entirely: the rule fired, but every consumer filtering onSecuritysaw nothing. - Interprocedural confinement post-conditions. Function summaries carry five new proved properties about a callee: a boolean prefix-check the caller branches on, a throw-if-false assert guard, a
Resultrejection guard, a same-origin URL normaliser, and astrncmp-confined return. Each closes a false positive on the patched form of a real CVE whose fix introduced a custom confinement helper that name matching missed. Suppressions are capability-scoped, so a confined value reaching a SQL or command sink still fires. - Validation-polarity fix. A negative-polarity validator (
isInvalidUrl,is_not_valid) marked the reject branch as validated, exactly inverting the intended narrowing. Both branches now resolve correctly. - Resource ownership. Leak, double-close, and use-after-close tracking moved from flat per-acquire tracking to an ownership model: a JDBC
StatementwhoseConnectionis body-local no longer reports, connections borrowed from a managed session (HibernateSession,EntityManager, LiquibaseDatabase) are distinguished from owned ones, and C/C++ struct-field handles are attributed to the containing object with an escape-analysis carve-out for locals that die in the same body. - Go multi-assign definition attribution.
f, err := os.Open(p)bound the handle toerrrather thanf. The first identifier is now the primary definition. Removes a false leak onerr, recovers a real missed leak on the handle, and recovers taint throughval, ok := os.LookupEnv(). - Explicit sink payload positions. Sinks declare which argument positions carry the payload instead of implicitly treating all of them as payload (Rust
fs::*/File::*, JS/TS raw-SQL builders, Go GORM query methods), andpromisifyaliases inherit the wrapped callee's restriction. The SSA sink scan is restricted to identifiers syntactically inside the inner sink sub-expression, so a tainted sibling field in an aggregate literal no longer implicates a constant-pathFile::open. - Multi-capability sink de-masking. A helper parameter consumed at two different sink classes now emits one finding per class instead of collapsing into a single conflated rule.
- Language coverage. C gains twelve out-parameter propagations (
strcpy,strlcat,snprintf,memcpy,realpathfamilies) so path assembly through destination buffers keeps taint. Java gains Apache HttpClient request constructors as SSRF sinks; PHP gains Symfony HttpFoundation request accessors as sources; Go gains GORM query-builder SQL sinks; Ruby gains rawpgdriver sinks withexec_paramscorrectly excluded, and modelsoperator_assignmentas an assignment. Rust resolveslet x: Tannotations andparse::<T>turbofish, settling theFromStrambiguity solet p: PathBuf = s.parse()?fires whilelet port: u16 = s.parse()?stays suppressed. - Corpus. Ground truth grows from 563 to 674 cases with no existing expectation modified or removed, including 18 new real-CVE vulnerable / patched pairs across all ten languages. Two cases that shared an id with an unrelated fixture were renamed (
rs-safe-009onsafe_match_guard.rstors-safe-013,py-auth-realrepo-007onvuln_fastapi_route_no_dependencies.pytopy-auth-realrepo-027) so per-case results can be keyed on the id alone. Regression floors tighten from P 0.897 / R 0.944 / F1 0.920 to 0.980 across the board: the old margins were calibrated against a 316-case corpus and were wide enough to hide a 13-case drift. - Hoisted-sink provenance. The search for a callback-nested sink no longer descends a call's callee / receiver side, so a chained call cannot inherit provenance from a sink nested in its receiver's callback. In
http.get(target, cb).on('error', cb2)the.onnode was recordingres.send('ok')from inside thehttp.getreceiver's callback, and that unrelated'ok'literal then read as a constant payload and suppressed a real SSRF finding.
#Static scan throughput
Full-scan wall clock on a 492-file Go service drops from 36.2s to 26.9s (1.35x) while reporting more findings, not fewer. Each optimization ships an equivalence argument and most an equivalence test; several carry a NYX_DISABLE_* toggle so the previous path can be measured from the same binary.
- Single-pass AST queries. Per-rule tree-sitter queries are concatenated into one multi-pattern query, so classification walks the tree once instead of once per rule.
- O(1) node text. A file's UTF-8 is validated once per parse and node text is taken as a boundary slice, replacing a per-node revalidation proportional to the node's byte range.
- Cheaper hashing and lookup.
FuncKeycaches a hash of its identity, turning the engine's most-hashed type from O(len) to O(1) per lookup; label matchers gained a last-byte dispatch index replacing a linear suffix scan; SSA lowering and the symbol interner moved to a faster hash; the CFG node map became a dense positional vector. - Taint worklist. Path environments and abstract state are shared copy-on-write instead of deep-cloned per worklist step, branch-condition classification is memoized, per-call confinement checks are hoisted out of bodies with no confiner summary, and a per-file function-name index replaces four linear scans per callee resolution.
- Dense constant-propagation results. Constant-propagation results moved from a hash map to a dense value-indexed table, removing a per-value hashed insert per body and making every consumer lookup an array index. The persisted format is unchanged.
- Fruitless-walk elision. Auth extraction skips whole-subtree walks that cannot produce a result for the file's language or framework, cutting the isolated Go auth pass by roughly a quarter.
#CLI
nyx scan --verify(enabled by default in standard builds) and--backend {auto,process,docker}select the dynamic-verification harness.--no-verifyskips verification for a single run without changing config.nyx scan --harden {standard,strict}picks the process-backend hardening profile.standardis no-new-privs plus a memory rlimit on Linux.strictlayers namespace unshare, chroot to the workdir, and a default-deny seccomp filter on Linux, or wraps the harness withsandbox-execon macOS.- Patch-validation CI mode.
--baseline FILEreads a previous scan's JSON (or a stripped.nyx/baseline.jsonwritten by--baseline-write) and diffs it against the current scan onstable_hash, emittingNew/Resolved/FlippedConfirmed/FlippedNotConfirmedtransitions.--gate {no-new-confirmed,resolve-all-confirmed}exits non-zero when the diff violates the policy so CI fails the build instead of merging an unreviewed regression. The stripped baseline carries onlystable_hash,dynamic_verdict,severity,path, andrule_id, so persisting it between scans does not leak source. - Repository triage in CI.
nyx scannow reads the same.nyx/triage.jsonfile written bynyx serve. Terminal triage states (false_positive,accepted_risk,suppressed,fixed) are hidden from CLI output and excluded from--fail-onby default, while--show-suppressedincludes them withtriage_state/triage_notemetadata for JSON, SARIF, and console output. nyx scan --verify-all-confidencedrops the Medium cutoff and re-verifies everything.nyx scan --unsafe-sandboxdisables hardening (development only, never for CI).nyx verify-feedback <finding_id> --wrong <reason> | --rightrecords a correction or confirmation for a finding's verdict in the local telemetry log.nyx scan --explain-engineprints the effective engine configuration and exits without scanning.nyx surface(described above) with--format {text,json,dot,svg}and--build.nyx reprosubcommand. Replays dynamic repro bundles by finding id, spec hash, or explicit bundle path, with--docker,--print-path, and--listhelpers. The CLI now matches the browser UI's reproduced command and uses bundle manifests to bridge stable finding ids to spec-hash cache directories.
#Frontend
- Project target selector in
nyx serve. The sidebar now remembers scan roots, lets you switch the active target, and accepts a new project path without restarting the server./api/targetsbacks the selector, scans can opt into a differentscan_root, andnyx scan/nyx index buildregister the projects they touch sonyx servecan pick them up later. - Surface page with ELK auto-layout and the shared node-style palette.
- Verdict badge on finding detail, plus a dynamic-verdict section that surfaces the verdict, the payload that triggered it, and a link to the repro bundle.
- Scan compare gains a dynamic-verdict diff column so two scans can be compared on what was confirmed versus what was downgraded.
#License
- Internal license grants documentation at
LICENSE-GRANTS.md. Grant 1 covers Nyctos derived works. The repo stays GPL-3.0-or-later; the grants document scope of internal product licensing.
#[0.7.0] - 2026-05-11
Seven new vulnerability classes, two SSA sidecars for XML and XPath parser hardening, deeper cross-file FastAPI authorization, and roughly a thousand auth false positives closed on Go DAO helpers plus the dominant Hibernate Criteria SQL cluster. Performance work on the auth extractor, SCCP, and the global summaries map. New nyx rules list CLI, brand-aligned web UI, and CVE corpus growth across Python, PHP, JavaScript, and C.
#Highlights
- New caps for LDAP injection, XPath injection, header / CRLF injection, open redirect, server-side template injection, XXE, and prototype pollution, with per-language label rules across all eight supported languages.
- Cross-file FastAPI authorization:
include_routerchains and module-levelAPIRouter(dependencies=[...])lift onto every attached route, withSecurity(..., scopes=[...])recognised distinctly fromDepends(...). - Type-tracked XML and XPath hardening via two SSA sidecars: parser bodies setting
secure_processing/processEntities: false/resolve_entities=False, andXPathinstances bound tosetXPathVariableResolver(...), count as safe. - ~957
go.auth.missing_ownership_checkfindings closed on gitea-shaped DAO helpers (id-scalar precision pass), 169 of 216 openmrscfg-unguarded-sinkfindings closed on Hibernate Criteria-API receivers, joomla and drupalphp.deser.unserializeclosed onSerializable::unserialize($input)magic-method bodies. nyx rules listCLI subcommand, brand-alignednyx servevisual refresh, regenerated README / docs screenshots and GIFs.
#Detector classes
- New
Capbits and canonical rule ids:Cap::LDAP_INJECTION/taint-ldap-injection,Cap::XPATH_INJECTION/taint-xpath-injection,Cap::HEADER_INJECTION/taint-header-injection,Cap::OPEN_REDIRECT/taint-open-redirect,Cap::SSTI/taint-template-injection,Cap::XXE/taint-xxe,Cap::PROTOTYPE_POLLUTION/taint-prototype-pollution. Each ships per-language sink, sanitizer, and gated-sink rules across JS/TS, Python, Java, PHP, Go, Ruby, Rust, and C/C++. Severity, OWASP 2021 mapping, and description live inCAP_RULE_REGISTRY(src/labels/mod.rs);cap_rule_meta()andrule_id_for_caps()are the public lookups. Capwidened fromu16tou32to fit the new bits, withEvidence.sink_capsandRuleInfo.cap_bitsfollowing. The serde decoder accepts any unsigned integer width, so caches written before the bump still load. SQLite schema bumped from 3 to 4 to force a rescan: oldersource_caps/sanitizer_caps/sink_capsblobs predate the new bits.owasp_bucket_forconsultsCAP_RULE_REGISTRYfirst, so adding a cap class no longer needs a second-table edit. The match requires an exact rule id or a recognised separator (,(,.), so a futuretaint-ssrf-allowlist-violationcannot inherittaint-ssrf's bucket. The legacy family-token table also routesxpath,header, andxxeto A03 / A05.issue_category_label(dashboard badge) routes the seven new rule-id prefixes to dedicated labels: LDAP Injection, XPath Injection, Header Injection, Open Redirect, Template Injection, XXE, Prototype Pollution.
#Engine
src/ssa/xml_config.rstracks XML-parser configuration alongside type-fact analysis, carrying per-receiversecure_processing/disallow_doctype/external_entitiesflags through copy assignments and phi joins (meet for safe flags, sticky union for the unsafeexternal_entitiespolarity).xxe_safe()stripsCap::XXEat the type-qualifiedXmlParser.parsesink when the parser was provably hardened (JAXPsetFeature(FEATURE_SECURE_PROCESSING, true), lxmlXMLParser(resolve_entities=False, no_network=True), fast-xml-parserprocessEntities: false). Persisted toOptimizeResult.xml_parser_config.src/ssa/xpath_config.rsmirrors it for JavaXPathinstances:setXPathVariableResolver(...)flips the receiver'shas_resolverflag, copy assignments union, phi joins meet.xpath_safe()stripsCap::XPATH_INJECTIONatxpath.evaluate(expr, ...)/xpath.compile(expr)sinks when the receiver was provably bound to a resolver. Persisted toOptimizeResult.xpath_config.- Five
TypeKindvariants:LdapClient(JNDIInitialDirContext/InitialLdapContext, SpringLdapTemplate, ldapjscreateClient, python-ldapinitialize, ldap3Connection),XPathClient(JAXPnewXPath, lxmletree.XPath, npmxpath),XmlParser(JAXP factory productsnewDocumentBuilder,newSAXParser,getXMLReader),Template(FreeMarkernew Template(...)/Configuration.getTemplate),NullPrototypeObject(JS/TS values fromObject.create(null)). Wired intoconstructor_typefor return-type inference andTypeKind::label_prefix()for type-qualified callee resolution.XPathClientstays distinct fromDatabaseConnectionso a genericpdo->querySQL_QUERY sink cannot collide withxpath.query. GateActivation::LiteralOnlyfires only when the activation argument is a literal matchingdangerous_values/dangerous_prefixes; unknown or dynamic activation suppresses, with no conservativeALL_ARGS_PAYLOADpush. For shapes identifiable only by an explicit literal flag, e.g.jQuery.extend(true, target, src)deep-merge against Backbone'sModel.extend({proto}).- Two path-state predicates for inline open-redirect sanitisers.
RelativeUrlValidatedcoversx.startsWith("/"),x.starts_with("/"),x.startswith("/"), PHPstrpos($x, "/") === 0, andx[0] === "/".HostAllowlistValidatedcoversnew URL(x).host === ALLOWED,urlparse(x).netloc == ALLOWED, multi-statementparsed.host_str() == "..."(Rust),parsed.Host == "..."/parsed.Hostname() == "..."(Go). Both clearCap::OPEN_REDIRECTonly on the validated branch; non-redirect taint downstream still fires on its own caps. The Go form gates on case-sensitive capitalH, so a lowercaseu.host == Xfield comparison falls through to the genericComparisonpredicate. is_object_create_null_callincfg/literals.rsmatchesObject.create(null)plus parenthesised, awaited, and TS type-cast wrappers, taggingCallMeta.produces_null_proto = true. Type-fact analysis lifts the flag toTypeKind::NullPrototypeObjecton the returned SSA value, suppressing the synthetic__index_set__sink flow-sensitively. Phi joins drop the tag back toUnknown, so a partial null-proto receiver still fires on the unsafe path.- CFG-layer prototype-pollution suppression at the synthetic
__index_set__sink (JS/TS, recognised by the existingtry_lower_subscript_writelowering). Three flow-insensitive shapes elide theSink(PROTOTYPE_POLLUTION)label before SSA sees the node: constant-key fold (literal key not in__proto__/constructor/prototype), reject pattern (siblingif (idx === "__proto__" || ...) return / throw / break;), allowlist pattern (ancestorif (idx === "name" || idx === "id") { obj[idx] = v }). Walks stop at the enclosing function, so closure-captured guards in an outer scope cannot authorise inner assignments. - Spring MVC
return "redirect:" + taintedrecogniser (Java).try_lower_spring_redirect_returnincfg/mod.rsmatches the leftmost+-chain rooted at aredirect:string literal and emits a synthetic__spring_redirect__Call sink withSink(Cap::OPEN_REDIRECT)between the predecessors and the Return node. Concatenated identifiers from anywhere in the right-hand chain feed the node'sarg_uses[0], so a tainted suffix carries through OPEN_REDIRECT. - Subscript-set form classification for header sinks.
response.headers["X-Foo"] = bar/headers["X-Foo"] = bar(Rubyelement_reference, JS/TSsubscript_expression, Pythonsubscript) had nopropertyfield on the LHS.push_nodewalks into the subscript'sobjectand classifies its member-expression text, soCap::HEADER_INJECTIONfires on the bare bracket form alongsidesetHeader/res.set/headers_mut.insert. - PHP literal extraction extended in
cfg/literals.rs:encapsed_string(double-quoted) when every child is a pure-literal segment; boolean literals (true/false) for the jQueryextend(true, ...)LiteralOnlygate; leading-stringbinary_expressionconcat ("Location: " . $url, JS/TS"Location: " + url) sodangerous_prefixesmatching activates on partially dynamic concatenations. - PHP receiver-text strip in
helpers::root_receiver_textdrops the leading$fromvariable_namenodes, so$smarty->fetch(...)/$twig->createTemplate(...)reconstruct asSmarty.fetch/Environment.createTemplatefor suffix-matcher gates. - Gate-callee resolution hardened for member-source rewrites. Where
first_member_labelrewrites a call'stextto a Source likereq.body, the gate matcher reads the call'sfunction/method/namefield instead, sosetValue(target, req.body, ...)matches thesetValueproto-pollution gate. Whitespace is stripped from the function field so multi-line chains still match flat gate matchers. - Ruby option-constant lookup in gate activation. Bare
scope_resolution/constantnodes (Nokogiri::XML::ParseOptions::NOENT) fall back to the macro-arg extractor used by C/C++/PHP, so Nokogiri XXE gates activate on idiomatic option-flag arguments. - PHP
unary_op_expressionnegation recognition. tree-sitter-php emitsunary_op_expressionfor unary!; CFGdetect_negationand condition-chain decomposition now match it, soif (!validate($x))no longer carriescondition_negated=falseand the surviving branch is the rejection arm, not the validated one. - PHP container kinds
declaration_list,interface_declaration,trait_declaration,enum_declaration,enum_declaration_listmapped toKind::Block, so methods inside them participate in CFG construction. - Go variadic
parameter_declarationnamed-field handling forcollect_param_names:nameandtypeare read directly, so type-segment identifiers no longer pollute the param-name set (info *PackageInfono longer contributesPackageInfo). - Empty-formals SSA lowering signal: per-parameter summary probing seeds via
BodyMeta.param_destructured_fields, and JS/TS arrow() => {...}lowers withwith_params=true, counting as "explicitly zero formals" rather than "no formals info".
#Authorization
- FastAPI cross-file
include_routerdependency tracking.auth_analysis/router_facts.rscaptures per-file router declarations (<router> = X(deps=[...])) and<parent>.include_router(<child_module>.<child_var>)edges in pass 1, persists them intoGlobalSummaries::router_facts_by_module, and resolves them into the active file'sAuthorizationModel::cross_file_router_depsat pass 2 entry. Transitive lifts (grandparent to parent to child) use an iterative index walk. Module identity is the file basename without.py. Closes the airflow execution-API shape where a child router lives inroutes/task_instances.pyand its auth is declared on the parent inroutes/__init__.py. - FastAPI router-level
dependencies=[...]propagation. Module-levelrouter = APIRouter(dependencies=[Security(...)])is pre-walked once per file and merged onto every@<router>.<verb>(...)route attached in that file. Closes airflow execution-API routes re-using oneti_id_routerdeclared at module scope. - FastAPI
Security(callable, scopes=[...])recognised distinctly fromDepends(callable). Scoped Security promotes the syntheticAuthChecktoAuthCheckKind::Other(route-level scope-checked authorization), not Login. A scope-tracking boolean threads throughexpand_decorator_callsandextract_fastapi_dependencies. - Caller-scope IPA: same-file route-handler-to-helper auth lift.
apply_caller_scope_propagationwalks every non-route helper unit; if its in-file callers are non-empty AND every caller is itself an authorized route handler (route-level non-Login auth check) or already authorized by this propagation, the caller's checks lift onto the helper as syntheticis_route_level=trueAuthChecks. Iterated to a small fixpoint to cover transitive chains (route to mid_helper to leaf_helper). Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers. Cross-file lifting is not implemented. Closes the dominant FastAPI / Django / Flask shape on sentry / saleor / airflow: route authenticates via decorator or dependency, then delegates to a private helper that performs the sink. - Go DAO-helper id-scalar precision pass. For non-route Go units, a parameter whose declared type is a bounded primitive scalar (
int64,uint32,string,bool,byte,rune,float64, ...) and whose name is id-shaped (id,*Id,*_id,*ids) is dropped fromunit.paramsbefore ownership-check evaluation. Real Go HTTP handlers always carry a framework-request-typed param (*http.Request,*gin.Context,echo.Context,*fiber.Ctx), and per-framework route extractors setinclude_id_like_typed=trueso id-shaped path params survive on real routes. Mirrors the Pythonis_python_id_like_typed_paramfilter. Closes ~957go.auth.missing_ownership_checkfindings on gitea backend DAO helpers (func GetRunByRepoAndID(ctx, repoID, runID int64),func DeleteRunner(ctx, id int64), the wholemodels/...layer whose ownership check sits in the calling route handler) and equivalent shapes in minio / Go ORM codebases. - Bare-callee verb-name fallback gate:
list(...),filter(...),update(...),create_audit_entry(...),update_coding_agent_state(...)(no receiver dot at all) no longer classify asDbMutation/DbCrossTenantReadvia the loose verb-name fallback. Real ORM/DB calls carry a receiver (User.find(id),Model.objects.filter,repo.save(x)); barelist(events)is the Python builtin andfilter(fn, xs)isIterable.filter. Newreceiver_is_simple_chain(callee)requires a non-chained receiver dot. Realtime / outbound / cache prefix dispatches still match by chain root.
#Type-aware sinks and validators
- Java JPA / Hibernate Criteria API treated as structural SQL.
TypeKind::JpaCriteriaQuerycoversCriteriaQuery<T>,CriteriaUpdate<T>,CriteriaDelete<T>,Subquery<T>,TypedQuery<T>.sink_args_jpa_criteria_query_safeclearscfg-unguarded-sinkSQL_QUERY when any positional argument is JpaCriteriaQuery-typed; the receiver is excluded, being the Session/EntityManager channel insession.createQuery(cq)and never the SQL payload.cb.createQuery(...),em.getCriteriaBuilder(), and the JpaCriteriaQuery type chain are inferred via constructor / factory return-type hints intype_facts.rs. Closes the dominant FP cluster on openmrs (169 of 216 cfg-unguarded-sink), xwiki, and keycloak Hibernate DAO methods. - Receiver-side validator registry.
labels::lookup_receiver_validator(lang, callee)clearsCapfrom the receiver value (and call equivalents) on success, unlikeSanitizer, which clears caps from the return value. Python registersrelative_to => Cap::FILE_IO, sopath.relative_to(base)drops the file-IO cap on the path. Closes the CVE-2024-23334 patched aiohttpstatic_root_path.joinpath(filename).resolve().relative_to(static_root_path)shape. - JS/TS Array-method validator-callback narrowing.
arr.filter(isSafeIdentifier),arr.find(isValidId),arr.findLast(...)with aBooleanTrueIsValidcallback (isValid...,isSafe...,hasValid...and snake-case variants) propagatevalidated_mustthrough the return value. Callback name resolves frominfo.arg_callees(call-shape arguments) and SSAvalue_defs[v].var_name(bare-identifier callbacks, the dominant patched-CVE form). Strict-additive: anonymous arrows and opaque identifiers leave existing propagation untouched.findIndex/every/someexcluded (scalar return shape). Motivated by CVE-2026-42353. - JS/TS ternary-branch source classification.
let arr = cond ? req.query.lng : "";lowered each branch to a labelless Assign with empty uses, so the join phi saw no taint.lower_ternary_branchrunsfirst_member_labelon the branch AST when noSourcelabel is attached. - PHP
fopenmodeled asSink(Cap::SSRF), the same dual SSRF / LFI shape asfile_get_contents; fires only on tainted argument. Closes CVE-2026-33486 (roadiz/documentsDownloadedFile::fromUrlwrappingfopen($url, 'r')). - PHP
Serializable::unserialize($input)magic-method passthrough recognition. The legacySerializablecontract (deprecated since PHP 8.1) requires the implementation to call\unserialize($input)on its formal parameter insidepublic function unserialize($x) { ... }, and PHP invokes the method when restoring an instance, so the call cannot be removed without breaking the interface.php.deser.unserializesuppresses inside that exact shape (method namedunserialize, single formal, bare-parameter argument). The actionable signal is the class-levelSerializableimplementation, the fix being migration to__serialize/__unserialize. Closes joomla / drupal Serializable-implementing class FPs. - SQLAlchemy query-builder chained-call recognition.
select(X).filter_by(...),query(X).filter(...),select().join().where()anchor through the chain root primitive when the chain receiver type is opaque. Newdb_query_builder_rootsconfig (Python defaultsselect,query). Closes airflowsession.scalar(select(C).filter_by(conn_id=user_input))shapes that dropped under the chained-call suppression inclassify_sink_class. - Python non-sink container constructor recognition. Bare-callee
set()/dict()/list()/tuple()/frozenset()/defaultdict(...)counts as a non-sink constructor, soverified_ids = set(); verified_ids.update(myteams)does not classify the.updatecall asDbMutation. Type-annotation hintsset[int]/dict[str, int]are recognised via PEP 585 generic suffix strip alongside the existing angle-bracket strip. - Python
request.match_infosource label (aiohttp path-parameter source). - New Python pattern
py.xss.make_response_format(Tier B) for Flaskmake_response(<f-string-or-concat>)reflection, matching baremake_response(...)andflask.make_response(...). Closes CVE-2023-6568 (mlflow authcreate_userreflecting an attacker-controlledContent-Typeheader into the response body).
#Language coverage
Per-language label rules expanded for the seven new caps.
- JavaScript / TypeScript: ldapjs
LdapClient.search,escapeXpath/xpathEscape,document.evaluate/ npmxpath.select,setHeader/res.set/res.append/res.headers[]=,stripCRLF/escapeHeader, lodash / dot-prop / object-path deep-merge prototype-pollution gates, Handlebars / EJS / Mustache template sinks, fast-xml-parser / xml2js withprocessEntities-aware activation,redirect/Locationopen-redirect sinks. - Python: python-ldap
LDAPObject.search_s, ldap3Connection.search, lxmletree.XPath/lxml.etree.parsewith parser-config awareness, Flaskresponse.headers[]=/make_response, Jinja2Template(...)and MakoTemplate(...)SSTI sinks,flask.redirect/aiohttp HTTPFoundopen-redirect. - Java / Kotlin:
DirContext.search,XPath.evaluate/XPath.compile, JAXPDocumentBuilder.parse/SAXParser.parse/XMLReader.parse, FreeMarkerTemplate.process, Springredirect:view-name synthetic sink,HttpServletResponse.setHeader/addHeader. - PHP:
ldap_search/ldap_list/ldap_read,DOMXPath::query/DOMXPath::evaluate,header()with leading-prefix activation, Smartyfetch/ TwigcreateTemplate/ Blade compile +evaltemplate forms,loadXML/simplexml_load_stringwithLIBXML_NOENTactivation. - Go:
go-ldap conn.Search,etree.Path/xmlpath.Compile,http.Header.Set/Response.Header().Set,html/templateandtext/templateParse(...),encoding/xml.Unmarshal/Decoder.Decode,http.Redirectwith relative-URL / host-allowlist gating. - Ruby:
Net::LDAP#search,Nokogiri::XML::Document#xpath,response.headers[]=,ERB.newSSTI,Nokogiri::XML.parsewithNOENT/DTDLOADactivation,redirect_towith relative-URL gate. - C / C++: libldap
ldap_search_ext_s, libxml2xmlXPathEval,curl_easy_setoptwith header-list activation, libxml2xmlReadFile/xmlReadMemorywithXML_PARSE_NOENTactivation. - Rust: actix-web
HeaderMap.insert/HeaderValue::from_strheader-injection gates.Redirect::toretagged fromCap::SSRFtoCap::OPEN_REDIRECTso the open-redirect rule fires distinctly from the SSRF rule.
NYX_PYTHON_PROTO_POLLUTION opt-in flag: Python dict.update / __dict__.update proto-pollution gates are off by default because bare update overlaps too broadly with Counter.update and ordinary state mutation to ship as a default sink.
#CVE corpus
- C: CVE-2017-1000117 (git argv injection via
ssh://-oProxyCommand=...) vulnerable + patched fixtures undertests/benchmark/cve_corpus/c/CVE-2017-1000117/. Remaining gap: array-element taint propagation,c.cmdi.exec*AST patterns, dash-prefix-byte sanitizer recognition. - Python: CVE-2023-6568 (mlflow reflected XSS), CVE-2024-21513 (langchain SQL / Jinja), CVE-2024-23334 (aiohttp static-file path traversal) vulnerable + patched fixtures.
- PHP: CVE-2026-33486 (roadiz/documents SSRF) vulnerable + patched fixtures.
- JavaScript: CVE-2026-42353 (i18next-http-middleware path traversal) vulnerable + patched fixtures.
#CLI
nyx rules listsurfaces the same registry the dashboard's/api/rulespage reads: built-in cap-class entries (one perCapwith a canonical rule id), per-language label rules (sink / source / sanitizer), gated sinks, custom rules from config. Filters--lang <slug>,--kind <class|source|sink|sanitizer>,--class-onlyfor registry entries only,--no-classfor per-language rules only.--jsonfor machine output. Cap-class entries carrylanguage = "all", so a language filter still surfaces them unless--no-classis set.- New
RuleInfo.is_class/RuleInfo.emission_activeflags. Cap-class entries carryis_class = trueso dashboards can group them separately.emission_active = falsemarks legacy classes (SQL_QUERY, SSRF, FILE_IO, FMT_STRING, DESERIALIZE, CODE_EXEC, CRYPTO) whose findings still surface under the catch-alltaint-unsanitised-flowrule id; the seven new classes plusunauthorized_idanddata_exfilareemission_active = true.cap_rule_registry_emission_active_set_is_pinnedpins the active set so migrating a legacy cap cannot drift silently. parse_capandCapName::FromStraccept the new short names:ldap_injection/ldapi,xpath_injection/xpathi,header_injection/crlf/response_splitting,open_redirect/redirect,ssti/template_injection,xxe,prototype_pollution/proto_pollution, plus the existingdata_exfilalias. Thenyx config add-rule --capflag and[analysis.languages.*.rules]entries take any of these.
#Frontend
- Local web UI restyled around the mint-cyan Nyx brand: warmer light surfaces, deep green accents, updated severity / confidence colors, tighter typography, smaller radii, denser cards, table, badge, button, header, and sidebar styling, matched graph / code-viewer colors.
nyx servesurfaces reworked for a more operational layout. Overview uses the refreshed health-score card and chart grid; Scans has a fixed compact table with capped language badges; Scan Detail places summary and timing side by side; Triage, Rules, Config, Explorer, Finding Detail, Scan Compare, and Debug got spacing, overflow, and density fixes.- Branded asset set shared between the SPA and the embedded server bundle: PNG favicons, Apple touch icon, sidebar logo image, refreshed SVG favicon, Rust static handlers for the new
/logo.pngand favicon files. RuleListItemandRuleDetailViewcarry the newis_classflag so the dashboard's Rules page can group cap-class entries separately.- README and docs screenshots and GIFs regenerated against the new UI at 1600x992, saving raw originals before framing and adding CLI GIF plus combined CLI-to-serve demo GIF capture. The capture workflow gained mint-led framing copy, optional
nyxsec.devasset mirroring, WebP regeneration for mirrored PNGs, raw_rawimage / GIF outputs.
#Performance
collect_top_level_unitshoisted out of the per-extractor loop inextract_authorization_model. Multi-extractor languages (Go gin+echo, JS/TS express+koa+fastify, Python flask+django, Rust axum+actix_web+rocket, Ruby sinatra) had been re-walking the whole AST and rebuilding theFunction-kind unit set per extractor, then deduping by span. NewAuthExtractor::requires_top_level_units()opt-out for Spring / Rails, which build their own. Was 46% ofextract_authorization_modelwall-clock on the mattermost/server/channels/app subtree.- Single
AuthorizationModelbuild per file in fused mode. The diag path and the per-file summary path each ran their ownextract_authorization_model, duplicating the hoisted unit pass and every framework extractor's AST walk. Auth summaries now extract from the base model (pre var-types, pre helper-lifting), so the persisted per-file summary matches the legacyextract_auth_summaries_by_keypath bit-for-bit. - O(N) shallow value-ref emission in
collect_unit_state. Per-nodeextract_value_refs(node, bytes)walked the entire subtree at every recursion level (O(N²) per body) even though the recursion below already visits every descendant once. Newappend_shallow_value_refemits the node's own ref and lets recursion handle the descent; public callers (collect_call,collect_condition, assignment-side extraction) keep the deep walk. Was ~17% + 15% + 11% of wall-clock acrossbuild_function_unit_with_meta,collect_unit_state, andextract_value_refson mattermost. - Per-
ParsedFilebody_const_facts_cache: OnceCell. SSA + const-prop + type-fact build was running 2-3× per body acrossrun_cfg_analyses_with_lowered,run_auth_analyses, andcollect_file_var_types. Single-pass cache; gin profile dropped from 13.6% to ~4.5%. - SCCP switched from
HashMap<SsaValue, _>andHashSet<(BlockId, BlockId)>to a denseVecper-value lattice and per-destination predecessorSmallVec<[BlockId; 2]>, so the inner fixed-point loop no longer SipHashes a 64-bit pair for every operand of every phi. PublicConstPropResultshape unchanged (one final O(num_values) HashMap conversion). GlobalSummaries.by_keyswitched toFxHashMap(rustc-hash 2.1) from stdlib SipHash.FuncKeycarries 3 String fields, so any HashMap operation hashes at least 30 bytes; FxHash is ~5× faster on this workload. Seed is fixed (no DoS hardening), fine for an in-process index keyed by program-derived names.large_go_module.goperf fixture (1493 lines) added tobenches/perf_fixtures/;benches/scan_bench.rsextended with auth-extractor, SCCP, and summary-resolution rows.
#Fixed (false positives)
Object.create(null)receivers no longer fire prototype-pollution at the synthetic__index_set__sink. Suppression is flow-sensitive viaTypeKind::NullPrototypeObjectso a phi join that only sometimes resolves to a null-proto receiver still fires on the unsafe path.cfg-unguarded-sinkover-fires on JS/TS object-literal property writes guarded by an explicit__proto__/constructor/prototyperejectif(earlyreturn/throw/break) or by an allowlistifwhose true arm contains the assignment.- Spring MVC
return "redirect:" + urlflagged generictaint-unsanitised-floweven when the redirect destination was the load-bearing taint. Now routed through the synthetic__spring_redirect__sink, so the finding emerges astaint-open-redirect. $smarty->fetch(...)/$twig->createTemplate(...)no longer drop their SSTI gate match on idiomatic PHP receiver shapes.setValue(target, req.body, ...)and similar wrappers no longer gate-match on the rewritten Sourcereq.bodytext.- Nokogiri / lxml / fast-xml-parser parser bodies hardened with
setFeature/processEntities: false/XMLParser(resolve_entities=False)no longer firetaint-xxe. XPathinstances bound tosetXPathVariableResolver(...)no longer firetaint-xpath-injectionon subsequentxpath.evaluate(expr, ...)sinks.- Inline
if (!url.startsWith("/")) rejectandif (new URL(url).host !== ALLOWED) rejectopen-redirect sanitisers narrowCap::OPEN_REDIRECTon the validated branch instead of falling through to the genericComparisonpredicate. - Rust
Redirect::tono longer firestaint-ssrffor what is structurally an open redirect; retagged toCap::OPEN_REDIRECT. - ~957 gitea backend DAO
go.auth.missing_ownership_checkfindings (id-scalar precision pass). - 169 of 216 openmrs
cfg-unguarded-sinkfindings (JpaCriteriaQuery type). Equivalent reductions on xwiki / keycloak Hibernate DAO clusters. - joomla and drupal
php.deser.unserializeflagged insideSerializable::unserialize($input)magic-method bodies. - airflow execution-API routes flagged
missing_ownership_checkdespite being authorized via cross-fileinclude_routerchains and module-levelAPIRouter(dependencies=[...])declarations. - sentry
verified_ids = set(); verified_ids.update(myteams)flagged asDbMutation. - aiohttp
path.relative_to(static_root_path)not recognised as a path-traversal validator. - i18next-http-middleware
arr.filter(utils.isSafeIdentifier)not narrowing taint on the result. cond ? req.query.lng : ""ternary lostSourcelabel on the truthy branch.if (!validate($x))rejection-arm narrowing flipped on PHP unary!.- mlflow
make_response(f"Invalid content type: '{content_type}'")(Tier B pattern). - Bare-callee verb-name dispatch on Python builtins / locally-defined helpers (
list,filter,update,create_audit_entry,update_coding_agent_state). - FastAPI
Depends(...)/Security(...)deps declared on a module-levelAPIRouterno longer dropped on every attached route. - FastAPI
Security(callable, scopes=[...])no longer downgraded to a Login-only check.
#Tests
- New per-cap integration suites
tests/{xpath_injection,xxe,ssti,prototype_pollution,header_injection,open_redirect,ldap_injection}_tests.rs, pluspython_proto_pollution_tests.rsfor the env-gated Python form. Per-cap fixture trees undertests/fixtures/<class>/<lang>/cover safe, unsafe, and irrelevant-baseline shapes for every supported language. - Cross-file FastAPI integration test
tests/fastapi_cross_file_include_router_tests.rswith airflow-shaped fixture tree undertests/fixtures/auth_cross_file/airflow_execution_api_includes/. - New
cfg/cfg_tests.rscovers ternary-branch CFG lowering shapes. - New
summary/tests.rscovers cross-fileinclude_routersummary persistence and resolution. - Per-language safe / vuln auth and detector fixtures across Python, Java, Go, PHP, JS, TS.
#Other
- Refactor passes across
auth_analysis,ssa/const_prop,ssa/type_facts,summary, and the per-framework auth extractors (cleaner conditional checks, simpler function signatures, deduplicated assertions). No behaviour change. - README links to a Simplified Chinese translation (
README.zh-CN.md).
#[0.6.1] - 2026-05-03
A precision pass on auth and resource analysis, three fresh CVE corpus pairs, and a UTF-8 slice panic in the path abstract domain. Closes ~1900 Go auth FPs on gitea-shaped helpers, the mastodon/diaspora private-callback Ruby controller pattern, and a phantom-taint outbreak from JS/TS / Java lambda shorthand in jest-style nested test callbacks.
#Added
- Java JDBC raw-SQL sinks
Statement.execute,Statement.executeBatch,Statement.executeLargeUpdatemodeled asSQL_QUERY, classified via type-qualified resolution (DatabaseConnection.execute) so bareexecute(Runnable, Executor, HttpClient) does not over-fire.conn.createStatement()andconn.prepareCall()infer return typeDatabaseConnection, so the JDBC chainStatement s = conn.createStatement(); s.execute(q)typesscorrectly. Closes GHSA-h8cj-hpmg-636v (Appsmith FilterDataServiceCE.dropTable). Vulnerable + patched Java fixtures added. - Java/Kotlin
Pattern.matcher(value).matches()chain recognised as aValidationCallallowlist. The receiver of.matcher(must containregexorpattern; the validation target is the.matcher()argument, not the bare.matches()receiver. Branch narrowing appliesvalidated_mustto the input variable on the surviving branch. Same GHSA as above (FILTER_TEMP_TABLE_NAME_PATTERN.matcher(tableName).matches()). - Per-parameter SSA summary probe receives
BodyMeta.param_types, soextract_ssa_func_summaryruns a localanalyze_types_with_param_typespass before extraction. Helper bodies whose sinks resolve only via type-qualified callees (e.g.DatabaseConnection.executefor JDBCStatement.execute) no longer drop the sink during cross-function summary extraction. Fixes the Appsmith helperexecuteDbQuery(query)routing SQL throughstatement.execute(query). - Short-circuit branch condition CFG nodes mirror
condition_varsintotaint.uses, soapply_branch_predicatesinterns the variable for short-circuit-decomposed validators (if (x == null || !regex.matcher(x).matches()) throw). Previously the per-disjunct cond nodes frombuild_condition_chainno-opped andxnever reachedvalidated_muston the surviving branch. - Go
goqu.L(s)andgoqu.Lit(s)raw-SQL literal builders modeled asSQL_QUERYsinks. Safe siblings (goqu.Iidentifier,goqu.Ccolumn,goqu.Ttable,goqu.Vparameterised value,goqu.SUM,goqu.COUNT, ...) stay unlabeled. Gin source list extended with the array-returning siblings of the existing scalar helpers:c.QueryArray,c.GetQueryArray,c.PostFormArray,c.GetPostFormArray. Closes CVE-2026-41422 (daptin:c.QueryArray("column")reachinggoqu.L(project), loop variable lifted throughfor _, project := range columns). Vulnerable + patched Go corpus pair undertests/benchmark/cve_corpus/go/CVE-2026-41422/. - Go
for ident := range iterdef-use lifting. Therange_clausechild offor_statementis consulted whenleft/rightaren't direct fields of thefornode, so taint from the iterable reaches the loop binding. Required for the daptin CVE shape above. - Java
enhanced_for_statement, PHPforeach, and Rubyfordef-use lifting, completing the loop forms the Gorange_clausefix started. TheKind::Fordef-use arm only knew the JS/Pythonleft/rightpair and Go'srange_clause; Java carries the binding onnameand the iterable onvalue, Ruby'sforonpattern/value, and PHP'sforeachkeeps both as unnamed children split by theaskeyword, so none recorded the loop variable as a define and taint on the iterable never reached the binding (for (Cookie c : req.getCookies()) { ... c.getValue() ... }lost the flow atc). Each form folds onto the shared define/use path. Lifts Java OWASP Benchmark recall: path_traversal 0.21 to 0.32, sqli 0.16 to 0.28, cmdi 0.04 to 0.08. - Iterable-expression classification for the loop forms above: the loop node is classified against its iterable text, so a source-returning iterable (
req.getCookies(),req.getParameterValues("v"),$_GET['list']) lands aSourceon the loop node and the binding inherits its taint, the same rewrite JS/Pythonfor ... of/for ... inalready had. Subscript iterables ($_GET['x'],params[:list]) classify on their base object, since sources key on the base name, not the index. - Java iterable-returning request accessors modeled as sources:
getParameterValues,getParameterMap,getParameterNames,getHeaders,getHeaderNames. ThegetParameter/getHeadermatchers are word-boundary suffix matches and never covered the plural collection variants feeding for-each loops (for (String s : req.getParameterValues("v"))), the dominant OWASP Benchmark vulnerable-source shape. - Rust format-string named-argument lifting (
format!("...{x}..."), stable since 1.58). Identifiers captured by{name}/{name:fmt-spec}are pulled into the call'susesfor known format-style macros:format,print/println,eprint/eprintln,write/writeln,panic,format_args,assert/debug_assert,todo,unimplemented,unreachable, plus log-crate severity macros (info,warn,error,debug,trace). Recursive descent through one or two layers of expression wrapping (format!("{x}").to_owned(), RHS chained method calls). Previously taint stopped at the macro boundary:let q = format!("...{x}...")carried noxbecause the identifier lives in format-string bytes rather than a separate AST argument node. Mirrors the Python f-string lifter. - Rust CVE corpus extended with CVE-2023-42456, CVE-2024-32884, CVE-2025-53549 vulnerable + patched fixtures under
tests/benchmark/cve_corpus/rust/. - Java lambda shorthand recognised by
extract_param_meta.lambda_expression'sparametersfield as a bareidentifier(cmd -> ...) or as aninferred_parameterswrapper around identifiers ((a, b) -> ...) did not match the formal_parameter / spread_parameter kinds inPARAM_CONFIG, so the lambda appeared parameterless and the SSA pipeline treated its formals as closure captures. Mirrors the JS/TS arrow shorthand path.
#Fixed
- Panic on non-ASCII input to
has_first_char_absolute_checkin the path abstract domain. The 32-byte search window around[0]was sliced as&clause[lo..hi](str), which panicked whenhilanded inside a multi-byte UTF-8 char (e.g. the em dash—, bytes 34..37). Switched to&bytes[lo..hi]withwindows()byte-pattern checks; all needles are ASCII so the searches are equivalent. Surfaced bycargo fuzz(scan_bytestarget,.cextension path, embedded—in a comment nears[0] == '/'). Regression test added.
#Fixed (false positives)
cfg-unguarded-sinkparameter-only trace no longer clears a sink argument whose reaching definition is a loop binding. Once the loop variable resolves to its iterable (the def-use lifting above), aforeach ($param as $v) { sink($v) }element looked like a baresink($p)wrapper pass-through and the structural finding was dropped. A loop element over a parameter collection is not wrapper plumbing, so the finding survives for loop-bound sink arguments; literal-keyed arrays stay suppressed throughsink_arg_uses_safe_foreach_key, keeping the negative case infp_guard_php_foreach_safe_literal_keysfiring.- Go
unit_has_user_input_evidenceframework-request-name allow-list narrowed for Go.ctx,context,info,body,path,payload,dto,form,queryare no longer user-input indicators on Go: there they arecontext.Context(stdlib cancellation/value-bag) or struct-pointer payload params (info *PackageInfo,opts *FooOptions), not request bindings. Go HTTP frameworks bind the request to per-framework typed params (r *http.Request,c *gin.Context,c echo.Context,c *fiber.Ctx), which arrive at the gate viaRouteHandlerkind or the type-aware param filter below. Stdlibreq/request(the*http.Requestconvention) preserved. Other languages keep the broader allow-list. - Go param collection drops
ctx context.Contextandctx context.CancelFuncparameters entirely rather than seeding their names intounit.params. Tree-sitter-go'sparameter_declarationexposesnameandtypeas named fields; descend only intonameso type-segment identifiers don't pollute the param-name set (info *PackageInfono longer contributesPackageInfo). With the allow-list narrowing above, closes ~1900go.auth.missing_ownership_checkfindings on gitea backend helpers whose only "user-input evidence" was the ubiquitousctx context.Contextfirst param. - Ruby controller method visibility + filter-callback gate. Methods marked
private(bareprivatedirective, targetedprivate :foo, :bar, orprotected) and Rails filter callback targets (before_action,after_action,around_action, theirprepend_*/append_*/skip_*siblings, and the legacy*_filteraliases) are no longer emitted asFunctionunits. Visibility tracking is class-body source-order with two directive forms (bare toggles default visibility, targeted explicitly marks named methods). Block-form filters (before_action do ... end) carry no symbol arg and are ignored. Closes the mastodon / diasporarb.auth.missing_ownership_checkflood onset_Xrow-fetch helpers used asbefore_actioncallbacks. - Field-LHS resource acquires no longer counted as local resource leaks at the
apply_assignmentsite.e->name = (char *)e + sizeof(*e)(sub-buffer alias inside a returned struct) andmem->buf = ptr(local-into-field ownership transfer) mark the RHS localMOVEDand stop tracking the field as a separately OPEN resource; the parent struct owns the field's lifecycle. Cross-language, distinct from the Go-onlyapply_callfield-LHS gate, which is restricted because JS/TS class-field acquiresthis.fd = fs.openSync(...)are the documented expected leak pattern in that path. Closes curlentry_newand equivalent C/C++ shapes in openssl / postgres. - Empty-formals SSA lowering signal.
lower_to_ssa_with_paramssetswith_params=trueeven whenformal_paramsis empty, so an arrow() => {...}counts as "explicitly zero formals" rather than "no formals info". External vars in a zero-formal arrow are tagged as synthetic closure captures, so the JS/TS / Java auto-seed pass cannot mistake a bubbled-up free var (e.g.userIdlifted from a nested jest test callback) for a real handler formal. Closes 934 phantom taint findings on the outline test suite (describe("...", () => { test("...", () => { server.post(...) }) })-shaped fixtures). - Rust integer-typed values suppress
Cap::FILE_IOat the abstract-domain leaf gate (previously HTML_ESCAPE only). An integer's decimal representation is digits with optional leading-, never path metacharacters (/,\,.); magnitude is irrelevant. Closes the sudo-rs RUSTSEC-2023-0069 patched FPlet uid: u32 = user.parse()?; path.push(uid.to_string()).
#[0.6.0] - 2026-05-02
Data exfiltration splits off from SSRF, with sinks for outbound HTTP request bodies across all 10 languages and calibration tuned so plain user input echoed back upstream does not fire.
#Added
- New
taint-data-exfiltrationrule, separate from SSRF. Fires when a Sensitive-tier source (cookie, header, env, file, database, caught exception) reaches the body, headers, or json payload of an outbound HTTP call. Plain user input is suppressed at emission time, so a gateway echoingreq.bodyback upstream is not flagged. - Sinks ship for
fetchbody,XMLHttpRequest.send, Pythonrequests.postandhttpx.AsyncClient.post, Java JDKHttpClient.sendwithBodyPublishers, OkHttp builder chains, Apache HttpClientexecute, RestTemplate, WebClient, Gohttp.Postandhttp.NewRequest+Do, Rustreqwest/ureq/surf/hyperbody/json/form/multipart chains, RubyNet::HTTP.postand RestClient, C and C++curl_easy_setopt(CURLOPT_POSTFIELDS, ...)gated by the macro arg. - Three suppression knobs:
- Sanitizer convention.
logEvent,forwardPayload,tracker.send,analytics.track,metrics.report,serializeForUpstreamcount asSanitizer(data_exfil)by default. Add your own with the standard custom-rule path. - Trusted destination allowlist in
detectors.data_exfil.trusted_destinations, matched against the abstract-string domain prefix; a literal or template prefix beginning with one of these entries drops the cap. - Detector toggle
detectors.data_exfil.enabled = falsestrips the cap before emission. Other taint classes are unaffected.
- Sanitizer convention.
- Calibration. Severity is High for cookie or env sources, Medium for header, file, database, or caught-exception sources. Confidence stays at Medium even with strong corroboration, drops to Low without abstract or symbolic backing, and drops one tier on path-validated flows. SARIF output carries a
properties.data_exfil_fieldentry on data-exfil findings, set to the destination object-literal field the leak reached (body,headers, orjson). - Benchmark coverage: 13 vulnerable fixtures across 8 languages under
tests/benchmark/corpus/{lang}/data_exfil/and 6 paired safe fixtures for the sensitivity gate and sanitizer convention. Newdata_exfilrow in the per-class breakdown. Per-class CI floor at P, R, F1 ≥ 0.85 (current baseline is 1.000). - Backwards taint walk recognises
Cap::DATA_EXFILand emits the same rule ID. - Ruby SSRF coverage:
OpenURI.open_uriclassified as a sink (the low-level fetcherURI.opendelegates to). Closes the CarrierWave CVE-2021-21288 download path and equivalent gem shapes routing throughOpenURIdirectly. - Ruby chained-call wrapper classification: statement-level wrappers like
YAML.safe_load(File.read(filename))andMarshal.load(File.read(p))classify the inner sink for cross-function summary extraction. Previously the outer call became a non-sink node and the inner sink was lost when the helper was summarised. - Ruby CVE corpus: vulnerable + patched fixtures for CVE-2021-21288 (CarrierWave SSRF) and CVE-2023-38337 (rswag path traversal).
- Lodash
_.templatemodeled as a gatedCap::CODE_EXECsink. Activates on the template-string argument; suppresses when arg-1 carries a literal{ evaluate: false }. Closes Strapi CVE-2023-22621 (server-side template injection to RCE via<% ... %>evaluate blocks). Vulnerable + patched fixtures added undertests/benchmark/cve_corpus/javascript/CVE-2023-22621/. - JS/TS gated-sink kwarg extractor falls back to inspecting arg-1 object literals (
fn(x, { evaluate: false })) when the language has nokeyword_argumentnode, so the lodash gate can read its options object. - Lodash double-call form (
_.template(t)(data)) routes throughfind_chained_inner_callso the outer call's gated-sink rebinding fires. - Cross-function helper-validation propagation. New
SsaFuncSummary.validated_params_to_returnfield records parameter indices whose taint flow to the return value is fully validated by a dominating predicate (regex allowlist, type check, validation call) on every return path. At call sites, each tainted argument passed to a validated position, and the call's own return value, are markedvalidated_must/validated_mayin the caller's SSA taint state, as an inlineif (!regex.test(x)) throwwould. Closes the helper-validator gap behind PayloadCMS CVE-2026-25544 (Drizzle SQL injection insanitizeValue). Vulnerable + patched TypeScript fixtures added. - Destructured-arg sibling expansion in per-parameter taint summary probing. JS/TS object-pattern formals (
({ column, operator, value }) => ...) seed every binding sharing the slot, and any sibling reachingvalidated_mustcounts as the slot being validated. NewBodyMeta.param_destructured_fieldscarries sibling lists alongsideparamsandparam_types. JSPARAM_CONFIGacceptsassignment_pattern(default-value formals) andobject_pattern(destructured formals). - Regex-allowlist branch narrowing.
<X>.test(value)/<X>.match(value)/<X>.matches(value)where the receiver name containsregexorpatternclassifies as aValidationCalland narrows the call's first argument, not the regex receiver. Also extended toextract_validation_targetso the surviving branch validatesvalue, not the regex object. Motivated by Payload CVE-2026-25544 (if (!SAFE_STRING_REGEX.test(value)) throw ...). - TypeScript template-substring (
${fn(arg)}) call-resolution arity-hint fallback. When CFG lowering dropsarg_usesbutargsis non-empty, the resolver passesNoneso the unique-name fallback can still pick up the lone candidate. - Caller-scope-entity exemption in
rs.auth.missing_ownership_check.<entity>.id/<entity>.pkno longer fires when<entity>is a unit parameter named after a multi-tenant scope primitive:organization/org,project,team,workspace,tenant,account,community,group,repository/repo,company. Other field names (.name,.slug) still flag, anduser/member/actorare deliberately excluded (handled byis_actor_context_subject). Closes a flood of FPs in Sentry / Saleor / Discourse / Mastodon-shaped multi-tenant helpers (get_environments(request, organization),_filter_releases_by_query(qs, organization, ...)). - Auth value-ref walker recurses into the
valuechild ofkeyword_argument/keyword_arg/named_argumentnodes, soModel.objects.filter(organization_id=org.id)no longer surfaces the kwarg key (organization_id) as a bare-identifier user-input subject. The schema column name is fixed at call time. - Test-decorator denylist for Flask route extraction.
mock.patch,mock.patch.object/.dict/.multiple,unittest.mock.*,monkeypatch.setattr/setenv/delattr/delenv, andpytest.mark.parametrizeno longer collide with<app>.patchroute registration. Stops every@mock.patch("...")-decorated test method from being attached as a Flask PATCH handler and flagged asmissing_ownership_check. - Typed-extractor route-level guard injection for axum and actix-web. Handlers registered via attribute macros (
#[get("/path")],#[routes::path(...)]) or external service-config builders never had their typed-extractor guards seeded. Newapply_typed_extractor_guards_to_unitswalks everyFunction-kind unit and injects guard checks from typed-extractor params, complementing the route-walk path that already covered.route(...)registration. - New auth config key
policy_guard_names. Typed-extractor wrappers proving route-level capability/policy enforcement (e.g. meilisearch'sGuardedData<ActionPolicy<X>, _>) are recognised distinctly from authentication-only wrappers. Matched as last-segment + case-insensitivestarts_with. Rust default:["Guarded"]. Distinct fromlogin_guard_namesso the pattern doesn't pollute regular call recognition (a function likeguarded_load(..)is not a login guard). - Outer-wrapper-aware classification of typed extractors.
GuardedData<ActionPolicy<X>, Data<AuthController>>is classified by the outerGuardedData(policy-bearing, soAuthCheckKind::Other), not by whether an inner generic arg substring-matchesauth. Bare data-only extractors (Path<u64>,Query<X>,Json<X>,Form<X>,State<X>,Extension<X>,Data<X>) outer-name-match early-return toNoneregardless of inner type tokens. Reference-marker (&,&mut,&'a) and module-path (std::collections::) prefixes stripped before matching. - Project-level web-framework signal in Rust auth analysis.
FrameworkContext::lang_has_web_framework(lang)is three-valued:Some(true)when the manifest names a framework,Some(false)when it was inspected and named none,Nonewhen no manifest was inspected.rust_file_imports_web_frameworkprobes the 8 KB file head foraxum::/actix_web::/rocket::/axum_extra::imports. When Cargo.toml was inspected and lists no Rust web framework AND the file imports none, thecontext_inputsand param-name-heuristic arms ofunit_has_user_input_evidenceare suppressed;RouteHandlerclassification (concrete route-registration evidence) still bypasses the gate. Closesmissing_ownership_checkFPs in non-web Rust crates such as zed-style desktop / GUI codebases where a debug-session handle namedsessiontripsmatches_session_contextonsession.update(cx, ...). Rust-only; other languages keep prior behavior (None). - Rust auth corpus extended with
safe_actix_guarded_data_extractor.rsandunsafe_actix_no_guarded_data_extractor.rs(typed-extractor guard injection);safe_non_web_rust_project/andunsafe_actix_web_project_no_check/(full Cargo.toml + src/lib.rs project shapes for the framework-signal gate). - Python auth corpus extended with
vuln_user_id_param_no_auth.py,safe_django_orm_caller_scoped_entity.py(caller-scope-entity exemption),safe_mock_patch_test_method.py(test-decorator denylist). - Go safe corpus extended with
safe_inner_call_close_in_arg.go(require.NoError(t, f.Close())shape),safe_struct_field_resource_owned_by_struct.go(field-LHS ownership transfer), and avuln_resource_leak_no_close.goregression guard.
#Fixed (false positives)
- C++
cpp.memory.reinterpret_castno longer fires when the target type is well-defined by C++ aliasing rules. Suppressed targets: byte-pointer family (char*,unsigned char*,signed char*,wchar_t*,uint8_t*,int8_t*,std::byte*,byte*),void*, integer round-trip (uintptr_t,intptr_t, andstd::variants, no pointer required), and the BSD socket address family (sockaddr*,struct sockaddr*,sockaddr_in*,sockaddr_in6*,sockaddr_un*,sockaddr_storage*). User-defined struct or class pointer targets keep firing. Closes ~70% over-fire on serialization, hashing, IPC, and socket-API code where the cast is the standard-blessed idiom. - PHP
php.crypto.md5andphp.crypto.sha1suppress when the call's consuming context yields a non-cryptographic identifier name. Recognised contexts: assignment LHS (variable,$obj->property,$arr['key']), array element keys, subscript indices, return statements (resolved to the enclosing method or function name withgetprefix stripped), and method-call arguments where the method is a key/cache/lookup verb (get,set,has,delete,fetch,store,find,getItem,setItem). Names containing a crypto keyword (password,secret,token,signature,hmac,digest,salt,key) keep firing. Closes ETag generation, cache-key hashing, dedup fingerprint, andgetCacheKey()-style false positives in real PHP repos (phpmyadmin, nextcloud). - JS and TS
secrets.fallback_secretno longer fire on empty-string fallbacks (process.env.X || ""). Developers write|| ""to satisfy non-undefined string types without committing a real secret. Non-empty literal fallbacks still fire. - Path-traversal sink suppression accepts canonicalised-and-rooted shapes. New
PathFact::is_path_traversal_safepredicate clearsCap::FILE_IOwhen the path is dotdot-free and either non-absolute or carrying a verified prefix-lock. NewOPAQUE_PREFIX_LOCKmarker records the structural invariant ("rooted under SOME prefix") when thestarts_with-style guard's argument is a method call, field access, or configured root rather than a string literal. Closes the RubyFile.expand_path + start_with?(root)shape (rswag CVE-2023-38337 patched counterpart), the Pythonos.path.realpath + .startswith(root)shape, and the JSpath.resolve + .startsWith(root)shape.classify_path_assertionextended to JS.startsWith(...), Python.startswith(...), Ruby.start_with?(...)(paren and paren-less), and Gostrings.HasPrefix(...). - Branch narrowing flips prefix-lock attachment under condition negation. For
if !target.startsWith(ROOT) { return; }the lock attaches to the surviving block, not the rejection arm. Rejection-axis narrowing is unchanged because the rejection classifier is text-level and already accounts for leading!. - Go field-LHS resource acquires no longer counted as local resource leaks.
b.cpuprof = os.Create(...)transfers ownership to the containing struct; closure responsibility belongs to a pairedStop()/Release()method on the struct's lifecycle. Gated in bothstate/transfer.rs::apply_callandcfg_analysis/resources.rs::run, restricted to Go (Lang::Gocheck). JS/TS class-field acquires (this.fd = fs.openSync(...)) keep being tracked because the leak fixtures rely on it. Production trigger: prometheuscmd/promtool/tsdb.go::startProfilingcluster (b.cpuprof,b.memprof,b.blockprof,b.mtxprof). - Go inner-call release in argument position:
require.NoError(t, f.Close()),errs = append(errs, f.Close()), JUnitassertEquals(0, in.read())mark the receiverCLOSED. Bare-receiver inner calls only (chained-receiver releases stay owned bychain_proxies); marksCLOSEDonly with noDoubleCloseattribution; respectsin_deferfor symmetry.
#Other
- Action download script warning for the mutable
latesttag now referencesv0.6.0instead ofv0.5.0.
#[0.5.0] - 2026-04-29
The biggest release since launch. The taint engine was rebuilt on top of an SSA IR, cross-file analysis was deepened across the board, and Nyx now ships a local web UI for triaging findings without leaving your machine.
Heads-up: false positives or regressions on cross-file flows are possible. Please open an issue with a minimal reproduction if you hit one.
#Highlights
- New SSA-based taint engine: block-level worklist analysis over a pruned SSA IR, replacing the legacy BFS engine across all 10 languages. More precise, easier to extend, and the foundation for everything else in this release.
- Cross-file analysis: function summaries (including the new SSA summaries) flow across files via SQLite-backed persistence. Callee bodies can be inlined for context-sensitive analysis (k=1) and walked symbolically across file boundaries.
- Symbolic execution layer: candidate findings are walked symbolically from source to sink, producing concrete attack witnesses, pruning infeasible paths, and optionally handing constraints off to Z3.
- Local web UI (
nyx serve): React + Vite frontend for browsing findings, viewing flow paths, and triaging results. Triage decisions persist to.nyx/triage.jsonso they version with your code. - Hostile-repo hardening: path containment, loopback-only serving, CSRF tokens, bounded artifact reads. Safe to run on untrusted code.
- Tighter false-positive controls: type-aware sink suppression, abstract interpretation (intervals + string prefixes), constraint solving, allowlist and type-check guard recognition, confidence scoring on every finding.
#Engine
- SSA IR with dominance-frontier phi insertion. The optimization pipeline runs constant propagation, branch pruning, copy propagation, alias analysis, DCE, type facts, and points-to in sequence.
- Multi-label classification. A single API can carry both Source and Sink labels (e.g. PHP
file_get_contents, JavareadObject). - Gated sinks.
setAttribute,parseFromString, etc. only activate when the constant attribute argument is dangerous, and only the payload argument is treated as taint-bearing. - Container taint with per-index precision and bounded points-to. Aliased containers share heap identity correctly.
- Loop-aware analysis: induction-variable pruning, widening at loop heads, bounded unrolling in symex.
- Path-sensitive phi evaluation propagates validation when all tainted predecessors are guarded.
- Per-return-path summaries decompose function effects when paths produce different taint behavior.
- Cross-file SCC fixed-point. Mutually recursive functions across files now reach a joint convergence.
- Demand-driven backwards analysis (off by default) annotates findings with cutoff diagnostics.
- Direction-aware engine notes (
UnderReport,OverReport,Bail) flow into confidence scoring, ranking, and the new--require-convergedstrict mode. - Synthetic field-write inheritance:
u.Path = "/foo"no longer drops taint carried by other fields ofu. Fixes Owncast CVE-2023-3188 (SSRF). - Phantom-Param-aware field suppression skips method/function references that share a base name with a tainted variable.
- Validation err-check narrowing for the two-statement Go idiom
_, err := strconv.Atoi(input); if err != nil { return }:inputis marked validated on the survivingerr == nilbranch. - Go:
strings.Replace/strings.ReplaceAllrecognised as a sanitizer when the OLD literal contains a known-dangerous payload (shell metachars, path-traversal, HTML, SQL) and the NEW literal does not reintroduce one. - Go: literal-strip cap detection extended to shell metachars (
;,|,&,$, backtick) and SQL metachars (',",--). - Go:
interpreted_string_literal/raw_string_literalhandled in tree-sitter so const-string arg extraction works for Go's double-quoted and backtick forms.
#Symbolic Execution
- Expression trees (
SymbolicValue) preserve computation structure through the path walk: integers, strings, binary ops, concatenations, calls, phi merges. - Witness strings reconstruct concrete attack payloads at sink nodes.
- Bounded multi-path forking with reachability pruning.
- Cross-file: callee summaries are modeled directly, and pre-lowered callee bodies are loaded from SQLite so witnesses can keep walking across files.
- Interprocedural mode: nested frames with full state propagation, transitive descent up to 3 levels, structured cutoff tracking.
- Field-sensitive symbolic heap with bounded fields per object.
- Symbolic string theory:
Substr,Replace,ToLower,ToUpper,Trim,StrLenmodeled with concrete folding and sanitizer pattern detection. - Optional Z3 integration (compile-time
smtfeature) for cross-variable constraint solving.
#Security & Coverage
- Vulnerability classes added: SSRF (10 languages), deserialization (Python, Ruby, Java, PHP), and
Cap::UNAUTHORIZED_IDfor auth-as-taint (off by default behind config flag). - Auth analysis: receiver-type sink gating, row-level ownership-equality detection, self-actor recognition (
let user = require_auth()), sink classification (in-memory vs realtime vs outbound), helper-summary lifting, SQL JOIN-through-ACL recognition. - State analysis (resource lifecycle, use-after-close, leaks, unauthed access) is now on by default. RAII-aware for Rust and C++; recognizes Python
with, Godefer, Java try-with-resources. - Framework rule packs: Express, Flask/Django, Spring/JNDI, Rails. Per-language label depth expanded.
- C/C++ taint depth: output-parameter source propagation, implicit definitions for uninitialized declarations.
- Negative test corpus (30 fixtures) and a 262-case benchmark with CI gates on rule-level Precision/Recall/F1.
#Detection metrics
- Aggregate rule-level F1 reaches 0.998 (P=0.995, R=1.000). All real-CVE fixtures fire; only one open FP (
go-safe-009). - Go: 98.0% F1 on the 53-case corpus (1 FP / 0 FNs).
- CVE-2023-3188 (owncast SSRF) now detects.
#CLI & Output
nyx serve: local web UI onlocalhostonly (refuses non-loopback binds).--require-convergedfilters out findings where the engine bailed early.- Analysis-engine toggles graduated from
NYX_*env vars to first-class flags and[analysis.engine]config:--constraint-solving,--abstract-interp,--context-sensitive,--symex,--cross-file-symex,--symex-interproc,--smt,--parse-timeout-ms. Old env vars still work when Nyx is consumed as a library. - Confidence (
High/Medium/Low) shown on every finding, including console headers. - Engine notes surfaced in console (
[capped: N notes, over-report]), JSON (engine_notes,confidence_capped), and SARIF (result.properties.loss_direction). - Flow paths reconstructed step-by-step with file/line/snippet for each hop.
- Concrete attack witness strings synthesized by the symbolic executor.
- Primary sink locations now point at the callee's real sink line; caller call sites are preserved as flow steps.
- Scan progress reports explicit stages, timing breakdowns, language counters, skipped/reused file counts.
- Tighter taint-finding deduplication.
#Hardening
- Centralized path containment rejects traversal, symlink escapes, and oversized reads across UI, debug, and triage routes.
nyx servevalidatesHostheaders, requires per-session CSRF tokens for mutations, and refuses scans outside the original repo root.- Walker re-validates symlink targets against the scan root.
- Bounded reads on framework manifests and
.nyx/triage.jsonimports. - UI falls back to plain text on pathologically long lines to defeat regex-DoS in syntax highlighting.
- Parser timeout is now configuration-backed with hostile-input regression coverage.
#Persistence
- SQLite schema bumped to v2. Anonymous-function identity is now a structural DFS index instead of a byte offset, so inserting a line above an unchanged function no longer invalidates its
FuncKey. Pre-0.5.0 caches are silently cleared on open; triage data and scan history are preserved. - Engine-version metadata; persisted summaries and file hashes invalidate on mismatch.
- Stale SSA tables recreate when required columns are missing; deserialization failures log instead of silently dropping rows.
#Frontend
- Replaced the legacy
app.jswith a React + Vite + TypeScript SPA. - Interactive graph workspace for CFG and call-graph views (Graphology + ELK + Sigma) with neighborhood reduction and a full-page inspector.
- Triage UI with database-backed decisions (true positive, false positive, accepted risk, suppressed) and
.nyx/triage.jsonround-trip. - Scan history, rules management, and finding detail panels with evidence and flow visualization.
- Vitest browser-side test suite wired into CI.
- Bumped to React 19, Vite 8, TypeScript 6.0, ESLint 10,
@vitejs/plugin-react6, with aligned@types/react*. SSEContext: typedreconnectTimerref asReturnType<typeof setTimeout> | undefinedto satisfy TS 6's stricteruseRefoverloads.FindingsPage: includedtoastinuseCallbackdeps to avoid stale-closure warnings.tsconfig.json: droppedbaseUrl, using a relative./src/*path mapping instead.
#Removed
- Legacy BFS taint engine,
TaintTransfer,TaintState, and theNYX_LEGACYfallback. - Legacy vanilla-JS frontend (
app.js).
#[0.4.0] - 2026-02-25
A precision and ergonomics release. Findings are now ranked, lower-noise by default, and easier to triage in CI.
#Highlights
- Attack-surface ranking: every finding gets an exploitability score combining severity, analysis kind, evidence strength, and path-validation. Console output shows the score in the header line;
--no-rankopts out. - Low-noise prioritization: Quality-category findings are excluded by default (
--include-qualitybrings them back). High-frequency Quality rules are rolled up per(file, rule)with example occurrences. LOW budgets cap noise without ever displacing High/Medium findings. - State-model dataflow analysis: per-variable resource-lifecycle and auth-level analysis catches use-after-close, double-close, must-leak, may-leak (branch-aware), and unauthenticated-sink access. Opt-in via
scanner.enable_state_analysis. - Inline
nyx:ignoresuppressions with same-line and next-line directives, comma lists, wildcard suffixes, and string-literal guards across all 10 languages. - AST pattern overhaul: all 10 language pattern files rewritten with consistent metadata, namespaced IDs (
<lang>.<category>.<specific>), and 30+ new patterns. 11 broken tree-sitter queries fixed. - Monotone forward-dataflow taint engine replaces the BFS engine with a worklist over a finite lattice. Termination is guaranteed by lattice height, eliminating BFS-budget bailouts on large files.
- Path-sensitive taint analysis: branch predicates flow with the analysis. Contradictory guards prune infeasible paths; validation calls produce annotated findings without changing severity.
- Interprocedural call graph: whole-program graph with three-valued callee resolution (
Resolved/NotFound/Ambiguous), SCC analysis, and topo ordering ready for bottom-up taint propagation.
#CLI & Output
--severity <EXPR>replaces--high-only. SupportsHIGH,HIGH,MEDIUM,>=MEDIUM. Filtering is now applied at the output stage so taint and CFG findings are correctly downgraded too.--mode <full|ast|cfg|taint>replaces--ast-onlyand--cfg-only.--index <auto|off|rebuild>replaces--no-indexand--rebuild-index.--fail-on <SEVERITY>for CI exit-code gating.--min-score <N>for ranking-aware filtering.--show-suppressedreveals suppressed findings dimmed with[SUPPRESSED].--keep-nonprod-severity(renamed from--include-nonprod).--quietmirrorsoutput.quiet.- Console renderer overhauled: severity is the strongest visual anchor, file paths are dim blue, taint flows use
→arrows, multi-line call chains are normalized. - Confidence shown alongside score in the header line.
- Pattern-level confidence is now set at the pattern definition site, not heuristically inferred from severity.
#Breaking
- Config and data directory renamed from
dev.ecpeter23.nyxtonyx. Existing config and SQLite indexes at the old path won't be picked up. Copy them across or re-runnyx scan. Severity::from_strnow returnsErrfor unknown values instead of silently defaulting to Low.
#Notable Fixes
- KINDS-map audit across all 10 languages: 89 missing tree-sitter node types added. Switch/case, try/catch/finally, class bodies, lambdas, closures, and namespaces are no longer silently dropped.
else_clausemapping fixed for C, C++, Rust, JS, TS, Python, PHP. Code inside else blocks was being dropped from the CFG.- Rust
if let/while lettaint propagation now works. - Taint BFS non-termination on large JS files (the BFS engine has since been replaced).
- C++
popenpattern ID collision with C. - Constant-arg sink suppression for AST patterns.
#[0.3.0] - 2026-02-25
Configurability, SARIF, and an aggressive false-positive purge.
#Highlights
- Configurable analysis rules: sources, sanitizers, sinks, terminators, and event handlers can be defined per language in
nyx.localor vianyx config add-rule/add-terminator. Config rules take priority over built-in rules. nyx configCLI subcommand withshow,path,add-rule,add-terminator.- SARIF 2.1.0 output (
-f sarif), spec-compliant for GitHub Code Scanning, Azure DevOps, and other SARIF consumers. SourceKindtaint classification: findings carry an inferred source kind (UserInput,EnvironmentConfig,FileSystem,Database,Unknown) and severity is derived from it instead of being hardcoded to High.- Non-prod severity downgrade by default. Findings in tests, vendor, benchmarks, examples, fixtures, build scripts, and
*.min.jsare downgraded one tier.--include-nonprodrestores original severity. - Resource leak detection for Python, Ruby, PHP, JavaScript, and TypeScript (file handles, sockets, locks, mysqli, curl, fs streams).
- Progress bars and quiet mode: Indicatif-driven progress for discovery, Pass 1, and Pass 2 (auto-hidden in JSON/SARIF/quiet modes).
#Performance
- Single fused parse+CFG pass replaces the previous two-parse summary extraction.
- Light-weight dataflow sweep in CFG builder is now O(N) per function instead of O(N²) over the whole file.
- Parallel summary merging via rayon fold/reduce.
- Indexed scans now read and hash each file once instead of up to 4 times.
- SQLite mutex mode relaxed (r2d2 + WAL provides safety without global lock).
- Zero-allocation taint hashing and in-place taint transfer.
#Notable Fixes
- One-hop constant-binding suppression:
cmd = "git"; subprocess.run([cmd, ...])no longer flags. - Exec-path guards (
which,resolve_binary,shutil.which) recognized. signal.connect/event.connectno longer match Python db-connection acquire patterns.threading.Lock()without.acquire()no longer flags as unreleased.FileResponse(f)/send_file(f)recognized as ownership transfer.el.hrefno longer matcheslocation.hrefpatterns.- Constant-only sink calls (
subprocess.run(["make","clean"])) suppressed. std::coutno longer treated as a sink.- Break/continue inside loops correctly wires into the loop header/exit, fixing false unreachable-code findings.
- Preprocessor
#ifdef/#endifblocks no longer orphan subsequent code in C/C++. freopenno longer matchesfopenacquire patterns.- Struct-field, linked-list, and global assignment recognized as ownership transfers.
#[0.2.0] - 2026-02-24
The cross-file release.
- Two-pass cross-file taint analysis. Pass 1 extracts
FuncSummaryper function (caps, propagation, callees), Pass 2 runs BFS taint propagation with cross-file callee resolution. - CFG analysis engine with five detectors: unguarded sinks, auth gaps in web handlers, unreachable security code, error fallthrough, resource leaks.
- Cross-language interop via explicit
InteropEdgestructs (no false-positive name collisions). - Function summaries persisted to SQLite (
function_summariestable). - Multi-language CFG + taint support for all 10 languages.
- Resource leak detection for C/C++, Go, Rust, and Java.
- Finding scoring system combining severity, entry-point proximity, path complexity, taint confirmation, and confidence.
- Analysis modes:
Full(default),Ast(--ast-only),Taint(--cfg-only). - Cap bitflags expanded:
ENV_VAR,HTML_ESCAPE,SHELL_ESCAPE,URL_ENCODE,JSON_PARSE,FILE_IO. - Performance: read-once/hash-once via
_from_bytesvariants, lock-free rayon, SQLite WAL + 8 MB cache + 256 MB mmap. - Tracing instrumentation on all pipeline stages; criterion benchmark suite.
#[0.2.0-alpha] - 2025-06-28
- Experimental intra-procedural CFG + taint analysis for Rust. Builds a CFG, applies dataflow, and flags unsanitised Source to Sink paths (e.g.
env::vartoCommand::new). - O(1) node-kind lookup via per-language PHF tables.
- Debug channel
target=cfg(RUST_LOG=nyx::cfg=debug) to inspect generated graphs. - Fixed Windows release pipeline (PowerShell has no
zipcommand).
#[0.1.1-alpha] - 2025-06-25
- Fixed
scan --no-indexnot respecting themax_resultsconfig setting (#1). - Integration tests covering indexing and scanning pipelines (#3, #4, #5, #8).
#[0.1.0-alpha] - 2025-06-25
Initial alpha release.
- Multi-language AST pattern scanning via
tree-sitterfor Rust, C/C++, Java, Go, PHP, Python, Ruby, TypeScript, JavaScript. scancommand: filesystem walker, pattern execution, console output.indexcommand: build, rebuild, and status reporting of SQLite-backed index.listcommand: list indexed projects with optional verbosity.cleancommand: remove one or all project indexes.- Configuration system with
nyx.conf(generated) andnyx.local(user overrides). - Default severity levels: High, Medium, Low.