Release/0.5.0 (#35)

* feat: Introduce function-scoped variable interning for state analysis with new tests and fixtures

* feat: Add Phase 26 symbolic execution enhancements with bitwise operator support, abstract interpretation refinements, and new taint analysis tests

* feat: Refine state analysis to handle factory-pattern resource returns with mixed-path tests and leak detection enhancements

* feat: Add Phase 27 debug views with symbolic execution, abstract interpretation, SSA, and call graph viewers; integrate with debug layout and styles

* feat: Add Phase 31 type-qualified symbolic resolution with receiver-based callee disambiguation and testing

* feat: Extend symbolic execution with state iteration, enhanced debug views, and debounced input handling

* feat: Add Phase 13 resource and auth pattern extensions with new tests and fixtures

* feat: Introduce CFG debug graph renderer with compact mode, toolbar, and DAG layout integration

* feat: Add Phase 28 encoding and decoding transform modeling with structural symex enhancements and new taint analysis tests

* feat: Extend abstract interpretation with type facts and constant value tracking in debug views and server logic

* feat: Add linear path handling and witness extraction to symbolic execution with Phase 28 transform mismatch detection

* feat: Refine Go auth and sanitizer handling with enhanced rules, state updates, and benchmark improvements

* feat: Enable auth-state analysis by default and update relevant tests in benchmark config

* test: Update state_tests to reflect default enablement of auth-state analysis and add auth suppression test

* docs: update CHANGELOG.md

* feat: Introduce per-index taint tracking in `HeapState` with `HeapSlot`, overflow handling, and revised SSA transfers

* feat: Introduce C/C++ language labels and refine heap state tracking in SSA transfers

* feat: Implement per-index array slot tracking in symbolic heap with overflow collapse

* feat: Add implicit definition handling for uninitialized declarations in SSA value allocation

* feat: Refactor function parameters and constants for improved clarity and maintainability

* refactor: Reorder module imports and improve formatting for consistency

* refactor: Fix formatting erorrs

* refactor: Fix clippy warnings

* refactor: Fix fmt warnings (again)

* chore: Update dependencies and improve feature configuration

* Add comprehensive tests for undertested modules (#36) (COPILOT)

* Add comprehensive tests for undertested modules

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/f3fc877e-f386-49ba-9793-fc93d3805083

* Add comprehensive tests for ext, project, walk, and errors modules

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/f3fc877e-f386-49ba-9793-fc93d3805083

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>

* chore: Update dependencies and improve feature configuration

* fix: formatting errors in new tests

* chore: Update license list in about.toml

* chore: made functions input inline

* chore: updated cfg graph to take up the full page

* chore: add Prettier configuration and update code formatting

* Add frontend test suite with Vitest (111 tests) (#37)

* Add Vitest test suite for frontend - 111 tests across utils, components, hooks, and graph utilities

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/7cf0dba2-ecff-4740-ba4d-92717e74a0b7

* ci: add frontend test step to CI workflow

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/5bc0ac9f-0a32-4d03-9cb7-7a15aea53fca

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>

* chore: simplify array initialization in test files for consistency

* ran typecheck

* feat: add AnalysisWorkspace component and integrate it into CfgViewerPage

* feat: update routing in AppLayout and improve empty state message in ExplorerPage

* feat: enhance scan progress tracking with additional metrics and stages

* feat: update license information and add license check script

* feat: implement cross-file symbolic execution with callee body persistence

* feat: replace dagre graphs with Graphology + ELK + Sigma for more advanced call stack and cfg rendering

* feat: ensure CFG function view is scoped to the selected function, preventing bleed into sibling functions

* feat: enhance resource tracking with proxy method summaries and improve finding extraction

* feat: add terminal function exit detection for accurate resource leak analysis

* feat: add warnings for loops and functions without bodies to improve error recovery

* feat: update lambda expression handling to ensure proper function classification and control flow

* feat: remove bounded formatting/string ops and add JSON.parse sanitizer for improved data handling

* feat: add inline return taint analysis and regression tests for improved security checks

* feat: add engine version management and migration handling for database schema updates

* feat: enhance first_call_ident to skip nested function bodies and add regression tests

* feat: enhance callee name resolution with two-segment normalization and disambiguation

* feat: add cross-file context flags and debug assertions for taint analysis

* feat: refactor taint analysis structure to unify context handling and improve clarity

* feat: enhance dead code elimination to preserve Sink, Source, and Sanitizer labels with new tests

* docs: updated CHANGELOG.md

* fmt: formatting fixes

* fix: fixed frontend formatting and lint warnings

* fix: optimized ci

* fix: optimized ci

* Add comprehensive multi-file test coverage to Nyx (#38)

* Initial checklist for multi-file test suite expansion

Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/e550cb88-9767-4442-94d4-101bf5bb0e23

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>

* Add 12 new multi-file test fixtures with TP/TN/near-miss coverage

Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/e550cb88-9767-4442-94d4-101bf5bb0e23

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>

* deleted root repo

* rebuilt to test for regressions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>
Co-authored-by: elipeter <elicpeter@gmail.com>

* feat: enhance import alias resolution and taint tracking

* feat: implement security hardening with CSRF protection and path validation

* feat: add support for import alias bindings in Python, PHP, and Rust

* feat: enhance CFG analysis modes and improve code readability

* feat: add detection for parameterized SQL queries to enhance security

* feat: add safe internal redirect handling and enhance session destroy validation

* feat: implement security improvements by addressing vulnerabilities in execAsync, session management, and file downloads

* feat: enhance taint detection by adding support for inline source member expressions in call arguments

* feat: implement pre-emission of Source nodes for inline source member expressions in call arguments

* feat: add support for Throw statement in control flow and error handling

* feat: add debug and echo endpoints with potential information leakage

* feat: implement internal redirect suppression and enhance taint detection

* feat: implement module alias tracking for dynamic dispatch in JS/TS

* feat: add authorization analysis module with Express support

* feat: add authorization analysis module with Express support

* feat: add tests for admin guard requirements and clean checks in authorization analysis

* feat: integrate Koa and Fastify frameworks into authorization analysis

* feat: add Flask and Django support to authorization analysis module

* feat: add support for Rails and Sinatra frameworks in authorization analysis

* feat: add support for Axum, ActixWeb, and Rocket frameworks in authorization analysis

* feat: add support for ActixWeb, Axum, and Rocket frameworks in authorization analysis

* feat: add support for Rails and Sinatra in authorization analysis

* chore: add .DS_Store to .gitignore

* refactor: simplify conditional checks and improve readability in multiple files

* refactor: update usage of Option methods for improved clarity and consistency

* refactor: improve code readability by simplifying conditional checks and formatting

* refactor: improve code formatting and readability by simplifying conditional checks

* refactor: simplify conditional checks and improve readability in multiple files

* refactor: simplify conditional checks in axum.rs for improved readability

* feat: add CodeQL analysis configuration for enhanced security scanning

* test: add comprehensive tests for `src/output.rs` SARIF builder (#39)

* chore: start test coverage improvement work

Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/cd7ff398-134e-4728-a5e7-0353a0744423

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>

* test: add comprehensive tests for src/output.rs SARIF builder

Agent-Logs-Url: https://github.com/elicpeter/nyx/sessions/cd7ff398-134e-4728-a5e7-0353a0744423

Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>

* refactor: improve code formatting and readability in output.rs

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: elicpeter <54954007+elicpeter@users.noreply.github.com>
Co-authored-by: elipeter <elicpeter@gmail.com>

* refactor: improve code formatting and readability in output.rs

* Potential fix for code scanning alert no. 210: Uncontrolled data used in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for code scanning alert no. 211: Uncontrolled data used in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* refactor: enhance triage file path handling with improved error management and validation

* refactor: updated func summaries for richer detail

* refactor: update SSA summary extraction to use canonical FuncKey for distinct entries

* refactor: enhance callee metadata structure to support arity, receiver, and qualifier for better overload resolution

* refactor: add support for keyword arguments in function calls and enhance receiver extraction for method-style calls

* refactor: implement new Flask routes for safe and unsafe shell command execution

* refactor: separate receiver handling in SSA operations and enhance taint propagation

* refactor: improve arity handling by using arg_uses for positional argument count and enhance witness scoring for tainted arguments

* refactor: implement auth decorator extraction and classification for multiple languages

* refactor: enhance Rust module path resolution and use map handling for cross-file disambiguation

* refactor: introduce CalleeQuery struct for structured callee resolution and enhance resolver logic

* refactor: implement same-file identity collision handling for `runTask` to ensure correct resolver behavior

* refactor: standardize default struct initialization across multiple files

* feat: add scripts for formatting checks and auto-fixes with test summaries

* refactor: simplify character splitting and enhance namespace qualifier handling

* refactor: improve documentation clarity and enhance code readability in resolver logic

* refactor: replace default struct initialization with explicit field assignments for clarity

* feat: enhance anonymous function naming by deriving context-based bindings

* refactor: streamline match expressions for improved readability and performance

* refactor: streamline match expressions for improved readability and performance

* refactor: replace loop with while let for improved clarity and performance

* feat: add SSA constant propagation support to analysis context for improved accuracy

* feat: add SSA constant propagation support to analysis context for improved accuracy

* feat: implement shell metacharacter validation and bounded-length checks in Rust analysis

* feat: add static map analysis for command injection suppression and type safety

* refactor: simplify match statements and reduce line breaks for improved readability

* feat(summary): phase 1/5 SinkSite data model for primary sink-location attribution

Introduce SinkSite (file_rel, line, col, snippet, cap) carrying the
primary sink source-location through function summaries. Swap
SsaFuncSummary.param_to_sink and FuncSummary.param_to_sink from a coarse
Cap map to a deduped SmallVec<[SinkSite; 1]> per parameter, with a
backward-compatible cap_sites() helper and serde defaults so pre-phase-1
on-disk rows continue to deserialise cleanly.

Extraction: SinkSiteLocator bundles the tree/bytes/file_rel needed by
extract_ssa_func_summary; ParsedFile::extract_ssa_artifacts wires the
locator in for the persisted pass-1 path, while pass-2 intra-file
transient summaries fall back to cap-only sites (behavior unchanged).
Merge: GlobalSummaries::insert now unions sink sites with
(file_rel, line, col, cap) dedup via shared union_param_sink_sites
helper.

Database: JSON-serialised summary columns carry the new shape
automatically; no schema change needed.

Phase 2 will consume SinkSite in build_taint_diag() to overwrite the
caller-site Finding.line with the callee's sink line when resolved via
summary. Phase 1 keeps behavior unchanged: scanning
tests/benchmark/corpus/rust/cmdi/cmdi_indirect.rs still produces the
same (wrong) line 10 finding.

Adds round-trip tests covering SinkSite solo, SsaFuncSummary with sink
sites, legacy-JSON default handling for both summary types, and merge
dedup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(taint): phase 2/5 thread SinkSite into SsaTaintEvent and Finding

Plumb Phase 1's SinkSite through the event pipeline into Findings,
no output change yet.  SsaTaintEvent gains `primary_sink_site:
Option<SinkSite>`; when the main or callback sink-emission path has
non-empty `param_to_sink_sites`, filter to sites whose
`(line != 0) && (cap ∩ sink_caps != ∅)` and emit one event per
distinct site — the multi-primary collapse keeps each downstream
Finding single-primary.

Resolution: ResolvedSummary and SinkInfo gain mirror
`param_to_sink_sites` fields, populated from `SsaFuncSummary.param_to_sink`
(SSA + callback paths) and `FuncSummary.param_to_sink` (global paths).
Label, local-summary, and interop resolution paths leave the field
empty — they only ever had cap-level info to begin with.

Finding: new `primary_location: Option<SinkLocation>` with
`file_rel/line/col`.  `ssa_events_to_findings` maps
`event.primary_sink_site` → `Finding.primary_location`, filtering
cap-only sites (`line == 0`) to `None` so the (0,0) sentinel never
leaks to formatters.  Dedup key extended with the primary location
so multi-site events aren't collapsed back together.

Invariants (debug_assert!):
* every SinkSite reaching emission has `line != 0 && cap ∩ sink_caps
  != ∅` — enforced by the pick_primary_sink_sites* filters;
* every populated Finding.primary_location has `line != 0` AND
  non-empty `file_rel` — the cap-only → None translation upstream
  guarantees this.

Deliberately independent of `uses_summary`: that flag tracks whether
the *taint chain* used a summary, whereas primary attribution
requires only that the *sink* itself was summary-resolved.  A local
source reaching a cross-file sink produces `uses_summary=false`
alongside a populated primary_location — documented on
Finding.primary_location, covered by
`cross_file_sink_finding_carries_primary_location`.

build_taint_diag, SARIF/JSON/explanation formatters, and the
benchmark scorer remain untouched: finding.line still comes from
`cfg_graph[finding.sink]`, so cmdi_indirect.rs still reports line 10
and the benchmark's rs-cmdi-003 row still shows FN in the LOC column.

Tests: `cross_file_sink_finding_carries_primary_location` (proves
plumbing via a synthetic FuncSummary carrying a SinkSite at 42:5) and
`cross_file_sink_cap_only_site_leaves_primary_location_none`
(regression guard against cap-only sites surfacing).  All 1566 lib
tests + integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(output): phase 3/5 consume primary sink location in diag + SARIF

When a finding's primary_location (populated in phase 2 from a callee
summary's SinkSite) names the dangerous instruction inside a callee
body, attribute the diagnostic line to that location instead of the
caller's call site. The call site is demoted to a Call step in
flow_steps, and a synthetic Sink step at the primary location is
appended so analysts still see the full trace.

Changes:
- Add scan_root parameter to build_taint_diag so file_rel can be
  resolved back to an absolute path via a shared resolve_file_rel
  helper. Empty file_rel (single-file scans where namespace == "")
  resolves to the file under analysis.
- Extend SinkLocation with snippet, carried from the upstream
  SinkSite so the formatter needs no second file read.
- Relax the ssa_events_to_findings debug_assert to allow empty
  file_rel, which is valid when scan root equals the file itself.
- SARIF: emit data-flow as codeFlows[0].threadFlows[0].locations[];
  locations[0] already reflects the primary sink position via the
  updated diag line/col.

Acceptance: scan on tests/benchmark/corpus/rust/cmdi/cmdi_indirect.rs
now reports line 5 (Command::new) as the primary sink, with the call
site at line 10 visible in flow_steps.

Two expect.json fixtures updated (must_match line_range widened):
- javascript/taint/context_sensitive_call: 12-14 -> 7-14 (line 8 is
  the real sink inside run()).
- rust/cfg/closure_async: 10-10 -> 10-11 (line 11 is Command::new
  inside the closure).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(bench): phase 4/5 validate primary sink attribution across corpus

Extend the benchmark scorer and ground truth to lock in phase 3's
primary-location behavior, and add fixtures that exercise the new
capability end-to-end.

Scorer (tests/benchmark_test.rs):
- Add optional `expected_call_site_lines: Option<Vec<[usize; 2]>>` on
  Case. When present, score_location_level additionally requires at
  least one flow_step in the finding's evidence trace to fall within
  ±2 of the call-site range. When absent, the check is skipped —
  fully forward-compatible with existing fixtures.
- Retain ±2 tolerance on expected_sink_lines (compared against the
  now-primary Diag.line post-phase-3).

Ground truth edits:
- rs-cmdi-cross-001: expected_sink_lines [8,8] -> [9,9]. Line 8 is the
  transform::wrap call site (a cross-file propagator, not a sink);
  line 9 is Command::new, the real sink. The ±2 tolerance happened to
  mask this stale attribution but it was semantically wrong — phase 4
  is the right time to correct it. Also adds expected_call_site_lines
  [8,8] so the new field is exercised on an existing cross-file case.
- rs-cmdi-003: adds expected_call_site_lines [10,10] (run_cmd call).
  This fixture's sink (Command::new inside run_cmd at line 5) was the
  motivating case for phases 1-3; adding the call-site assertion
  guards against regression to caller-line attribution.

New fixtures:
- rust/cmdi/cmdi_indirect_multisink.rs (rs-cmdi-009): helper run_both
  takes two tainted params and invokes two Command sinks on
  consecutive lines. Locks in that primary line lands inside the
  helper (lines 5-6), not at the caller (line 12). Notes document
  that SinkSite is currently one-per-callee so both findings today
  collapse onto the first sink; expected_sink_lines=[5,6] and
  expected_call_site_lines=[12,12] stay valid either way.
- python/cmdi/cross_indirect_sink/{app.py,helper.py} (py-cmdi-cross-
  004): sink os.system lives in helper.py (cross-file), caller in
  app.py reads env source and calls run_cmd. Verifies phase 3's
  cross-file primary attribution: Diag.path = helper.py, Diag.line =
  5, with app.py:7 recorded in flow_steps as a Call step.

Acceptance:
- `cargo test --test benchmark_test -- --ignored --nocapture` passes.
- rs-cmdi-003 is TP/TP/TP (the target flip FN->TP at LOC). All
  pre-existing TP/TP/TP fixtures remain TP/TP/TP; 2 new fixtures are
  TP/TP/TP.
- Aggregate rule-level: TP=158 FP=10 FN=1 TN=97, P=0.940 R=0.994
  F1=0.966 on the 266-case corpus (was TP=156 FP=10 FN=1 TN=97 on
  264 pre-phase-4, delta is the +2 new cases both resolving TP).
- Full `cargo test` green (1566 lib tests + all integration tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(taint): phase 5/5 lock Finding.primary_location contract via regression test

Add a regression test in src/taint/ssa_transfer.rs that wires up a synthetic
SsaFuncSummary with a SinkSite at other.rs:42:10 and drives the three
emission stages (pick_primary_sink_sites → emit_ssa_taint_events →
ssa_events_to_findings) against a minimal caller SSA body.  Asserts the
resulting Finding.primary_location is exactly that triple.

The existing integration tests in src/taint/tests.rs cover the coarse
FuncSummary path end-to-end through analyse_file.  This test locks in the
lower-level SSA-side plumbing so a future refactor that silently drops the
site between pick → emit → findings fails here rather than only at the
benchmark layer.

Also refreshes tests/benchmark/results/latest.json (timestamp only; rs-cmdi-003
remains TP/TP/TP and the aggregate P/R/F1 are unchanged from phase 4).

Closes the primary sink-location attribution feature (phases 1-5/5):
* Phase 1 — SinkSite data model on summaries.
* Phase 2 — SinkSite threaded into SsaTaintEvent and Finding.
* Phase 3 — diag + SARIF consume primary_location.
* Phase 4 — benchmark validates primary_call_site_lines across corpus.
* Phase 5 — regression test locks the event→finding contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: clean up formatting and improve readability in multiple files

* refactor: simplify type definition for deduplication key in findings

* test(harness): add must_not_match expectation for FP regression guards

Extends ExpectedFinding with must_not_match field that asserts a
diagnostic must NOT fire — presence is a hard failure. Non-consuming
scan so it coexists with must_match entries on the same rule_id.
Adds forbidden_violations accumulator and updates summary line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(regression): update expectations to ensure must_not_match for various taint and resource leak rules

* feat: implement auto-seeding for JS/TS handler parameters to enhance taint tracking

* feat: update switch statement handling to improve control flow analysis

* feat: implement promisify alias handling for JS/TS to enhance taint tracking

* feat: enhance taint tracking by refining expectation handling and adding mode filtering

* feat: refine SQL handling in stream processing and enhance auto-seeding for handler parameters

* feat: update taint tracking rules to enforce full mode matching and improve flow analysis

* feat: enhance Ruby subshell handling to improve taint tracking and flow analysis

* feat: update xss_response expectations to refine taint flow analysis and enhance regression guarding

* feat: refine framework detection and update expectation handling for Echo and Sinatra

* feat: implement max_count for taint tracking expectations and deduplicate findings

* feat: add strict_unexpected handling for taint-unsanitised-flow in expectation files

* feat: enhance deduplication of taint-unsanitised-flow findings by collapsing based on line and severity

* feat: add strict_unexpected handling for taint-unsanitised-flow in multiple expectation files

* feat: add structural invariant checks for SSA bodies

* feat: ensure deterministic phi emission order using BTreeSet

* feat: enhance handling of terminators to ensure authoritative flow through successor edges

* feat: enhance Goto terminator handling to ensure all successors are marked executable

* feat: refactor code for improved readability and organization

* feat: simplify predicate checks and enhance readability in SSA handling

* feat: implement per-file parse timeout and enhance file size handling

* feat: migrate analysis engine toggles from environment variables to configuration file

* feat: remove unnecessary whitespace in hostile_input_tests.rs

* feat: remove unnecessary whitespace in hostile_input_tests.rs

* feat: update dependencies and enhance documentation on language maturity

* feat: enhance security headers and improve request body limits

* feat: implement sink capability bits for deduplication and enhance evidence tagging

* feat: implement dynamic activation handling for gated sinks and enhance validation logic

* feat: enhance configuration documentation and clarify inline analysis cache behavior

* feat: implement panic recovery during analysis to continue scans past errors

* feat: add expectations configuration for taint analysis and performance metrics

* feat: enhance error handling and logging during file reading and mutex locking

* feat: add cross-file body loading tests and plumbing for CF-1 phase

* feat: implement cross-file k=1 context-sensitive inline taint analysis with new tests and fixtures

* feat: implement indexed-scan parity in cross-file inline analysis with new dropdown and copy functionality

* feat: enhance classification span handling in CFG and AST for improved source attribution

* feat: add new Express routes for handling user input and telemetry data

* feat: implement ternary expression handling in CFG with diamond structure for JS/TS

* feat: implement Phase CF-3 abstract-domain transfer channels in summaries

* feat: add support for string-prefix transfer in cross-file calls and update tests

* docs: reduce RESULTS.md doc size

* feat: implement Phase CF-4 per-return-path summary decomposition with tests

* feat: update parameter handling in pass1 and refactor SsaFuncSummary initialization

* feat: implement Phase CF-5 for cross-file SCC joint fixed-point convergence with new flags and tests

* feat: implement Phase CF-6 with parameter-granularity points-to summaries and associated tests

* refactor: update comments and documentation for clarity and consistency

* style: format code for consistency and readability

* refactor: simplify verdict handling and improve edge checking logic

* refactor: optimize path and identifier collection by avoiding unnecessary cloning

* chore: update Cargo.toml for Rust version 1.85 and add ignored files; modify CHANGELOG and README for clarity on state analysis defaults

* refactor: update documentation and improve clarity in configuration files

* refactor: update documentation and improve clarity in configuration files

* feat: add JS/TS pass-2 convergence tests and expectations configuration

* feat: add Phase 5 regression tests for inline cache origin attribution and update related logic

* feat: implement Phase 7 deduplication and alternative path linking for taint findings

* feat: implement structural DFS index for anonymous functions and update naming conventions

* feat: add Phase 8 regression tests for container-element taint in JS and Python

* feat: add engine-depth profiles and explain-engine option for CLI

* feat: update expectations and add new README fixtures for multi-file scan regression

* feat: implement Phase 11 callback-alias and factory patterns with regression tests

* feat: implement Terminator::Switch for multi-way dispatch and add regression tests

* feat: add real-CVE benchmark fixtures for CVE-2023-48022, CVE-2019-14939, and CVE-2023-26159 with corresponding patched variants

* refactor: extract cfg and ssa_transfer to submodules

* refactor: cargo fmt

* refactor: remove unnecessary blank line in cfg_tests.rs

* refactor: remove unnecessary planning file

* chore: update Rust version to 1.88 and bump dependencies in Cargo files

* feat: enhance triage UI with new layout and controls, update README for clarity

* feat: enhance triage UI with new layout and controls, update README for clarity

* chore: remove outdated section from README for version 0.5.0

* docs: improve clarity and consistency in README content

* chore: add "GPL-3.0-or-later" to license options in about.toml

* chore: update license handling in about.toml and check-licenses.mjs

* style: format code for improved readability in TriagePage component

* style: format code for improved readability in TriagePage component

* chore: enhance license handling and improve body_id scoping in seed lookup

* feat: introduce owner and parent body IDs for enhanced seed scoping

* feat: implement direction-aware engine provenance with new CLI flag for strict CI gating

* feat: add Undef SSA operation for improved control-flow handling

* style: improve code formatting for consistency and readability in multiple files

* feat: add 16-function chain SCC across multiple files for enhanced analysis

* style: simplify code formatting for improved readability in multiple files

* fix: update CapHitReason default implementation and improve README clarity

* docs: enhance README with detailed explanations of taint analysis and limitations

* docs: refine README for clarity and consistency in taint analysis section

* style: improve code formatting for better readability in NewScanModal and scans

* fix: update cargo-about command to use --offline for deterministic license generation

* fix: update cargo-about command to use --offline for deterministic license generation

* ci: add step to prime cargo registry cache for deterministic license generation

* feat: add support for non-sink collections in authorization analysis

* feat: enhance authorization checks with row-level ownership equality and binding tracking

* feat: implement self-scoped user handling and enhance ownership checks

* refactor: simplify assertions and formatting in authorization analysis tests

* fix: normalize line endings in THIRDPARTY-LICENSES.html generation and update README with AI disclosure

* docs: update AI disclosure section for clarity and conciseness

* feat: add AI Contribution Policy and update contributing guidelines for AI assistance disclosure

* feat: enhance authorization analysis with SSA-derived variable type classification

* feat: implement auth_finding_to_diag function for enhanced security diagnostics

* feat: add args_value_refs to CallSite struct for enhanced argument tracking

* feat: add args_value_refs to CallSite struct for enhanced argument tracking

* feat: add direction-aware engine provenance with LossDirection classification and new CLI flag

* feat: simplify strip_cap_from_call_args call by removing unnecessary line breaks

* feat: enhance error message handling in cli_validation_tests for better Windows compatibility

* feat: optimize release profile settings in Cargo.toml and update CodeQL configuration

* feat: enhance release build process with SBOM generation and SLSA provenance

* feat: update actions/checkout and actions/setup-node to v6, enhance CLI options, and improve auth-check summaries

* feat: introduce PathFact handling for path safety checks and rejection logic

* feat: introduce PathFact handling for path safety checks and rejection logic

* feat: update benchmark data and enhance path sanitization logic with new safety checks

* feat: document AI assistance in frontend UI development and human review process

* feat: add return path facts for enhanced path safety checks and update documentation

* chore: update release date for version 0.5.0 in CHANGELOG.md

* chore: clean up ci.yml by removing outdated comments and clarifying steps

* feat: implement cross-language path sanitizers and validators for enhanced security

* feat: enhance SSA value usage tracking by including block terminators and improve path safety checks

* feat: enhance switch statement handling by adding per-case path constraints and support for exclusive cases

* refactor: simplify conditional formatting and improve code readability in executor and lower modules

* feat: add vulnerable examples for various languages demonstrating authentication and sanitization issues

* feat: enhance actor context recognition for self-actor identifiers and add support for global non-sink receivers

* feat: enhance actor context recognition for self-actor identifiers and add support for global non-sink receivers

* feat: add transform classifiers for Java, Go, and Ruby with corresponding tests

* refactor: clarify comments on reassign-to-constant idiom and sink behavior in guards.rs

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Eli Peter 2026-04-25 17:59:11 -04:00 committed by GitHub
parent c4ce08b452
commit 41128177d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2144 changed files with 201812 additions and 8927 deletions

View file

@ -0,0 +1,171 @@
import { useCallback, useEffect, useRef, useState } from 'react';
type Status = 'idle' | 'working' | 'copied' | 'failed';
interface CopyMarkdownButtonProps {
getMarkdown: () => string | Promise<string>;
label?: string;
className?: string;
title?: string;
stopPropagation?: boolean;
iconOnly?: boolean;
}
const COPIED_MS = 1500;
const FAILED_MS = 2000;
const ICON_SIZE = 14;
function CopyIcon() {
return (
<svg
width={ICON_SIZE}
height={ICON_SIZE}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="5" y="5" width="9" height="9" rx="1.5" />
<path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" />
</svg>
);
}
function CheckIcon() {
return (
<svg
width={ICON_SIZE}
height={ICON_SIZE}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M3 8.5l3 3 7-7" />
</svg>
);
}
function FailIcon() {
return (
<svg
width={ICON_SIZE}
height={ICON_SIZE}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M4 4l8 8M12 4l-8 8" />
</svg>
);
}
export function CopyMarkdownButton({
getMarkdown,
label = 'Copy',
className,
title,
stopPropagation,
iconOnly,
}: CopyMarkdownButtonProps) {
const [status, setStatus] = useState<Status>('idle');
const timeoutRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (timeoutRef.current != null) {
window.clearTimeout(timeoutRef.current);
}
};
}, []);
const scheduleReset = useCallback((ms: number) => {
if (timeoutRef.current != null) window.clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
setStatus('idle');
timeoutRef.current = null;
}, ms);
}, []);
const handleClick = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
if (stopPropagation) e.stopPropagation();
if (status === 'working') return;
if (
typeof navigator === 'undefined' ||
!navigator.clipboard ||
typeof navigator.clipboard.writeText !== 'function'
) {
setStatus('failed');
scheduleReset(FAILED_MS);
return;
}
setStatus('working');
try {
const text = await getMarkdown();
await navigator.clipboard.writeText(text);
setStatus('copied');
scheduleReset(COPIED_MS);
} catch (err) {
console.error('CopyMarkdownButton: failed to copy', err);
setStatus('failed');
scheduleReset(FAILED_MS);
}
},
[getMarkdown, scheduleReset, status, stopPropagation],
);
const displayLabel =
status === 'working'
? 'Copying…'
: status === 'copied'
? 'Copied!'
: status === 'failed'
? 'Failed'
: label;
const classes = [
'btn',
'btn-sm',
'copy-btn',
iconOnly ? 'copy-btn--icon' : '',
status === 'copied' ? 'copy-btn--copied' : '',
status === 'failed' ? 'copy-btn--failed' : '',
className || '',
]
.filter(Boolean)
.join(' ');
const icon =
status === 'copied' ? (
<CheckIcon />
) : status === 'failed' ? (
<FailIcon />
) : (
<CopyIcon />
);
return (
<button
type="button"
className={classes}
title={title ?? (iconOnly ? displayLabel : undefined)}
aria-label={iconOnly ? displayLabel : undefined}
disabled={status === 'working'}
onClick={handleClick}
>
{iconOnly ? icon : displayLabel}
</button>
);
}

View file

@ -0,0 +1,84 @@
export interface BarItem {
label: string;
value: number;
color?: string;
}
interface HorizontalBarChartProps {
items: BarItem[];
maxValue?: number;
width?: number;
}
export function HorizontalBarChart({
items,
maxValue,
width = 400,
}: HorizontalBarChartProps) {
if (!items || items.length === 0) {
return (
<div className="empty-state" style={{ padding: 20 }}>
<p>No data</p>
</div>
);
}
const barH = 22;
const gap = 4;
const labelW = 110;
const valueW = 45;
const barAreaW = width - labelW - valueW - 16;
const totalH = items.length * (barH + gap);
const maxVal = maxValue ?? Math.max(...items.map((i) => i.value), 1);
return (
<div className="chart-container">
<svg
viewBox={`0 0 ${width} ${totalH}`}
width="100%"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
>
{items.map((item, i) => {
const y = i * (barH + gap);
const w = Math.max((item.value / maxVal) * barAreaW, 2);
const color = item.color || 'var(--accent)';
return (
<g key={item.label}>
<text
x={labelW - 8}
y={y + barH / 2 + 4}
textAnchor="end"
fontSize={11}
fontFamily="var(--font)"
fill="var(--text-secondary)"
>
{item.label}
</text>
<rect
x={labelW}
y={y + 2}
width={w}
height={barH - 4}
rx={3}
fill={color}
opacity={0.85}
/>
<text
x={labelW + barAreaW + 8}
y={y + barH / 2 + 4}
textAnchor="start"
fontSize={11}
fontFamily="var(--font-mono)"
fontWeight={600}
fill="var(--text)"
>
{item.value}
</text>
</g>
);
})}
</svg>
</div>
);
}

View file

@ -0,0 +1,139 @@
import { formatShortDate } from '../../utils/formatDate';
export interface LinePoint {
label: string;
value: number;
}
interface LineChartProps {
points: LinePoint[];
color?: string;
width?: number;
height?: number;
}
export function LineChart({
points,
color = 'var(--accent)',
width = 400,
height = 160,
}: LineChartProps) {
if (!points || points.length < 2) {
return (
<div className="empty-state" style={{ padding: 20 }}>
<p>Need multiple scans for trends</p>
</div>
);
}
const pad = { top: 15, right: 15, bottom: 30, left: 40 };
const plotW = width - pad.left - pad.right;
const plotH = height - pad.top - pad.bottom;
const maxVal = Math.max(...points.map((p) => p.value), 1);
const minVal = 0;
const yRange = maxVal - minVal || 1;
const xStep = plotW / Math.max(points.length - 1, 1);
const coords = points.map((p, i) => ({
x: pad.left + i * xStep,
y: pad.top + plotH - ((p.value - minVal) / yRange) * plotH,
label: p.label,
value: p.value,
}));
const polyPoints = coords.map((c) => `${c.x},${c.y}`).join(' ');
const areaPoints = `${coords[0].x},${pad.top + plotH} ${polyPoints} ${coords[coords.length - 1].x},${pad.top + plotH}`;
// Y-axis grid lines
const yTicks = 4;
const gridLines = [];
for (let i = 0; i <= yTicks; i++) {
const y = pad.top + (i / yTicks) * plotH;
const val = Math.round(maxVal - (i / yTicks) * yRange);
gridLines.push({ y, val });
}
// X-axis label sampling
const maxLabels = 6;
const step = Math.max(1, Math.ceil(coords.length / maxLabels));
return (
<div className="chart-container">
<svg
viewBox={`0 0 ${width} ${height}`}
width="100%"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
>
{/* Grid lines */}
{gridLines.map((g, i) => (
<g key={i}>
<line
x1={pad.left}
y1={g.y}
x2={pad.left + plotW}
y2={g.y}
stroke="var(--border-light)"
strokeWidth={1}
/>
<text
x={pad.left - 6}
y={g.y + 3}
textAnchor="end"
fontSize={9}
fontFamily="var(--font-mono)"
fill="var(--text-tertiary)"
>
{g.val}
</text>
</g>
))}
{/* Area fill */}
<polygon points={areaPoints} fill={color} opacity={0.08} />
{/* Line */}
<polyline
points={polyPoints}
fill="none"
stroke={color}
strokeWidth={2}
strokeLinejoin="round"
strokeLinecap="round"
/>
{/* Dots */}
{coords.map((c, i) => (
<circle
key={i}
cx={c.x}
cy={c.y}
r={3}
fill={color}
stroke="var(--bg)"
strokeWidth={2}
/>
))}
{/* X-axis labels */}
{coords.map((c, i) => {
if (i % step !== 0 && i !== coords.length - 1) return null;
return (
<text
key={i}
x={c.x}
y={height - 4}
textAnchor="middle"
fontSize={9}
fontFamily="var(--font)"
fill="var(--text-tertiary)"
>
{formatShortDate(c.label)}
</text>
);
})}
</svg>
</div>
);
}

View file

@ -0,0 +1,185 @@
import { useEffect, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import { apiGet } from '../../api/client';
import { highlightSyntax, escapeHtml } from '../../utils/syntaxHighlight';
import type { FileResponse, ExplorerFinding } from '../../api/types';
interface LineHighlights {
sourceLine?: number;
sinkLine?: number;
findingLine?: number;
}
interface CodeViewerProps {
filePath: string;
findings?: ExplorerFinding[];
highlights?: LineHighlights;
highlightLine?: number;
flowLines?: Set<number>;
language?: string;
className?: string;
initialScrollTop?: number;
onScrollPositionChange?: (scrollTop: number) => void;
}
export function CodeViewer({
filePath,
findings,
highlights,
highlightLine,
flowLines,
language,
className,
initialScrollTop,
onScrollPositionChange,
}: CodeViewerProps) {
const bodyRef = useRef<HTMLDivElement>(null);
const {
data: fileData,
isLoading,
error,
} = useQuery({
queryKey: ['files', filePath],
queryFn: ({ signal }) =>
apiGet<FileResponse>(
`/files?path=${encodeURIComponent(filePath)}`,
signal,
),
enabled: !!filePath,
staleTime: 5 * 60_000,
});
const scrollTarget = highlightLine ?? highlights?.findingLine;
useEffect(() => {
if (!fileData || !scrollTarget || !bodyRef.current) return;
const timer = requestAnimationFrame(() => {
const target = bodyRef.current?.querySelector(
`[data-line="${scrollTarget}"]`,
);
if (target)
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
});
return () => cancelAnimationFrame(timer);
}, [fileData, scrollTarget]);
useEffect(() => {
if (
!fileData ||
scrollTarget ||
initialScrollTop == null ||
!bodyRef.current
) {
return;
}
const timer = requestAnimationFrame(() => {
if (bodyRef.current) {
bodyRef.current.scrollTop = initialScrollTop;
}
});
return () => cancelAnimationFrame(timer);
}, [fileData, initialScrollTop, scrollTarget]);
// Build a set of finding lines for gutter markers
const findingsByLine = new Map<number, ExplorerFinding>();
if (findings) {
for (const f of findings) {
// Keep the highest severity per line
const existing = findingsByLine.get(f.line);
if (
!existing ||
severityRank(f.severity) > severityRank(existing.severity)
) {
findingsByLine.set(f.line, f);
}
}
}
const lang = (language || '').toLowerCase();
if (isLoading) {
return (
<div className={className} style={{ padding: 40, textAlign: 'center' }}>
Loading file...
</div>
);
}
if (error) {
return (
<div className={className}>
<div className="error-state" style={{ padding: 40 }}>
<p>
Could not load file:{' '}
{error instanceof Error ? error.message : 'Unknown error'}
</p>
</div>
</div>
);
}
if (!fileData) return null;
return (
<div
className={`code-viewer-body ${className || ''}`}
ref={bodyRef}
onScroll={(event) =>
onScrollPositionChange?.(event.currentTarget.scrollTop)
}
>
{fileData.lines.map((l) => {
let cls = 'code-line';
if (highlights) {
if (l.number === highlights.sourceLine) cls += ' highlight-source';
else if (l.number === highlights.sinkLine) cls += ' highlight-sink';
else if (l.number === highlights.findingLine)
cls += ' highlight-finding';
else if (flowLines?.has(l.number)) cls += ' highlight-flow';
} else if (highlightLine && l.number === highlightLine) {
cls += ' highlight-finding';
}
const gutterFinding = findingsByLine.get(l.number);
return (
<div key={l.number} className={cls} data-line={l.number}>
<span className="line-gutter">
{gutterFinding ? (
<span
className={`gutter-marker sev-${gutterFinding.severity.toLowerCase()}`}
title={`${gutterFinding.rule_id}: ${gutterFinding.message || gutterFinding.category}`}
/>
) : (
<span className="gutter-marker-spacer" />
)}
</span>
<span className="line-number">{l.number}</span>
<span
className="line-content"
dangerouslySetInnerHTML={{
__html: highlightSyntax(escapeHtml(l.content), lang),
}}
/>
</div>
);
})}
</div>
);
}
function severityRank(s: string): number {
switch (s.toUpperCase()) {
case 'HIGH':
return 3;
case 'MEDIUM':
return 2;
case 'LOW':
return 1;
default:
return 0;
}
}

View file

@ -0,0 +1,155 @@
import { FolderIcon } from '../icons/Icons';
import type { TreeEntry } from '../../api/types';
interface FileTreeProps {
entries: TreeEntry[];
expandedPaths: Set<string>;
selectedPath: string | null;
onToggleExpand: (path: string) => void;
onSelectFile: (path: string) => void;
loadedChildren: Map<string, TreeEntry[]>;
}
export function FileTree({
entries,
expandedPaths,
selectedPath,
onToggleExpand,
onSelectFile,
loadedChildren,
}: FileTreeProps) {
return (
<div className="file-tree">
{entries.map((entry) => (
<FileTreeNode
key={entry.path}
entry={entry}
depth={0}
expandedPaths={expandedPaths}
selectedPath={selectedPath}
onToggleExpand={onToggleExpand}
onSelectFile={onSelectFile}
loadedChildren={loadedChildren}
/>
))}
</div>
);
}
interface FileTreeNodeProps {
entry: TreeEntry;
depth: number;
expandedPaths: Set<string>;
selectedPath: string | null;
onToggleExpand: (path: string) => void;
onSelectFile: (path: string) => void;
loadedChildren: Map<string, TreeEntry[]>;
}
function FileTreeNode({
entry,
depth,
expandedPaths,
selectedPath,
onToggleExpand,
onSelectFile,
loadedChildren,
}: FileTreeNodeProps) {
const isDir = entry.entry_type === 'dir';
const isExpanded = expandedPaths.has(entry.path);
const isSelected = selectedPath === entry.path;
const children = loadedChildren.get(entry.path);
const sevClass =
entry.finding_count > 0 && entry.severity_max
? ` sev-${entry.severity_max.toLowerCase()}`
: '';
const handleClick = () => {
if (isDir) {
onToggleExpand(entry.path);
} else {
onSelectFile(entry.path);
}
};
return (
<>
<div
className={`tree-node${isSelected ? ' selected' : ''}${sevClass}`}
style={{ paddingLeft: 8 + depth * 16 }}
onClick={handleClick}
>
<span className={`tree-chevron${isDir ? '' : ' invisible'}`}>
{isDir ? (isExpanded ? '▾' : '▸') : ''}
</span>
<span className="tree-node-icon">
{isDir ? (
<FolderIcon size={14} />
) : (
<FileIcon language={entry.language} />
)}
</span>
<span className="tree-node-name" title={entry.path}>
{entry.name}
</span>
{entry.finding_count > 0 && (
<span className="tree-node-badge">{entry.finding_count}</span>
)}
</div>
{isDir && isExpanded && children && (
<div className="tree-children">
{children.map((child) => (
<FileTreeNode
key={child.path}
entry={child}
depth={depth + 1}
expandedPaths={expandedPaths}
selectedPath={selectedPath}
onToggleExpand={onToggleExpand}
onSelectFile={onSelectFile}
loadedChildren={loadedChildren}
/>
))}
</div>
)}
</>
);
}
function FileIcon({ language }: { language?: string }) {
const label = (language || '').charAt(0).toUpperCase() || '·';
const color = langColor(language);
return (
<span className="file-icon" style={{ color }} title={language || 'file'}>
{label}
</span>
);
}
function langColor(lang?: string): string {
switch (lang?.toLowerCase()) {
case 'javascript':
return '#f0db4f';
case 'typescript':
return '#3178c6';
case 'python':
return '#3572a5';
case 'rust':
return '#dea584';
case 'go':
return '#00add8';
case 'java':
return '#b07219';
case 'ruby':
return '#cc342d';
case 'php':
return '#4f5d95';
case 'c':
return '#555555';
case 'c++':
return '#f34b7d';
default:
return 'var(--text-tertiary)';
}
}

View file

@ -0,0 +1,35 @@
import type { ReactNode } from 'react';
interface AnalysisWorkspaceProps {
canvas: ReactNode;
inspector?: ReactNode;
inspectorTitle?: string;
inspectorSide?: 'left' | 'right';
}
export function AnalysisWorkspace({
canvas,
inspector,
inspectorTitle,
inspectorSide = 'right',
}: AnalysisWorkspaceProps) {
const hasInspector = Boolean(inspector);
const inspectorPanel = hasInspector ? (
<aside className="analysis-inspector">
{inspectorTitle && <h3>{inspectorTitle}</h3>}
{inspector}
</aside>
) : null;
return (
<div
className={`analysis-workspace${hasInspector ? ' analysis-workspace-with-inspector' : ''}${
hasInspector ? ` analysis-workspace-inspector-${inspectorSide}` : ''
}`}
>
{inspectorSide === 'left' && inspectorPanel}
<div className="analysis-canvas">{canvas}</div>
{inspectorSide === 'right' && inspectorPanel}
</div>
);
}

View file

@ -0,0 +1,170 @@
import type { FC, SVGProps } from 'react';
export interface IconProps {
className?: string;
size?: number;
}
type SvgBaseProps = SVGProps<SVGSVGElement> & IconProps;
function svgProps({ className, size = 18 }: IconProps): SvgBaseProps {
return {
className,
width: size,
height: size,
fill: 'none',
stroke: 'currentColor',
strokeWidth: 1.5,
strokeLinecap: 'round',
strokeLinejoin: 'round',
};
}
export function OverviewIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<rect x="2" y="2" width="5.5" height="5.5" rx="1" />
<rect x="10.5" y="2" width="5.5" height="5.5" rx="1" />
<rect x="2" y="10.5" width="5.5" height="5.5" rx="1" />
<rect x="10.5" y="10.5" width="5.5" height="5.5" rx="1" />
</svg>
);
}
export function FindingsIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<path d="M9 2L2 6v5c0 3.5 3 6 7 7 4-1 7-3.5 7-7V6L9 2z" />
<path d="M9 6v4" />
<circle cx="9" cy="12.5" r="0.5" fill="currentColor" stroke="none" />
</svg>
);
}
export function ScansIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<path d="M14.5 9A5.5 5.5 0 1 1 9 3.5" />
<polyline points="9 5 9 9 12 11" />
</svg>
);
}
export function RulesIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<path d="M4 5h10" />
<path d="M4 9h10" />
<path d="M4 13h10" />
<polyline points="2 4.5 2.8 5.5 4 4" />
<polyline points="2 8.5 2.8 9.5 4 8" />
<polyline points="2 12.5 2.8 13.5 4 12" />
</svg>
);
}
export function TriageIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<path d="M10 2L4 3v9l6 4 6-4V3l-6-1z" />
<path d="M10 6v4" />
<circle cx="10" cy="12.5" r="0.5" fill="currentColor" stroke="none" />
</svg>
);
}
export function ConfigIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<line x1="3" y1="5" x2="15" y2="5" />
<line x1="3" y1="9" x2="15" y2="9" />
<line x1="3" y1="13" x2="15" y2="13" />
<circle cx="6" cy="5" r="1.5" fill="var(--bg-secondary)" />
<circle cx="11" cy="9" r="1.5" fill="var(--bg-secondary)" />
<circle cx="7" cy="13" r="1.5" fill="var(--bg-secondary)" />
</svg>
);
}
export function ExplorerIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<path d="M3 3v12h12" />
<path d="M7 3v4h4V3" />
<path d="M7 11v4h4v-4" />
<path d="M11 7h4v4h-4" />
</svg>
);
}
export function DebugIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<polyline points="4 5 2 5 2 16 13 16 13 14" />
<polyline points="6 2 16 2 16 12 6 12 6 2" />
<path d="M9 5.5h4" />
<path d="M9 8h4" />
</svg>
);
}
export function SettingsIcon({ className, size = 18 }: IconProps) {
return (
<svg {...svgProps({ className, size })} viewBox="0 0 18 18">
<circle cx="9" cy="9" r="2.5" />
<path d="M9 1.5v2M9 14.5v2M1.5 9h2M14.5 9h2M3.7 3.7l1.4 1.4M12.9 12.9l1.4 1.4M14.3 3.7l-1.4 1.4M5.1 12.9l-1.4 1.4" />
</svg>
);
}
export function FolderIcon({ className, size = 14 }: IconProps) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 3.5C2 2.95 2.45 2.5 3 2.5h2.5l1.5 1.5H11c.55 0 1 .45 1 1v5.5c0 .55-.45 1-1 1H3c-.55 0-1-.45-1-1V3.5z" />
</svg>
);
}
export function TagIcon({ className, size = 14 }: IconProps) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1.5 7.8V2.5c0-.6.4-1 1-1h5.3L13 6.7l-5.3 5.3L1.5 7.8z" />
<circle cx="5" cy="5" r="0.8" fill="currentColor" stroke="none" />
</svg>
);
}
/** Map of icon name to component, for dynamic lookup */
export const ICONS: Record<string, FC<IconProps>> = {
overview: OverviewIcon,
findings: FindingsIcon,
scans: ScansIcon,
rules: RulesIcon,
triage: TriageIcon,
config: ConfigIcon,
explorer: ExplorerIcon,
debug: DebugIcon,
settings: SettingsIcon,
folder: FolderIcon,
tag: TagIcon,
};

View file

@ -0,0 +1,67 @@
import { useState, useCallback } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Sidebar } from './Sidebar';
import { HeaderBar } from './HeaderBar';
import { NewScanModal } from '../../modals/NewScanModal';
import { OverviewPage } from '../../pages/OverviewPage';
import { FindingsPage } from '../../pages/FindingsPage';
import { FindingDetailPage } from '../../pages/FindingDetailPage';
import { ScansPage } from '../../pages/ScansPage';
import { ScanDetailPage } from '../../pages/ScanDetailPage';
import { ScanComparePage } from '../../pages/ScanComparePage';
import { RulesPage } from '../../pages/RulesPage';
import { TriagePage } from '../../pages/TriagePage';
import { ConfigPage } from '../../pages/ConfigPage';
import { StubPage } from '../../pages/StubPage';
import { ExplorerPage } from '../../pages/ExplorerPage';
import { DebugLayout } from '../../pages/debug/DebugLayout';
import { CallGraphPage } from '../../pages/debug/CallGraphPage';
import { SummaryExplorerPage } from '../../pages/debug/SummaryExplorerPage';
export function AppLayout() {
const [scanModalOpen, setScanModalOpen] = useState(false);
const handleStartScan = useCallback(() => {
setScanModalOpen(true);
}, []);
return (
<div id="app">
<Sidebar />
<div className="main-panel">
<HeaderBar onStartScan={handleStartScan} />
<main className="content">
<Routes>
<Route path="/" element={<OverviewPage />} />
<Route path="/findings" element={<FindingsPage />} />
<Route path="/findings/:id" element={<FindingDetailPage />} />
<Route path="/scans" element={<ScansPage />} />
<Route
path="/scans/compare/:left/:right"
element={<ScanComparePage />}
/>
<Route path="/scans/:id" element={<ScanDetailPage />} />
<Route path="/rules" element={<RulesPage />} />
<Route path="/rules/:id" element={<RulesPage />} />
<Route path="/triage" element={<TriagePage />} />
<Route path="/config" element={<ConfigPage />} />
<Route path="/explorer" element={<ExplorerPage />} />
<Route path="/debug" element={<DebugLayout />}>
<Route
index
element={<Navigate to="/debug/call-graph" replace />}
/>
<Route path="call-graph" element={<CallGraphPage />} />
<Route path="summaries" element={<SummaryExplorerPage />} />
</Route>
<Route path="/settings" element={<StubPage />} />
</Routes>
</main>
</div>
<NewScanModal
open={scanModalOpen}
onClose={() => setScanModalOpen(false)}
/>
</div>
);
}

View file

@ -0,0 +1,90 @@
import { Link, useLocation } from 'react-router-dom';
const SECTION_TITLES: Record<string, string> = {
overview: 'Overview',
findings: 'Findings',
scans: 'Scans',
rules: 'Rules',
triage: 'Triage',
config: 'Config',
explorer: 'Explorer',
debug: 'Debug',
settings: 'Settings',
};
const ROUTE_TITLES: Record<string, string> = {
'/debug/cfg': 'CFG Viewer',
'/debug/ssa': 'SSA Viewer',
'/debug/call-graph': 'Call Graph',
'/debug/taint': 'Taint Debugger',
};
function pathToSection(pathname: string): string {
if (pathname === '/') return 'overview';
const first = pathname.split('/')[1];
return first || 'overview';
}
function buildBreadcrumbs(pathname: string) {
const section = pathToSection(pathname);
const sectionTitle = SECTION_TITLES[section] ?? section;
const crumbs: Array<{ label: string; path?: string }> = [];
// Always show section as root breadcrumb
const sectionPath = section === 'overview' ? '/' : `/${section}`;
crumbs.push({ label: sectionTitle, path: sectionPath });
// If we have a sub-route, show it
if (ROUTE_TITLES[pathname]) {
crumbs.push({ label: ROUTE_TITLES[pathname] });
} else {
const parts = pathname.split('/').filter(Boolean);
if (parts.length > 1) {
// e.g. /findings/123 or /scans/compare/1/2
const sub = parts.slice(1).join('/');
crumbs.push({ label: sub });
}
}
return crumbs;
}
interface HeaderBarProps {
onStartScan?: () => void;
}
export function HeaderBar({ onStartScan }: HeaderBarProps) {
const { pathname } = useLocation();
const crumbs = buildBreadcrumbs(pathname);
return (
<header className="header-bar">
<div className="header-left">
<nav className="breadcrumbs">
{crumbs.map((crumb, i) => {
const isLast = i === crumbs.length - 1;
return (
<span key={i}>
{i > 0 && <span className="breadcrumb-sep">/</span>}
{isLast || !crumb.path ? (
<span className="breadcrumb-current">{crumb.label}</span>
) : (
<Link to={crumb.path} className="breadcrumb-link">
{crumb.label}
</Link>
)}
</span>
);
})}
</nav>
</div>
<div className="header-right">
{onStartScan && (
<button className="btn btn-primary btn-sm" onClick={onStartScan}>
Start Scan
</button>
)}
</div>
</header>
);
}

View file

@ -0,0 +1,178 @@
import { NavLink } from 'react-router-dom';
import {
OverviewIcon,
FindingsIcon,
ScansIcon,
RulesIcon,
TriageIcon,
ConfigIcon,
ExplorerIcon,
DebugIcon,
SettingsIcon,
FolderIcon,
TagIcon,
} from '../icons/Icons';
import type { FC } from 'react';
import type { IconProps } from '../icons/Icons';
import { useHealth } from '../../api/queries/health';
import { useSSE } from '../../contexts/SSEContext';
interface NavItem {
id: string;
label: string;
path: string;
Icon: FC<IconProps>;
group: 'primary' | 'secondary' | 'footer';
}
const NAV_SECTIONS: NavItem[] = [
{
id: 'overview',
label: 'Overview',
path: '/',
Icon: OverviewIcon,
group: 'primary',
},
{
id: 'findings',
label: 'Findings',
path: '/findings',
Icon: FindingsIcon,
group: 'primary',
},
{
id: 'scans',
label: 'Scans',
path: '/scans',
Icon: ScansIcon,
group: 'primary',
},
{
id: 'rules',
label: 'Rules',
path: '/rules',
Icon: RulesIcon,
group: 'primary',
},
{
id: 'triage',
label: 'Triage',
path: '/triage',
Icon: TriageIcon,
group: 'primary',
},
{
id: 'config',
label: 'Config',
path: '/config',
Icon: ConfigIcon,
group: 'secondary',
},
{
id: 'explorer',
label: 'Explorer',
path: '/explorer',
Icon: ExplorerIcon,
group: 'secondary',
},
{
id: 'debug',
label: 'Debug',
path: '/debug',
Icon: DebugIcon,
group: 'secondary',
},
{
id: 'settings',
label: 'Settings',
path: '/settings',
Icon: SettingsIcon,
group: 'footer',
},
];
function navLinkClass({ isActive }: { isActive: boolean }) {
return `nav-link${isActive ? ' active' : ''}`;
}
export function Sidebar() {
const { data: health } = useHealth();
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');
return (
<aside className="sidebar">
<div className="sidebar-header">
<span className="logo">nyx</span>
{health?.version && <span className="version">v{health.version}</span>}
</div>
<ul className="nav-list">
{primary.map((item) => (
<li key={item.id}>
<NavLink
to={item.path}
end={item.path === '/'}
className={navLinkClass}
>
<span className="nav-icon">
<item.Icon />
</span>
<span>{item.label}</span>
</NavLink>
</li>
))}
<li className="nav-separator" />
{secondary.map((item) => (
<li key={item.id}>
<NavLink to={item.path} className={navLinkClass}>
<span className="nav-icon">
<item.Icon />
</span>
<span>{item.label}</span>
</NavLink>
</li>
))}
</ul>
<div className="sidebar-footer">
<ul className="nav-list" style={{ flex: 'none' }}>
{footer.map((item) => (
<li key={item.id}>
<NavLink to={item.path} className={navLinkClass}>
<span className="nav-icon">
<item.Icon />
</span>
<span>{item.label}</span>
</NavLink>
</li>
))}
</ul>
</div>
<div className="sidebar-meta">
{health?.scan_root && (
<div className="sidebar-meta-item" title={health.scan_root}>
<FolderIcon />
<span>{health.scan_root}</span>
</div>
)}
{health?.version && (
<div className="sidebar-meta-item">
<TagIcon />
<span>v{health.version}</span>
</div>
)}
<div className={`scan-indicator${isScanRunning ? ' visible' : ''}`}>
<span className="status-dot running" />
Scanning...
</div>
</div>
</aside>
);
}

View file

@ -0,0 +1,103 @@
import {
useCallback,
useEffect,
useRef,
useState,
type ReactNode,
} from 'react';
interface DropdownProps {
trigger: (opts: { open: boolean }) => ReactNode;
children: (opts: { close: () => void }) => ReactNode;
align?: 'left' | 'right';
className?: string;
}
export function Dropdown({
trigger,
children,
align = 'left',
className,
}: DropdownProps) {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
const close = useCallback(() => setOpen(false), []);
useEffect(() => {
if (!open) return;
const handlePointer = (e: MouseEvent) => {
if (!rootRef.current) return;
if (!rootRef.current.contains(e.target as Node)) setOpen(false);
};
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', handlePointer);
document.addEventListener('keydown', handleKey);
return () => {
document.removeEventListener('mousedown', handlePointer);
document.removeEventListener('keydown', handleKey);
};
}, [open]);
return (
<div
ref={rootRef}
className={`dropdown${open ? ' dropdown--open' : ''}${className ? ` ${className}` : ''}`}
>
<div
className="dropdown-trigger"
onClick={() => setOpen((v) => !v)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setOpen((v) => !v);
}
}}
>
{trigger({ open })}
</div>
{open && (
<div className={`dropdown-menu dropdown-menu--${align}`} role="menu">
{children({ close })}
</div>
)}
</div>
);
}
interface DropdownItemProps {
onClick: () => void;
children: ReactNode;
checked?: boolean;
hint?: string;
tone?: 'default' | 'warning';
}
export function DropdownItem({
onClick,
children,
checked,
hint,
tone = 'default',
}: DropdownItemProps) {
return (
<button
type="button"
role="menuitem"
className={`dropdown-item dropdown-item--${tone}`}
onClick={onClick}
>
<span className="dropdown-item-check" aria-hidden>
{checked ? '✓' : ''}
</span>
<span className="dropdown-item-label">{children}</span>
{hint && <span className="dropdown-item-hint">{hint}</span>}
</button>
);
}

View file

@ -0,0 +1,17 @@
import type { ReactNode } from 'react';
interface EmptyStateProps {
message?: string;
children?: ReactNode;
icon?: ReactNode;
}
export function EmptyState({ message, children, icon }: EmptyStateProps) {
return (
<div className="empty-state">
{icon && <div className="empty-state-icon">{icon}</div>}
{message && <p>{message}</p>}
{children}
</div>
);
}

View file

@ -0,0 +1,13 @@
interface ErrorStateProps {
title?: string;
message: string;
}
export function ErrorState({ title = 'Error', message }: ErrorStateProps) {
return (
<div className="error-state">
<h3>{title}</h3>
<p>{message}</p>
</div>
);
}

View file

@ -0,0 +1,7 @@
interface LoadingStateProps {
message?: string;
}
export function LoadingState({ message = 'Loading...' }: LoadingStateProps) {
return <div className="loading">{message}</div>;
}

View file

@ -0,0 +1,38 @@
import { useEffect, useCallback, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
open: boolean;
onClose: () => void;
className?: string;
children: ReactNode;
}
export function Modal({ open, onClose, className, children }: ModalProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
},
[onClose],
);
useEffect(() => {
if (!open) return;
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [open, handleKeyDown]);
if (!open) return null;
return createPortal(
<div
className={className || 'code-modal-overlay'}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
{children}
</div>,
document.body,
);
}

View file

@ -0,0 +1,75 @@
interface PaginationProps {
page: number;
perPage: number;
total: number;
onPageChange: (page: number) => void;
onPerPageChange?: (perPage: number) => void;
}
const PER_PAGE_OPTIONS = [25, 50, 100];
export function Pagination({
page,
perPage,
total,
onPageChange,
onPerPageChange,
}: PaginationProps) {
const totalPages = Math.ceil(total / perPage) || 1;
return (
<div className="pagination">
<div className="pagination-left">
<span>Per page:</span>
<select
value={perPage}
onChange={(e) => onPerPageChange?.(Number(e.target.value))}
>
{PER_PAGE_OPTIONS.map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</div>
<div className="pagination-center">
<button
className="btn btn-sm"
disabled={page <= 1}
onClick={() => onPageChange(1)}
>
First
</button>
<button
className="btn btn-sm"
disabled={page <= 1}
onClick={() => onPageChange(Math.max(1, page - 1))}
>
Prev
</button>
<span>
Page {page} of {totalPages}
</span>
<button
className="btn btn-sm"
disabled={page >= totalPages}
onClick={() => onPageChange(Math.min(totalPages, page + 1))}
>
Next
</button>
<button
className="btn btn-sm"
disabled={page >= totalPages}
onClick={() => onPageChange(totalPages)}
>
Last
</button>
</div>
<div className="pagination-right">
<span>{total} total</span>
</div>
</div>
);
}

View file

@ -0,0 +1,32 @@
interface StatCardProps {
label: string;
value: string | number;
delta?: number | null;
color?: string;
subtitle?: string;
}
export function StatCard({
label,
value,
delta,
color,
subtitle,
}: StatCardProps) {
const colorStyle = color ? { color } : undefined;
return (
<div className="overview-stat-card">
<div className="stat-label">{label}</div>
<div className="stat-value" style={colorStyle}>
{value}
{delta != null && delta !== 0 && (
<span className={`stat-delta delta-${delta > 0 ? 'up' : 'down'}`}>
{delta > 0 ? '\u25B2' : '\u25BC'}&nbsp;{Math.abs(delta)}
</span>
)}
</div>
{subtitle && <div className="stat-subtitle">{subtitle}</div>}
</div>
);
}