Merge branch 'master' into dynamic
3
.gitignore
vendored
|
|
@ -13,3 +13,6 @@
|
|||
.z3-trace
|
||||
.pitboss
|
||||
.node_modules-target
|
||||
node_modules
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
56
CHANGELOG.md
|
|
@ -4,10 +4,53 @@ All notable changes to Nyx are documented here. The format is based on [Keep a C
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
A round of cross-file FastAPI auth, two new sink/validator classes, a ~957-FP Go DAO helper precision pass, four CVE corpus pairs, and a performance pass on the auth extractor pipeline plus SCCP and the global summaries hash map.
|
||||
A round of cross-file FastAPI auth, two new sink/validator classes, a ~957-FP Go DAO helper precision pass, four CVE corpus pairs, a local web UI visual refresh, and a performance pass on the auth extractor pipeline plus SCCP and the global summaries hash map.
|
||||
|
||||
This branch also adds seven new vulnerability classes (LDAP injection, XPath injection, header / CRLF injection, open redirect, server-side template injection, XXE, prototype pollution), a `nyx rules` CLI subcommand, two SSA configuration sidecars (XML parser hardening, XPath variable resolver), two new path-state predicates for inline open-redirect sanitisers, and a flow-sensitive `Object.create(null)` recogniser for prototype-pollution suppression.
|
||||
|
||||
### Detector classes
|
||||
|
||||
- New `Cap` bits 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 with per-language sink, sanitizer, and (where applicable) gated-sink rules across JS/TS, Python, Java, PHP, Go, Ruby, Rust, and C/C++. Severity, OWASP 2021 mapping, and human-readable description live in a single `CAP_RULE_REGISTRY` table in `src/labels/mod.rs`; `cap_rule_meta()` and `rule_id_for_caps()` are the public lookups.
|
||||
- `Cap` widened from `u16` to `u32` to fit the new bits. `Evidence.sink_caps` is now `u32`; `RuleInfo.cap_bits` is also `u32`. The serde decoder accepts any unsigned integer width so caches written before the bump still load. SQLite schema bumped 3 to 4 to force a rescan, since older `source_caps` / `sanitizer_caps` / `sink_caps` blobs were emitted before any of the new bits could appear.
|
||||
- `owasp_bucket_for` consults `CAP_RULE_REGISTRY` first so adding a new cap class does not require a second-table edit. The match requires an exact rule id or a recognised separator (` `, `(`, `.`) so a future `taint-ssrf-allowlist-violation` can no longer silently inherit `taint-ssrf`'s OWASP bucket. The legacy family-token table now also routes `xpath`, `header`, and `xxe` to 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.
|
||||
|
||||
### Changed
|
||||
|
||||
- Refreshed the 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 the main `nyx serve` surfaces for a more operational layout. Overview now 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.
|
||||
- Updated the SPA and embedded server bundle to share branded assets: PNG favicons, Apple touch icon, sidebar logo image, refreshed SVG favicon, and Rust static handlers for the new `/logo.png` and favicon files.
|
||||
- Regenerated README/docs screenshots and GIFs against the new UI treatment 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 `nyxscan.dev` asset mirroring, WebP regeneration for mirrored PNGs, and raw `_raw` image/GIF outputs for downstream reuse.
|
||||
|
||||
### Added
|
||||
|
||||
- `nyx rules list` CLI subcommand. Surfaces the same registry the dashboard's `/api/rules` page reads from: built-in cap-class entries (one per `Cap` with 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-only` for registry entries only, `--no-class` for per-language rules only. `--json` for machine output. Cap-class entries carry `language = "all"` so a language filter still surfaces them unless `--no-class` is set.
|
||||
- `RuleInfo.is_class` / `RuleInfo.emission_active` flags. Cap-class entries carry `is_class = true` so dashboards can group them separately from per-language label rules. `emission_active = false` marks legacy classes (SQL_QUERY, SSRF, FILE_IO, FMT_STRING, DESERIALIZE, CODE_EXEC, CRYPTO) whose findings still surface under the catch-all `taint-unsanitised-flow` rule id; the seven new classes plus `unauthorized_id` and `data_exfil` are `emission_active = true`. The active set is pinned in `cap_rule_registry_emission_active_set_is_pinned` so a future migration of a legacy cap to its specific rule id can't drift silently.
|
||||
- XML-parser configuration tracking. New `src/ssa/xml_config.rs` runs alongside type-fact analysis and carries per-receiver `secure_processing` / `disallow_doctype` / `external_entities` flags forward through copy assignments and phi joins (meet for safe flags, sticky union for the unsafe `external_entities` polarity). `xxe_safe()` queries the result at the type-qualified `XmlParser.parse` sink and strips `Cap::XXE` when the parser was provably hardened (JAXP `setFeature(FEATURE_SECURE_PROCESSING, true)`, lxml `XMLParser(resolve_entities=False, no_network=True)`, fast-xml-parser `processEntities: false`). Persisted to `OptimizeResult.xml_parser_config`.
|
||||
- XPath-receiver configuration tracking. New `src/ssa/xpath_config.rs` mirrors the XML sidecar for Java's `XPath` instances: `setXPathVariableResolver(...)` flips the receiver's `has_resolver` flag, copy assignments union, phi joins meet. `xpath_safe()` strips `Cap::XPATH_INJECTION` at `xpath.evaluate(expr, ...)` / `xpath.compile(expr)` sinks when the receiver was provably bound to a resolver (parameterised XPath shape). Persisted to `OptimizeResult.xpath_config`.
|
||||
- Five new `TypeKind` variants: `LdapClient` (JNDI `InitialDirContext` / `InitialLdapContext`, Spring `LdapTemplate`, ldapjs `createClient`, python-ldap `initialize`, ldap3 `Connection`), `XPathClient` (JAXP `newXPath`, lxml `etree.XPath`, npm `xpath`), `XmlParser` (JAXP factory products: `newDocumentBuilder`, `newSAXParser`, `getXMLReader`), `Template` (Apache FreeMarker `new Template(...)` / `Configuration.getTemplate`), and `NullPrototypeObject` for JS/TS values produced by `Object.create(null)`. Each is wired into `constructor_type` for return-type inference and into `TypeKind::label_prefix()` for type-qualified callee resolution. `XPathClient` is kept distinct from `DatabaseConnection` so a generic `pdo->query` SQL_QUERY sink does not collide with `xpath.query`.
|
||||
- `GateActivation::LiteralOnly`. Strict literal-value activation: the gate fires only when the activation argument is a literal that matches `dangerous_values` / `dangerous_prefixes`. Unknown or dynamic activation argument suppresses (no conservative `ALL_ARGS_PAYLOAD` push). Used for ambiguously named matchers where the dangerous shape is identifiable only by an explicit literal flag, e.g. bare `extend` where `jQuery.extend(true, target, src)` is the deep-merge prototype-pollution form but Backbone's `Model.extend({proto})` shares the suffix.
|
||||
- Two new `PredicateKind` variants in `src/taint/path_state.rs` for inline open-redirect sanitisers. `RelativeUrlValidated` covers `x.startsWith("/")`, `x.starts_with("/")`, `x.startswith("/")`, PHP `strpos($x, "/") === 0`, and direct `x[0] === "/"`. `HostAllowlistValidated` covers `new URL(x).host === ALLOWED`, `urlparse(x).netloc == ALLOWED`, multi-statement `parsed.host_str() == "..."` for Rust, and `parsed.Host == "..."` / `parsed.Hostname() == "..."` for Go. Both are cap-aware: they clear `Cap::OPEN_REDIRECT` only on the validated branch, leaving any non-redirect taint downstream to fire on its own caps. The Go form gates on case-sensitive capital `H` so a lowercase `u.host == X` field comparison falls through to the generic `Comparison` predicate.
|
||||
- `Object.create(null)` recogniser. New `is_object_create_null_call` in `cfg/literals.rs` matches `Object.create(null)` (and parenthesised, awaited, or TS type-cast wrappers) and tags `CallMeta.produces_null_proto = true` for JS/TS calls. Type-fact analysis lifts the flag to `TypeKind::NullPrototypeObject` on the returned SSA value so the synthetic `__index_set__` sink is suppressed flow-sensitively. Phi joins drop the tag back to `Unknown` so a partial null-proto receiver still fires on the unsafe path.
|
||||
- CFG-layer prototype-pollution suppression on the synthetic `__index_set__` sink (JS/TS only, recognised by the existing `try_lower_subscript_write` lowering). Three flow-insensitive shapes elide the `Sink(PROTOTYPE_POLLUTION)` label before SSA sees the node: constant-key fold (literal key not in `__proto__` / `constructor` / `prototype`); reject pattern (an enclosing-block sibling `if (idx === "__proto__" || ...) return / throw / break;`); allowlist pattern (an ancestor `if (idx === "name" || idx === "id") { obj[idx] = v }`). Walks stop at the enclosing function so closure-captured guards in an outer scope can't silently authorise inner assignments.
|
||||
- Spring MVC `return "redirect:" + tainted` open-redirect recogniser (Java only). New `try_lower_spring_redirect_return` in `cfg/mod.rs` matches the leftmost `+`-chain whose root is a `redirect:` string literal and emits a synthetic `__spring_redirect__` Call sink with `Sink(Cap::OPEN_REDIRECT)` between the predecessors and the Return node. Concatenated identifiers from anywhere in the right-hand chain feed the synthetic node's `arg_uses[0]`, so the existing 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` (Ruby `element_reference`, JS/TS `subscript_expression`, Python `subscript`) had no `property` field on the LHS, so the existing classification path skipped it. `push_node` now walks into the subscript's `object` and classifies its member-expression text (`response.headers`, `res.headers`, `self.response.headers`), so `Cap::HEADER_INJECTION` fires on the bare bracket form alongside `setHeader` / `res.set` / `headers_mut.insert`.
|
||||
- PHP literal extraction extended in `cfg/literals.rs`. `extract_const_string_arg` now folds: PHP `encapsed_string` (double-quoted) when every child is a pure-literal segment; boolean literals (`true` / `false`) so jQuery's `extend(true, target, src)` deep-merge marker activates the `LiteralOnly` gate; leading-string `binary_expression` concat (PHP `"Location: " . $url`, JS/TS `"Location: " + url`) so `dangerous_prefixes` matching activates on partially dynamic concatenations.
|
||||
- PHP receiver-text strip for chain construction. `helpers::root_receiver_text` now drops the leading `$` from `variable_name` nodes so `$smarty->fetch(...)` / `$twig->createTemplate(...)` reconstruct as `Smarty.fetch` / `Environment.createTemplate` for suffix-matcher gates instead of carrying a `$smarty.fetch` form that fails the boundary rule.
|
||||
- Gate-callee resolution hardening for member-source rewrites. When `first_member_label` rewrites a call's `text` to a Source like `req.body` (because the wrapper carries a member-source argument), the gate matcher now reads the call's `function` / `method` / `name` field instead, so `setValue(target, req.body, ...)` matches the `setValue` proto-pollution gate instead of the rewritten `req.body` text. 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` / `constant` nodes (`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 rather than firing conservatively on every positional arg.
|
||||
- Per-language label rules expanded to cover the seven new caps:
|
||||
- JavaScript / TypeScript: ldapjs `LdapClient.search`, `escapeXpath` / `xpathEscape`, `document.evaluate` / npm `xpath.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 with `processEntities`-aware activation, `redirect` / `Location` open-redirect sinks.
|
||||
- Python: python-ldap `LDAPObject.search_s`, ldap3 `Connection.search`, lxml `etree.XPath` / `lxml.etree.parse` with parser-config awareness, Flask `response.headers[]=` / `make_response`, Jinja2 `Template(...)` and Mako `Template(...)` SSTI sinks, `flask.redirect` / `aiohttp HTTPFound` open-redirect.
|
||||
- Java / Kotlin: `DirContext.search`, `XPath.evaluate` / `XPath.compile`, JAXP `DocumentBuilder.parse` / `SAXParser.parse` / `XMLReader.parse`, FreeMarker `Template.process`, Spring `redirect:` view-name synthetic sink, `HttpServletResponse.setHeader` / `addHeader`.
|
||||
- PHP: `ldap_search` / `ldap_list` / `ldap_read`, `DOMXPath::query` / `DOMXPath::evaluate`, `header()` with leading-prefix activation, Smarty `fetch` / Twig `createTemplate` / Blade compile + `eval` template forms, `loadXML` / `simplexml_load_string` with `LIBXML_NOENT` activation.
|
||||
- Go: `go-ldap conn.Search`, `etree.Path` / `xmlpath.Compile`, `http.Header.Set` / `Response.Header().Set`, `html/template` and `text/template` `Parse(...)`, `encoding/xml.Unmarshal` / `Decoder.Decode`, `http.Redirect` with relative-URL / host-allowlist gating.
|
||||
- Ruby: `Net::LDAP#search`, `Nokogiri::XML::Document#xpath`, `response.headers[]=`, `ERB.new` SSTI, `Nokogiri::XML.parse` with `NOENT` / `DTDLOAD` activation, `redirect_to` with relative-URL gate.
|
||||
- C / C++: libldap `ldap_search_ext_s`, libxml2 `xmlXPathEval`, `curl_easy_setopt` with header-list activation, libxml2 `xmlReadFile` / `xmlReadMemory` with `XML_PARSE_NOENT` activation.
|
||||
- Rust: actix-web `HeaderMap.insert` / `HeaderValue::from_str` header-injection gates. `Redirect::to` retagged from `Cap::SSRF` to `Cap::OPEN_REDIRECT` so the open-redirect rule fires distinctly from the SSRF rule.
|
||||
- `NYX_PYTHON_PROTO_POLLUTION` env var flag. Python `dict.update` / `__dict__.update` proto-pollution gates are opt-in: bare `update` overlaps too broadly with `Counter.update` and ordinary state-mutation patterns to ship as a default sink. When the var is set to `1` / `true` / `yes` / `on` the merged slice is leaked into a `'static` reference so the registry's lifetime invariant holds.
|
||||
- New per-cap integration suites: `tests/{xpath_injection,xxe,ssti,prototype_pollution,header_injection,open_redirect,ldap_injection}_tests.rs`, plus `python_proto_pollution_tests.rs` for the env-gated Python form. Per-cap fixture trees under `tests/fixtures/<class>/<lang>/` cover safe, unsafe, and irrelevant-baseline shapes for every supported language.
|
||||
- FastAPI cross-file `include_router` dependency tracking. New `auth_analysis/router_facts.rs` captures per-file router declarations (`<router> = X(deps=[…])`) and `<parent>.include_router(<child_module>.<child_var>)` edges in pass 1, persists them into `GlobalSummaries::router_facts_by_module`, and resolves them into the active file's `AuthorizationModel::cross_file_router_deps` at pass 2 entry. Transitive lifts (`grandparent → parent → child`) handled by iterative index walk. Module identity is the file basename without `.py` (approximate, but sufficient for airflow-style `task_instances.router` naming). Closes the airflow execution-API shape where a child router lives in `routes/task_instances.py` and its auth is declared on the parent in `routes/__init__.py`.
|
||||
- FastAPI router-level `dependencies=[...]` propagation. Module-level `router = APIRouter(dependencies=[Security(...)])` declarations are pre-walked once per file, then merged onto every `@<router>.<verb>(...)` route attached in the same file. Closes airflow's execution-API routes that re-use a single `ti_id_router` declared once at module scope.
|
||||
- FastAPI `Security(callable, scopes=[...])` recognised distinctly from `Depends(callable)`. Scoped Security promotes the synthetic `AuthCheck` to `AuthCheckKind::Other` (route-level scope-checked authorization), not just Login. New scope-tracking boolean threaded through `expand_decorator_calls` and `extract_fastapi_dependencies`.
|
||||
|
|
@ -47,6 +90,15 @@ A round of cross-file FastAPI auth, two new sink/validator classes, a ~957-FP Go
|
|||
|
||||
### Fixed (false positives)
|
||||
|
||||
- `Object.create(null)` receivers no longer fire prototype-pollution at the synthetic `__index_set__` sink. Suppression is flow-sensitive via `TypeKind::NullPrototypeObject` so a phi join that only sometimes resolves to a null-proto receiver still fires on the unsafe path.
|
||||
- `cfg-unguarded-sink` over-fires on JS/TS object-literal property writes guarded by an explicit `__proto__` / `constructor` / `prototype` reject `if` (early `return` / `throw` / `break`) or by an allowlist `if` whose true arm contains the assignment. Resolved at the CFG layer before the SSA sink scan.
|
||||
- Spring MVC `return "redirect:" + url` flagged generic `taint-unsanitised-flow` even when the redirect destination was the load-bearing taint. Now routed through the synthetic `__spring_redirect__` sink so the finding emerges as `taint-open-redirect`.
|
||||
- `$smarty->fetch(...)` / `$twig->createTemplate(...)` no longer drop their SSTI gate match on idiomatic PHP receiver shapes. Receiver text strip in `helpers::root_receiver_text` rebuilds the chain text with `.` separators.
|
||||
- `setValue(target, req.body, ...)` and similar wrappers no longer gate-match on the rewritten Source `req.body` text. Gate matcher now reads the call's `function` / `method` / `name` field when a Source label override has clobbered the call text.
|
||||
- Nokogiri / lxml / fast-xml-parser parser bodies hardened with `setFeature` / `processEntities: false` / `XMLParser(resolve_entities=False)` no longer fire `taint-xxe`. Suppression runs through the new `xml_parser_config` sidecar.
|
||||
- `XPath` instances bound to `setXPathVariableResolver(...)` no longer fire `taint-xpath-injection` on subsequent `xpath.evaluate(expr, ...)` sinks. Suppression runs through the new `xpath_config` sidecar.
|
||||
- Inline `if (!url.startsWith("/")) reject` and `if (new URL(url).host !== ALLOWED) reject` open-redirect sanitisers now narrow the `Cap::OPEN_REDIRECT` bit on the validated branch instead of falling through to the generic `Comparison` predicate. Cap-aware: other taint downstream still fires on its own caps.
|
||||
- Rust `Redirect::to` no longer fires `taint-ssrf` for what is structurally an open redirect. Retagged to `Cap::OPEN_REDIRECT` so the report classifies the issue under the correct cap.
|
||||
- ~957 gitea backend DAO `go.auth.missing_ownership_check` findings (id-scalar precision pass, see Added).
|
||||
- 169 of 216 openmrs `cfg-unguarded-sink` findings (JpaCriteriaQuery type, see Added). Equivalent reductions on xwiki / keycloak Hibernate DAO clusters.
|
||||
- joomla and drupal `php.deser.unserialize` flagged inside `Serializable::unserialize($input)` magic-method bodies (passthrough recognition, see Added).
|
||||
|
|
@ -66,6 +118,8 @@ A round of cross-file FastAPI auth, two new sink/validator classes, a ~957-FP Go
|
|||
- New `cfg/cfg_tests.rs` covers ternary-branch CFG lowering shapes.
|
||||
- New `summary/tests.rs` covers cross-file `include_router` summary persistence and resolution.
|
||||
- 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.
|
||||
- `parse_cap` and `CapName::FromStr` accept 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 existing `data_exfil` alias). The `nyx config add-rule --cap` flag and `[analysis.languages.*.rules]` entries take any of these.
|
||||
- Frontend `RuleListItem` carries the new `is_class` flag so the dashboard's Rules page can group cap-class entries separately. `RuleDetailView` adds the same field.
|
||||
|
||||
## [0.6.1] - 2026-05-03
|
||||
|
||||
|
|
|
|||
58
Cargo.lock
generated
|
|
@ -126,9 +126,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.41"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
|
||||
checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac"
|
||||
dependencies = [
|
||||
"compression-codecs",
|
||||
"compression-core",
|
||||
|
|
@ -257,9 +257,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
version = "1.2.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
|
|
@ -378,9 +378,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.37"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
|
||||
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
|
|
@ -389,9 +389,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.31"
|
||||
version = "0.4.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
|
|
@ -977,10 +977,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.95"
|
||||
version = "0.3.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
|
@ -999,9 +1001,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.185"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
|
|
@ -1748,9 +1750,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
|||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
|
|
@ -1919,9 +1921,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
version = "1.52.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
|
|
@ -2025,9 +2027,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
checksum = "a28f0d049ccfaa566e14e9663d304d8577427b368cb4710a20528690287a738b"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bitflags",
|
||||
|
|
@ -2351,9 +2353,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.118"
|
||||
version = "0.2.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
|
||||
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
|
|
@ -2364,9 +2366,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.118"
|
||||
version = "0.2.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
|
||||
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -2374,9 +2376,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.118"
|
||||
version = "0.2.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
|
||||
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
|
|
@ -2387,9 +2389,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.118"
|
||||
version = "0.2.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
|
||||
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -2430,9 +2432,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.95"
|
||||
version = "0.3.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
|
||||
checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Everything stays on your machine: loopback-only bind, host-header enforcement, C
|
|||
|
||||
The same engine runs headless for CI pipelines. SARIF output uploads directly to GitHub Code Scanning.
|
||||
|
||||
<p align="center"><img src="assets/screenshots/cli-scan.png" alt="nyx scan console output: HIGH taint findings across a JS and Python file with source → sink arrows" width="820"/></p>
|
||||
<p align="center"><img src="assets/screenshots/cli-scan.gif" alt="nyx scan console output: HIGH taint findings across a JS and Python file with source → sink arrows" width="820"/></p>
|
||||
|
||||
```bash
|
||||
# Fail the job on medium or higher, emit SARIF
|
||||
|
|
@ -186,7 +186,7 @@ Two passes over the filesystem, with an optional SQLite index to skip unchanged
|
|||
3. **Pass 2**: re-analyze each file with cross-file context under bounded context sensitivity (k=1 inlining for intra-file callees, SCC fixpoint capped at 64 iterations, and summary fallback for callees above the inline body-size cap). A forward dataflow worklist propagates taint through the SSA lattice with guaranteed convergence. Call-graph SCCs iterate to fixed-point (within the cap) so mutually recursive functions get accurate summaries.
|
||||
4. **Rank, dedupe, emit**: findings are scored by severity × evidence strength × source-kind exploitability, then emitted to console, JSON, or SARIF.
|
||||
|
||||
Detector families: taint (cross-file source→sink), CFG structural (auth gaps, unguarded sinks, resource leaks), state model (use-after-close, double-close, must-leak, unauthed-access), AST patterns (tree-sitter structural match). Full detector docs: [Detectors](https://elicpeter.github.io/nyx/detectors.html).
|
||||
Detector families: taint (cross-file source→sink, with cap-specific rule classes for SQLi, XSS, command/code exec, deserialization, SSRF, path traversal, format string, crypto, LDAP injection, XPath injection, HTTP header / response splitting, open redirect, server-side template injection, XXE, prototype pollution, data exfiltration, and the auth fold-in), CFG structural (auth gaps, unguarded sinks, resource leaks), state model (use-after-close, double-close, must-leak, unauthed-access), AST patterns (tree-sitter structural match). Full detector docs: [Detectors](https://elicpeter.github.io/nyx/detectors.html).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ kind = "sanitizer"
|
|||
cap = "html_escape"
|
||||
```
|
||||
|
||||
Or add rules interactively: `nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape`. Caps: `env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `data_exfil`, `code_exec`, `crypto`, `unauthorized_id`, `all`. Full schema: [Configuration](https://elicpeter.github.io/nyx/configuration.html).
|
||||
Or add rules interactively: `nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape`. Caps: `env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `data_exfil`, `code_exec`, `crypto`, `unauthorized_id`, `ldap_injection`, `xpath_injection`, `header_injection`, `open_redirect`, `ssti`, `xxe`, `prototype_pollution`, `all`. Full schema: [Configuration](https://elicpeter.github.io/nyx/configuration.html). Run `nyx rules list` to browse the registry from the terminal.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@
|
|||
|
||||
<h2>Overview of licenses:</h2>
|
||||
<ul class="licenses-overview">
|
||||
<li><a href="#Apache-2.0">Apache License 2.0</a> (160)</li>
|
||||
<li><a href="#MIT">MIT License</a> (71)</li>
|
||||
<li><a href="#Apache-2.0">Apache License 2.0</a> (156)</li>
|
||||
<li><a href="#MIT">MIT License</a> (70)</li>
|
||||
<li><a href="#Zlib">zlib License</a> (2)</li>
|
||||
<li><a href="#BSD-2-Clause">BSD 2-Clause "Simplified" License</a> (1)</li>
|
||||
<li><a href="#BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</a> (1)</li>
|
||||
|
|
@ -1549,7 +1549,6 @@
|
|||
<li><a href=" https://github.com/clap-rs/clap ">clap_derive 4.6.1</a></li>
|
||||
<li><a href=" https://github.com/clap-rs/clap ">clap_lex 1.1.0</a></li>
|
||||
<li><a href=" https://github.com/rust-cli/anstyle.git ">colorchoice 1.0.5</a></li>
|
||||
<li><a href=" https://github.com/srijs/rust-crc32fast ">crc32fast 1.5.0</a></li>
|
||||
<li><a href=" https://github.com/sfackler/rust-fallible-iterator ">fallible-iterator 0.3.0</a></li>
|
||||
<li><a href=" https://github.com/sfackler/fallible-streaming-iterator ">fallible-streaming-iterator 0.1.9</a></li>
|
||||
<li><a href=" https://github.com/polyfill-rs/is_terminal_polyfill ">is_terminal_polyfill 1.70.2</a></li>
|
||||
|
|
@ -2616,13 +2615,16 @@ limitations under the License.</pre>
|
|||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/bluss/arrayvec ">arrayvec 0.7.6</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">async-compression 0.4.42</a></li>
|
||||
<li><a href=" https://github.com/smol-rs/atomic-waker ">atomic-waker 1.1.2</a></li>
|
||||
<li><a href=" https://github.com/cuviper/autocfg ">autocfg 1.5.0</a></li>
|
||||
<li><a href=" https://github.com/bitflags/bitflags ">bitflags 2.11.1</a></li>
|
||||
<li><a href=" https://github.com/BurntSushi/bstr ">bstr 1.12.1</a></li>
|
||||
<li><a href=" https://github.com/japaric/cast.rs ">cast 0.3.0</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/cc-rs ">cc 1.2.60</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/cc-rs ">cc 1.2.61</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/cfg-if ">cfg-if 1.0.4</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">compression-codecs 0.4.38</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">compression-core 0.4.32</a></li>
|
||||
<li><a href=" https://github.com/servo/core-foundation-rs ">core-foundation-sys 0.8.7</a></li>
|
||||
<li><a href=" https://github.com/criterion-rs/criterion.rs ">criterion-plot 0.8.2</a></li>
|
||||
<li><a href=" https://github.com/criterion-rs/criterion.rs ">criterion 0.8.2</a></li>
|
||||
|
|
@ -2636,7 +2638,6 @@ limitations under the License.</pre>
|
|||
<li><a href=" https://github.com/smol-rs/fastrand ">fastrand 2.4.1</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/cc-rs ">find-msvc-tools 0.1.9</a></li>
|
||||
<li><a href=" https://github.com/petgraph/fixedbitset ">fixedbitset 0.5.7</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/flate2-rs ">flate2 1.1.9</a></li>
|
||||
<li><a href=" https://github.com/servo/rust-url ">form_urlencoded 1.2.2</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/glob ">glob 0.3.3</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/hashbrown ">hashbrown 0.14.5</a></li>
|
||||
|
|
@ -3689,215 +3690,6 @@ APPENDIX: How to apply the Apache License to your work.
|
|||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
</pre>
|
||||
</li>
|
||||
<li class="license">
|
||||
<h3 id="Apache-2.0">Apache License 2.0</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/oyvindln/adler2 ">adler2 2.0.1</a></li>
|
||||
</ul>
|
||||
<pre class="license-text"> Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
|
@ -4335,17 +4127,13 @@ limitations under the License.
|
|||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/zrzka/anes-rs ">anes 0.1.6</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">async-compression 0.4.41</a></li>
|
||||
<li><a href=" https://github.com/BLAKE3-team/BLAKE3 ">blake3 1.8.5</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">compression-codecs 0.4.37</a></li>
|
||||
<li><a href=" https://github.com/Nullus157/async-compression ">compression-core 0.4.31</a></li>
|
||||
<li><a href=" https://github.com/cesarb/constant_time_eq ">constant_time_eq 0.4.2</a></li>
|
||||
<li><a href=" https://github.com/soc/directories-rs ">directories 6.0.0</a></li>
|
||||
<li><a href=" https://github.com/dirs-dev/dirs-sys-rs ">dirs-sys 0.5.0</a></li>
|
||||
<li><a href=" https://github.com/VoidStarKat/half-rs ">half 2.7.1</a></li>
|
||||
<li><a href=" https://github.com/dtolnay/itoa ">itoa 1.0.18</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/libc ">libc 0.2.185</a></li>
|
||||
<li><a href=" https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide ">miniz_oxide 0.8.9</a></li>
|
||||
<li><a href=" https://github.com/rust-lang/libc ">libc 0.2.186</a></li>
|
||||
<li><a href=" https://github.com/jhpratt/num-conv ">num-conv 0.2.1</a></li>
|
||||
<li><a href=" https://github.com/taiki-e/pin-project-lite ">pin-project-lite 0.2.17</a></li>
|
||||
<li><a href=" https://github.com/taiki-e/portable-atomic ">portable-atomic 1.13.1</a></li>
|
||||
|
|
@ -4361,7 +4149,7 @@ limitations under the License.
|
|||
<li><a href=" https://github.com/dtolnay/path-to-error ">serde_path_to_error 0.1.20</a></li>
|
||||
<li><a href=" https://github.com/nox/serde_urlencoded ">serde_urlencoded 0.7.1</a></li>
|
||||
<li><a href=" https://github.com/comex/rust-shlex ">shlex 1.3.0</a></li>
|
||||
<li><a href=" https://github.com/jedisct1/rust-siphash ">siphasher 1.0.2</a></li>
|
||||
<li><a href=" https://github.com/jedisct1/rust-siphash ">siphasher 1.0.3</a></li>
|
||||
<li><a href=" https://github.com/dtolnay/syn ">syn 2.0.117</a></li>
|
||||
<li><a href=" https://github.com/Actyx/sync_wrapper ">sync_wrapper 1.0.2</a></li>
|
||||
<li><a href=" https://github.com/dtolnay/thiserror ">thiserror-impl 2.0.18</a></li>
|
||||
|
|
@ -5338,7 +5126,7 @@ DEALINGS IN THE SOFTWARE.
|
|||
<h3 id="MIT">MIT License</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/tower-rs/tower-http ">tower-http 0.6.8</a></li>
|
||||
<li><a href=" https://github.com/tower-rs/tower-http ">tower-http 0.6.9</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">Copyright (c) 2019-2021 Tower Contributors
|
||||
|
||||
|
|
@ -5736,7 +5524,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/tokio-rs/tokio ">tokio-stream 0.1.18</a></li>
|
||||
<li><a href=" https://github.com/tokio-rs/tokio ">tokio-util 0.7.18</a></li>
|
||||
<li><a href=" https://github.com/tokio-rs/tokio ">tokio 1.52.1</a></li>
|
||||
<li><a href=" https://github.com/tokio-rs/tokio ">tokio 1.52.2</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">MIT License
|
||||
|
||||
|
|
@ -5752,35 +5540,6 @@ furnished to do so, subject to the following conditions:
|
|||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</pre>
|
||||
</li>
|
||||
<li class="license">
|
||||
<h3 id="MIT">MIT License</h3>
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href=" https://github.com/mcountryman/simd-adler32 ">simd-adler32 0.3.9</a></li>
|
||||
</ul>
|
||||
<pre class="license-text">MIT License
|
||||
|
||||
Copyright (c) [2021] [Marvin Countryman]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
|
|
|
|||
BIN
assets/logo.png
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 520 KiB |
|
|
@ -6,5 +6,5 @@
|
|||
font-weight="700"
|
||||
font-size="100"
|
||||
letter-spacing="-1"
|
||||
fill="#5856d6">nyx</text>
|
||||
fill="#72f3d7">nyx</text>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 392 B |
BIN
assets/screenshots/cli-scan.gif
Normal file
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 257 KiB |
BIN
assets/screenshots/cli-scan_raw.gif
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/screenshots/cli-scan_raw.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
assets/screenshots/demo-combo.gif
Normal file
|
After Width: | Height: | Size: 19 MiB |
|
Before Width: | Height: | Size: 15 MiB After Width: | Height: | Size: 24 MiB |
BIN
assets/screenshots/demo_raw.gif
Normal file
|
After Width: | Height: | Size: 12 MiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 72 KiB |
BIN
assets/screenshots/docs/cli-configshow_raw.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 222 KiB |
BIN
assets/screenshots/docs/cli-explain-engine_raw.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 257 KiB |
BIN
assets/screenshots/docs/cli-failon_raw.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 62 KiB |
BIN
assets/screenshots/docs/cli-idxstatus_raw.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 132 KiB |
BIN
assets/screenshots/docs/serve-config_raw.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 137 KiB |
BIN
assets/screenshots/docs/serve-explorer_raw.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 160 KiB |
BIN
assets/screenshots/docs/serve-finding-detail_raw.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 145 KiB |
BIN
assets/screenshots/docs/serve-findings-list_raw.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 134 KiB |
BIN
assets/screenshots/docs/serve-overview_raw.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 168 KiB |
BIN
assets/screenshots/docs/serve-rules_raw.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 109 KiB |
BIN
assets/screenshots/docs/serve-scan-detail_raw.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 85 KiB |
BIN
assets/screenshots/docs/serve-scans_raw.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 101 KiB |
BIN
assets/screenshots/docs/serve-triage_raw.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 355 KiB After Width: | Height: | Size: 166 KiB |
|
|
@ -110,7 +110,7 @@ nyx scan . --severity ">=MEDIUM" --min-confidence medium
|
|||
|
||||
Auth findings render alongside taint findings in the [browser UI](serve.md). The flow visualiser shows the sink call, the actor reference (when one was found), and any helper-summary path the engine traversed; the How to fix panel mirrors the rule's recommendation.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-finding-detail.png" alt="Nyx finding detail: numbered source → call → sink walk with a How to fix panel and an inline evidence object" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-finding-detail.png" alt="Nyx finding detail: numbered source → call → sink walk with a How to fix panel and an inline evidence object" width="900"/></p>
|
||||
|
||||
## Benchmark corpus
|
||||
|
||||
|
|
|
|||
47
docs/cli.md
|
|
@ -95,11 +95,11 @@ nyx scan [PATH] [OPTIONS]
|
|||
|
||||
`--fail-on` returns a non-zero exit code when the threshold trips, so CI jobs fail without further wiring:
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/cli-failon.png" alt="nyx scan with --fail-on HIGH against a small fixture: three HIGH taint findings printed, followed by exit=1 from the shell" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/cli-failon.png" alt="nyx scan with --fail-on HIGH against a small fixture: three HIGH taint findings printed, followed by exit=1 from the shell" width="900"/></p>
|
||||
|
||||
Quality-category and rollup-prone Low findings are filtered down by default. The footer tells you exactly what got dropped and which knob to turn:
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/cli-rollup-tail.png" alt="nyx scan tail: warning '*' generated 57 issues; Suppressed 92 LOW/Quality findings; Active filters max_low=20, max_low_per_file=1, max_low_per_rule=10; Use --include-quality, --max-low, or --all to adjust" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/cli-rollup-tail.png" alt="nyx scan tail: warning '*' generated 57 issues; Suppressed 92 LOW/Quality findings; Active filters max_low=20, max_low_per_file=1, max_low_per_rule=10; Use --include-quality, --max-low, or --all to adjust" width="900"/></p>
|
||||
|
||||
### Analysis Engine Toggles
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ Individual flags override the profile. For example, `--engine-profile fast --ba
|
|||
nyx scan --engine-profile deep --no-smt --explain-engine
|
||||
```
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/cli-explain-engine.png" alt="nyx scan --engine-profile deep --explain-engine output: resolved config showing every analysis pass, its current state, and the CLI flag/env var that controls it" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/cli-explain-engine.png" alt="nyx scan --engine-profile deep --explain-engine output: resolved config showing every analysis pass, its current state, and the CLI flag/env var that controls it" width="900"/></p>
|
||||
|
||||
### Examples
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ nyx index status [PATH]
|
|||
|
||||
Display index statistics (file count, size, last modified) for the given path.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/cli-idxstatus.png" alt="nyx index status output: project name, index path under the platform config dir, exists/size/modified fields" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/cli-idxstatus.png" alt="nyx index status output: project name, index path under the platform config dir, exists/size/modified fields" width="900"/></p>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -256,7 +256,7 @@ Manage configuration.
|
|||
|
||||
Print the effective merged configuration as TOML. Useful for sanity-checking what the scanner is actually using after `nyx.conf` and `nyx.local` merge:
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/cli-configshow.png" alt="nyx config show output: TOML dump of the merged scanner config showing [scanner] mode/min_severity/excluded_extensions/excluded_directories, [database] settings, and resolved engine toggles" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/cli-configshow.png" alt="nyx config show output: TOML dump of the merged scanner config showing [scanner] mode/min_severity/excluded_extensions/excluded_directories, [database] settings, and resolved engine toggles" width="900"/></p>
|
||||
|
||||
### `nyx config path`
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ Add a custom taint rule. Written to `nyx.local`.
|
|||
| `--lang` | `rust`, `javascript`, `typescript`, `python`, `go`, `java`, `c`, `cpp`, `php`, `ruby` |
|
||||
| `--matcher` | Function or property name to match |
|
||||
| `--kind` | `source`, `sanitizer`, `sink` |
|
||||
| `--cap` | `env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `code_exec`, `crypto`, `unauthorized_id`, `all` |
|
||||
| `--cap` | `env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `code_exec`, `crypto`, `unauthorized_id`, `data_exfil`, `ldap_injection`, `xpath_injection`, `header_injection`, `open_redirect`, `ssti`, `xxe`, `prototype_pollution`, `all` |
|
||||
|
||||
### `nyx config add-terminator`
|
||||
|
||||
|
|
@ -287,6 +287,41 @@ Add a terminator function (e.g. `process.exit`). Written to `nyx.local`.
|
|||
|
||||
---
|
||||
|
||||
## `nyx rules`
|
||||
|
||||
Browse the built-in rule registry from the terminal. Same dataset the dashboard's Rules page reads from: cap-class entries (one per `Cap` with a canonical rule id), per-language label rules (sink / source / sanitizer), gated sinks, and any custom rules from your config.
|
||||
|
||||
### `nyx rules list`
|
||||
|
||||
```
|
||||
nyx rules list [--lang <SLUG>] [--kind <KIND>] [--class-only|--no-class] [--json]
|
||||
```
|
||||
|
||||
| Flag | Values |
|
||||
|------|--------|
|
||||
| `--lang` | Language slug (`javascript`, `typescript`, `python`, `java`, `php`, `go`, `ruby`, `rust`, `c`, `cpp`). Cap-class entries (`language = "all"`) still surface alongside any language filter unless `--no-class` is set. |
|
||||
| `--kind` | `class` (cap-class entry), `source`, `sink`, `sanitizer` |
|
||||
| `--class-only` | Show only the cap-class registry entries, suppressing per-language label rules and gated sinks. |
|
||||
| `--no-class` | Suppress cap-class registry entries, show only per-language label rules and gated sinks. Conflicts with `--class-only`. |
|
||||
| `--json` | Emit JSON instead of the human-readable table. Schema matches the `/api/rules` response. |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Browse the seven new vulnerability classes
|
||||
nyx rules list --class-only
|
||||
|
||||
# All Java sinks
|
||||
nyx rules list --lang java --kind sink
|
||||
|
||||
# JSON output for scripted filtering
|
||||
nyx rules list --json | jq '.[] | select(.cap == "ldap_injection")'
|
||||
```
|
||||
|
||||
The `enabled` column reflects the `analysis.disabled_rules` overlay from your config, so a rule disabled in `nyx.local` shows up here too. Custom rules added via `nyx config add-rule` appear at the end with `is_custom: true`.
|
||||
|
||||
---
|
||||
|
||||
## Exit codes
|
||||
|
||||
See [output.md](output.md#exit-codes). Summary: `0` on success (including findings without `--fail-on`), `1` when `--fail-on` trips, non-zero on scan errors.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Nyx uses TOML configuration files. A default config is auto-generated on first run. If you'd rather edit settings and rules from the browser, the [Config page in `nyx serve`](serve.md#config) is a live editor that writes back to `nyx.local`:
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-config.png" alt="Nyx config page: General settings, Triage Sync toggle, Sources panel with language/matcher/capability dropdowns and a per-language matcher table" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-config.png" alt="Nyx config page: General settings, Triage Sync toggle, Sources panel with language/matcher/capability dropdowns and a per-language matcher table" width="900"/></p>
|
||||
|
||||
## File Locations
|
||||
|
||||
|
|
@ -253,9 +253,14 @@ cap = "html_escape" # "env_var" | "html_escape" | "shell_escape" |
|
|||
# "url_encode" | "json_parse" | "file_io" |
|
||||
# "fmt_string" | "sql_query" | "deserialize" |
|
||||
# "ssrf" | "data_exfil" | "code_exec" | "crypto" |
|
||||
# "unauthorized_id" | "all"
|
||||
# "unauthorized_id" | "ldap_injection" |
|
||||
# "xpath_injection" | "header_injection" |
|
||||
# "open_redirect" | "ssti" | "xxe" |
|
||||
# "prototype_pollution" | "all"
|
||||
```
|
||||
|
||||
Aliases accepted by `parse_cap` and `[..rules].cap`: `data_exfiltration` for `data_exfil`, `ldapi` for `ldap_injection`, `xpathi` for `xpath_injection`, `crlf` and `response_splitting` for `header_injection`, `redirect` for `open_redirect`, `template_injection` for `ssti`, `proto_pollution` for `prototype_pollution`.
|
||||
|
||||
---
|
||||
|
||||
## Example Configurations
|
||||
|
|
|
|||
|
|
@ -13,11 +13,20 @@ The taint family is split into cap-specific rule classes when a sink callee carr
|
|||
|
||||
| Rule id | Cap | Surface |
|
||||
|---|---|---|
|
||||
| `taint-unsanitised-flow` | every cap except `data_exfil` and `unauthorized_id` | Default taint flow class |
|
||||
| `taint-unsanitised-flow` | `sql_query`, `ssrf`, `code_exec`, `file_io`, `fmt_string`, `deserialize`, `crypto` | Catch-all class for the legacy caps that have not migrated to a dedicated rule id yet. |
|
||||
| `taint-ldap-injection` | `ldap_injection` | Attacker-controlled data concatenated into an LDAP filter or DN without RFC 4515 escaping. Receivers typed as `LdapClient` (JNDI `DirContext`, Spring `LdapTemplate`, ldapjs `Client`, python-ldap `LDAPObject`, ldap3 `Connection`) and chained `.search` / `.searchByEntity` / `.search_s` form the sink set. |
|
||||
| `taint-xpath-injection` | `xpath_injection` | Attacker-controlled string passed as the XPath expression to `xpath.evaluate` / `xpath.compile` / `document.evaluate` / `DOMXPath::query` / `etree.XPath`. Suppressed when the receiver was bound to an `XPathVariableResolver` (parameterised XPath shape). |
|
||||
| `taint-header-injection` | `header_injection` | Attacker-controlled bytes landing in an HTTP response header without `\r\n` stripping (response splitting, cache poisoning). Covers `setHeader` / `res.set` / `res.append` / `headers["X-Foo"] = bar` / `Header().Set` / `add_header` / `setcookie` / `http.Header.Set`. |
|
||||
| `taint-open-redirect` | `open_redirect` | Attacker-controlled URL driving a redirect / `Location` header without an allowlist or relative-URL check. Includes the Spring MVC `return "redirect:" + url` view-name shape via the `__spring_redirect__` synthetic sink. Suppressed by `RelativeUrlValidated` (`startsWith("/")` family) and `HostAllowlistValidated` (`new URL(x).host === ALLOWED`, `urlparse(x).netloc == ...`) inline predicates. |
|
||||
| `taint-template-injection` | `ssti` | Attacker controls the *template source string* fed to a server-side renderer (Jinja2 / Mako / FreeMarker / Twig / Handlebars / EJS / Mustache / ERB / `text/template` / `html/template` / Smarty / Blade `Template(...)` / `compile(...)`), distinct from rendering a trusted template with tainted variables. |
|
||||
| `taint-xxe` | `xxe` | Attacker-controlled XML reaching a parser that resolves external entities. Covers JAXP `DocumentBuilder.parse` / `SAXParser.parse` / `XMLReader.parse`, lxml `etree.parse`, Nokogiri, fast-xml-parser, xml2js, libxml2 `xmlReadFile` / `xmlReadMemory`. Suppressed when the receiver carries a hardening fact in `xml_parser_config` (`secure_processing`, `disallow_doctype`, `processEntities: false`, `LIBXML_NOENT` not set). |
|
||||
| `taint-prototype-pollution` | `prototype_pollution` | Attacker-controlled key reaching an object property assignment that can mutate `Object.prototype`. JS/TS only. Covers `obj[tainted] = v` (synthetic `__index_set__` sink), library-mediated deep-merge / set helpers (`_.merge`, `_.set`, `dotProp.set`, `objectPath.set`, `setValue`), and jQuery's `extend(true, target, src)` deep-merge form via the `LiteralOnly` activation gate. Suppressed by constant-key fold (`__proto__` / `constructor` / `prototype` filtering), reject / allowlist guards on the key, and `Object.create(null)` receivers (flow-sensitive `NullPrototypeObject` type). Python equivalent (`dict.update`) is opt-in via `NYX_PYTHON_PROTO_POLLUTION=1`. |
|
||||
| `taint-data-exfiltration` | `data_exfil` | Sensitive data flowing into the payload of an outbound network request (body / headers / json on `fetch`, body on `XMLHttpRequest.send`). Distinct from SSRF: the destination is fixed but attacker-influenced bytes leave the process. |
|
||||
| `rs.auth.missing_ownership_check.taint` | `unauthorized_id` | Rust auth subsystem fold-in; see [auth.md](auth.md). |
|
||||
|
||||
A single call site can fire several of these at once when it carries multiple gates — `fetch(taintedUrl, {body: tainted})` produces both an SSRF finding (URL flow) and a `taint-data-exfiltration` finding (body flow), each with its own cap mask rather than a conflated union.
|
||||
A single call site can fire several of these at once when it carries multiple gates. `fetch(taintedUrl, {body: tainted})` produces both an SSRF finding (URL flow) and a `taint-data-exfiltration` finding (body flow), each with its own cap mask rather than a conflated union.
|
||||
|
||||
Each cap-class entry is registered in `CAP_RULE_REGISTRY` (`src/labels/mod.rs`) with its title, severity, OWASP 2021 code, and description. Browse the registry from the CLI with `nyx rules list --class-only`, or `nyx rules list --kind class --json` for machine output.
|
||||
|
||||
For Rust auth-specific rules (`rs.auth.*`), see [auth.md](auth.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ AST-only mode gives you structural pattern matches without taint.
|
|||
|
||||
In the browser UI, taint findings render as a numbered flow walk so you can see each hop the engine took:
|
||||
|
||||
<p align="center"><img src="../../assets/screenshots/docs/serve-finding-detail.png" alt="Nyx finding detail: HIGH taint-unsanitised-flow with numbered source → call → sink steps and How to fix guidance" width="900"/></p>
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-finding-detail.png" alt="Nyx finding detail: HIGH taint-unsanitised-flow with numbered source → call → sink steps and How to fix guidance" width="900"/></p>
|
||||
|
||||
## Example
|
||||
|
||||
|
|
@ -135,10 +135,17 @@ Sources, sanitizers, and sinks are linked by named capabilities. A sanitizer onl
|
|||
| `sql_query` | | parameterized query binders | `cursor.execute`, `db.query` with concatenation |
|
||||
| `deserialize` | | | `pickle.loads`, `yaml.load`, `Marshal.load` |
|
||||
| `ssrf` | | URL-prefix locks | `requests.get`, `fetch` URL arg, outbound HTTP destination |
|
||||
| `data_exfil` | cookies, headers, env, db rows, file reads (Sensitive-tier sources only) | | `fetch` body / headers / json, `XMLHttpRequest.send` body |
|
||||
| `code_exec` | | | `eval`, `exec`, `Function` |
|
||||
| `crypto` | | | weak-algorithm constructors |
|
||||
| `unauthorized_id` | request-bound scoped IDs (Rust auth analysis) | ownership check | row-level write |
|
||||
| `ldap_injection` | | `ldap-escape` filter / dn helpers, project-local `escapeLdapFilter` | `DirContext.search`, `LdapClient.search`, `ldap_search`, `Net::LDAP#search`, `ldap_search_ext_s` |
|
||||
| `xpath_injection` | | bound `XPathVariableResolver`, `escapeXpath` / `xpathEscape` helpers | `XPath.evaluate`, `DOMXPath::query`, `document.evaluate`, `xpath.select`, `etree.XPath` |
|
||||
| `header_injection` | | `stripCRLF` / `escapeHeader` / `sanitizeHeader` | `setHeader`, `res.set`, `res.append`, `headers["X-Foo"] = bar`, `Header().Set`, `header()`, `setcookie` |
|
||||
| `open_redirect` | | leading-slash check (`startsWith("/")`), URL-parse + host allowlist (`new URL(x).host === ALLOWED`) | `Redirect::to`, Spring `redirect:` view name, `flask.redirect`, `http.Redirect`, `redirect_to` |
|
||||
| `ssti` | | | template constructors fed by tainted source: `Jinja2 Template(...)`, `freemarker.Template`, `Twig::createTemplate`, Handlebars `compile`, `ERB.new`, Mako `Template(...)` |
|
||||
| `xxe` | | hardened parser config (`secure_processing`, `disallow-doctype-decl`, `processEntities: false`, `LIBXML_NOENT` not set) | `DocumentBuilder.parse`, `SAXParser.parse`, `xml2js`, `fast-xml-parser`, `lxml.etree.parse`, `xmlReadFile` |
|
||||
| `prototype_pollution` | | constant-key fold, reject / allowlist guards on the key, `Object.create(null)` receivers | `obj[tainted] = v` synthetic `__index_set__`, `_.merge`, `_.set`, `dotProp.set`, `objectPath.set`, jQuery `extend(true, ...)` |
|
||||
| `data_exfil` | cookies, headers, env, db rows, file reads (Sensitive-tier sources only) | | `fetch` body / headers / json, `XMLHttpRequest.send` body |
|
||||
| `all` | Sources typically use `all` so they match any sink | | |
|
||||
|
||||
Sources typically use `cap = "all"` so they match every sink. Sinks declare the specific cap they need. Sanitizers only clear the cap they name.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ First run builds a SQLite index under `.nyx/`; later runs skip files whose conte
|
|||
|
||||
## What a finding looks like
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/cli-scan.png" alt="nyx scan output: HIGH taint flows from req.params.user, req.query.url, and req.query.path into exec/fetch/fs.readFileSync, framed by the brand purple gradient" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/cli-scan.png" alt="nyx scan output: HIGH taint flows from req.params.user, req.query.url, and req.query.path into exec/fetch/fs.readFileSync, framed by the brand mint-cyan gradient" width="900"/></p>
|
||||
|
||||
The same scan in console form:
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Every finding Nyx emits has a rule ID. This page enumerates the IDs that ship wi
|
|||
|
||||
If you'd rather browse rules interactively, [`nyx serve`](serve.md) ships a Rules page that lists every loaded matcher with its language, kind, and capability:
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-rules.png" alt="Nyx Rules page: filterable list of 218 rules with language, kind (SOURCE/SANITIZER/SINK), capability, and finding count columns" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-rules.png" alt="Nyx Rules page: filterable list of 218 rules with language, kind (SOURCE/SANITIZER/SINK), capability, and finding count columns" width="900"/></p>
|
||||
|
||||
## ID format
|
||||
|
||||
|
|
@ -24,13 +24,22 @@ Language prefixes: `rs`, `c`, `cpp`, `go`, `java`, `js`, `ts`, `py`, `php`, `rb`
|
|||
|
||||
### Taint
|
||||
|
||||
One rule covers every source-to-sink flow. The parenthetical identifies the source location.
|
||||
The taint family is split into cap-specific rule classes. The `taint-unsanitised-flow` id is the catch-all for the legacy caps that have not migrated to a dedicated rule id yet (`sql_query`, `ssrf`, `code_exec`, `file_io`, `fmt_string`, `deserialize`, `crypto`). The seven new vulnerability classes plus auth and data-exfil emerge under their own rule id. The parenthetical identifies the source location.
|
||||
|
||||
| Rule ID | Severity |
|
||||
|---|---|
|
||||
| `taint-unsanitised-flow (source L:C)` | Varies by source kind and sink capability |
|
||||
| Rule ID | Cap | Severity |
|
||||
|---|---|---|
|
||||
| `taint-unsanitised-flow (source L:C)` | `sql_query` / `ssrf` / `code_exec` / `file_io` / `fmt_string` / `deserialize` / `crypto` | Varies |
|
||||
| `taint-ldap-injection` | `ldap_injection` | High |
|
||||
| `taint-xpath-injection` | `xpath_injection` | High |
|
||||
| `taint-header-injection` | `header_injection` | High |
|
||||
| `taint-open-redirect` | `open_redirect` | Medium |
|
||||
| `taint-template-injection` | `ssti` | High |
|
||||
| `taint-xxe` | `xxe` | High |
|
||||
| `taint-prototype-pollution` | `prototype_pollution` | High |
|
||||
| `taint-data-exfiltration` | `data_exfil` | High / Medium |
|
||||
| `rs.auth.missing_ownership_check.taint` | `unauthorized_id` | High |
|
||||
|
||||
The matcher sets (sources, sanitizers, sinks, gated sinks) live per-language in `src/labels/<lang>.rs`. [Language maturity](language-maturity.md) gives per-language counts and what's covered.
|
||||
Each cap-class entry is registered in `CAP_RULE_REGISTRY` (`src/labels/mod.rs`). Browse the registry from the CLI with `nyx rules list --class-only`, or via the dashboard's Rules page. The matcher sets (sources, sanitizers, sinks, gated sinks) live per-language in `src/labels/<lang>.rs`. [Language maturity](language-maturity.md) gives per-language counts and what's covered.
|
||||
|
||||
### CFG structural
|
||||
|
||||
|
|
@ -257,6 +266,8 @@ The tables below are generated from `src/patterns/<lang>.rs` by [`tools/docgen`]
|
|||
|
||||
`nyx config add-rule --cap <name>` and `[analysis.languages.*.rules]` in config accept:
|
||||
|
||||
`env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `code_exec`, `crypto`, `unauthorized_id`, `all`
|
||||
`env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `code_exec`, `crypto`, `unauthorized_id`, `data_exfil`, `ldap_injection`, `xpath_injection`, `header_injection`, `open_redirect`, `ssti`, `xxe`, `prototype_pollution`, `all`
|
||||
|
||||
Source for both the enum and the `to_cap` mapping: [`src/labels/mod.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/mod.rs) (`Cap`) and [`src/utils/config.rs`](https://github.com/elicpeter/nyx/blob/master/src/utils/config.rs) (`CapName`).
|
||||
Aliases: `data_exfiltration` for `data_exfil`, `ldapi` for `ldap_injection`, `xpathi` for `xpath_injection`, `crlf` and `response_splitting` for `header_injection`, `redirect` for `open_redirect`, `template_injection` for `ssti`, `proto_pollution` for `prototype_pollution`.
|
||||
|
||||
Source for both the enum and the `to_cap` mapping: [`src/labels/mod.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/mod.rs) (`Cap` and `CAP_RULE_REGISTRY`) and [`src/utils/config.rs`](https://github.com/elicpeter/nyx/blob/master/src/utils/config.rs) (`CapName`).
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ nyx serve --no-browser # don't auto-open
|
|||
|
||||
Persistent settings live under `[server]` in `nyx.conf` / `nyx.local`.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-overview.png" alt="Nyx UI overview: total findings, severity breakdown, language and category distribution, top affected files" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-overview.png" alt="Nyx UI overview: total findings, severity breakdown, language and category distribution, top affected files" width="900"/></p>
|
||||
|
||||
## What it serves, and what it doesn't
|
||||
|
||||
|
|
@ -88,11 +88,11 @@ Ceilings are calibrated for the current scanner false-positive rates. As symex c
|
|||
|
||||
The findings list is filterable by severity, confidence, category, language, rule ID, and triage state.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-findings-list.png" alt="Nyx findings list: 13 findings filtered by severity/confidence/rule, with status badges, file paths, and language tags" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-findings-list.png" alt="Nyx findings list: 13 findings filtered by severity/confidence/rule, with status badges, file paths, and language tags" width="900"/></p>
|
||||
|
||||
Clicking through opens the **flow visualiser**: a numbered walk from source to sink with the snippet at each step, cross-file markers when the path leaves the current file, the rule's "How to fix" guidance, and the engine's evidence object inline.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-finding-detail.png" alt="Nyx finding detail: HIGH taint-unsanitised-flow showing source → call → sink steps, How to fix guidance, and evidence panel" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-finding-detail.png" alt="Nyx finding detail: HIGH taint-unsanitised-flow showing source → call → sink steps, How to fix guidance, and evidence panel" width="900"/></p>
|
||||
|
||||
Engine notes call out when precision was bounded for that finding (`OriginsTruncated`, `PointsToTruncated`, `PathWidened`, `ForwardBailed`, etc.). Anything tagged `under-report` means the emitted flow is real and the result set is a lower bound; `over-report` means widening or bail. `--require-converged` in the CLI drops the over-report ones for strict gates.
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ Engine notes call out when precision was bounded for that finding (`OriginsTrunc
|
|||
|
||||
Each finding carries a triage state: `open`, `investigating`, `false_positive`, `accepted_risk`, `suppressed`, or `fixed`. The triage page bulk-updates them and shows the audit trail.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-triage.png" alt="Nyx triage page: 13 findings need attention, severity breakdown, Findings/Suppression rules/Audit log tabs, rule chips, Investigate buttons" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-triage.png" alt="Nyx triage page: 13 findings need attention, severity breakdown, Findings/Suppression rules/Audit log tabs, rule chips, Investigate buttons" width="900"/></p>
|
||||
|
||||
State writes are persisted to SQLite immediately, and (when `[server].triage_sync = true`, default on) mirrored to `.nyx/triage.json` in the project root. Commit that file:
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ It carries decisions across machines so a teammate's local scan reflects yours.
|
|||
|
||||
A file tree with per-file finding counts, syntax-highlighted source, and a right rail with the file's symbols and findings. Useful for "what's wrong with this module" rather than "what's wrong with this finding".
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-explorer.png" alt="Nyx explorer: file tree with per-file finding counts, syntax-highlighted Python source with red sink marker on the os.system line, file-summary right rail with findings" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-explorer.png" alt="Nyx explorer: file tree with per-file finding counts, syntax-highlighted Python source with red sink marker on the os.system line, file-summary right rail with findings" width="900"/></p>
|
||||
|
||||
The path query string preselects a file: `/explorer?file=src/handler.rs`.
|
||||
|
||||
|
|
@ -122,11 +122,11 @@ The path query string preselects a file: `/explorer?file=src/handler.rs`.
|
|||
|
||||
Past runs are persisted when `[runs].persist = true` (off by default to avoid disk growth on heavy users). When persistence is on, `/scans` lists historical runs.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-scans.png" alt="Nyx scans list: completed scan run with root, duration, finding count, languages, and started timestamp" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-scans.png" alt="Nyx scans list: completed scan run with root, duration, finding count, languages, and started timestamp" width="900"/></p>
|
||||
|
||||
Each run drills into a detail page with files scanned, findings count, duration, languages, and a per-pass timing breakdown.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-scan-detail.png" alt="Nyx scan detail: Summary tab with files scanned, findings, duration, languages; Details panel with Scan ID, Root, Engine version, started/finished timestamps; Timing breakdown bar showing Walk/Pass 1/Call Graph/Pass 2/Post" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-scan-detail.png" alt="Nyx scan detail: Summary tab with files scanned, findings, duration, languages; Details panel with Scan ID, Root, Engine version, started/finished timestamps; Timing breakdown bar showing Walk/Pass 1/Call Graph/Pass 2/Post" width="900"/></p>
|
||||
|
||||
Pick two scans to diff and see what got introduced, fixed, or rediscovered between runs. The retention cap is `[runs].max_runs` (default 100). Each run can also optionally save its log and stdout (`save_logs`, `save_stdout`); both are off by default. Code snippets are saved (`save_code_snippets = true`); turn off if storage is tight.
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ Pick two scans to diff and see what got introduced, fixed, or rediscovered betwe
|
|||
|
||||
Every rule the engine knows about, built-in plus user-added. Each row shows the matchers, kind (source / sanitiser / sink), capability, language, and how many findings it produced in the latest scan. Filter by language, by kind, or by free text.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-rules.png" alt="Nyx rules page: 218 rules with language/kind dropdowns and a matcher search; rows showing rule title, language, kind (SOURCE/SANITIZER/SINK), cap, and finding count" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-rules.png" alt="Nyx rules page: 218 rules with language/kind dropdowns and a matcher search; rows showing rule title, language, kind (SOURCE/SANITIZER/SINK), cap, and finding count" width="900"/></p>
|
||||
|
||||
User-added rules can be deleted from this page; built-ins are immutable. Built-ins live in `src/labels/<lang>.rs` and `src/patterns/<lang>.rs`; user-added entries write to `nyx.local`.
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ User-added rules can be deleted from this page; built-ins are immutable. Built-i
|
|||
|
||||
A live config editor. Reads the merged config (`nyx.conf` + `nyx.local`), lets you flip switches and add custom source / sanitizer / sink rules, and writes back to `nyx.local`. Changes apply to the next scan; the running server uses its initial config snapshot.
|
||||
|
||||
<p align="center"><img src="../assets/screenshots/docs/serve-config.png" alt="Nyx config page: General settings (analysis mode, max file size, excluded extensions, attack-surface ranking), Triage Sync toggle, Sources section with language/matcher/capability dropdowns and a per-language matcher table" width="900"/></p>
|
||||
<p align="center"><img src="assets/screenshots/docs/serve-config.png" alt="Nyx config page: General settings (analysis mode, max file size, excluded extensions, attack-surface ranking), Triage Sync toggle, Sources section with language/matcher/capability dropdowns and a per-language matcher table" width="900"/></p>
|
||||
|
||||
The custom-rule form picks a language, a matcher (function or property name), and a capability. The capability list matches the `Cap` bitflags the taint engine uses; see [rules.md](rules.md#capability-list-for-custom-rules) for what each one means.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,16 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nyx Scanner</title>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>Nyx</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="/favicon-64.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
270
frontend/package-lock.json
generated
|
|
@ -9,13 +9,13 @@
|
|||
"version": "0.6.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.100.6",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"elkjs": "^0.11.1",
|
||||
"graphology": "^0.26.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-router-dom": "^7.14.2",
|
||||
"sigma": "^3.0.2"
|
||||
"react-router-dom": "^7.15.0",
|
||||
"sigma": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
|
@ -26,15 +26,15 @@
|
|||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"jsdom": "^29.1.0",
|
||||
"globals": "^17.6.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"license-checker-rseidelsohn": "^4.4.2",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.1",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
|
|
@ -113,9 +113,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
||||
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
|
||||
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -274,9 +274,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
||||
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
|
||||
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1180,9 +1180,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.100.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.6.tgz",
|
||||
"integrity": "sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==",
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
|
||||
"integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -1190,12 +1190,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.100.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.6.tgz",
|
||||
"integrity": "sha512-uVSrps0PV16Cxmcn2rvL+dUhwTpTUtiRW347AEeYxMZXO2pZe9ja7E24PAMGoQ5u2g89DD8u4QhOviBk+RN8RA==",
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
|
||||
"integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.100.6"
|
||||
"@tanstack/query-core": "5.100.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
|
@ -1296,9 +1296,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
|
@ -1374,17 +1374,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
|
||||
"integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/type-utils": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/type-utils": "8.59.2",
|
||||
"@typescript-eslint/utils": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
|
|
@ -1397,7 +1397,7 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
|
|
@ -1413,16 +1413,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
|
||||
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz",
|
||||
"integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1438,14 +1438,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
|
||||
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz",
|
||||
"integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.1",
|
||||
"@typescript-eslint/types": "^8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.59.2",
|
||||
"@typescript-eslint/types": "^8.59.2",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1460,14 +1460,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
|
||||
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz",
|
||||
"integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1"
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1478,9 +1478,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1495,15 +1495,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
|
||||
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz",
|
||||
"integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/utils": "8.59.2",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
|
|
@ -1520,9 +1520,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
|
||||
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
|
||||
"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1534,16 +1534,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
|
||||
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz",
|
||||
"integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.59.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/visitor-keys": "8.59.1",
|
||||
"@typescript-eslint/project-service": "8.59.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/visitor-keys": "8.59.2",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
|
|
@ -1575,16 +1575,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
|
||||
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz",
|
||||
"integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1"
|
||||
"@typescript-eslint/scope-manager": "8.59.2",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1599,13 +1599,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
|
||||
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz",
|
||||
"integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.59.1",
|
||||
"@typescript-eslint/types": "8.59.2",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1922,9 +1922,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.24",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz",
|
||||
"integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==",
|
||||
"version": "2.10.27",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
|
||||
"integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
@ -2204,9 +2204,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.344",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
|
||||
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
|
||||
"version": "1.5.350",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.350.tgz",
|
||||
"integrity": "sha512-/KWD4qK8nMqIoJh35Rpc37fiVyOe80mcUQKpfje0Dp9uot2ROuipsh+EriCdfInxjleD5v1S4OlIn41I0LXP0g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
|
@ -2267,9 +2267,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz",
|
||||
"integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz",
|
||||
"integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2688,9 +2688,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.5.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
|
||||
"integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
|
||||
"version": "17.6.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
|
||||
"integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -2850,13 +2850,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
"hasown": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -2975,9 +2975,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "29.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.0.tgz",
|
||||
"integrity": "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg==",
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -3016,9 +3016,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/lru-cache": {
|
||||
"version": "11.3.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
|
||||
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
|
|
@ -3578,9 +3578,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -3818,9 +3818,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -3942,9 +3942,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
||||
"integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz",
|
||||
"integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
|
|
@ -3964,12 +3964,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz",
|
||||
"integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.0.tgz",
|
||||
"integrity": "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.14.2"
|
||||
"react-router": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
|
@ -4147,9 +4147,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sigma": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz",
|
||||
"integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.3.tgz",
|
||||
"integrity": "sha512-5H0zFlx6/NTQpqBg4Rm569ZOpnBOXMaS25UQThIWMU3XyzI5AhmorK/gnl87BvJBLhQd0tW4C0LIp3enWzMoNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events": "^3.3.0",
|
||||
|
|
@ -4408,9 +4408,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
|
||||
"integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -4445,22 +4445,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.29",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz",
|
||||
"integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==",
|
||||
"version": "7.0.30",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz",
|
||||
"integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.29"
|
||||
"tldts-core": "^7.0.30"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.29",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz",
|
||||
"integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==",
|
||||
"version": "7.0.30",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz",
|
||||
"integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -4549,16 +4549,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.59.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz",
|
||||
"integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==",
|
||||
"version": "8.59.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz",
|
||||
"integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"@typescript-eslint/typescript-estree": "8.59.1",
|
||||
"@typescript-eslint/utils": "8.59.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.59.2",
|
||||
"@typescript-eslint/parser": "8.59.2",
|
||||
"@typescript-eslint/typescript-estree": "8.59.2",
|
||||
"@typescript-eslint/utils": "8.59.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -5016,9 +5016,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@
|
|||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.100.6",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"elkjs": "^0.11.1",
|
||||
"graphology": "^0.26.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-router-dom": "^7.14.2",
|
||||
"sigma": "^3.0.2"
|
||||
"react-router-dom": "^7.15.0",
|
||||
"sigma": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
|
@ -35,15 +35,15 @@
|
|||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"jsdom": "^29.1.0",
|
||||
"globals": "^17.6.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"license-checker-rseidelsohn": "^4.4.2",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.1",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
|
|
|
|||
BIN
frontend/public/favicon-180.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/public/favicon-64.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
frontend/public/logo.png
Normal file
|
After Width: | Height: | Size: 432 KiB |
|
|
@ -5,7 +5,7 @@ let csrfTokenPromise: Promise<string> | null = null;
|
|||
export class ApiError extends Error {
|
||||
/**
|
||||
* Stable machine-readable code (matches backend `ApiError`'s `code` field).
|
||||
* Falls back to a synthetic value when the response wasn't structured —
|
||||
* Falls back to a synthetic value when the response was not structured,
|
||||
* `network` for fetch failures, `http_<status>` for plain-text responses.
|
||||
*/
|
||||
public code: string;
|
||||
|
|
@ -49,7 +49,7 @@ async function errorFromResponse(res: Response): Promise<ApiError> {
|
|||
const code = typeof parsed.code === 'string' ? parsed.code : undefined;
|
||||
return new ApiError(res.status, msg, code, parsed.detail);
|
||||
} catch {
|
||||
// Plain-text body — use as-is.
|
||||
// Plain-text body, use as-is.
|
||||
return new ApiError(res.status, text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,6 +355,7 @@ export interface RuleListItem {
|
|||
enabled: boolean;
|
||||
is_custom: boolean;
|
||||
is_gated: boolean;
|
||||
is_class: boolean;
|
||||
case_sensitive: boolean;
|
||||
finding_count: number;
|
||||
suppression_rate: number;
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ export function HorizontalBarChart({
|
|||
);
|
||||
}
|
||||
|
||||
const barH = 22;
|
||||
const gap = 4;
|
||||
const labelW = 110;
|
||||
const valueW = 45;
|
||||
const barH = 32;
|
||||
const gap = 12;
|
||||
const labelW = 120;
|
||||
const valueW = 48;
|
||||
const barAreaW = width - labelW - valueW - 16;
|
||||
const totalH = items.length * (barH + gap);
|
||||
const maxVal = maxValue ?? Math.max(...items.map((i) => i.value), 1);
|
||||
|
|
@ -49,7 +49,7 @@ export function HorizontalBarChart({
|
|||
x={labelW - 8}
|
||||
y={y + barH / 2 + 4}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
fontSize={13}
|
||||
fontFamily="var(--font)"
|
||||
fill="var(--text-secondary)"
|
||||
>
|
||||
|
|
@ -68,7 +68,7 @@ export function HorizontalBarChart({
|
|||
x={labelW + barAreaW + 8}
|
||||
y={y + barH / 2 + 4}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
fontSize={13}
|
||||
fontFamily="var(--font-mono)"
|
||||
fontWeight={600}
|
||||
fill="var(--text)"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function LineChart({
|
|||
points,
|
||||
color = 'var(--accent)',
|
||||
width = 400,
|
||||
height = 160,
|
||||
height = 240,
|
||||
}: LineChartProps) {
|
||||
if (!points || points.length < 2) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export function HeaderBar({ onStartScan, onOpenPalette }: HeaderBarProps) {
|
|||
className="btn btn-primary btn-sm"
|
||||
onClick={onStartScan}
|
||||
>
|
||||
Start Scan
|
||||
Start scan
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import type { FC } from 'react';
|
||||
import type { IconProps } from '../icons/Icons';
|
||||
import { useHealth } from '../../api/queries/health';
|
||||
import { useOverview } from '../../api/queries/overview';
|
||||
import { useSSE } from '../../contexts/SSEContext';
|
||||
|
||||
interface NavItem {
|
||||
|
|
@ -89,17 +90,19 @@ function navLinkClass({ isActive }: { isActive: boolean }) {
|
|||
|
||||
export function Sidebar() {
|
||||
const { data: health } = useHealth();
|
||||
const { data: overview } = useOverview();
|
||||
const { isScanRunning } = useSSE();
|
||||
|
||||
const primary = NAV_SECTIONS.filter((n) => n.group === 'primary');
|
||||
const secondary = NAV_SECTIONS.filter((n) => n.group === 'secondary');
|
||||
const footer = NAV_SECTIONS.filter((n) => n.group === 'footer');
|
||||
const findingsCount =
|
||||
overview && overview.state !== 'empty' ? overview.total_findings : null;
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<span className="logo">nyx</span>
|
||||
{health?.version && <span className="version">v{health.version}</span>}
|
||||
<img src="/logo.png" alt="Nyx" className="sidebar-logo-img" />
|
||||
</div>
|
||||
|
||||
<ul className="nav-list">
|
||||
|
|
@ -113,12 +116,15 @@ export function Sidebar() {
|
|||
<span className="nav-icon">
|
||||
<item.Icon />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
{item.id === 'findings' && findingsCount != null && (
|
||||
<span className="nav-badge">{findingsCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="nav-separator" />
|
||||
<li className="nav-section-header">Tools</li>
|
||||
|
||||
{secondary.map((item) => (
|
||||
<li key={item.id}>
|
||||
|
|
@ -126,7 +132,7 @@ export function Sidebar() {
|
|||
<span className="nav-icon">
|
||||
<item.Icon />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -140,7 +146,7 @@ export function Sidebar() {
|
|||
<span className="nav-icon">
|
||||
<item.Icon />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type {
|
||||
HealthScore,
|
||||
|
|
@ -25,8 +26,17 @@ export function HealthScoreCard({
|
|||
posture?: PostureSummary;
|
||||
}) {
|
||||
const gradeClass = `grade-${health.grade.toLowerCase()}`;
|
||||
const gradeAccent =
|
||||
health.grade === 'A' || health.grade === 'B'
|
||||
? 'var(--green)'
|
||||
: health.grade === 'C'
|
||||
? 'var(--amber)'
|
||||
: 'var(--red)';
|
||||
return (
|
||||
<div className="card health-card">
|
||||
<div
|
||||
className="card health-card"
|
||||
style={{ '--health-accent': gradeAccent } as React.CSSProperties}
|
||||
>
|
||||
<div className="health-eyebrow">Health Score</div>
|
||||
<div className="health-headline">
|
||||
<div className={`health-grade-block ${gradeClass}`}>
|
||||
|
|
@ -44,12 +54,26 @@ export function HealthScoreCard({
|
|||
)}
|
||||
</div>
|
||||
<div className="health-components">
|
||||
{health.components.map((c) => (
|
||||
<div className="health-component" key={c.label} title={c.detail}>
|
||||
<div className="health-component-score">{c.score}</div>
|
||||
<div className="health-component-label">{c.label}</div>
|
||||
</div>
|
||||
))}
|
||||
{health.components.map((c) => {
|
||||
const barColor =
|
||||
c.score >= 70
|
||||
? 'var(--green)'
|
||||
: c.score >= 40
|
||||
? 'var(--amber)'
|
||||
: 'var(--red)';
|
||||
return (
|
||||
<div className="health-component" key={c.label} title={c.detail}>
|
||||
<div className="health-component-label">{c.label}</div>
|
||||
<div className="health-component-bar-track">
|
||||
<div
|
||||
className="health-component-fill"
|
||||
style={{ width: `${c.score}%`, background: barColor }}
|
||||
/>
|
||||
</div>
|
||||
<div className="health-component-score">{c.score}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -113,7 +137,13 @@ export function BacklogCard({ backlog }: { backlog: BacklogStats }) {
|
|||
function BucketBar({ buckets }: { buckets: OverviewCount[] }) {
|
||||
const total = buckets.reduce((s, b) => s + b.count, 0);
|
||||
if (total === 0) return null;
|
||||
const colors = ['#3498db', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c'];
|
||||
const colors = [
|
||||
'var(--accent)',
|
||||
'var(--green)',
|
||||
'var(--amber)',
|
||||
'var(--red)',
|
||||
'var(--muted)',
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="bucket-bar"
|
||||
|
|
@ -149,10 +179,10 @@ export function ConfidenceDistributionChart({
|
|||
);
|
||||
}
|
||||
const segments = [
|
||||
{ label: 'High', value: dist.high, color: '#27ae60' },
|
||||
{ label: 'Medium', value: dist.medium, color: '#f39c12' },
|
||||
{ label: 'Low', value: dist.low, color: '#95a5a6' },
|
||||
{ label: 'None', value: dist.none, color: '#bdc3c7' },
|
||||
{ label: 'High', value: dist.high, color: 'var(--green)' },
|
||||
{ label: 'Medium', value: dist.medium, color: 'var(--amber)' },
|
||||
{ label: 'Low', value: dist.low, color: 'var(--muted)' },
|
||||
{ label: 'None', value: dist.none, color: 'var(--subtle)' },
|
||||
];
|
||||
return (
|
||||
<div className="confidence-dist">
|
||||
|
|
@ -484,7 +514,7 @@ export function SuppressionHygieneCard({
|
|||
<div className="kv-row kv-row-emphasis">
|
||||
<dt
|
||||
className="kv-label"
|
||||
title="Share of suppressions that are not pinned to a specific finding fingerprint. Lower is better — it means triage is decisive rather than blanket-silencing whole rules or files."
|
||||
title="Share of suppressions that are not pinned to a specific finding fingerprint. Lower is better because triage is decisive rather than blanket-silencing whole rules or files."
|
||||
>
|
||||
Blanket rate
|
||||
<span className="kv-hint">Lower is better</span>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface PaletteCommand {
|
|||
id: string;
|
||||
/** Visible label. */
|
||||
label: string;
|
||||
/** Optional secondary line — section, hint, shortcut. */
|
||||
/** Optional secondary line such as section, hint, or shortcut. */
|
||||
hint?: string;
|
||||
/** Group label for visual separation. */
|
||||
group?: string;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ interface LoadingStateProps {
|
|||
/**
|
||||
* Suppresses the spinner for the first ~150ms so trivially-fast queries
|
||||
* don't flash a spinner on screen. The text shows instantly so there's
|
||||
* always *something* — but the visible spin only kicks in if work is
|
||||
* always something, but the visible spin only kicks in if work is
|
||||
* actually slow.
|
||||
*/
|
||||
delaySpinnerMs?: number;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function resolve(pref: ThemePreference): ResolvedTheme {
|
|||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [preference, setPreference] = usePersistedState<ThemePreference>(
|
||||
'theme',
|
||||
'system',
|
||||
'light',
|
||||
);
|
||||
|
||||
const resolved = useMemo(() => resolve(preference), [preference]);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ interface ToastContextValue {
|
|||
t: Omit<Toast, 'id' | 'durationMs'> & { durationMs?: number },
|
||||
) => number;
|
||||
dismiss: (id: number) => void;
|
||||
/** Convenience helpers — call sites read more naturally as toast.error('…'). */
|
||||
/** Convenience helpers so call sites read naturally as toast.error('...'). */
|
||||
info: (message: string, title?: string) => number;
|
||||
success: (message: string, title?: string) => number;
|
||||
warning: (message: string, title?: string) => number;
|
||||
|
|
@ -37,7 +37,7 @@ const DEFAULT_DURATION: Record<ToastTone, number> = {
|
|||
info: 4000,
|
||||
success: 4000,
|
||||
warning: 6000,
|
||||
// Error toasts stick longer — failures usually need a deliberate read.
|
||||
// Error toasts stick longer because failures usually need a deliberate read.
|
||||
error: 8000,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,20 +18,20 @@ export interface EdgeStyle {
|
|||
}
|
||||
|
||||
const FALLBACK_PALETTE: GraphThemePalette = {
|
||||
background: '#ffffff',
|
||||
backgroundSecondary: '#f7f7f8',
|
||||
text: '#1a1a1a',
|
||||
textSecondary: '#6b6b76',
|
||||
textTertiary: '#9b9ba7',
|
||||
border: '#e5e5ea',
|
||||
borderLight: '#f0f0f4',
|
||||
accent: '#5856d6',
|
||||
accentSoft: '#ededfc',
|
||||
success: '#2ecc71',
|
||||
warning: '#e67e22',
|
||||
danger: '#e74c3c',
|
||||
neutral: '#607187',
|
||||
neutralSoft: '#8c99ab',
|
||||
background: '#f9f8f4',
|
||||
backgroundSecondary: '#f2f0ea',
|
||||
text: '#0d0c0a',
|
||||
textSecondary: '#3c3830',
|
||||
textTertiary: '#6c6660',
|
||||
border: '#e5e1d7',
|
||||
borderLight: '#ede9df',
|
||||
accent: '#0b3d2a',
|
||||
accentSoft: '#ecf3ee',
|
||||
success: '#1c5c38',
|
||||
warning: '#8c6310',
|
||||
danger: '#9d2f25',
|
||||
neutral: '#6c6660',
|
||||
neutralSoft: '#9c9690',
|
||||
};
|
||||
|
||||
function readVar(name: string, fallback: string): string {
|
||||
|
|
@ -150,14 +150,14 @@ function cfgNodeStyle(
|
|||
};
|
||||
case 'Loop':
|
||||
return {
|
||||
fill: '#4f78c2',
|
||||
stroke: '#3c5f9a',
|
||||
fill: '#6c6660',
|
||||
stroke: '#3c3830',
|
||||
textFill: '#ffffff',
|
||||
secondaryFill: withAlpha('#ffffff', 0.8),
|
||||
shape: 'rect',
|
||||
strokeWidth: 2.1,
|
||||
accentFill: palette.accent,
|
||||
neighborFill: withAlpha('#4f78c2', 0.74),
|
||||
neighborFill: withAlpha('#6c6660', 0.74),
|
||||
};
|
||||
case 'Call':
|
||||
return {
|
||||
|
|
@ -200,8 +200,8 @@ function callGraphNodeStyle(
|
|||
metadata?: GraphMetadata,
|
||||
): NodeStyle {
|
||||
const isRecursive = metadata?.isRecursive === true;
|
||||
const fill = isRecursive ? '#7d6450' : palette.neutral;
|
||||
const stroke = isRecursive ? '#6a5444' : withAlpha(palette.neutral, 0.84);
|
||||
const fill = isRecursive ? '#5a5042' : palette.neutral;
|
||||
const stroke = isRecursive ? '#3c3830' : withAlpha(palette.neutral, 0.84);
|
||||
|
||||
return {
|
||||
fill,
|
||||
|
|
@ -245,7 +245,7 @@ export function getEdgeStyle(
|
|||
case 'False':
|
||||
return { color: palette.danger, width: 1.8, dash: [] };
|
||||
case 'Back':
|
||||
return { color: '#4f78c2', width: 1.6, dash: [7, 4] };
|
||||
return { color: palette.textTertiary, width: 1.6, dash: [7, 4] };
|
||||
case 'Exception':
|
||||
return { color: palette.warning, width: 1.6, dash: [3, 3] };
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const FINDINGS_DEFAULTS: FindingsURLState = {
|
|||
};
|
||||
|
||||
/** Subset of state we remember across sessions. Filters intentionally are
|
||||
* NOT persisted — they're scan-specific and should reset by default, but the
|
||||
* NOT persisted because they're scan-specific and should reset by default, but the
|
||||
* URL still reflects them so a shared link reproduces them exactly. */
|
||||
interface PersistedFindingsPrefs {
|
||||
per_page: string;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface Shortcut {
|
|||
handler: (event: KeyboardEvent) => void;
|
||||
/**
|
||||
* If true, the shortcut still fires when focus is in an input/textarea/
|
||||
* contenteditable. Default is false — shortcuts shouldn't hijack typing.
|
||||
* contenteditable. Default is false, so shortcuts should not hijack typing.
|
||||
*/
|
||||
allowInInput?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function write<T>(key: string, value: T): void {
|
|||
try {
|
||||
window.localStorage.setItem(storageKey(key), JSON.stringify(value));
|
||||
} catch {
|
||||
// Quota exceeded or storage disabled — silently degrade.
|
||||
// Quota exceeded or storage disabled, so silently degrade.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ function write<T>(key: string, value: T): void {
|
|||
* `useState` that persists to `localStorage` under `nyx:<key>`.
|
||||
*
|
||||
* Suitable for view preferences (theme, sidebar collapse, default page size).
|
||||
* Not suitable for sensitive data — `localStorage` is not encrypted.
|
||||
* Not suitable for sensitive data because `localStorage` is not encrypted.
|
||||
*
|
||||
* Cross-tab sync is not implemented; if the user opens two tabs they get
|
||||
* independent state until next load. That's the common-case ergonomic.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ interface NewScanModalProps {
|
|||
|
||||
const MODE_HINTS: Record<ScanMode, string> = {
|
||||
full: 'AST + CFG + taint (default)',
|
||||
ast: 'AST patterns only — fastest',
|
||||
ast: 'AST patterns only. Fastest.',
|
||||
cfg: 'CFG structural + taint',
|
||||
taint: 'Taint flows only',
|
||||
};
|
||||
|
|
@ -26,7 +26,7 @@ const MODE_HINTS: Record<ScanMode, string> = {
|
|||
const PROFILE_HINTS: Record<EngineProfile, string> = {
|
||||
fast: 'Basic taint. No abstract-interp / context-sensitive / symex / backwards.',
|
||||
balanced: 'Default. Adds abstract-interp + context-sensitive inlining.',
|
||||
deep: 'Adds symex (cross-file + interproc) and demand-driven backwards taint. ~2–3× slower.',
|
||||
deep: 'Adds symex (cross-file + interproc) and demand-driven backwards taint. About 2 to 3x slower.',
|
||||
};
|
||||
|
||||
export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
||||
|
|
@ -67,7 +67,7 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
return (
|
||||
<Modal open={open} onClose={onClose} className="scan-modal-overlay">
|
||||
<div className="scan-modal">
|
||||
<h3>Start New Scan</h3>
|
||||
<h3>Start new scan</h3>
|
||||
<div className="scan-modal-form">
|
||||
<div className="form-group">
|
||||
<label>Scan Root</label>
|
||||
|
|
@ -114,7 +114,7 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
onClick={handleStart}
|
||||
disabled={startScan.isPending}
|
||||
>
|
||||
{startScan.isPending ? 'Starting...' : 'Start Scan'}
|
||||
{startScan.isPending ? 'Starting...' : 'Start scan'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ function KvGrid({ entries }: { entries: Array<[string, React.ReactNode]> }) {
|
|||
}
|
||||
|
||||
function fmt(v: unknown): React.ReactNode {
|
||||
if (v === null || v === undefined) return <span className="muted">—</span>;
|
||||
if (v === null || v === undefined) return <span className="muted">-</span>;
|
||||
if (typeof v === 'boolean')
|
||||
return (
|
||||
<span className={v ? 'pill pill-on' : 'pill pill-off'}>
|
||||
|
|
@ -387,7 +387,7 @@ function RawEditor() {
|
|||
value={draft ?? ''}
|
||||
spellCheck={false}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
placeholder="# nyx.local — overrides for the default config. # Anything you set here wins over nyx.conf."
|
||||
placeholder="# nyx.local - overrides for the default config. # Anything you set here wins over nyx.conf."
|
||||
/>
|
||||
<p className="config-help">
|
||||
Edits are validated against the full config schema before being written.
|
||||
|
|
@ -608,7 +608,7 @@ export function ConfigPage() {
|
|||
setProfileName('');
|
||||
}, [profileName, addProfile]);
|
||||
|
||||
if (configLoading) return <LoadingState message="Loading configuration…" />;
|
||||
if (configLoading) return <LoadingState message="Loading configuration..." />;
|
||||
if (configError) return <ErrorState message={configError.message} />;
|
||||
|
||||
const cfg = config as Record<string, Record<string, unknown>> | undefined;
|
||||
|
|
@ -616,15 +616,7 @@ export function ConfigPage() {
|
|||
const triageSyncOn = !!server?.triage_sync;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Config</h2>
|
||||
<span className="page-header-sub">
|
||||
Edit defaults, rules, profiles, and the raw <code>nyx.local</code>{' '}
|
||||
file
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="config-page page-shell">
|
||||
<div className="config-tabs">
|
||||
{(
|
||||
[
|
||||
|
|
@ -866,6 +858,6 @@ export function ConfigPage() {
|
|||
)}
|
||||
|
||||
{tab === 'raw' && <RawEditor />}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const STATE_REMEDIATION_HINTS: Record<string, string[]> = {
|
|||
'Prefer a language-native cleanup pattern (defer, with, try-with-resources, RAII).',
|
||||
],
|
||||
'state-resource-leak-possible': [
|
||||
'Ensure the resource is closed on all code paths — including error and early-return paths.',
|
||||
'Ensure the resource is closed on all code paths, including error and early-return paths.',
|
||||
'Put cleanup in a finally/defer block rather than after the happy path.',
|
||||
],
|
||||
'state-unauthed-access': [
|
||||
|
|
@ -636,7 +636,7 @@ const TAINT_REMEDIATION: Record<string, string[]> = {
|
|||
'If HTML is unavoidable, run input through a well-maintained sanitizer (DOMPurify, Bleach).',
|
||||
],
|
||||
sql: [
|
||||
'Use parameterized queries or a prepared statement — never concatenate user input into SQL.',
|
||||
'Use parameterized queries or a prepared statement. Never concatenate user input into SQL.',
|
||||
'Prefer an ORM or query builder that escapes parameters automatically.',
|
||||
'Validate input type (integer, enum, allowlist) before the query.',
|
||||
],
|
||||
|
|
@ -878,6 +878,12 @@ export function FindingDetailPage() {
|
|||
const hasRelated = f.related_findings && f.related_findings.length > 0;
|
||||
const hasLabels = f.labels && f.labels.length > 0;
|
||||
const hasCode = !!f.code_context;
|
||||
const sourcePath = evidence?.source
|
||||
? `${evidence.source.path}:${evidence.source.line}:${evidence.source.col}`
|
||||
: null;
|
||||
const sinkPath = evidence?.sink
|
||||
? `${evidence.sink.path}:${evidence.sink.line}:${evidence.sink.col}`
|
||||
: null;
|
||||
|
||||
const metaParts: string[] = [];
|
||||
if (f.category) metaParts.push(f.category);
|
||||
|
|
@ -894,7 +900,7 @@ export function FindingDetailPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="detail-panel finding-detail">
|
||||
<div className="detail-panel finding-detail page-shell">
|
||||
<div className="detail-title-row">
|
||||
<h2 className="finding-heading">
|
||||
<span
|
||||
|
|
@ -931,6 +937,22 @@ export function FindingDetailPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{(sourcePath || sinkPath) && (
|
||||
<div className="path-trace" aria-label="Source to sink path">
|
||||
<div className="path-trace-card">
|
||||
<span className="path-trace-label">Source</span>
|
||||
<code className="path-trace-path">{sourcePath || 'Unknown'}</code>
|
||||
</div>
|
||||
<div className="path-trace-arrow" aria-hidden>
|
||||
→
|
||||
</div>
|
||||
<div className="path-trace-card">
|
||||
<span className="path-trace-label">Sink</span>
|
||||
<code className="path-trace-path">{sinkPath || 'Unknown'}</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusControl
|
||||
finding={f}
|
||||
onTriage={handleTriage}
|
||||
|
|
@ -974,13 +996,6 @@ export function FindingDetailPage() {
|
|||
<HowToFix finding={f} />
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Evidence (collapsed by default — overlaps with taint flow) */}
|
||||
{hasEvidence && (
|
||||
<CollapsibleSection title="Evidence" defaultOpen={false}>
|
||||
<EvidenceSection evidence={evidence!} skipStateCard={isState} />
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Analysis Notes */}
|
||||
{hasNotes && (
|
||||
<CollapsibleSection title="Analysis Notes" defaultOpen={false}>
|
||||
|
|
@ -1002,20 +1017,6 @@ export function FindingDetailPage() {
|
|||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Labels */}
|
||||
{hasLabels && (
|
||||
<CollapsibleSection title="Labels" defaultOpen={false}>
|
||||
<div className="label-list">
|
||||
{f.labels.map(([k, v], i) => (
|
||||
<span key={i} className="label-item">
|
||||
<span className="label-key">{k}:</span>{' '}
|
||||
<span className="label-value">{v}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Code Preview */}
|
||||
{hasCode && (
|
||||
<CollapsibleSection title="Code Preview" defaultOpen={false}>
|
||||
|
|
|
|||
|
|
@ -567,15 +567,7 @@ export function FindingsPage() {
|
|||
const totalPages = Math.ceil(data.total / data.per_page) || 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Findings</h2>
|
||||
<span className="filter-count">
|
||||
{data.total} finding{data.total !== 1 ? 's' : ''}
|
||||
{hasActiveFilters ? ' (filtered)' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="findings-page page-shell">
|
||||
{/* Filter bar */}
|
||||
<div className="filter-bar">
|
||||
<input
|
||||
|
|
@ -793,6 +785,6 @@ export function FindingsPage() {
|
|||
onClose={() => setSuppressModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function OverviewPage() {
|
|||
|
||||
const categoryItems = (overview.issue_categories || [])
|
||||
.slice(0, 8)
|
||||
.map((b) => ({ label: b.label, value: b.count, color: '#5856d6' }));
|
||||
.map((b) => ({ label: b.label, value: b.count, color: 'var(--accent)' }));
|
||||
|
||||
const trendData = (trends || []).map((t) => ({
|
||||
label: t.timestamp,
|
||||
|
|
@ -74,11 +74,7 @@ export function OverviewPage() {
|
|||
const hotSinks = overview.hot_sinks || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Overview</h2>
|
||||
</div>
|
||||
|
||||
<div className="overview-page page-shell">
|
||||
{/* Baseline strip */}
|
||||
<BaselinePinControl
|
||||
baseline={overview.baseline}
|
||||
|
|
@ -117,7 +113,7 @@ export function OverviewPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Stat cards — kept lean: 5 cards, severity stacks live in Top Files
|
||||
{/* Stat cards kept lean: 5 cards, severity stacks live in Top Files
|
||||
and Per-Language. Cross-file / Symex moved into Scanner Quality. */}
|
||||
<div className="overview-stat-grid overview-stat-grid-5">
|
||||
<StatCard
|
||||
|
|
@ -145,7 +141,7 @@ export function OverviewPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
{/* Charts — 3-col: Findings (col1 span2) | OWASP+Confidence (col2) | Categories (col3 span2) */}
|
||||
<div className="overview-chart-grid">
|
||||
<div className="card">
|
||||
<div className="card-header">Findings Over Time</div>
|
||||
|
|
@ -158,14 +154,8 @@ export function OverviewPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">OWASP Top 10 (2021)</div>
|
||||
{overview.owasp_buckets && overview.owasp_buckets.length > 0 ? (
|
||||
<OwaspChart buckets={overview.owasp_buckets} />
|
||||
) : (
|
||||
<div className="empty-state" style={{ padding: 16 }}>
|
||||
<p>No OWASP-mapped findings.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="card-header">Issue Categories</div>
|
||||
<HorizontalBarChart items={categoryItems} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">Confidence Distribution</div>
|
||||
|
|
@ -180,8 +170,14 @@ export function OverviewPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">Issue Categories</div>
|
||||
<HorizontalBarChart items={categoryItems} />
|
||||
<div className="card-header">OWASP Top 10 (2021)</div>
|
||||
{overview.owasp_buckets && overview.owasp_buckets.length > 0 ? (
|
||||
<OwaspChart buckets={overview.owasp_buckets} />
|
||||
) : (
|
||||
<div className="empty-state" style={{ padding: 16 }}>
|
||||
<p>No OWASP-mapped findings.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -289,7 +285,7 @@ export function OverviewPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,20 +271,7 @@ export function RulesPage() {
|
|||
if (error) return <ErrorState message={error.message} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Rules</h2>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
marginLeft: 'var(--space-3)',
|
||||
}}
|
||||
>
|
||||
{(rules || []).length} rules
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rules-page page-shell">
|
||||
<div className="rules-layout">
|
||||
<div className="rules-list-panel">
|
||||
<div className="rules-filters">
|
||||
|
|
@ -310,14 +297,7 @@ export function RulesPage() {
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
fontSize: 'var(--text-sm)',
|
||||
}}
|
||||
>
|
||||
<label className="rules-custom-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={customOnly}
|
||||
|
|
@ -357,6 +337,6 @@ export function RulesPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ function CompareByGroup({
|
|||
}, [data, groupField]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="scan-compare-page page-shell">
|
||||
{groups.map(([key, items]) => {
|
||||
const counts = { new: 0, fixed: 0, changed: 0, unchanged: 0 };
|
||||
items.forEach(
|
||||
|
|
@ -267,7 +267,7 @@ function CompareByGroup({
|
|||
</CollapsibleSection>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -300,16 +300,10 @@ export function ScanComparePage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
style={{ marginBottom: 'var(--space-4)' }}
|
||||
onClick={() => navigate('/scans')}
|
||||
>
|
||||
Back to Scans
|
||||
</button>
|
||||
|
||||
<div className="page-header">
|
||||
<h2>Scan Comparison</h2>
|
||||
<div className="page-action-row">
|
||||
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
|
||||
Back to Scans
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="compare-header">
|
||||
|
|
|
|||
|
|
@ -71,125 +71,127 @@ function SummaryTab({ scan }: { scan: ScanView }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">Details</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)', width: 140 }}>
|
||||
Scan ID
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-xs)',
|
||||
}}
|
||||
>
|
||||
{scan.id}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Root</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
}}
|
||||
>
|
||||
{scan.scan_root}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Engine</td>
|
||||
<td>{scan.engine_version || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Started</td>
|
||||
<td>{fmtDate(scan.started_at)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Finished</td>
|
||||
<td>{fmtDate(scan.finished_at)}</td>
|
||||
</tr>
|
||||
{scan.error && (
|
||||
<div className="scan-summary-grid">
|
||||
<div className="card scan-detail-card">
|
||||
<div className="card-header">Details</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Error</td>
|
||||
<td style={{ color: 'var(--sev-high)' }}>{scan.error}</td>
|
||||
<td style={{ color: 'var(--text-secondary)', width: 140 }}>
|
||||
Scan ID
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-xs)',
|
||||
}}
|
||||
>
|
||||
{scan.id}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{timing && total > 0 && (
|
||||
<div className="card" style={{ marginTop: 'var(--space-4)' }}>
|
||||
<div className="card-header">Timing Breakdown</div>
|
||||
<div className="timing-bar">
|
||||
<div
|
||||
className="timing-bar-segment walk"
|
||||
style={{ width: `${pct(timing.walk_ms)}%` }}
|
||||
title={`Walk: ${timing.walk_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass1"
|
||||
style={{ width: `${pct(timing.pass1_ms)}%` }}
|
||||
title={`Pass 1: ${timing.pass1_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment callgraph"
|
||||
style={{ width: `${pct(timing.call_graph_ms)}%` }}
|
||||
title={`Call Graph: ${timing.call_graph_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass2"
|
||||
style={{ width: `${pct(timing.pass2_ms)}%` }}
|
||||
title={`Pass 2: ${timing.pass2_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment postprocess"
|
||||
style={{ width: `${pct(timing.post_process_ms)}%` }}
|
||||
title={`Post-process: ${timing.post_process_ms}ms`}
|
||||
></div>
|
||||
</div>
|
||||
<div className="timing-legend">
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-low)' }}
|
||||
></span>{' '}
|
||||
Walk {timing.walk_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--accent)' }}
|
||||
></span>{' '}
|
||||
Pass 1 {timing.pass1_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-medium)' }}
|
||||
></span>{' '}
|
||||
Call Graph {timing.call_graph_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--success)' }}
|
||||
></span>{' '}
|
||||
Pass 2 {timing.pass2_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--text-tertiary)' }}
|
||||
></span>{' '}
|
||||
Post {timing.post_process_ms}ms
|
||||
</span>
|
||||
</div>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Root</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
}}
|
||||
>
|
||||
{scan.scan_root}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Engine</td>
|
||||
<td>{scan.engine_version || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Started</td>
|
||||
<td>{fmtDate(scan.started_at)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Finished</td>
|
||||
<td>{fmtDate(scan.finished_at)}</td>
|
||||
</tr>
|
||||
{scan.error && (
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>Error</td>
|
||||
<td style={{ color: 'var(--sev-high)' }}>{scan.error}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{timing && total > 0 && (
|
||||
<div className="card scan-timing-card">
|
||||
<div className="card-header">Timing Breakdown</div>
|
||||
<div className="timing-bar">
|
||||
<div
|
||||
className="timing-bar-segment walk"
|
||||
style={{ width: `${pct(timing.walk_ms)}%` }}
|
||||
title={`Walk: ${timing.walk_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass1"
|
||||
style={{ width: `${pct(timing.pass1_ms)}%` }}
|
||||
title={`Pass 1: ${timing.pass1_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment callgraph"
|
||||
style={{ width: `${pct(timing.call_graph_ms)}%` }}
|
||||
title={`Call Graph: ${timing.call_graph_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment pass2"
|
||||
style={{ width: `${pct(timing.pass2_ms)}%` }}
|
||||
title={`Pass 2: ${timing.pass2_ms}ms`}
|
||||
></div>
|
||||
<div
|
||||
className="timing-bar-segment postprocess"
|
||||
style={{ width: `${pct(timing.post_process_ms)}%` }}
|
||||
title={`Post-process: ${timing.post_process_ms}ms`}
|
||||
></div>
|
||||
</div>
|
||||
<div className="timing-legend">
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-low)' }}
|
||||
></span>{' '}
|
||||
Walk {timing.walk_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--accent)' }}
|
||||
></span>{' '}
|
||||
Pass 1 {timing.pass1_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--sev-medium)' }}
|
||||
></span>{' '}
|
||||
Call Graph {timing.call_graph_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--success)' }}
|
||||
></span>{' '}
|
||||
Pass 2 {timing.pass2_ms}ms
|
||||
</span>
|
||||
<span className="timing-legend-item">
|
||||
<span
|
||||
className="timing-legend-dot"
|
||||
style={{ background: 'var(--text-tertiary)' }}
|
||||
></span>{' '}
|
||||
Post {timing.post_process_ms}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -212,7 +214,7 @@ function FindingsTab({ scanId }: { scanId: string }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="scan-detail-page page-shell">
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
|
|
@ -266,7 +268,7 @@ function FindingsTab({ scanId }: { scanId: string }) {
|
|||
>
|
||||
Showing {data.findings.length} of {data.total} findings
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -416,31 +418,22 @@ export function ScanDetailPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-2)',
|
||||
marginBottom: 'var(--space-4)',
|
||||
}}
|
||||
>
|
||||
<div className="page-action-row">
|
||||
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
|
||||
Back to Scans
|
||||
</button>
|
||||
{prevScanId && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
className="btn btn-sm page-action-push"
|
||||
onClick={() => navigate(`/scans/compare/${prevScanId}/${id}`)}
|
||||
>
|
||||
Compare with Previous
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="page-header">
|
||||
<h2>Scan Detail</h2>
|
||||
<span className={`status-badge ${scan.status}`}>
|
||||
<span
|
||||
className={`status-badge ${scan.status}`}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
<span className={`status-dot ${scan.status}`}></span>
|
||||
{scan.status}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -174,11 +174,7 @@ export function ScansPage() {
|
|||
const showCheckboxes = completedScans.length >= 2;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-header">
|
||||
<h2>Scans</h2>
|
||||
</div>
|
||||
|
||||
<div className="scans-page page-shell">
|
||||
{(runningScans.length > 0 || isScanRunning) && scanProgress && (
|
||||
<ScanProgress data={scanProgress} />
|
||||
)}
|
||||
|
|
@ -210,90 +206,103 @@ export function ScansPage() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<table className="scans-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{showCheckboxes && <th style={{ width: 32 }}></th>}
|
||||
<th>Status</th>
|
||||
<th>Root</th>
|
||||
<th>Duration</th>
|
||||
<th>Findings</th>
|
||||
<th>Languages</th>
|
||||
<th>Started</th>
|
||||
<th style={{ width: 60 }}></th>
|
||||
{showCheckboxes && <th className="scan-select-col"></th>}
|
||||
<th className="scan-status-col">Status</th>
|
||||
<th className="scan-root-col">Root</th>
|
||||
<th className="scan-duration-col">Duration</th>
|
||||
<th className="scan-findings-col">Findings</th>
|
||||
<th className="scan-languages-col">Languages</th>
|
||||
<th className="scan-started-col">Started</th>
|
||||
<th className="scan-actions-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{scans.map((s: ScanView) => (
|
||||
<tr
|
||||
key={s.id}
|
||||
className="clickable"
|
||||
onClick={() => navigate(`/scans/${s.id}`)}
|
||||
>
|
||||
{showCheckboxes && (
|
||||
{scans.map((s: ScanView) => {
|
||||
const languages = s.languages || [];
|
||||
const visibleLanguages = languages.slice(0, 4);
|
||||
const hiddenLanguageCount =
|
||||
languages.length - visibleLanguages.length;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={s.id}
|
||||
className="clickable"
|
||||
onClick={() => navigate(`/scans/${s.id}`)}
|
||||
>
|
||||
{showCheckboxes && (
|
||||
<td>
|
||||
{s.status === 'completed' && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="scan-compare-cb"
|
||||
checked={selectedScans.has(s.id)}
|
||||
onClick={(e) => handleCheckbox(e, s.id)}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
{s.status === 'completed' && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="scan-compare-cb"
|
||||
checked={selectedScans.has(s.id)}
|
||||
onClick={(e) => handleCheckbox(e, s.id)}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<span className={`status-badge ${s.status}`}>
|
||||
<span className={`status-dot ${s.status}`}></span>
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="scan-root-cell" title={s.scan_root}>
|
||||
{truncPath(s.scan_root)}
|
||||
</td>
|
||||
<td className="scan-number-cell">
|
||||
{s.duration_secs != null
|
||||
? s.duration_secs.toFixed(2) + 's'
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="scan-number-cell">
|
||||
{s.finding_count ?? '-'}
|
||||
</td>
|
||||
<td className="scan-languages-cell">
|
||||
{languages.length > 0 ? (
|
||||
<span className="scan-language-list">
|
||||
{visibleLanguages.map((l) => (
|
||||
<span key={l} className="lang-badge">
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
{hiddenLanguageCount > 0 && (
|
||||
<span className="lang-badge lang-badge-more">
|
||||
+{hiddenLanguageCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<span className={`status-badge ${s.status}`}>
|
||||
<span className={`status-dot ${s.status}`}></span>
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.82rem',
|
||||
}}
|
||||
>
|
||||
{truncPath(s.scan_root)}
|
||||
</td>
|
||||
<td>
|
||||
{s.duration_secs != null
|
||||
? s.duration_secs.toFixed(2) + 's'
|
||||
: '-'}
|
||||
</td>
|
||||
<td>{s.finding_count ?? '-'}</td>
|
||||
<td>
|
||||
{(s.languages || []).length > 0
|
||||
? (s.languages || []).map((l) => (
|
||||
<span key={l} className="lang-badge">
|
||||
{l}
|
||||
</span>
|
||||
))
|
||||
: '-'}
|
||||
</td>
|
||||
<td>{relTime(s.started_at)}</td>
|
||||
<td>
|
||||
{s.status !== 'running' && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm('Delete this scan?')) {
|
||||
deleteScan.mutate(s.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<td>{relTime(s.started_at)}</td>
|
||||
<td className="scan-actions-cell">
|
||||
{s.status !== 'running' && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm('Delete this scan?')) {
|
||||
deleteScan.mutate(s.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,22 @@ function TriageSummary({
|
|||
<div className="triage-hero">
|
||||
<div className="triage-hero-row">
|
||||
<h1 className="triage-hero-title">{headline}</h1>
|
||||
{showSeverity && totalCount > 0 && (
|
||||
<div className="triage-hero-severity">
|
||||
{SEVERITIES.map((sev) => (
|
||||
<span
|
||||
key={sev}
|
||||
className={`triage-sev-stat triage-sev-${sev.toLowerCase()}`}
|
||||
>
|
||||
<span className="triage-sev-dot" aria-hidden />
|
||||
<span className="triage-sev-count">
|
||||
{(openBySev[sev] ?? 0).toLocaleString()}
|
||||
</span>
|
||||
<span className="triage-sev-name">{sev}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{totalCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -142,22 +158,6 @@ function TriageSummary({
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showSeverity && totalCount > 0 && (
|
||||
<div className="triage-hero-severity">
|
||||
{SEVERITIES.map((sev) => (
|
||||
<span
|
||||
key={sev}
|
||||
className={`triage-sev-stat triage-sev-${sev.toLowerCase()}`}
|
||||
>
|
||||
<span className="triage-sev-dot" aria-hidden />
|
||||
<span className="triage-sev-count">
|
||||
{(openBySev[sev] ?? 0).toLocaleString()}
|
||||
</span>
|
||||
<span className="triage-sev-name">{sev}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{expanded && (
|
||||
<div className="triage-state-row">
|
||||
<button
|
||||
|
|
@ -1124,7 +1124,7 @@ export function TriagePage() {
|
|||
GROUP_MODES.find((g) => g.value === groupMode)?.label ?? 'None';
|
||||
|
||||
return (
|
||||
<div className="triage-page">
|
||||
<div className="triage-page page-shell">
|
||||
<TriageSummary
|
||||
totalCount={totalCount}
|
||||
needsAttention={needsAttention}
|
||||
|
|
@ -1260,7 +1260,7 @@ export function TriagePage() {
|
|||
<input
|
||||
className="triage-search"
|
||||
type="search"
|
||||
placeholder="Search rule, file, message…"
|
||||
placeholder="Search rule, file, message..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function FunctionSelector({
|
|||
}
|
||||
|
||||
function formatFunctionLabel(fn: FunctionInfo): string {
|
||||
const sig = `(${fn.param_count} params) — L${fn.line}`;
|
||||
const sig = `(${fn.param_count} params), L${fn.line}`;
|
||||
if (fn.func_kind === 'closure' && fn.container) {
|
||||
return `${fn.name} [closure in ${fn.container}] ${sig}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function TypeFactsAnalysisPanel({
|
|||
{securityFacts.length > 0 && (
|
||||
<TypeFactGroup
|
||||
title="Security-Relevant Types"
|
||||
subtitle="HttpClient, DatabaseConnection, Url, … — drive type-qualified callee resolution and sink suppression"
|
||||
subtitle="HttpClient, DatabaseConnection, Url, and related types drive type-qualified callee resolution and sink suppression"
|
||||
facts={securityFacts}
|
||||
highlight
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getNodeStyle, getEdgeStyle } from '@/graph/styles';
|
|||
describe('getNodeStyle', () => {
|
||||
it('returns a style for Entry nodes', () => {
|
||||
const s = getNodeStyle('Entry');
|
||||
expect(s.fill).toBe('#2ecc71');
|
||||
expect(s.fill).toBe('#1c5c38');
|
||||
expect(s.shape).toBe('double');
|
||||
});
|
||||
|
||||
|
|
@ -47,26 +47,26 @@ describe('getNodeStyle', () => {
|
|||
|
||||
it('returns a specialized style for recursive call graph nodes', () => {
|
||||
const s = getNodeStyle('Call', 'callgraph', { isRecursive: true });
|
||||
expect(s.fill).toBe('#7d6450');
|
||||
expect(s.fill).toBe('#5a5042');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEdgeStyle', () => {
|
||||
it('returns green color for True edges', () => {
|
||||
const s = getEdgeStyle('True');
|
||||
expect(s.color).toBe('#2ecc71');
|
||||
expect(s.color).toBe('#1c5c38');
|
||||
expect(s.dash).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns red color for False edges', () => {
|
||||
const s = getEdgeStyle('False');
|
||||
expect(s.color).toBe('#e74c3c');
|
||||
expect(s.color).toBe('#9d2f25');
|
||||
expect(s.dash).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns dashed style for Back edges', () => {
|
||||
const s = getEdgeStyle('Back');
|
||||
expect(s.color).toBe('#4f78c2');
|
||||
expect(s.color).toBe('#6c6660');
|
||||
expect(s.dash).toEqual([7, 4]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const full: FindingView = {
|
|||
language: 'python',
|
||||
status: 'new',
|
||||
triage_state: 'investigating',
|
||||
triage_note: 'Looks real — assigned to alice.',
|
||||
triage_note: 'Looks real - assigned to alice.',
|
||||
code_context: {
|
||||
start_line: 138,
|
||||
highlight_line: 141,
|
||||
|
|
@ -120,7 +120,7 @@ const full: FindingView = {
|
|||
describe('findingToMarkdown', () => {
|
||||
it('renders the full finding with all sections', () => {
|
||||
const md = findingToMarkdown(full);
|
||||
expect(md).toContain('## py-sqli — User input flows into SQL query.');
|
||||
expect(md).toContain('## py-sqli - User input flows into SQL query.');
|
||||
expect(md).toContain('- **Rule**: `py-sqli` (category: `sqli`)');
|
||||
expect(md).toContain('- **Severity**: High | **Confidence**: High');
|
||||
expect(md).toContain('- **Location**: `src/handlers/login.py:141:10`');
|
||||
|
|
@ -130,7 +130,7 @@ describe('findingToMarkdown', () => {
|
|||
expect(md).toContain('### Explanation\nUntrusted input reaches');
|
||||
expect(md).toContain('### Evidence');
|
||||
expect(md).toContain(
|
||||
'**Source** — `src/handlers/login.py:138:7` (kind: UserInput)',
|
||||
'**Source**: `src/handlers/login.py:138:7` (kind: UserInput)',
|
||||
);
|
||||
expect(md).toContain('```python\nrequest.args.get("name")\n```');
|
||||
expect(md).toContain('**Guards**: none');
|
||||
|
|
@ -146,19 +146,19 @@ describe('findingToMarkdown', () => {
|
|||
expect(md).toContain('### Notes');
|
||||
expect(md).toContain('- Source type: User Input');
|
||||
expect(md).toContain('- Path length: 3 blocks');
|
||||
expect(md).toContain('### Triage note\nLooks real — assigned to alice.');
|
||||
expect(md).toContain('### Triage note\nLooks real - assigned to alice.');
|
||||
expect(md).toContain('### Confidence reasoning');
|
||||
expect(md).toContain('Score: 8.7');
|
||||
expect(md).toContain('- **source_kind**: direct user input');
|
||||
expect(md).toContain('### Related findings');
|
||||
expect(md).toContain(
|
||||
'- `#99` `py-xss` — `src/handlers/login.py:160` (Medium)',
|
||||
'- `#99` `py-xss` - `src/handlers/login.py:160` (Medium)',
|
||||
);
|
||||
});
|
||||
|
||||
it('skips optional sections for a lean finding', () => {
|
||||
const md = findingToMarkdown(lean);
|
||||
expect(md).toContain('## js-xss — xss');
|
||||
expect(md).toContain('## js-xss - xss');
|
||||
expect(md).toContain('**Confidence**: unknown');
|
||||
expect(md).not.toContain('### Message');
|
||||
expect(md).not.toContain('### Evidence');
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ function formatEvidence(ev: Evidence, lang: string | undefined): string {
|
|||
if (hasSpans) {
|
||||
const lines: string[] = ['### Evidence'];
|
||||
if (ev.source) {
|
||||
lines.push(`**Source** — ${formatSpan(ev.source, lang)}`);
|
||||
lines.push(`**Source**: ${formatSpan(ev.source, lang)}`);
|
||||
}
|
||||
if (ev.sink) {
|
||||
lines.push(`**Sink** — ${formatSpan(ev.sink, lang)}`);
|
||||
lines.push(`**Sink**: ${formatSpan(ev.sink, lang)}`);
|
||||
}
|
||||
if (ev.source || ev.sink) {
|
||||
if (!ev.guards || ev.guards.length === 0) {
|
||||
|
|
@ -68,7 +68,7 @@ function formatEvidence(ev: Evidence, lang: string | undefined): string {
|
|||
const st = ev.state;
|
||||
const subj = st.subject ? ` ${st.subject}:` : '';
|
||||
lines.push(
|
||||
`**State**: ${st.machine} —${subj} ${st.from_state} → ${st.to_state}`,
|
||||
`**State**: ${st.machine} -${subj} ${st.from_state} -> ${st.to_state}`,
|
||||
);
|
||||
}
|
||||
parts.push(lines.join('\n'));
|
||||
|
|
@ -87,7 +87,7 @@ function formatFlow(steps: FlowStep[]): string {
|
|||
const lines: string[] = [`### Flow (${steps.length} steps)`];
|
||||
for (const s of steps) {
|
||||
const segs: string[] = [`${s.step}. **${s.kind}** \`${s.file}:${s.line}\``];
|
||||
if (s.snippet) segs.push(`— \`${s.snippet}\``);
|
||||
if (s.snippet) segs.push(`- \`${s.snippet}\``);
|
||||
if (s.variable) segs.push(`(var \`${s.variable}\`)`);
|
||||
if (s.callee) segs.push(`(callee \`${s.callee}\`)`);
|
||||
if (s.is_cross_file) segs.push(`[cross-file]`);
|
||||
|
|
@ -117,7 +117,7 @@ function formatRelated(related: RelatedFindingView[]): string {
|
|||
const lines: string[] = ['### Related findings'];
|
||||
for (const r of related) {
|
||||
lines.push(
|
||||
`- \`#${r.index}\` \`${r.rule_id}\` — \`${r.path}:${r.line}\` (${r.severity})`,
|
||||
`- \`#${r.index}\` \`${r.rule_id}\` - \`${r.path}:${r.line}\` (${r.severity})`,
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
|
|
@ -128,7 +128,7 @@ export function findingToMarkdown(f: FindingView): string {
|
|||
const heading = firstLine(f.message || '').trim() || f.category;
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(`## ${f.rule_id} — ${heading}`);
|
||||
parts.push(`## ${f.rule_id} - ${heading}`);
|
||||
|
||||
const meta: string[] = [];
|
||||
meta.push(`- **Rule**: \`${f.rule_id}\` (category: \`${f.category}\`)`);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Local helpers for repo-wide checks and a couple of one-off tools.
|
|||
| `check.sh` | Verify only (no fixes). Mirrors the GitHub Actions CI workflow. |
|
||||
| `cached-cargo-test.sh` | Wrap `cargo test` with a source-hash cache; concurrent invocations of the same args share one run. |
|
||||
| `capture-screenshots.mjs`| Capture the README stills and demo GIF from a running `nyx serve`. Needs Playwright and ffmpeg. |
|
||||
| `frame-screenshots.py` | Wrap a PNG in the brand purple gradient. Called by `capture-screenshots.mjs` as its final phase, but can be run standalone. |
|
||||
| `frame-screenshots.py` | Wrap a PNG in the brand mint-cyan gradient. Called by `capture-screenshots.mjs` as its final phase, but can be run standalone. |
|
||||
|
||||
Fixers stream their output (so you can see what changed); tests run quietly and
|
||||
only show output if they fail. Both scripts print a green/red summary at the end
|
||||
|
|
@ -73,8 +73,9 @@ Stills are captured in two phases:
|
|||
`serve-scan-detail.png`, `serve-rules.png`, `serve-config.png`.
|
||||
|
||||
Then `frame-screenshots.py` runs over every captured PNG and wraps it in
|
||||
the brand purple gradient (1800x1113 outer, 1600x992 inner, 12px rounded
|
||||
corners, top-left `#8a5bf5` to bottom-right `#4d1d97`). Finally,
|
||||
the brand mint-led four-corner gradient (1800x1113 outer, 1600x992 inner,
|
||||
12px rounded corners: TL `#72f3d7`, TR `#ff6aa2`, BL `#f8c56b`, BR
|
||||
`#4cc9ff`). Finally,
|
||||
`docs/serve-overview.png` is copied to the top-level `overview.png`
|
||||
because that is the path the README references.
|
||||
|
||||
|
|
|
|||
316
scripts/capture-screenshots.mjs
Normal file → Executable file
|
|
@ -21,7 +21,7 @@
|
|||
* two-scan history (overview trend, scans list,
|
||||
* scan detail) plus the static-ish ones
|
||||
* (triage, explorer, rules, config)
|
||||
* 7. frame — composite the brand purple gradient around every
|
||||
* 7. frame — composite the brand mint-cyan gradient around every
|
||||
* captured PNG via scripts/frame-screenshots.py
|
||||
*
|
||||
* Prerequisites (script asserts each before starting):
|
||||
|
|
@ -37,8 +37,12 @@
|
|||
* node scripts/capture-screenshots.mjs --all # both, in one orchestrated run
|
||||
*
|
||||
* Output (under assets/screenshots/):
|
||||
* demo.gif (~25–30s walkthrough)
|
||||
* demo.gif (~25–30s serve walkthrough)
|
||||
* demo_raw.gif (unframed source — saved before compositing)
|
||||
* cli-scan.gif (~15s CLI scan walkthrough — requires vhs on PATH)
|
||||
* cli-scan_raw.gif (unframed source)
|
||||
* overview.png (mirror of docs/serve-overview.png; used by README)
|
||||
* *_raw.png / *_raw.gif (unframed originals for every captured asset)
|
||||
* docs/serve-overview.png (overview after scan #2 — trend going down)
|
||||
* docs/serve-findings-list.png (post-scan-#1 list with multiple highs)
|
||||
* docs/serve-finding-detail.png (5-hop taint flow visualizer)
|
||||
|
|
@ -49,16 +53,17 @@
|
|||
* docs/serve-rules.png
|
||||
* docs/serve-config.png
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import {
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { extname, join } from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const URL_BASE = process.env.NYX_URL || 'http://127.0.0.1:9876';
|
||||
|
|
@ -66,15 +71,19 @@ const SCAN_ROOT = process.env.SCAN_ROOT || '/tmp/nyx-demo-app';
|
|||
const OUT_DIR = process.env.OUT_DIR || '/Users/elipeter/nyx/assets/screenshots';
|
||||
const FRAMER = process.env.FRAMER || '/Users/elipeter/nyx/scripts/frame-screenshots.py';
|
||||
const NYX_BIN = process.env.NYX_BIN || '/Users/elipeter/nyx/target/release/nyx';
|
||||
const VIEW = { width: 1440, height: 900 };
|
||||
// Sibling marketing site that mirrors a small subset of these assets.
|
||||
// Set NYXSCAN_DIR=skip to disable the mirror step.
|
||||
const NYXSCAN_DIR = process.env.NYXSCAN_DIR || '/Users/elipeter/nyxscan.dev/assets/screenshots';
|
||||
const VIEW = { width: 1600, height: 992 };
|
||||
const COLOR_SCHEME = 'light';
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const wantStills = args.has('--stills') || args.has('--all');
|
||||
const wantGif = args.has('--gif') || args.has('--all');
|
||||
const wantCli = args.has('--cli') || args.has('--all');
|
||||
if (!wantStills && !wantGif && !wantCli) {
|
||||
console.error('usage: capture-screenshots.mjs [--stills|--gif|--cli|--all]');
|
||||
const wantCombo = args.has('--combo') || args.has('--all');
|
||||
if (!wantStills && !wantGif && !wantCli && !wantCombo) {
|
||||
console.error('usage: capture-screenshots.mjs [--stills|--gif|--cli|--combo|--all]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
|
|
@ -330,9 +339,9 @@ async function captureGifFrames(page) {
|
|||
.waitForSelector('.health-score-card, [class*="health"]', { timeout: 10_000 })
|
||||
.catch(() => {});
|
||||
await sleep(1800);
|
||||
await page.evaluate(() => window.scrollBy({ top: 360, behavior: 'smooth' }));
|
||||
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
|
||||
await sleep(1500);
|
||||
await page.evaluate(() => window.scrollBy({ top: 360, behavior: 'smooth' }));
|
||||
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
|
||||
await sleep(1500);
|
||||
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
||||
await sleep(800);
|
||||
|
|
@ -387,18 +396,61 @@ async function captureGifFrames(page) {
|
|||
await sleep(1500);
|
||||
}
|
||||
|
||||
// Combo GIF browser storyboard — data already present from VHS scan phase -----
|
||||
|
||||
async function captureGifFramesCombo(page) {
|
||||
console.error('[combo/gif] scene 1: overview with scan data');
|
||||
await page.goto(URL_BASE + '/');
|
||||
await page
|
||||
.waitForSelector('.health-score-card, [class*="health"]', { timeout: 15_000 })
|
||||
.catch(() => {});
|
||||
await sleep(2200);
|
||||
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
|
||||
await sleep(1500);
|
||||
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
||||
await sleep(900);
|
||||
|
||||
console.error('[combo/gif] scene 2: findings list');
|
||||
await page.click('a.nav-link:has-text("Findings"), .sidebar a:has-text("Findings")');
|
||||
await page.waitForURL('**/findings', { timeout: 10_000 });
|
||||
await page.waitForSelector('tbody tr', { timeout: 10_000 });
|
||||
await sleep(1500);
|
||||
|
||||
console.error('[combo/gif] scene 3: 5-hop taint finding detail');
|
||||
const taintRow = await findFirstTaintRow(page);
|
||||
await taintRow.click();
|
||||
await page.waitForURL(/\/findings\/\d+/, { timeout: 10_000 });
|
||||
await sleep(2500);
|
||||
await page.evaluate(() => window.scrollBy({ top: 480, behavior: 'smooth' }));
|
||||
await sleep(1600);
|
||||
await page.evaluate(() => window.scrollBy({ top: 360, behavior: 'smooth' }));
|
||||
await sleep(1600);
|
||||
|
||||
console.error('[combo/gif] scene 4: open Evidence + Analysis Notes');
|
||||
for (const title of ['Evidence', 'Analysis Notes']) {
|
||||
const toggle = page.locator(`.section-toggle:has-text("${title}")`).first();
|
||||
if (await toggle.count()) {
|
||||
await toggle.scrollIntoViewIfNeeded();
|
||||
await sleep(500);
|
||||
await toggle.click();
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
await sleep(1200);
|
||||
}
|
||||
|
||||
async function convertWebmToGif(webm, gifOut) {
|
||||
const palette = '/tmp/nyx-demo-palette.png';
|
||||
console.error('[gif] generating palette');
|
||||
execFileSync('ffmpeg', [
|
||||
'-y', '-ss', '1.0', '-i', webm,
|
||||
'-vf', 'fps=15,scale=1440:-1:flags=lanczos,palettegen',
|
||||
'-vf', 'fps=15,scale=1280:-1:flags=lanczos,palettegen',
|
||||
palette,
|
||||
], { stdio: 'inherit' });
|
||||
console.error('[gif] palette → gif');
|
||||
execFileSync('ffmpeg', [
|
||||
'-y', '-ss', '1.0', '-i', webm, '-i', palette,
|
||||
'-lavfi', 'fps=15,scale=1440:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
|
||||
'-lavfi', 'fps=15,scale=1280:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
|
||||
gifOut,
|
||||
], { stdio: 'inherit' });
|
||||
}
|
||||
|
|
@ -412,6 +464,8 @@ async function convertWebmToGif(webm, gifOut) {
|
|||
// the framer never resamples the captured text.
|
||||
|
||||
const CLI_RENDERER = '/Users/elipeter/nyx/scripts/render-cli.py';
|
||||
const VHS_BIN = process.env.VHS_BIN || 'vhs';
|
||||
const CLI_GIF = join(OUT_DIR, 'cli-scan.gif');
|
||||
|
||||
function renderCli(shellCommand, outFile) {
|
||||
execFileSync(
|
||||
|
|
@ -449,9 +503,46 @@ function stageDemoConfigHome() {
|
|||
writeFileSync(join(cfgDir, 'nyx.local'), DEMO_NYX_LOCAL);
|
||||
}
|
||||
|
||||
function captureCliGif() {
|
||||
console.error('[cli-gif/setup] writing v1 demo');
|
||||
writeDemo('v1');
|
||||
|
||||
const tapePath = '/tmp/nyx-cli-scan.tape';
|
||||
const innerGif = '/tmp/nyx-cli-scan.gif';
|
||||
|
||||
// VHS tape: terminal set to exact inner dimensions so frame-screenshots.py
|
||||
// fixed-mode doesn't need to resample — 1600x992 matches INNER_W x INNER_H.
|
||||
const tape = [
|
||||
`Output "${innerGif}"`,
|
||||
'',
|
||||
'Set Shell "bash"',
|
||||
'Set FontSize 22',
|
||||
'Set Width 1600',
|
||||
'Set Height 992',
|
||||
'Set Framerate 15',
|
||||
'Env CLICOLOR_FORCE "1"',
|
||||
'',
|
||||
'Sleep 500ms',
|
||||
`Type "${NYX_BIN} scan ${SCAN_ROOT}"`,
|
||||
'Sleep 300ms',
|
||||
'Enter',
|
||||
'Sleep 12s',
|
||||
'Sleep 3s',
|
||||
].join('\n');
|
||||
|
||||
writeFileSync(tapePath, tape);
|
||||
console.error('[cli-gif] recording with vhs');
|
||||
execFileSync(VHS_BIN, [tapePath], { stdio: 'inherit' });
|
||||
copyFileSync(innerGif, CLI_GIF);
|
||||
console.error(`[cli-gif] wrote ${CLI_GIF}`);
|
||||
}
|
||||
|
||||
function captureCli() {
|
||||
// Re-stage v1 so cli-scan output shows the richer set of findings
|
||||
// (the previous --stills phase patched the demo to v2).
|
||||
captureCliGif();
|
||||
|
||||
// Re-stage v1 so static cli-scan output shows the richer set of findings
|
||||
// (captureCliGif already wrote v1; this is a safety re-stage in case
|
||||
// the previous --stills phase patched the demo to v2).
|
||||
console.error('[cli/setup] writing v1 demo');
|
||||
writeDemo('v1');
|
||||
|
||||
|
|
@ -492,6 +583,143 @@ function captureCli() {
|
|||
// without a much larger fixture; the existing image is left alone.
|
||||
}
|
||||
|
||||
// Combo GIF ------------------------------------------------------------------
|
||||
// Single GIF: CLI scan (VHS terminal) → hard cut → serve UI (Playwright).
|
||||
// The VHS portion is a visual recording only — nyx scan (standalone CLI)
|
||||
// writes to a separate store that nyx serve does not read. After VHS we
|
||||
// wipe state and trigger a real scan through the serve API so Playwright
|
||||
// has live data to explore.
|
||||
|
||||
async function captureComboGif() {
|
||||
function wipeState() {
|
||||
rmSync(join(SCAN_ROOT, '.nyx'), { recursive: true, force: true });
|
||||
const homeDir = process.env.HOME || '/Users/elipeter';
|
||||
const sysDbBase = join(homeDir, 'Library/Application Support/nyx/nyx-demo-app.sqlite');
|
||||
for (const suffix of ['', '-wal', '-shm']) {
|
||||
try { unlinkSync(sysDbBase + suffix); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Clean state + write demo.
|
||||
try { execFileSync('pkill', ['-f', 'nyx serve'], { stdio: 'ignore' }); } catch {}
|
||||
await sleep(800);
|
||||
wipeState();
|
||||
writeDemo('v1');
|
||||
|
||||
// 2. VHS: scan → results pause → type nyx serve → see it start.
|
||||
const cliGifPath = '/tmp/nyx-combo-cli.gif';
|
||||
const tapePath = '/tmp/nyx-combo.tape';
|
||||
const tape = [
|
||||
`Output "${cliGifPath}"`,
|
||||
'',
|
||||
'Set Shell "bash"',
|
||||
'Set FontSize 22',
|
||||
'Set Width 1600',
|
||||
'Set Height 992',
|
||||
'Set Framerate 15',
|
||||
'Env CLICOLOR_FORCE "1"',
|
||||
'',
|
||||
'Sleep 500ms',
|
||||
`Type "${NYX_BIN} scan ${SCAN_ROOT}"`,
|
||||
'Sleep 300ms',
|
||||
'Enter',
|
||||
'Sleep 1500ms',
|
||||
`Type "${NYX_BIN} serve --port 9876 --no-browser ${SCAN_ROOT}"`,
|
||||
'Sleep 300ms',
|
||||
'Enter',
|
||||
'Sleep 2000ms',
|
||||
].join('\n');
|
||||
writeFileSync(tapePath, tape);
|
||||
console.error('[combo] recording CLI portion with vhs');
|
||||
execFileSync(VHS_BIN, [tapePath], { stdio: 'inherit' });
|
||||
|
||||
// 3. Wipe state again and start a fresh host serve. The VHS scan wrote
|
||||
// to standalone storage that nyx serve doesn't read, so we drive a
|
||||
// real scan through the serve API to populate the browser session.
|
||||
try { execFileSync('pkill', ['-f', 'nyx serve'], { stdio: 'ignore' }); } catch {}
|
||||
await sleep(800);
|
||||
wipeState();
|
||||
|
||||
const serveProc = spawn(NYX_BIN, [
|
||||
'serve', '--port', '9876', '--no-browser', SCAN_ROOT,
|
||||
], { detached: false, stdio: 'ignore' });
|
||||
serveProc.unref();
|
||||
await waitForServer();
|
||||
|
||||
const comboToken = await csrfToken();
|
||||
const comboBefore = await currentScanId();
|
||||
await startScanViaApi(comboToken);
|
||||
await waitForScanComplete(comboBefore);
|
||||
|
||||
// 4. Playwright: record browser walkthrough against the live scan data.
|
||||
const videoDir = '/tmp/nyx-combo-video';
|
||||
if (existsSync(videoDir)) rmSync(videoDir, { recursive: true });
|
||||
mkdirSync(videoDir, { recursive: true });
|
||||
const { chromium } = await import('playwright');
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: VIEW,
|
||||
colorScheme: COLOR_SCHEME,
|
||||
recordVideo: { dir: videoDir, size: VIEW },
|
||||
});
|
||||
await ctx.addInitScript(() => {
|
||||
try { localStorage.setItem('theme', 'light'); } catch {}
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
await captureGifFramesCombo(page);
|
||||
await page.close();
|
||||
await ctx.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
try { execFileSync('pkill', ['-f', 'nyx serve'], { stdio: 'ignore' }); } catch {}
|
||||
|
||||
// 5. Find Playwright webm.
|
||||
const webms = readdirSync(videoDir).filter((f) => f.endsWith('.webm'));
|
||||
if (!webms.length) throw new Error('[combo] no webm captured for browser portion');
|
||||
const webmPath = join(videoDir, webms[0]);
|
||||
|
||||
// 6. ffmpeg: three-step to avoid OOM from single-pass concat+palettegen.
|
||||
// Step A: concat VHS gif + browser webm → intermediate webm.
|
||||
// Step B: generate global palette from intermediate.
|
||||
// Step C: palette → final GIF.
|
||||
const comboOut = join(OUT_DIR, 'demo-combo.gif');
|
||||
const comboIntermediate = '/tmp/nyx-combo-intermediate.mp4';
|
||||
const comboPalette = '/tmp/nyx-combo-palette.png';
|
||||
|
||||
console.error('[combo] step A: concat → intermediate webm');
|
||||
execFileSync('ffmpeg', [
|
||||
'-y',
|
||||
'-ignore_loop', '1', '-r', '15', '-i', cliGifPath,
|
||||
'-ss', '1.0', '-r', '15', '-i', webmPath,
|
||||
'-filter_complex',
|
||||
'[0:v]scale=1600:992:flags=lanczos,fps=15[cli];' +
|
||||
'[1:v]scale=1600:992:flags=lanczos,fps=15[bro];' +
|
||||
'[cli][bro]concat=n=2:v=1:a=0[out]',
|
||||
'-map', '[out]',
|
||||
'-c:v', 'libx264', '-crf', '28', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p',
|
||||
comboIntermediate,
|
||||
], { stdio: 'inherit' });
|
||||
|
||||
console.error('[combo] step B: generate palette');
|
||||
execFileSync('ffmpeg', [
|
||||
'-y', '-i', comboIntermediate,
|
||||
'-vf', 'fps=15,palettegen',
|
||||
'-update', '1', '-frames:v', '1',
|
||||
comboPalette,
|
||||
], { stdio: 'inherit' });
|
||||
|
||||
console.error('[combo] step C: palette → gif');
|
||||
execFileSync('ffmpeg', [
|
||||
'-y', '-i', comboIntermediate, '-i', comboPalette,
|
||||
'-lavfi', 'fps=15 [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle',
|
||||
comboOut,
|
||||
], { stdio: 'inherit' });
|
||||
console.error(`[combo] wrote ${comboOut}`);
|
||||
}
|
||||
|
||||
// Frame phase ----------------------------------------------------------------
|
||||
|
||||
const STILLS_PNGS = [
|
||||
|
|
@ -514,14 +742,25 @@ const CLI_PNGS = [
|
|||
'docs/cli-configshow.png',
|
||||
];
|
||||
|
||||
function saveRawCopies(paths) {
|
||||
for (const p of paths) {
|
||||
if (!existsSync(p)) continue;
|
||||
const ext = extname(p);
|
||||
const rawPath = p.slice(0, p.length - ext.length) + '_raw' + ext;
|
||||
copyFileSync(p, rawPath);
|
||||
console.error(`[raw] ${rawPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function applyFrames(captured, { natural = false } = {}) {
|
||||
// Frame only paths captured this run. Re-framing a previously-
|
||||
// framed PNG would treat the framed result as the next inner
|
||||
// content and produce a frame inside a frame.
|
||||
const paths = captured.filter((p) => existsSync(p));
|
||||
if (paths.length === 0) return;
|
||||
saveRawCopies(paths);
|
||||
const label = natural ? 'natural-size' : 'fixed';
|
||||
console.error(`[frame] applying purple gradient frame (${label}) to ${paths.length} files`);
|
||||
console.error(`[frame] applying mint-led four-corner frame (${label}) to ${paths.length} files`);
|
||||
const args = natural ? ['--natural', ...paths] : paths;
|
||||
execFileSync('python3', [FRAMER, ...args], { stdio: 'inherit' });
|
||||
// Mirror the framed serve-overview.png to the top-level path the
|
||||
|
|
@ -536,6 +775,46 @@ function applyFrames(captured, { natural = false } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// Mirror the small subset of assets used by the nyxscan.dev landing site
|
||||
// so its screenshots can't drift from the canonical ones in this repo.
|
||||
// Mirrors the *_raw originals (unframed) — nyxscan.dev draws its own
|
||||
// frame/hero treatment in CSS and does not want the in-repo brand frame.
|
||||
// Regenerates webp variants for the PNGs (used by hero <picture>/image-set).
|
||||
// Skips silently when NYXSCAN_DIR=skip, the dir is missing, or cwebp is
|
||||
// not on PATH; this is a convenience step, not a hard requirement.
|
||||
const NYXSCAN_MIRROR = [
|
||||
['docs/serve-overview_raw.png', 'overview.png'],
|
||||
['docs/serve-finding-detail_raw.png', 'finding-detail.png'],
|
||||
['cli-scan_raw.gif', 'cli-scan.gif'],
|
||||
['demo-combo.gif', 'demo-combo.gif'],
|
||||
];
|
||||
function syncNyxscanDev() {
|
||||
if (NYXSCAN_DIR === 'skip') return;
|
||||
if (!existsSync(NYXSCAN_DIR)) {
|
||||
console.error(`[nyxscan] skip — ${NYXSCAN_DIR} does not exist`);
|
||||
return;
|
||||
}
|
||||
let cwebpAvailable = true;
|
||||
try {
|
||||
execFileSync('cwebp', ['-version'], { stdio: 'ignore' });
|
||||
} catch {
|
||||
cwebpAvailable = false;
|
||||
console.error('[nyxscan] cwebp not on PATH — copying PNGs only, webp will be stale');
|
||||
}
|
||||
for (const [srcRel, dstName] of NYXSCAN_MIRROR) {
|
||||
const src = join(OUT_DIR, srcRel);
|
||||
if (!existsSync(src)) continue;
|
||||
const dst = join(NYXSCAN_DIR, dstName);
|
||||
copyFileSync(src, dst);
|
||||
console.error(`[nyxscan] ${srcRel} -> ${dstName}`);
|
||||
if (cwebpAvailable && dstName.endsWith('.png')) {
|
||||
const webp = dst.slice(0, -4) + '.webp';
|
||||
execFileSync('cwebp', ['-quiet', '-q', '82', dst, '-o', webp], { stdio: 'inherit' });
|
||||
console.error(`[nyxscan] ${dstName.slice(0, -4)}.webp`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main -----------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
|
|
@ -615,7 +894,11 @@ async function main() {
|
|||
captureCli();
|
||||
}
|
||||
|
||||
if (wantStills || wantCli || wantGif) {
|
||||
if (wantCombo) {
|
||||
await captureComboGif();
|
||||
}
|
||||
|
||||
if (wantStills || wantCli || wantGif || wantCombo) {
|
||||
// Frame phase — only frame what was captured this run so that
|
||||
// already-framed PNGs from prior runs aren't framed again.
|
||||
// Stills and the GIF use the fixed 1600x992 inner; CLI captures
|
||||
|
|
@ -623,12 +906,15 @@ async function main() {
|
|||
const fixed = [];
|
||||
if (wantStills) fixed.push(...STILLS_PNGS.map((p) => join(OUT_DIR, p)));
|
||||
if (wantGif) fixed.push(join(OUT_DIR, 'demo.gif'));
|
||||
if (wantCli) fixed.push(CLI_GIF);
|
||||
if (fixed.length) applyFrames(fixed, { natural: false });
|
||||
|
||||
if (wantCli) {
|
||||
const cli = CLI_PNGS.map((p) => join(OUT_DIR, p));
|
||||
applyFrames(cli, { natural: true });
|
||||
}
|
||||
|
||||
syncNyxscanDev();
|
||||
}
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Frame Nyx screenshots with the brand purple gradient.
|
||||
"""Frame Nyx screenshots with the brand mint-led four-corner gradient.
|
||||
|
||||
Reads a list of PNG paths from argv (or all PNGs under
|
||||
assets/screenshots/ if no args) and overwrites each with a framed
|
||||
version: inner screenshot with rounded corners, centered on a
|
||||
diagonal purple gradient (top-left #8a5bf5 → bottom-right #4d1d97).
|
||||
four-corner mint-led gradient (TL #72f3d7, TR #ff6aa2,
|
||||
BL #f8c56b, BR #4cc9ff).
|
||||
|
||||
Two framing modes:
|
||||
- default inner is resampled to 1600x992, outer is 1800x1113.
|
||||
|
|
@ -37,14 +38,12 @@ PAD_R = OUTER_W - INNER_W - PAD_L # 100
|
|||
PAD_B = OUTER_H - INNER_H - PAD_T # 61
|
||||
CORNER_RADIUS = 12
|
||||
|
||||
# Four-corner bilinear gradient. Sampled from the existing CLI
|
||||
# screenshots so every framed asset matches: top-left is the lightest
|
||||
# (Tailwind violet-500), the off-diagonal corners are violet-600, and
|
||||
# bottom-right is violet-900.
|
||||
GRAD_TL = (139, 92, 246) # #8b5cf6 violet-500
|
||||
GRAD_TR = (124, 58, 237) # #7c3aed violet-600
|
||||
GRAD_BL = (124, 58, 237) # #7c3aed violet-600
|
||||
GRAD_BR = ( 76, 29, 149) # #4c1d95 violet-900
|
||||
# Four-corner bilinear gradient. The primary brand accent anchors the
|
||||
# frame, with distinct warm/cool corners for richer screenshot depth.
|
||||
GRAD_TL = (114, 243, 215) # #72f3d7
|
||||
GRAD_TR = (255, 106, 162) # #ff6aa2
|
||||
GRAD_BL = (248, 197, 107) # #f8c56b
|
||||
GRAD_BR = ( 76, 201, 255) # #4cc9ff
|
||||
|
||||
|
||||
def make_gradient(w: int, h: int) -> Image.Image:
|
||||
|
|
@ -135,7 +134,7 @@ def frame_one(src: Path, natural: bool = False) -> None:
|
|||
|
||||
def frame_gif(src: Path) -> None:
|
||||
"""Frame an animated GIF in place: every frame gets the same
|
||||
purple gradient frame, then the result is re-encoded as a single-
|
||||
mint-cyan gradient frame, then the result is re-encoded as a single-
|
||||
palette GIF. Calls ffmpeg for the final encode (Pillow's GIF
|
||||
output is noticeably worse for large animations).
|
||||
"""
|
||||
|
|
|
|||
48
src/ast.rs
|
|
@ -377,7 +377,7 @@ fn build_taint_diag(
|
|||
// Resolved sink capability bits, used by deduplication to distinguish
|
||||
// sinks with different cap types on the same source line (e.g.
|
||||
// `sink_sql(x); sink_shell(x);`).
|
||||
let sink_caps_bits: u16 = cfg_graph[finding.sink]
|
||||
let sink_caps_bits: u32 = cfg_graph[finding.sink]
|
||||
.taint
|
||||
.labels
|
||||
.iter()
|
||||
|
|
@ -385,7 +385,7 @@ fn build_taint_diag(
|
|||
crate::labels::DataLabel::Sink(c) => Some(c.bits()),
|
||||
_ => None,
|
||||
})
|
||||
.fold(0u16, |acc, b| acc | b);
|
||||
.fold(0u32, |acc, b| acc | b);
|
||||
|
||||
// Cap-specific rule-id routing.
|
||||
//
|
||||
|
|
@ -508,6 +508,14 @@ fn build_taint_diag(
|
|||
|| (finding.source_kind.sensitivity() >= crate::labels::Sensitivity::Sensitive
|
||||
&& (flow_has_body_bind || source_is_credential_bearing)));
|
||||
|
||||
// Cap-specific rule routing. Auth-as-taint and data-exfil keep their
|
||||
// pre-existing branches so the routing rules they encode (auth-finding
|
||||
// namespace alignment; body-bind / source-sensitivity gate) stay
|
||||
// exactly as before. New cap classes (LDAP / XPath / Header / Open
|
||||
// redirect / SSTI / XXE / Prototype pollution) route through
|
||||
// `cap_rule_meta()` so the canonical rule ids in the registry are the
|
||||
// single source of truth. Legacy generic taint findings continue to
|
||||
// emit `taint-unsanitised-flow`.
|
||||
let diag_id = if effective_caps.contains(crate::labels::Cap::UNAUTHORIZED_ID) {
|
||||
"rs.auth.missing_ownership_check.taint".to_string()
|
||||
} else if is_data_exfil_rule {
|
||||
|
|
@ -516,6 +524,25 @@ fn build_taint_diag(
|
|||
source_point.row + 1,
|
||||
source_point.column + 1
|
||||
)
|
||||
} else if let Some(meta) = [
|
||||
crate::labels::Cap::LDAP_INJECTION,
|
||||
crate::labels::Cap::XPATH_INJECTION,
|
||||
crate::labels::Cap::HEADER_INJECTION,
|
||||
crate::labels::Cap::OPEN_REDIRECT,
|
||||
crate::labels::Cap::SSTI,
|
||||
crate::labels::Cap::XXE,
|
||||
crate::labels::Cap::PROTOTYPE_POLLUTION,
|
||||
]
|
||||
.iter()
|
||||
.find(|c| effective_caps.contains(**c))
|
||||
.and_then(|c| crate::labels::cap_rule_meta(*c))
|
||||
{
|
||||
format!(
|
||||
"{} (source {}:{})",
|
||||
meta.rule_id,
|
||||
source_point.row + 1,
|
||||
source_point.column + 1
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"taint-unsanitised-flow (source {}:{})",
|
||||
|
|
@ -576,6 +603,23 @@ fn build_taint_diag(
|
|||
}
|
||||
_ => crate::patterns::Severity::Medium,
|
||||
}
|
||||
} else if let Some(meta) = [
|
||||
crate::labels::Cap::LDAP_INJECTION,
|
||||
crate::labels::Cap::XPATH_INJECTION,
|
||||
crate::labels::Cap::HEADER_INJECTION,
|
||||
crate::labels::Cap::OPEN_REDIRECT,
|
||||
crate::labels::Cap::SSTI,
|
||||
crate::labels::Cap::XXE,
|
||||
crate::labels::Cap::PROTOTYPE_POLLUTION,
|
||||
]
|
||||
.iter()
|
||||
.find(|c| effective_caps.contains(**c))
|
||||
.and_then(|c| crate::labels::cap_rule_meta(*c))
|
||||
{
|
||||
// New cap classes draw severity from the rule registry so a single
|
||||
// edit to `CAP_RULE_REGISTRY` cascades through SARIF, the dashboard,
|
||||
// and the integration suite without per-language source-kind nudges.
|
||||
meta.severity
|
||||
} else {
|
||||
severity_for_source_kind(finding.source_kind)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -206,8 +206,8 @@ pub fn run_auth_analysis_with_model(
|
|||
// (when provided) for cross-file helpers that live in other files.
|
||||
apply_helper_lifting(&mut model, lang, file_path, scan_root, global_summaries);
|
||||
|
||||
// Phase 1 caller-scope IPA: propagate route-handler-level auth
|
||||
// checks DOWN to callee helper units within the same file. See
|
||||
// Caller-scope IPA: propagate route-handler-level auth checks DOWN
|
||||
// to callee helper units within the same file. See
|
||||
// [`apply_caller_scope_propagation`] for the propagation rule.
|
||||
apply_caller_scope_propagation(&mut model);
|
||||
|
||||
|
|
@ -547,8 +547,8 @@ fn apply_helper_lifting(
|
|||
}
|
||||
}
|
||||
|
||||
/// Phase 1 caller-scope IPA: propagate route-handler-level auth checks
|
||||
/// DOWN to callee helper units within the same file.
|
||||
/// Caller-scope IPA: propagate route-handler-level auth checks DOWN to
|
||||
/// callee helper units within the same file.
|
||||
///
|
||||
/// `apply_helper_lifting` walks UPWARD: a helper that internally
|
||||
/// proves ownership / membership / etc. has its summary lifted onto
|
||||
|
|
|
|||
|
|
@ -1190,6 +1190,7 @@ fn clone_preserves_all_sub_structs() {
|
|||
destination_uses: None,
|
||||
gate_filters: Vec::new(),
|
||||
is_constructor: false,
|
||||
produces_null_proto: false,
|
||||
},
|
||||
taint: TaintMeta {
|
||||
labels: {
|
||||
|
|
@ -1841,9 +1842,12 @@ def outer(cmd):
|
|||
assert_eq!(kwargs[1].0, "check");
|
||||
}
|
||||
|
||||
/// Languages without keyword-argument grammar should leave `kwargs` empty.
|
||||
/// JS object-literal positional args lift their `pair` children into
|
||||
/// `kwargs` so consumers like xml_config's `processEntities` /
|
||||
/// `resolve_entities` opt-in detector can read them without re-walking
|
||||
/// the tree-sitter AST.
|
||||
#[test]
|
||||
fn call_node_kwargs_empty_for_javascript() {
|
||||
fn call_node_kwargs_lifts_javascript_object_literal_pairs() {
|
||||
let src = br"
|
||||
function outer(cmd) {
|
||||
child_process.exec(cmd, { shell: true });
|
||||
|
|
@ -1861,9 +1865,12 @@ fn call_node_kwargs_empty_for_javascript() {
|
|||
.is_some_and(|c| c.ends_with("exec"))
|
||||
})
|
||||
.expect("child_process.exec call node should exist");
|
||||
let kwargs = &call_node.call.kwargs;
|
||||
assert!(
|
||||
call_node.call.kwargs.is_empty(),
|
||||
"JS object-literal arg is not a keyword_argument — kwargs should stay empty"
|
||||
kwargs
|
||||
.iter()
|
||||
.any(|(k, vs)| k == "shell" && vs.iter().any(|v| v == "true")),
|
||||
"JS object-literal `{{ shell: true }}` should surface as kwarg, got {kwargs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//! Strictly additive: classes whose fields cannot be classified produce
|
||||
//! a `DtoFields` with an empty `fields` map, the caller must decide
|
||||
//! whether to use that as a "Dto with no inferred fields" or fall back
|
||||
//! to the pre-Phase-6 Object/Unknown classification.
|
||||
//! to the generic Object/Unknown classification.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
|
|
|
|||