#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.
#[Unreleased]
Three fronts this release: an attack-surface map, a sandboxed dynamic verifier, and a framework adapter registry that grounds both.
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 / NotConfirmed / Inconclusive / Unsupported verdict on each. The adapter registry (116 entries across 8 languages) covers HTTP, message-broker, scheduled-job, GraphQL, WebSocket, middleware, and migration entry points.
#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.
#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.
#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.
#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.
#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. 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.
#Frontend
- 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
A focused release that adds seven new vulnerability classes, ships two SSA sidecars for XML and XPath parser hardening, deepens cross-file authorization for FastAPI, trims roughly a thousand auth false positives on Go DAO helpers along with the dominant Hibernate Criteria SQL cluster, and runs a performance pass on the auth extractor, SCCP, and the global summaries map. A nyx rules list CLI surfaces the rule registry, the web UI gets a brand-aligned visual refresh, and the CVE corpus grows 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=[…])now lift onto every attached route, withSecurity(..., scopes=[...])recognised distinctly fromDepends(...). - Type-tracked XML and XPath hardening through two new SSA sidecars: parser bodies that set
secure_processing/processEntities: false/resolve_entities=False, andXPathinstances bound tosetXPathVariableResolver(...), are recognised 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, and 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 human-readable description live inCAP_RULE_REGISTRYinsrc/labels/mod.rs;cap_rule_meta()andrule_id_for_caps()are the public lookups. Capwidened fromu16tou32to fit the new bits.Evidence.sink_capsandRuleInfo.cap_bitsfollow. 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, since oldersource_caps/sanitizer_caps/sink_capsblobs were emitted before any of the new bits could appear.owasp_bucket_forconsultsCAP_RULE_REGISTRYfirst so adding a cap class no longer requires a second-table edit. The match requires an exact rule id or a recognised separator (,(,.) so a futuretaint-ssrf-allowlist-violationcannot silently inherittaint-ssrf's bucket. The legacy family-token table now 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
- XML-parser configuration tracking.
src/ssa/xml_config.rsruns alongside type-fact analysis and carries per-receiversecure_processing/disallow_doctype/external_entitiesflags forward through copy assignments and phi joins (meet for safe flags, sticky union for the unsafeexternal_entitiespolarity).xxe_safe()queries the result at the type-qualifiedXmlParser.parsesink and stripsCap::XXEwhen 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. - XPath-receiver configuration tracking.
src/ssa/xpath_config.rsmirrors the XML sidecar for Java'sXPathinstances: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 new
TypeKindvariants.LdapClient(JNDIInitialDirContext/InitialLdapContext, SpringLdapTemplate, ldapjscreateClient, python-ldapinitialize, ldap3Connection),XPathClient(JAXPnewXPath, lxmletree.XPath, npmxpath),XmlParser(JAXP factory products:newDocumentBuilder,newSAXParser,getXMLReader),Template(FreeMarkernew Template(...)/Configuration.getTemplate), andNullPrototypeObjectfor JS/TS values produced byObject.create(null). Wired intoconstructor_typefor return-type inference andTypeKind::label_prefix()for type-qualified callee resolution.XPathClientis kept distinct fromDatabaseConnectionso a genericpdo->querySQL_QUERY sink does not collide withxpath.query. GateActivation::LiteralOnly. Strict literal-value activation: the gate fires only when the activation argument is a literal that matchesdangerous_values/dangerous_prefixes. Unknown or dynamic activation argument suppresses (no conservativeALL_ARGS_PAYLOADpush). Used where the dangerous shape is identifiable only by an explicit literal flag, e.g.jQuery.extend(true, target, src)deep-merge against Backbone'sModel.extend({proto}).- Two new path-state predicates for inline open-redirect sanitisers.
RelativeUrlValidatedcoversx.startsWith("/"),x.starts_with("/"),x.startswith("/"), PHPstrpos($x, "/") === 0, and directx[0] === "/".HostAllowlistValidatedcoversnew URL(x).host === ALLOWED,urlparse(x).netloc == ALLOWED, multi-statementparsed.host_str() == "..."for Rust, andparsed.Host == "..."/parsed.Hostname() == "..."for Go. Both clearCap::OPEN_REDIRECTonly on the validated branch, leaving any non-redirect taint downstream to fire on its own caps. The Go form gates on case-sensitive capitalHso a lowercaseu.host == Xfield comparison falls through to the genericComparisonpredicate. Object.create(null)recogniser.is_object_create_null_callincfg/literals.rsmatchesObject.create(null)(and parenthesised, awaited, or TS type-cast wrappers) and tagsCallMeta.produces_null_proto = true. Type-fact analysis lifts the flag toTypeKind::NullPrototypeObjecton the returned SSA value so the synthetic__index_set__sink is suppressed flow-sensitively. Phi joins drop the tag back toUnknownso 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;), and 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 silently authorise inner assignments. - Spring MVC
return "redirect:" + taintedrecogniser (Java).try_lower_spring_redirect_returnincfg/mod.rsmatches the leftmost+-chain whose root is 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 synthetic node'sarg_uses[0], so the taint pipeline carries any tainted suffix 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_nodenow walks 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: PHPencapsed_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 hardening for member-source rewrites. When
first_member_labelrewrites a call'stextto a Source likereq.body, the gate matcher now reads the call'sfunction/method/namefield instead, sosetValue(target, req.body, ...)matches thesetValueproto-pollution gate. Whitespace 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) now 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::Blockso methods inside them participate in CFG construction. - Go variadic
parameter_declarationnamed-field handling forcollect_param_names.nameandtypenamed fields 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 now seeds via
BodyMeta.param_destructured_fields; JS/TS arrow() => {…}lowers withwith_params=trueso it is treated 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) handled by 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 the same file. Closes airflow execution-API routes that re-use a singleti_id_routerdeclared once at module scope. - FastAPI
Security(callable, scopes=[...])recognised distinctly fromDepends(callable). Scoped Security promotes the syntheticAuthChecktoAuthCheckKind::Other(route-level scope-checked authorization), not Login. New scope-tracking boolean threaded 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 via this same propagation, the caller's checks lift onto the helper as syntheticis_route_level=trueAuthChecks. Iterated to a small fixpoint so transitive helper chains (route to mid_helper to leaf_helper) are covered. 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 "route authenticates via decorator/dependency, then delegates to a private helper that performs the sink" FP shape on sentry / saleor / airflow. - 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); per-framework route extractors setinclude_id_like_typed=trueso id-shaped path params survive on real routes. Mirrors the existing 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 entiremodels/...layer where the 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)); a barelist(events)is the Python builtin andfilter(fn, xs)isIterable.filter. New helperreceiver_is_simple_chain(callee)requires a non-chained receiver dot. The realtime / outbound / cache prefix dispatches still match by chain root.
#Type-aware sinks and validators
- Java JPA / Hibernate Criteria API 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 to the sink call is JpaCriteriaQuery-typed (receiver excluded; receiver ofsession.createQuery(cq)is the Session/EntityManager channel, never the SQL payload).cb.createQuery(...),em.getCriteriaBuilder(), and the JpaCriteriaQuery type chain 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, distinct fromSanitizerwhich clears caps from the return value. Python registersrelative_to => Cap::FILE_IOsopath.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 call's return value. Resolves callback name frominfo.arg_callees(call-shape arguments) and SSAvalue_defs[v].var_name(bare-identifier callbacks, the dominant patched-CVE form). Strict-additive: anonymous arrows / 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 : "";previously lowered each branch to a labelless Assign with empty uses; the join phi saw no taint.lower_ternary_branchnow runsfirst_member_labelon the branch AST when noSourcelabel is already attached. - PHP
fopenmodeled asSink(Cap::SSRF)(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 legacySerializableinterface contract (deprecated since PHP 8.1) requires the implementation to call\unserialize($input)on the formal parameter insidepublic function unserialize($x) { ... }. PHP itself invokes the method when restoring an instance, so the body's call cannot be removed without breaking the interface.php.deser.unserializenow suppresses inside this exact shape (method namedunserialize, single formal, bare-parameter argument). Class-levelSerializableimplementation is the actionable signal (fix is 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()chains now anchor through the chain root primitive when the chain receiver type is opaque. Newdb_query_builder_rootsconfig (Python defaults:select,query). Closes airflowsession.scalar(select(C).filter_by(conn_id=user_input))shapes that previously dropped under the chained-call suppression inclassify_sink_class. - Python non-sink container constructor recognition. Bare-callee
set()/dict()/list()/tuple()/frozenset()/defaultdict(...)is treated as a non-sink constructor, soverified_ids = set(); verified_ids.update(myteams)does not classify the.updatecall asDbMutation. Type-annotation hint formset[int]/dict[str, int]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). Flaskmake_response(<f-string-or-concat>)reflection. Recognises both baremake_response(...)andflask.make_response(...). Closes CVE-2023-6568 (mlflow authcreate_userreflecting 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 patterns 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/. Known remaining gap: array-element taint propagation,c.cmdi.exec*AST patterns, and 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 listsubcommand. Surfaces the same registry the dashboard's/api/rulespage reads from: built-in cap-class entries (one perCapwith a canonical rule id), per-language label rules (sink / source / sanitizer), gated sinks, and any 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.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. The active set is pinned incap_rule_registry_emission_active_set_is_pinnedso a future migration of 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
- Refreshed local web UI visual system 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, and matched graph / code-viewer colors.
- Reworked
nyx servesurfaces 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 data side by side; Triage, Rules, Config, Explorer, Finding Detail, Scan Compare, and Debug pages received focused 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, and Rust static handlers for the new
/logo.pngand favicon files. - Frontend
RuleListItemandRuleDetailViewcarry the newis_classflag so the dashboard's Rules page can group cap-class entries separately. - Regenerated README and docs screenshots and GIFs against the new UI at 1600x992, saving raw originals before framing and adding CLI GIF plus combined CLI-to-serve demo GIF capture support. Extended the screenshot capture workflow with mint-led framing copy, optional
nyxsec.devasset mirroring, WebP regeneration for mirrored PNGs, and raw_rawimage / GIF outputs for downstream reuse.
#Performance
- Hoisted
collect_top_level_unitsout 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 entire 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. The previous per-nodeextract_value_refs(node, bytes)walked the entire subtree on 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 ofextract_value_refs(collect_call,collect_condition, assignment-side extraction) keep the deep walk. Was ~17% + 15% + 11% of wall-clock split 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 denseVecper-value lattice and per-destination predecessorSmallVec<[BlockId; 2]>. 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. Resolved at the CFG layer before the SSA sink scan.- 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. Other taint downstream still fires on its own caps. - 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 plus three fresh CVE corpus pairs, plus 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, andStatement.executeLargeUpdatemodeled asSQL_QUERYsinks, classified via type-qualified resolution (DatabaseConnection.execute) so bareexecute(Runnable, Executor, HttpClient) does not over-fire.conn.createStatement()andconn.prepareCall()now 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. Receiver of.matcher(must containregexorpattern. Validation target is the.matcher()argument, not the bare.matches()receiver. Branch narrowing applies thevalidated_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 now 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)that routed SQL throughstatement.execute(query). - Short-circuit branch condition CFG nodes now mirror
condition_varsintotaint.uses, soapply_branch_predicatesinterns the variable for short-circuit-decomposed validators (if (x == null || !regex.matcher(x).matches()) throw). Without this, the per-disjunct cond nodes built viabuild_condition_chainsilently no-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")→goqu.L(project)with the 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 now consulted whenleft/rightaren't direct fields of thefornode, so taint from the iterable reaches the loop binding. Required for the daptin CVE shape above. - 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). Without this, taint stopped at the macro boundary.let q = format!("...{x}...")carried noxbecause the identifier lives in format-string bytes rather than as a separate AST argument node. Mirrors the Python f-string lifter. - Rust CVE corpus extended. 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) -> …) was not matching 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)
- Go
unit_has_user_input_evidenceframework-request-name allow-list narrowed for Go.ctx,context,info,body,path,payload,dto,form,queryare no longer treated as user-input indicators on Go: in Go these arecontext.Context(cancellation/value-bag from the stdlib) 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); these 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). Together 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 correctly ignored. Closes 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) now 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_paramsnow setswith_params=trueeven whenformal_paramsis empty, so an arrow() => {…}is treated as "explicitly zero formals" rather than "no formals info". External vars in a zero-formal arrow are now correctly 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 now 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
A focused release that splits data-exfiltration off from SSRF and ships sinks for outbound HTTP request bodies across all 10 languages, with 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 gets 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,serializeForUpstreamare treated 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 that begins 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_urinow classified as an SSRF sink (the low-level fetcher thatURI.opendelegates to). Closes the CarrierWave CVE-2021-21288 download path and equivalent gem shapes that route throughOpenURIdirectly. - Ruby chained-call wrapper classification. Statement-level wrappers like
YAML.safe_load(File.read(filename))andMarshal.load(File.read(p))now classify the inner sink for cross-function summary extraction. Without this, 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 added 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 → 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. Required 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, the same way 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 }) => …) now 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. Was 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.Model.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 via external service-config builders previously 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 that prove 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 →AuthCheckKind::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. New
FrameworkContext::lang_has_web_framework(lang)is three-valued:Some(true)when manifest names a framework,Some(false)when the manifest was inspected and named none,Nonewhen no manifest was inspected. Newrust_file_imports_web_frameworkdoes a per-fileaxum::/actix_web::/rocket::/axum_extra::import probe (8 KB head). When the project's Cargo.toml is inspected and lists no Rust web framework AND the file does not directly import one, thecontext_inputsand param-name-heuristic arms ofunit_has_user_input_evidenceare suppressed.RouteHandlerclassification (concrete route-registration evidence) still bypasses the gate. Closes a flood ofmissing_ownership_checkFPs in non-web Rust crates such as zed-style desktop / GUI codebases where a debug-session handle namedsessionwould tripmatches_session_contextonsession.update(cx, …). Currently 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 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 carries 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 now 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()): releases that live in argument position now 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, and 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, and 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 significantly 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.
- Richer scan progress: 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. New 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. Replaced the BFS engine with a proper worklist over a finite lattice. Termination is now 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 now 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 → Sink paths (e.g.
env::var→Command::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.