diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..3e38a6e4 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,19 @@ +# nextest configuration +# +# See https://nexte.st/docs/configuration/ for the full schema. + +# ── Test groups ────────────────────────────────────────────────────────────── +# +# `hostile-input-timing` serialises the two timing-bounded +# `hostile_input_tests` cases that pass under nextest in isolation but fail +# under the full-suite parallel run on darwin (resource contention from the +# other ~4000 tests pushes them past their internal budget). Pinning them to +# a single thread within their own group keeps their wall-clock predictable +# without slowing the rest of the suite. + +[test-groups] +hostile-input-timing = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'binary(hostile_input_tests) and (test(very_long_single_line_parses) or test(many_small_functions_do_not_explode))' +test-group = 'hostile-input-timing' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..7a48f23d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @elicpeter diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..b6cb49a9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: elicpeter diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..5637b153 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,75 @@ +name: Bug report +description: Report a crash, incorrect output, or other broken behavior in Nyx. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug. **Please do not file security vulnerabilities here** — use the private advisory link in SECURITY.md. + + For false positives or missed detections (rule quality), this is the right place — those are quality bugs. + - type: textarea + id: summary + attributes: + label: Summary + description: One or two sentences describing what's wrong. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Reproduction + description: Minimal source snippet or repo + the exact `nyx` command you ran. The smaller, the better — ideally a single file. + render: shell + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: Include the finding (or lack of finding), error output, or stack trace. + validations: + required: true + - type: input + id: version + attributes: + label: Nyx version + description: Output of `nyx --version`. + placeholder: "nyx 0.7.0" + validations: + required: true + - type: input + id: os + attributes: + label: OS / arch + placeholder: "macOS 14.5 arm64" + validations: + required: true + - type: dropdown + id: language + attributes: + label: Target language (if applicable) + options: + - "n/a" + - JavaScript / TypeScript + - Python + - Java + - Go + - Ruby + - PHP + - Rust + - C / C++ + - Other + validations: + required: false + - type: textarea + id: extra + attributes: + label: Additional context + description: Logs, screenshots, related issues — anything else that helps. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..18ae00e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/elicpeter/nyx/security/advisories/new + about: Do NOT file public issues for security bugs. Use private disclosure (see SECURITY.md). + - name: Question or discussion + url: https://github.com/elicpeter/nyx/discussions + about: Open-ended questions, ideas, or help using Nyx belong in Discussions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..10897d6b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,27 @@ +name: Feature request +description: Suggest a new capability, rule, language, or UX improvement. +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What are you trying to do that Nyx can't do today? Concrete scenarios beat abstract wishes. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: How should it work? Sketches, example commands, or example findings are welcome. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches you've thought about, and why they don't fit. + - type: textarea + id: extra + attributes: + label: Additional context diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..bf15c9bc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +## Summary + + + +## Related issues + + + +## Checklist + +- [ ] `cargo test --bin nyx` passes +- [ ] `cargo clippy --all -- -D warnings` is clean +- [ ] `cargo fmt -- --check` passes +- [ ] User-visible changes are noted in `CHANGELOG.md` under `## [Unreleased]` +- [ ] Docs updated if behavior, flags, or config changed (`docs/`, `README.md`, `CONTRIBUTING.md`) +- [ ] New rules / language support include fixtures and integration tests + +## Notes for reviewers + + diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..193e4576 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,6 @@ +name: "CodeQL Config" + +paths-ignore: + - examples + - tests + - benches diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..bcbfbe3f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + cargo-minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + groups: + actions-minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: npm + directory: "/frontend" + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + frontend-minor-and-patch: + update-types: + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d64e40e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,404 @@ +name: CI + +permissions: + contents: read + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + frontend: + name: frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Frontend license check + working-directory: frontend + run: npm run license:check + + - name: Frontend format check + working-directory: frontend + run: npm run format:check + + - name: Frontend lint + working-directory: frontend + run: npm run lint + + - name: Frontend type check + working-directory: frontend + run: npm run typecheck + + - name: Frontend tests + working-directory: frontend + run: npm test + + - name: Frontend build + working-directory: frontend + run: npm run build + + rustfmt: + name: rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt + cache: true + + - name: Format check + run: cargo fmt --all -- --check + + clippy-stable: + name: clippy-stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: clippy + cache: true + + - name: Lint (Clippy) + run: cargo clippy --all-targets --all-features -- -D warnings + + cargo-deny: + name: cargo-deny + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@cargo-deny + + - name: License & advisory checks + run: cargo deny check advisories licenses bans sources + + unused-deps: + name: unused-deps + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: bnjbvr/cargo-machete@v0.9.2 + + third-party-licenses: + name: third-party-licenses + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-about@0.7.1 + + - name: Prime cargo registry cache + run: cargo fetch --locked + + - name: Regenerate license attribution + run: cargo about generate --offline about.hbs | tr -d '\r' > /tmp/THIRDPARTY-LICENSES.html + + - name: Diff against committed file + run: diff -u --strip-trailing-cr THIRDPARTY-LICENSES.html /tmp/THIRDPARTY-LICENSES.html + + docs-fresh: + name: docs-fresh + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - name: Regenerate rule reference + run: cargo run --features docgen --bin nyx-docgen + + - name: Verify docs/rules.md is fresh + run: | + if ! git diff --exit-code docs/rules.md; then + echo "::error::docs/rules.md is stale. Run 'cargo run --features docgen --bin nyx-docgen' and commit the result." + exit 1 + fi + + rustdoc: + name: rustdoc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - name: Check rustdoc links + env: + RUSTDOCFLAGS: "-D warnings" + run: cargo doc --workspace --no-deps --all-features + + rust-beta-build: + name: rust-beta-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: beta + cache: true + + - name: Beta compile compatibility check + run: cargo check --all-features --tests + + msrv: + name: msrv + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: "1.88" + cache: true + + - name: Compile check at MSRV + run: cargo check --all-features --tests + + rust-stable-test-linux-without-docker: + name: rust-stable-test / linux-without-docker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Rust tests (stable, no docker) + run: cargo nextest run --no-fail-fast --all-features + + rust-stable-test-linux-with-docker: + name: rust-stable-test / linux-with-docker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Pull language images for sandbox tests + run: | + docker pull python:3-slim + docker pull node:20-slim + docker pull eclipse-temurin:21-jre-jammy + docker pull php:8-cli + + - name: Smoke-test interpreter availability + run: | + docker run --rm python:3-slim python3 --version + docker run --rm node:20-slim node --version + docker run --rm eclipse-temurin:21-jre-jammy java -version + docker run --rm php:8-cli php --version + + - name: Rust tests with docker (sandbox escape gate) + run: cargo nextest run --no-fail-fast --all-features --test dynamic_sandbox_escape --test dynamic_parity + + escape-positive-control: + name: escape-positive-control + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Pull python image + run: docker pull python:3-slim + + - name: Escape positive control (gate wiring check) + run: | + cargo nextest run --no-fail-fast --all-features --test dynamic_sandbox_escape \ + -- --include-ignored positive_control_cap_sys_admin + + cross-platform-smoke: + name: cross-platform-smoke + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Build + run: cargo build --release --all-features + + - name: Smoke tests + run: cargo nextest run --no-fail-fast --all-features --test integration_tests --test pattern_tests --test cli_validation_tests + + rust-beta-test: + name: rust-beta-test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: beta + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Rust tests (beta) + run: cargo nextest run --no-fail-fast --all-features + + cargo-package: + name: cargo-package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - name: Build frontend + working-directory: frontend + run: | + npm ci + npm run build + + - name: Verify dist embedded in package + run: | + for f in src/server/assets/dist/index.html src/server/assets/dist/app.js src/server/assets/dist/style.css src/server/assets/favicon.svg default-nyx.conf build.rs; do + if ! cargo package --list --allow-dirty | grep -qx "$f"; then + echo "::error::missing from cargo package: $f" + exit 1 + fi + done + + - name: cargo package (verify build) + run: cargo package --allow-dirty + + benchmark-gate: + name: benchmark-gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + cache-key: benchmark-gate-release + + - uses: taiki-e/install-action@nextest + + - name: Build benchmark + perf test binaries + run: cargo nextest run --release --all-features --test benchmark_test --test perf_tests --no-run + + - name: Accuracy regression gate (P/R/F1) + run: cargo nextest run --no-fail-fast --release --all-features --test benchmark_test --run-ignored only --no-capture benchmark_evaluation + + - name: Performance regression gate + env: + NYX_CI_BENCH: "1" + run: cargo nextest run --no-fail-fast --release --all-features --test perf_tests --no-capture + + - name: Upload benchmark results + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmark-results + path: tests/benchmark/results/latest.json + if-no-files-found: warn + + corpus-marker-audit: + name: corpus-marker-audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Marker collision audit (§16.3) + run: python3 scripts/corpus_dashboard.py + # Exits non-zero if any oracle marker from one cap appears in another + # cap's payload bytes. This catches cross-cap oracle collisions that + # would cause false-positive confirmed verdicts. + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Corpus unit tests (no_marker_collisions, all_payloads_have_fixture_paths) + run: cargo nextest run --no-fail-fast --lib -p nyx-scanner dynamic::corpus + env: + RUST_LOG: error + + - name: Corpus dashboard sync check (Python/Rust payload table parity) + run: python3 scripts/check_corpus_sync.py diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..5a7ea414 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,45 @@ +name: "CodeQL Advanced" + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + schedule: + - cron: "0 9 * * 2" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + security-events: write + packages: read + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: rust + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/corpus_promote.yml b/.github/workflows/corpus_promote.yml new file mode 100644 index 00000000..744c7109 --- /dev/null +++ b/.github/workflows/corpus_promote.yml @@ -0,0 +1,167 @@ +name: Corpus Promote + +# Weekly automated promotion-PR template. +# +# Scans fuzz-discovered/ for candidates not yet in src/dynamic/corpus.rs +# and opens a PR proposing them for human review (§16.4 — no auto-merge). +# +# Also runs the marker-collision audit as a hard gate: if any collision is +# found the workflow fails rather than proposing the promotion. + +on: + schedule: + # Sundays at 09:00 UTC — offset from the fuzz run (06:00 UTC) so + # discovered candidates are ready before the promotion job runs. + - cron: "0 9 * * 0" + workflow_dispatch: + inputs: + dry_run: + description: "Dry run (print PR body but do not open)" + required: false + default: "false" + +permissions: + contents: write + pull-requests: write + +concurrency: + group: corpus-promote + cancel-in-progress: true + +jobs: + promote: + name: Propose corpus promotions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Build frontend + working-directory: frontend + run: | + npm ci + npm run build + + # ── Marker collision audit ────────────────────────────────────────────── + - name: Marker collision audit + run: | + set -euo pipefail + cargo build --features dynamic -p nyx-scanner 2>/dev/null || true + cd fuzz/dynamic_corpus + cargo run -- audit-markers + env: + RUST_LOG: error + + # ── Discover candidates ───────────────────────────────────────────────── + - name: Find promotion candidates + id: candidates + run: | + set -euo pipefail + count=0 + files="" + if [ -d fuzz-discovered ]; then + while IFS= read -r f; do + # Skip .gitkeep, sidecar JSONs, and files already listed in corpus.rs. + [[ "$f" == *".gitkeep" ]] && continue + [[ "$f" == *".json" ]] && continue + bytes=$(xxd -p "$f" | tr -d '\n') + if ! grep -q "$bytes" src/dynamic/corpus.rs 2>/dev/null; then + count=$((count + 1)) + files="$files $f" + fi + done < <(find fuzz-discovered -type f | sort) + fi + echo "count=$count" >> "$GITHUB_OUTPUT" + echo "files=$files" >> "$GITHUB_OUTPUT" + + - name: Skip if no new candidates + if: steps.candidates.outputs.count == '0' + run: | + echo "No new candidates found in fuzz-discovered/. Nothing to promote." + + # ── Open promotion PR ─────────────────────────────────────────────────── + - name: Open promotion PR + if: > + steps.candidates.outputs.count != '0' && + github.event.inputs.dry_run != 'true' + env: + GH_TOKEN: ${{ github.token }} + CANDIDATE_COUNT: ${{ steps.candidates.outputs.count }} + CANDIDATE_FILES: ${{ steps.candidates.outputs.files }} + run: | + set -euo pipefail + branch="corpus-promote-$(date +%Y%m%d)" + git checkout -b "$branch" + + # Stage candidate files into fuzz-discovered (already there). + # The PR body provides the reviewer with everything they need. + + # Build PR body into a temp file to avoid shell re-interpolation of + # sidecar JSON content (which may contain backticks or $(...) sequences). + body_file=$(mktemp) + + cat > "$body_file" <<'PREAMBLE' + ## Corpus Promotion Proposal + + This PR was generated automatically by the weekly corpus-promote workflow. + It does **not** auto-merge — a human reviewer must approve each candidate + before it can land in `src/dynamic/corpus.rs` (§16.4). + + ### Candidates + + The following payloads were discovered by the internal mutation fuzzer and + confirmed via `sink_hit && oracle_fired` against instrumented fixtures: + + PREAMBLE + + for f in $CANDIDATE_FILES; do + sidecar="${f}.json" + printf -- '- `%s`\n' "$f" >> "$body_file" + if [ -f "$sidecar" ]; then + printf ' ```json\n' >> "$body_file" + cat "$sidecar" >> "$body_file" + printf '\n ```\n' >> "$body_file" + fi + done + + cat >> "$body_file" <<'CHECKLIST' + + ### Review checklist + + - [ ] Bytes are a genuine attack vector, not a fixture artifact + - [ ] Oracle marker is unique (no collision with other caps) + - [ ] `fixture_paths` updated in `src/dynamic/corpus.rs` + - [ ] `since_corpus_version` set to next version + - [ ] `CORPUS_VERSION` bumped and bump history updated + + _Generated by corpus_promote.yml — do not auto-merge._ + CHECKLIST + + git add fuzz-discovered/ || true + git diff --cached --quiet || git commit -m "chore: add ${CANDIDATE_COUNT} fuzzer-discovered corpus candidates" + + git push origin "$branch" + + gh pr create \ + --title "chore(corpus): promote ${CANDIDATE_COUNT} fuzzer-discovered payload(s)" \ + --body "$(cat "$body_file")" \ + --base master \ + --label "corpus-promotion" || true + + rm -f "$body_file" + + - name: Dry run summary + if: github.event.inputs.dry_run == 'true' + run: | + echo "Dry run: would promote ${{ steps.candidates.outputs.count }} candidate(s)." + echo "Files: ${{ steps.candidates.outputs.files }}" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..170c8012 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,30 @@ +name: Dependabot auto-merge + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + # Skip fork PRs entirely (the merge would fail anyway, but no need to run). + if: >- + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable auto-merge for patch and minor updates + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-patch' || + steps.metadata.outputs.update-type == 'version-update:semver-minor' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..bcc9b344 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,53 @@ +name: docs + +on: + push: + branches: [master] + paths: + - "docs/**" + - "book.toml" + - ".github/workflows/docs.yml" + - "assets/screenshots/**" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - name: Cache mdbook + id: cache-mdbook + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/mdbook + key: mdbook-0.5.2-${{ runner.os }} + + - name: Install mdbook + if: steps.cache-mdbook.outputs.cache-hit != 'true' + run: cargo install mdbook --version 0.5.2 --locked + + - name: Build + run: mdbook build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: book + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/dynamic.yml b/.github/workflows/dynamic.yml new file mode 100644 index 00000000..da2cdc95 --- /dev/null +++ b/.github/workflows/dynamic.yml @@ -0,0 +1,146 @@ +# Phase 29 (Track I): dedicated dynamic-verification matrix. +# +# Three rows exercise the dynamic harness pipeline (`cargo nextest run +# --features dynamic`) under the host configurations the Phase 17–28 +# tracks documented as supported: +# +# linux-process-only — Ubuntu host, no docker daemon. Forces the +# process backend and exercises the Phase 17 +# Linux hardening primitives (chroot, seccomp, +# unshare, no_new_privs). `libc6-dev` is +# installed so the hardening probe + escape +# suite can `cc -static`; without it the +# chroot-leg of the escape suite skips silently +# (Phase 20 follow-up #4 in deferred.md). +# +# linux-with-docker — Ubuntu host with the runner Docker daemon. Exercises +# the docker backend (Phase 19) and the +# differential-confirmation parity tests. +# +# macos — macOS-latest, no docker. Exercises the +# Phase-18 `sandbox-exec` primitives plus the +# process backend on Darwin. Track-I acceptance +# literal: "cargo nextest run --features dynamic +# is green on macOS without docker." + +name: dynamic + +permissions: + contents: read + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + linux-process-only: + name: dynamic / linux-process-only + runs-on: ubuntu-latest + env: + # Force the process backend even when callers default to Auto so + # docker-unavailable paths cannot accidentally hide a regression. + NYX_SANDBOX_BACKEND: process + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + # Phase 17 / Phase 20 follow-up: the hardening probe + escape + # suite chroot leg need static glibc. Without these packages the + # `cc -static probe.c` step in tests/sandbox_hardening_linux.rs + + # tests/sandbox_escape_suite.rs falls back to dynamic linking and + # the chroot leg silently skips. + - name: Install fixture prerequisites (static libc) + run: | + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends libc6-dev libc-dev-bin + + - name: Smoke-test interpreter availability + run: | + python3 --version + node --version || sudo apt-get install -y --no-install-recommends nodejs + ruby --version || true + php --version || true + + - name: Dynamic suite (process backend only) + run: cargo nextest run --no-fail-fast --features dynamic + + linux-with-docker: + name: dynamic / linux-with-docker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Install fixture prerequisites (static libc) + run: | + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends libc6-dev libc-dev-bin + + - name: Pull language images for sandbox tests + run: | + docker pull python:3-slim + docker pull node:20-slim + docker pull eclipse-temurin:21-jre-jammy + docker pull php:8-cli + + - name: Smoke-test docker interpreter availability + run: | + docker run --rm python:3-slim python3 --version + docker run --rm node:20-slim node --version + docker run --rm eclipse-temurin:21-jre-jammy java -version + docker run --rm php:8-cli php --version + + - name: Dynamic suite (process + docker backends) + run: cargo nextest run --no-fail-fast --features dynamic + + macos: + name: dynamic / macos + runs-on: macos-latest + env: + # macOS runners ship without docker; force process backend so the + # `Auto` resolver in src/dynamic/sandbox.rs cannot accidentally + # pick up a stray Lima/Colima daemon and confuse the matrix. + NYX_SANDBOX_BACKEND: process + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + - name: Smoke-test sandbox-exec availability + run: | + /usr/bin/sandbox-exec -p '(version 1)(allow default)' /bin/echo ok + + - name: Smoke-test interpreter availability + run: | + python3 --version + node --version + ruby --version + + # Phase 29 acceptance literal: "cargo nextest run --features + # dynamic is green on macOS without docker (process-only row)." + - name: Dynamic suite (macOS, process backend) + run: cargo nextest run --no-fail-fast --features dynamic diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml new file mode 100644 index 00000000..b7ea9200 --- /dev/null +++ b/.github/workflows/eval.yml @@ -0,0 +1,348 @@ +# Real-corpus acceptance (Track R). +# +# * owasp (Phase 27 / Track R.0): Gate 6 vs a real OWASP BenchmarkJava +# checkout (Java). +# * jsts (Phase 28 / Track R.1): Gate 7 vs OWASP NodeGoat (Express, .js) +# and OWASP Juice Shop (TypeScript, .ts), one matrix row per corpus. +# * polyglot (Phase 29 / Track R.2): Gate 8 vs OWASP RailsGoat (Rails, .rb), +# DVWA (PHP), DVPWA (aiohttp, .py), gosec (Go) and the RustSec advisory-db +# (Rust negative control), one matrix row per corpus. +# +# Runs on every PR that touches the dynamic verifier (src/dynamic/), the +# eval-corpus harness (tests/eval_corpus/), or the gate script itself. +# +# Each gate enforces, against the committed ground truth: +# * verify wall-clock <= 15 min (CI budget; the dev reference is 10 min), +# * the per-(cap,lang) budget in tests/eval_corpus/budget.toml, +# * per-cap confirmed-rate / precision / recall — hard-gated only for caps +# in NYX_*_FLOOR_CAPS (empty by default → published report-only until a +# cap Confirms end to end), with destinations >= 40% / >= 0.85 / >= 0.40. +# +# No corpus is vendored. Each is cloned at a pinned ref and cached so reruns +# skip the clone. Before the gate runs, the committed ground truth is +# regenerated from its source against the fresh clone and asserted in sync, +# and the converter hard-errors on any labelled path missing from the corpus, +# so a corpus bump that drifts the labels fails the job loudly. + +name: eval + +permissions: + contents: read + +on: + push: + branches: ["master"] + paths: + - "src/dynamic/**" + - "tests/eval_corpus/**" + - "scripts/m7_ship_gate.sh" + - ".github/workflows/eval.yml" + pull_request: + branches: ["master"] + paths: + - "src/dynamic/**" + - "tests/eval_corpus/**" + - "scripts/m7_ship_gate.sh" + - ".github/workflows/eval.yml" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + owasp: + name: eval / owasp-benchmark-v1.2 + runs-on: ubuntu-latest + env: + # Gate 6 self-skips unless this points at a real checkout. + NYX_OWASP_CORPUS: ${{ github.workspace }}/.eval-corpus/owasp_benchmark_v1.2 + # CI wall-clock budget: 20 min. The 2740-file OWASP scan+verify lands + # right at the old 15-min ceiling on the hosted runners (observed 900.2s), + # so the gate tripped on CI variance alone; 1200s restores headroom. The + # dev reference stays 10 min — override locally to tighten. + NYX_OWASP_WALLCLOCK_BUDGET_SECONDS: "1200" + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + # The Phase 22 Java compile pool drives `com.sun.tools.javac` out of a + # warm JDK; temurin 21 ships the compiler module the pool loads. + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "21" + + - name: Cache OWASP BenchmarkJava (1.2beta) + id: cache-owasp + uses: actions/cache@v5 + with: + path: .eval-corpus/owasp_benchmark_v1.2 + key: owasp-benchmark-1.2beta + + - name: Clone OWASP BenchmarkJava (1.2beta tag) + if: steps.cache-owasp.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch 1.2beta \ + https://github.com/OWASP-Benchmark/BenchmarkJava \ + .eval-corpus/owasp_benchmark_v1.2 + + # No-compromise guard: the committed ground truth must be exactly what a + # fresh conversion of the pinned CSV produces. Catches GT drift (a + # corpus bump, a hand-edit) before the gate runs on stale labels. + - name: Verify ground truth is in sync with the pinned corpus + run: | + python3 tests/eval_corpus/owasp_gt_convert.py \ + --corpus-dir .eval-corpus/owasp_benchmark_v1.2 \ + --output /tmp/owasp_gt_regen.json + python3 - <<'PY' + import json, sys + committed = json.load(open("tests/eval_corpus/ground_truth/owasp_benchmark_v1.2.json")) + regen = json.load(open("/tmp/owasp_gt_regen.json")) + if committed != regen: + sys.exit("committed ground truth diverges from a fresh conversion of " + "the 1.2beta CSV; regenerate with owasp_gt_convert.py") + print(f"ground truth in sync: {len(committed)} records") + PY + + - name: eval-corpus harness regression tests + run: | + python3 tests/eval_corpus/test_tabulate_regression.py + python3 tests/eval_corpus/test_manifest_gt_convert.py + + - name: Gate 6 — OWASP Benchmark v1.2 acceptance + run: scripts/m7_ship_gate.sh --sets owasp + + jsts: + name: eval / ${{ matrix.corpus.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + corpus: + - name: nodegoat + repo: https://github.com/OWASP/NodeGoat + # NodeGoat ships no release tags; pin the default branch and let + # the cache key hold it stable. The manifest's path layout + # (app/, config/) has been constant for years. + ref: master + env: NYX_NODEGOAT_CORPUS + manifest: nodegoat.manifest.toml + ground_truth: nodegoat.json + - name: juiceshop + repo: https://github.com/juice-shop/juice-shop + ref: v15.0.0 + env: NYX_JUICESHOP_CORPUS + manifest: juiceshop.manifest.toml + ground_truth: juiceshop.json + env: + # CI wall-clock budget: 15 min. Override locally to tighten. + NYX_JSTS_WALLCLOCK_BUDGET_SECONDS: "900" + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + # The dynamic verifier's Node build pool (Phase 23) compiles its + # harnesses with a real node/npm toolchain. + - name: Set up Node 20 + uses: actions/setup-node@v6 + with: + node-version: "20" + + - name: Cache ${{ matrix.corpus.name }} + id: cache-corpus + uses: actions/cache@v5 + with: + path: .eval-corpus/${{ matrix.corpus.name }} + key: jsts-${{ matrix.corpus.name }}-${{ matrix.corpus.ref }} + + - name: Clone ${{ matrix.corpus.name }} (${{ matrix.corpus.ref }}) + if: steps.cache-corpus.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch ${{ matrix.corpus.ref }} \ + ${{ matrix.corpus.repo }} \ + .eval-corpus/${{ matrix.corpus.name }} + + # No-compromise guard: the committed ground truth must be exactly what a + # fresh conversion of the curated manifest produces *against this + # corpus*. manifest_gt_convert.py hard-errors on any labelled path that + # no longer exists in the clone (corpus drift / typo), and the diff + # below catches a stale committed JSON. + - name: Verify ground truth is in sync with the pinned corpus + run: | + python3 tests/eval_corpus/manifest_gt_convert.py \ + --manifest tests/eval_corpus/ground_truth/${{ matrix.corpus.manifest }} \ + --corpus-dir .eval-corpus/${{ matrix.corpus.name }} \ + --output /tmp/${{ matrix.corpus.name }}_gt_regen.json + python3 - <<'PY' + import json, sys + name = "${{ matrix.corpus.ground_truth }}" + committed = json.load(open(f"tests/eval_corpus/ground_truth/{name}")) + regen = json.load(open("/tmp/${{ matrix.corpus.name }}_gt_regen.json")) + if committed != regen: + sys.exit("committed ground truth diverges from a fresh conversion of " + "the manifest against the pinned corpus; regenerate with " + "manifest_gt_convert.py") + print(f"ground truth in sync: {len(committed)} records") + PY + + - name: eval-corpus harness regression tests + run: | + python3 tests/eval_corpus/test_tabulate_regression.py + python3 tests/eval_corpus/test_manifest_gt_convert.py + + - name: Gate 7 — ${{ matrix.corpus.name }} acceptance + run: | + export ${{ matrix.corpus.env }}="${{ github.workspace }}/.eval-corpus/${{ matrix.corpus.name }}" + scripts/m7_ship_gate.sh --sets ${{ matrix.corpus.name }} + + polyglot: + name: eval / ${{ matrix.corpus.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + corpus: + - name: railsgoat + repo: https://github.com/OWASP/railsgoat + ref: rails.5.0.0 + lang: ruby + env: NYX_RAILSGOAT_CORPUS + manifest: railsgoat.manifest.toml + ground_truth: railsgoat.json + - name: dvwa + repo: https://github.com/digininja/DVWA + ref: "2.5" + lang: php + env: NYX_DVWA_CORPUS + manifest: dvwa.manifest.toml + ground_truth: dvwa.json + - name: dvpwa + repo: https://github.com/anxolerd/dvpwa + # DVPWA ships no release tags; pin the default branch and let the + # cache key hold it stable. + ref: master + lang: python + env: NYX_DVPWA_CORPUS + manifest: dvpwa.manifest.toml + ground_truth: dvpwa.json + - name: gosec + repo: https://github.com/securego/gosec + ref: v2.26.1 + lang: go + env: NYX_GOSEC_CORPUS + manifest: gosec.manifest.toml + ground_truth: gosec.json + - name: rustsec + repo: https://github.com/rustsec/advisory-db + # advisory-db ships no release tags; pin the default branch. This + # is the Rust NEGATIVE CONTROL (advisory metadata, no scannable + # source) — its committed ground truth is empty by construction. + ref: main + lang: rust + env: NYX_RUSTSEC_CORPUS + manifest: rustsec.manifest.toml + ground_truth: rustsec.json + env: + # CI wall-clock budget: 15 min. Override locally to tighten. + NYX_POLYGLOT_WALLCLOCK_BUDGET_SECONDS: "900" + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - uses: taiki-e/install-action@nextest + + # The dynamic verifier's per-language build pool (Phase 22/23) compiles + # its harnesses with a real toolchain. Each matrix row sets up only the + # toolchain for its corpus's target language; the Rust row needs no extra + # step (the rust toolchain above covers it, and advisory-db has no + # buildable source anyway). + - name: Set up Ruby + if: matrix.corpus.lang == 'ruby' + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Set up PHP + if: matrix.corpus.lang == 'php' + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + + - name: Set up Python + if: matrix.corpus.lang == 'python' + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up Go + if: matrix.corpus.lang == 'go' + uses: actions/setup-go@v6 + with: + go-version: "1.22" + + - name: Cache ${{ matrix.corpus.name }} + id: cache-corpus + uses: actions/cache@v5 + with: + path: .eval-corpus/${{ matrix.corpus.name }} + key: polyglot-${{ matrix.corpus.name }}-${{ matrix.corpus.ref }} + + - name: Clone ${{ matrix.corpus.name }} (${{ matrix.corpus.ref }}) + if: steps.cache-corpus.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch ${{ matrix.corpus.ref }} \ + ${{ matrix.corpus.repo }} \ + .eval-corpus/${{ matrix.corpus.name }} + + # No-compromise guard: the committed ground truth must be exactly what a + # fresh conversion of the curated manifest produces *against this corpus*. + # manifest_gt_convert.py hard-errors on any labelled path that no longer + # exists in the clone (corpus drift / typo); the diff below catches a + # stale committed JSON. For the RustSec negative control the manifest + # carries `negative_control = true` and zero entries, so the converter + # emits an empty `[]` — still validated against the real clone. + - name: Verify ground truth is in sync with the pinned corpus + run: | + python3 tests/eval_corpus/manifest_gt_convert.py \ + --manifest tests/eval_corpus/ground_truth/${{ matrix.corpus.manifest }} \ + --corpus-dir .eval-corpus/${{ matrix.corpus.name }} \ + --output /tmp/${{ matrix.corpus.name }}_gt_regen.json + python3 - <<'PY' + import json, sys + name = "${{ matrix.corpus.ground_truth }}" + committed = json.load(open(f"tests/eval_corpus/ground_truth/{name}")) + regen = json.load(open("/tmp/${{ matrix.corpus.name }}_gt_regen.json")) + if committed != regen: + sys.exit("committed ground truth diverges from a fresh conversion of " + "the manifest against the pinned corpus; regenerate with " + "manifest_gt_convert.py") + print(f"ground truth in sync: {len(committed)} records") + PY + + - name: eval-corpus harness regression tests + run: | + python3 tests/eval_corpus/test_tabulate_regression.py + python3 tests/eval_corpus/test_manifest_gt_convert.py + + - name: Gate 8 — ${{ matrix.corpus.name }} acceptance + run: | + export ${{ matrix.corpus.env }}="${{ github.workspace }}/.eval-corpus/${{ matrix.corpus.name }}" + scripts/m7_ship_gate.sh --sets ${{ matrix.corpus.name }} diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..eb24b2e4 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,217 @@ +name: Fuzz + +on: + pull_request: + branches: ["master"] + paths: + - "src/**" + - "fuzz/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/fuzz.yml" + schedule: + # Long-form weekly run, Sundays at 06:00 UTC. + - cron: "0 6 * * 0" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + fuzz: + name: fuzz-${{ matrix.target }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [scan_bytes, extract_summaries, cross_file_taint] + steps: + - uses: actions/checkout@v6 + + # cargo-fuzz needs nightly for the libFuzzer codegen flags. + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + cache: true + cache-workspaces: | + . + fuzz + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-fuzz + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Build frontend + working-directory: frontend + run: | + npm ci + npm run build + + - name: Restore fuzz corpus + uses: actions/cache@v5 + with: + path: fuzz/corpus/${{ matrix.target }} + key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }} + restore-keys: | + fuzz-corpus-${{ matrix.target }}- + + # The harness reads inputs as , so we prefix + # each seed with its language index here at stage time. Files in + # fuzz/seed_corpus/ are committed as plain source without the byte + # because some IDEs strip 0x00 on save. + - name: Layer seed corpus + run: | + set -euo pipefail + target=${{ matrix.target }} + dest="fuzz/corpus/$target" + mkdir -p "$dest" + ext_to_idx() { + case "$1" in + rs) echo 0 ;; + js) echo 1 ;; + ts) echo 2 ;; + py) echo 3 ;; + go) echo 4 ;; + java) echo 5 ;; + rb) echo 6 ;; + php) echo 7 ;; + c) echo 8 ;; + cpp) echo 9 ;; + *) return 1 ;; + esac + } + stage() { + src="$1" + ext="${src##*.}" + idx=$(ext_to_idx "$ext") || return 0 + hash=$(sha256sum "$src" | cut -c1-16) + out="$dest/seed-${ext}-${hash}" + [ -e "$out" ] && return 0 + printf '%b' "$(printf '\\%03o' "$idx")" > "$out" + cat "$src" >> "$out" + } + for f in benches/fixtures/sample.*; do + [ -e "$f" ] && stage "$f" + done + while IFS= read -r f; do + stage "$f" + done < <(find tests/benchmark/corpus -type f \( \ + -name '*.rs' -o -name '*.js' -o -name '*.ts' \ + -o -name '*.py' -o -name '*.go' -o -name '*.java' \ + -o -name '*.rb' -o -name '*.php' -o -name '*.c' \ + -o -name '*.cpp' \)) + if [ -d "fuzz/seed_corpus/$target" ]; then + while IFS= read -r f; do + stage "$f" + done < <(find "fuzz/seed_corpus/$target" -type f \( \ + -name '*.rs' -o -name '*.js' -o -name '*.ts' \ + -o -name '*.py' -o -name '*.go' -o -name '*.java' \ + -o -name '*.rb' -o -name '*.php' -o -name '*.c' \ + -o -name '*.cpp' \)) + fi + echo "Corpus dir: $(ls "$dest" | wc -l) files" + + - name: Choose fuzz duration + id: budget + run: | + if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "seconds=18000" >> "$GITHUB_OUTPUT" + else + echo "seconds=600" >> "$GITHUB_OUTPUT" + fi + + - name: Run fuzz target + run: | + cargo fuzz run --target x86_64-unknown-linux-gnu ${{ matrix.target }} -- \ + -max_total_time=${{ steps.budget.outputs.seconds }} \ + -max_len=65536 \ + -timeout=60 \ + -rss_limit_mb=8192 \ + -dict=fuzz/dict/all.dict + + - name: Upload crash artifacts + if: failure() + uses: actions/upload-artifact@v7 + with: + name: fuzz-artifacts-${{ matrix.target }}-${{ github.run_id }} + path: fuzz/artifacts/${{ matrix.target }}/ + if-no-files-found: ignore + retention-days: 14 + + harness-fuzz: + name: harness-fuzz-${{ matrix.cap }} + runs-on: ubuntu-latest + # Run only on schedule and manual dispatch — 50 k iterations per cap is + # too slow for PR checks but is the right cadence for weekly corpus growth. + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + strategy: + fail-fast: false + matrix: + include: + - cap: sql_query + harness: tests/dynamic_fixtures/python/sqli_positive.py + - cap: code_exec + harness: tests/dynamic_fixtures/python/cmdi_positive.py + - cap: file_io + harness: tests/dynamic_fixtures/python/fileio_positive.py + - cap: ssrf + harness: tests/dynamic_fixtures/python/ssrf_positive.py + - cap: html_escape + harness: tests/dynamic_fixtures/python/xss_positive.py + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache: true + cache-workspaces: | + . + fuzz/dynamic_corpus + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Build frontend + working-directory: frontend + run: | + npm ci + npm run build + + - name: Build nyx-dynamic-corpus + working-directory: fuzz/dynamic_corpus + run: cargo build + + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Run harness fuzzer — ${{ matrix.cap }} + run: | + fuzz/dynamic_corpus/target/debug/nyx-dynamic-corpus run \ + --cap ${{ matrix.cap }} \ + --spec-hash "ci-${{ matrix.cap }}" \ + --harness-cmd "python3 ${{ matrix.harness }}" \ + --iterations 50000 \ + --output fuzz-discovered + + - name: Upload discovered candidates + if: always() + uses: actions/upload-artifact@v7 + with: + name: harness-fuzz-${{ matrix.cap }}-${{ github.run_id }} + path: fuzz-discovered/ + if-no-files-found: ignore + retention-days: 30 diff --git a/.github/workflows/image-builder.yml b/.github/workflows/image-builder.yml new file mode 100644 index 00000000..f1497072 --- /dev/null +++ b/.github/workflows/image-builder.yml @@ -0,0 +1,68 @@ +name: image-builder + +# Phase 19 (Track E.3): daily drift PR. +# +# Runs `nyx-image-builder build --all` on a Linux runner that has docker +# available, captures the rewritten `tools/image-builder/images.toml`, and +# opens a PR when any pinned digest changed. The PR is reviewed manually +# before merge so a hostile upstream image cannot silently land in +# `IMAGE_DIGESTS`. + +permissions: + contents: write + pull-requests: write + +on: + schedule: + # 04:23 UTC daily — off-peak for the major upstream registries so + # transient pull errors are rare. + - cron: "23 4 * * *" + workflow_dispatch: + +concurrency: + group: image-builder + cancel-in-progress: false + +jobs: + refresh-digests: + name: refresh image digests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - name: Verify docker is reachable + run: docker info + + - name: Build pinned-digest catalogue + run: | + cargo run -F image-builder --bin nyx-image-builder -- build --all + + - name: Verify catalogue against local pulls + run: | + cargo run -F image-builder --bin nyx-image-builder -- verify + + - name: Open PR on drift + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "image-builder: refresh pinned digests" + title: "image-builder: refresh pinned digests" + body: | + Automated digest refresh by `nyx-image-builder build --all`. + + The CI job pulled every base image in + `tools/image-builder/images.toml`, captured the resolved + `sha256:` digest, and wrote it back into the file. Review + the diff before merging — a hostile upstream image would + show up here as an unexpected digest change. + branch: image-builder/refresh-digests + base: master + delete-branch: true + labels: | + image-builder + automation diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index b7d41360..605899f4 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -3,31 +3,80 @@ name: Release build & publish on: release: types: [created] + workflow_dispatch: + inputs: + tag: + description: "Existing release tag to (re)build and publish (e.g. v0.5.0)" + required: true + type: string permissions: contents: write env: BIN_NAME: nyx + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} jobs: - build-and-upload: + frontend: + name: build-frontend + runs-on: ubuntu-latest + steps: + - name: Check out sources + uses: actions/checkout@v6 + with: + ref: ${{ env.RELEASE_TAG }} + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Build frontend + working-directory: frontend + run: npm run build + + - name: Upload frontend dist + uses: actions/upload-artifact@v7 + with: + name: frontend-dist + path: src/server/assets/dist/ + if-no-files-found: error + retention-days: 1 + + build: + needs: frontend strategy: matrix: include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest - target: x86_64-pc-windows-msvc os: windows-latest - target: x86_64-apple-darwin - os: macos-14 + os: macos-14 - target: aarch64-apple-darwin os: macos-14 runs-on: ${{ matrix.os }} steps: - name: Check out sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + ref: ${{ env.RELEASE_TAG }} + + - name: Download prebuilt frontend dist + uses: actions/download-artifact@v8 + with: + name: frontend-dist + path: src/server/assets/dist/ - name: Install Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -35,14 +84,23 @@ jobs: toolchain: stable target: ${{ matrix.target }} cache: true - + + - name: Install cross-compilation tools (ARM Linux) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml + echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml + - name: Install target run: rustup target add ${{ matrix.target }} - name: Build run: cargo build --release --bin ${{ env.BIN_NAME }} --target ${{ matrix.target }} - - name: Package + - name: Package (Linux & macOS) + if: runner.os != 'Windows' shell: bash run: | set -euo pipefail @@ -50,19 +108,181 @@ jobs: TARGET=${{ matrix.target }} EXT=$([[ "$TARGET" == *windows* ]] && echo ".exe" || echo "") BIN_PATH=target/$TARGET/release/$BIN$EXT - if [[ ! -f "$BIN_PATH" ]]; then - echo "::error ::Binary $BIN_PATH not found" - ls -R target/$TARGET/release || true - exit 1 - fi mkdir -p dist ARCHIVE=$BIN-$TARGET.zip - zip -9 "dist/$ARCHIVE" "$BIN_PATH" + files=("$BIN_PATH" THIRDPARTY-LICENSES.html) + shopt -s nullglob + license_files=(LICENSE* COPYING*) + shopt -u nullglob + files+=("${license_files[@]}") + zip -9 "dist/$ARCHIVE" "${files[@]}" echo "ASSET=$ARCHIVE" >> "$GITHUB_ENV" - - name: Upload to the release - uses: softprops/action-gh-release@v2 + - name: Package (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $Bin = '${{ env.BIN_NAME }}' + $Target = '${{ matrix.target }}' + $Ext = '.exe' + $BinPath = "target/$Target/release/$Bin$Ext" + New-Item -ItemType Directory -Path dist -Force | Out-Null + $Archive = "$Bin-$Target.zip" + $LicenseFiles = @(Get-ChildItem -Path 'LICENSE*', 'COPYING*' -File -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) + $Files = @($BinPath, 'THIRDPARTY-LICENSES.html') + $LicenseFiles + + Compress-Archive ` + -Path $Files ` + -DestinationPath "dist/$Archive" ` + -CompressionLevel Optimal + + Add-Content -Path $env:GITHUB_ENV -Value "ASSET=$Archive" + + - name: Upload build artifact + uses: actions/upload-artifact@v7 with: - files: dist/${{ env.ASSET }} + name: release-${{ matrix.target }} + path: dist/${{ env.ASSET }} + if-no-files-found: error + retention-days: 1 + + reproducibility: + name: reproducibility-check + needs: frontend + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Check out sources + uses: actions/checkout@v6 + with: + ref: ${{ env.RELEASE_TAG }} + + - name: Download prebuilt frontend dist + uses: actions/download-artifact@v8 + with: + name: frontend-dist + path: src/server/assets/dist/ + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: x86_64-unknown-linux-gnu + cache: true + + - name: Build twice and diff hashes + shell: bash + env: + RUSTFLAGS: "--remap-path-prefix=${{ github.workspace }}=/build" + run: | + set -euo pipefail + TARGET=x86_64-unknown-linux-gnu + BIN=${{ env.BIN_NAME }} + BIN_PATH="target/$TARGET/release/$BIN" + + SOURCE_DATE_EPOCH=$(git log -1 --format=%ct HEAD) + export SOURCE_DATE_EPOCH + echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" + + cargo build --release --bin "$BIN" --target "$TARGET" + HASH1=$(sha256sum "$BIN_PATH" | awk '{print $1}') + echo "first build: $HASH1" + + cargo clean --release --target "$TARGET" + cargo build --release --bin "$BIN" --target "$TARGET" + HASH2=$(sha256sum "$BIN_PATH" | awk '{print $1}') + echo "second build: $HASH2" + + if [ "$HASH1" != "$HASH2" ]; then + echo "::error::Reproducibility check failed: builds are not bit-identical" + echo " first: $HASH1" + echo " second: $HASH2" + exit 1 + fi + echo "::notice::Reproducible build verified (sha256=$HASH1)" + + publish: + name: publish-release + runs-on: ubuntu-latest + needs: [build] + permissions: + contents: write + id-token: write + attestations: write + steps: + - name: Check out sources + uses: actions/checkout@v6 + with: + ref: ${{ env.RELEASE_TAG }} + + - name: Generate CycloneDX SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: cyclonedx-json + output-file: nyx-${{ env.RELEASE_TAG }}.cdx.json + upload-artifact: false + upload-release-assets: false + + - name: Download all build artifacts + uses: actions/download-artifact@v8 + with: + path: release-artifacts + pattern: release-* + merge-multiple: true + + - name: Generate SHA256SUMS + run: | + set -euo pipefail + cd release-artifacts + ls -lh + sha256sum *.zip > SHA256SUMS + cat SHA256SUMS + + # Sigstore keyless signing. Verify with: + # cosign verify-blob --bundle .bundle \ + # --certificate-identity-regexp 'https://github.com/elicpeter/nyx/.*' \ + # --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + # + - name: Install cosign + uses: sigstore/cosign-installer@v4.1.2 + + - name: Cosign keyless sign release artifacts + shell: bash + run: | + set -euo pipefail + SBOM="nyx-${{ env.RELEASE_TAG }}.cdx.json" + ( + cd release-artifacts + for f in *.zip SHA256SUMS; do + cosign sign-blob --yes \ + --bundle "$f.bundle" \ + "$f" + done + ) + cosign sign-blob --yes \ + --bundle "$SBOM.bundle" \ + "$SBOM" + + # SLSA v1 provenance. Verify with `gh attestation verify --repo `. + - name: Generate SLSA build provenance + uses: actions/attest-build-provenance@v4 + with: + subject-path: | + release-artifacts/*.zip + release-artifacts/SHA256SUMS + nyx-${{ env.RELEASE_TAG }}.cdx.json + + - name: Upload to the release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ env.RELEASE_TAG }} + files: | + release-artifacts/*.zip + release-artifacts/*.zip.bundle + release-artifacts/SHA256SUMS + release-artifacts/SHA256SUMS.bundle + nyx-${{ env.RELEASE_TAG }}.cdx.json + nyx-${{ env.RELEASE_TAG }}.cdx.json.bundle env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/repro-bare.yml b/.github/workflows/repro-bare.yml new file mode 100644 index 00000000..9f78ebbe --- /dev/null +++ b/.github/workflows/repro-bare.yml @@ -0,0 +1,104 @@ +# Replay every tree-committed dynamic repro bundle with host language +# toolchains blocked so we catch regressions where a bundle silently +# depends on an interpreter the operator does not have. +# +# The setup step prepends deny-list wrappers for python3, node, ruby, +# php, and Java so the only toolchain the bundle can use is the docker +# daemon. reproduce.sh in --docker mode pulls the pinned base image +# (via docker_pull.sh) and runs the harness inside the container; if the +# bundle accidentally relied on a host interpreter the run falls over +# before the sentinel check. +# +# Adding a new fixture: extend the `matrix.fixture` list with the new +# `tests/repro_fixtures//` path. The bundle +# must already exist on disk, see tests/repro_fixture_bundles.rs for +# the regeneration recipe. + +name: repro-bare + +permissions: + contents: read + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + bare-image-replay: + name: repro-bare / ${{ matrix.fixture }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + fixture: + - tests/repro_fixtures/python-3.11/repro + steps: + - uses: actions/checkout@v6 + + - name: Block host language toolchains + run: | + set -euo pipefail + + # Do not mutate the hosted runner image. ubuntu-latest carries + # preinstalled and cached language runtimes, and apt package + # relationships can shift underneath us as the image is updated. + # A PATH-level deny layer gives this job the bare-host semantics it + # needs without depending on apt being able to uninstall core bits. + deny_dir="${RUNNER_TEMP}/nyx-deny-toolchains" + mkdir -p "$deny_dir" + for exe in \ + python python3 python3.10 python3.11 python3.12 python3.13 python3.14 \ + node npm npx corepack \ + ruby gem bundle \ + php \ + java javac jar + do + { + printf '%s\n' '#!/bin/sh' + printf '%s\n' 'echo "error: host language toolchain is disabled in repro-bare; use the Docker replay path" >&2' + printf '%s\n' 'exit 127' + } > "${deny_dir}/${exe}" + chmod +x "${deny_dir}/${exe}" + done + + export PATH="${deny_dir}:${PATH}" + echo "${deny_dir}" >> "${GITHUB_PATH}" + hash -r 2>/dev/null || true + + # Confirm the deny layer is active — surface the failure here + # rather than inside reproduce.sh where it would look like a + # bundle bug. + for exe in python3 node ruby php java; do + resolved="$(command -v "${exe}" || true)" + if [ "${resolved}" != "${deny_dir}/${exe}" ]; then + echo "error: ${exe} deny wrapper is not first on PATH (got ${resolved:-not found})" >&2 + exit 1 + fi + if "${exe}" --version >/dev/null 2>&1; then + echo "error: ${exe} still runs after host-toolchain block" >&2 + exit 1 + fi + done + + if ! command -v docker >/dev/null 2>&1; then + echo "error: docker is no longer reachable after host-toolchain block" >&2 + exit 1 + fi + + - name: Verify docker is reachable + run: docker info + + - name: Pre-pull pinned image + working-directory: ${{ matrix.fixture }} + run: ./docker_pull.sh + + - name: Replay bundle via docker + working-directory: ${{ matrix.fixture }} + run: ./reproduce.sh --docker diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 0bd700a8..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Rust - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run linter - run: cargo clippy --all-targets --all-features -- -D warnings - - name: Run tests - run: cargo test --verbose diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..42757097 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,45 @@ +name: OSSF Scorecard + +on: + branch_protection_rule: + schedule: + - cron: "0 7 * * 1" + push: + branches: ["master"] + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: scorecard + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + contents: read + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # Flip to true once we're happy with the score and want the badge. + publish_results: false + + - name: Upload SARIF artifact + uses: actions/upload-artifact@v7 + with: + name: scorecard-sarif + path: results.sarif + retention-days: 14 + + - name: Upload SARIF to Security tab + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index d81f12ed..fe7dc8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,22 @@ /target +/fuzz/target +/fuzz/corpus +/fuzz/dynamic_corpus/target +/fuzz/artifacts /.idea +/frontend/node_modules +/src/server/assets/dist +/marketing +/.nyx +/.nyx-build-cache +/logs +/book +.DS_Store +.z3-trace +.pitboss +.eval-corpus +.node_modules-target +node_modules +__pycache__/ +*.pyc +tools/sb-trace/*.trace.raw diff --git a/AI-POLICY.md b/AI-POLICY.md new file mode 100644 index 00000000..1ac2c275 --- /dev/null +++ b/AI-POLICY.md @@ -0,0 +1,36 @@ +# AI Contribution Policy + +Nyx accepts contributions that were drafted, refactored, or reviewed with the help of AI tools (LLMs, code assistants, agent systems). We care about the contribution, not the keystrokes. AI changes the failure modes though, so we ask contributors to follow a few rules. + +## What we ask of contributors + +By opening a pull request you affirm that: + +1. **You have read and understood every line you are submitting.** If you cannot explain a change under review, it is not ready to merge. "The model wrote it" is not an answer we will accept for a bug or a regression. +2. **You have the right to submit the code.** AI-generated code is only as license-clean as its training data and its prompt. Do not paste proprietary, GPL-incompatible, or confidential code into an AI tool and then submit the output here. If a model reproduced a substantial verbatim snippet from an identifiable source, disclose it. +3. **You take responsibility for the change.** The DCO `Signed-off-by:` trailer applies the same way to AI-assisted code as it does to hand-written code. You are certifying origin and right-to-submit. +4. **You disclose material AI use in the PR description.** A one-line note is enough. For example, "Drafted with an AI assistant; reviewed and tested by me." Trivial uses like tab-completion, renames, or formatting do not need to be called out. New analysis passes, rule logic, or security-relevant code do. + +## What we look for in review + +AI-assisted PRs face the same bar as any other PR, but reviewers will pay extra attention to: + +- **Tests that exercise the new behavior.** Not just "it compiles." Fixtures under `tests/fixtures/` and assertions in `expected.yaml` are how we verify security logic. +- **Consistency with the existing engine.** Drive-by refactors, speculative abstractions, or parallel implementations of existing passes will usually be rejected, even if they look clean in isolation. +- **Fabricated references.** AI tools sometimes invent function names, crate APIs, CVE IDs, or citations. Every symbol referenced in a PR must exist, and every external claim must be verifiable. +- **Rule metadata honesty.** Rule descriptions, CWE mappings, and severity ratings are part of how downstream users triage. Do not inflate severity or cite CWEs the rule does not actually detect. + +## What we will not accept + +- PRs that are clearly unreviewed agent output, such as changes in the wrong file, nonsense tests, hallucinated APIs, or code that does not compile. +- PRs that add "AI-generated" boilerplate, marketing copy, or filler documentation to pad scope. +- Mass-generated PRs across many unrelated areas in a single change. +- Code that was generated by pasting another project's proprietary source into an AI tool. + +## Project's own use of AI + +For transparency, the README includes an [AI Disclosure](README.md#ai-disclosure) describing where AI was used in Nyx itself. The short version: the analysis engine is predominantly human-written and human-reviewed, while documentation, fixtures, and rule metadata were drafted with AI assistance and audited before landing. We hold outside contributions to the same standard. + +## Questions + +If you are unsure whether a contribution falls inside this policy, open a draft PR or an issue and ask before investing time. We would rather have the conversation early than reject work at review. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..93d21329 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,551 @@ +# Changelog + +All notable changes to Nyx are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). For where Nyx is going, see the [Roadmap](ROADMAP.md). + +## [0.8.0] - 2026-06-06 + +The dynamic-verification release. An attack-surface map, a sandboxed dynamic verifier, a framework adapter registry that grounds both, the per-language build infrastructure that makes per-finding verification affordable at corpus scale, and the first real-corpus acceptance gates. + +The attack-surface map and chain composer turn the flat finding list into a route-to-sink graph. The dynamic verifier re-runs every Medium-or-higher finding against a payload corpus and stamps a Confirmed / PartiallyConfirmed / NotConfirmed / Inconclusive / Unsupported verdict on each. The adapter registry (130+ entries across 8 languages) covers HTTP, message-broker, scheduled-job, GraphQL, WebSocket, middleware, and migration entry points. Per-language build pools and copy-on-write workdirs hold the with-verify wall-clock to within 1.5x of a static-only scan. + +### Attack-surface map + +- **`nyx surface` subcommand.** Prints the project's entry points, datastores, external services, and dangerous local sinks as text, JSON, Graphviz `dot`, or rendered SVG. Loads the persisted `SurfaceMap` from the most recent indexed scan when available, or rebuilds inline from source. `--build` forces a full pass-1 + call-graph walk so DataStore / ExternalService / DangerousLocal nodes populate on an unscanned project. +- **Surface page in `nyx serve`.** New `SurfacePage` renders the same graph in the browser UI, with ELK layout, sidebar navigation, and a wide-canvas SVG viewer. Persists alongside the index so the frontend reloads without a rescan. +- **Chain findings.** `ChainFinding` records connect a route entry point to a downstream sink via the call graph + surface map. The composer scores `(impact × evidence)` per chain, queues the top-N for composite reverification, and wires the result into `findings.json` / SARIF / the dashboard. Chains rank above isolated findings. + +### Framework adapter registry + +`src/dynamic/framework/` ships a `FrameworkAdapter` trait with concrete adapters across 8 languages (116 entries today, growing per release). Each adapter binds a route / handler / consumer pattern to a `FrameworkBinding` so the surface map and dynamic verifier can locate entry points without re-walking the AST. + +- **HTTP routers.** Flask, Django, FastAPI, Starlette (Python); Express, Koa, NestJS, Fastify (JS/TS); Spring, Quarkus, Micronaut, Jakarta Servlet (Java); Gin, Echo, Fiber, Chi (Go); Axum, Actix, Rocket, Warp (Rust); Rails, Sinatra, Hanami (Ruby); Laravel, Symfony, CodeIgniter (PHP). +- **New `EntryKind` variants.** `ClassMethod`, `MessageHandler`, `ScheduledJob`, `GraphQLResolver`, `WebSocket`, `Middleware`, `Migration` join the existing `RouteHandler` / `Function` set so the surface map shows non-HTTP entry surfaces. +- **Message broker handlers.** Kafka, AWS SQS, Google Pub/Sub, NATS, and RabbitMQ consumers across Python, Node, Java, and Go. +- **Scheduled jobs.** Celery (Python), Sidekiq (Ruby), Quartz (Java), plain cron expression recognition. +- **GraphQL resolvers.** Apollo, Relay, gqlgen, Juniper, Graphene. +- **WebSocket handlers.** ws, Socket.IO, ActionCable, Django Channels. +- **Middleware + migrations.** Express, Laravel, Spring, Django, Rails middleware; Django, Flask, Laravel, Rails, Prisma, Sequelize migration scripts. +- **Sanitizer-aware adapter strengthening.** Every XXE, header-injection, open-redirect, SSTI, LDAP, XPath, deserialization, crypto, and data-exfiltration adapter rejects bindings when the surrounding source visibly hardens the parser (`disallow-doctype-decl`, `resolve_entities=False`, `libxml_disable_entity_loader`), routes the value through a known encoder (`LdapEncoder.filterEncode`, `escape_filter_chars`, `ldap_escape`), swaps a weak primitive for a CSPRNG (`secrets.token_bytes`, `crypto.randomBytes`, `SecureRandom`), or validates the destination host through an allowlist. Cuts adapter FPs without losing the genuinely dangerous calls. + +### Dynamic verification + +- **`nyx scan --verify`.** Every finding with `Confidence >= Medium` is re-executed inside a sandboxed harness against a curated payload corpus. The verdict (`Confirmed` / `NotConfirmed` / `Inconclusive` / `Unsupported`) lands on `Evidence.dynamic_verdict` and shows up in console output, JSON, SARIF, and the dashboard via a new `VerdictBadge` component on the finding detail page. +- **Backends.** In-process on Linux with `Standard` / `Strict` hardening (namespace unshare, chroot, RLIMIT cap, seccomp filter), in-process on macOS via `sandbox-exec` with a profile-per-policy wrap, Docker with a published image-builder catalogue, and a Firecracker trait stub for future microVM execution. The Docker backend ships native binary support for Rust and Go so harnesses no longer need to drag a toolchain into every image. +- **Language coverage.** Per-language harness emitters for Python, JS/TS, Go, Java, PHP, Ruby, Rust, C, and C++. Stub harness intercepts SQL, HTTP, Redis, and filesystem boundaries so the verdict reflects the sink, not the network. The `JSON_PARSE`, `UNAUTHORIZED_ID`, and `DATA_EXFIL` cap dispatchers are wired into every emitter that ships these caps (Python, JS, TS, Go, Java, PHP, Ruby, Rust), so the verdict pipeline closes the loop on each cap end-to-end rather than per-language piecemeal. +- **Abstract-interpretation and symex sanitizer suppression.** Symbolic execution and the interval/string abstract domain are now consulted at verdict time, so a payload that the static engine would call dangerous but symex can prove never reaches the sink lands as NotConfirmed. +- **Guard-aware verdicts.** When a known input-validation or output-sanitization middleware sits in front of a Confirmed sink (Spring `@PreAuthorize`, Express `helmet`, Nest `@UseGuards`, Django `@permission_classes`, and the per-language registry in `src/dynamic/framework/auth_markers.rs`), the verdict demotes to `ConfirmedWithKnownGuard` and the guard names land on `differential.known_guards`. Authentication-only filters do not trigger the demotion since they do not mitigate injection. +- **Repro bundles.** Every verified finding writes a hermetic bundle to `~/.cache/nyx/dynamic/repro//` with `reproduce.sh`, `expected/{verdict.json,outcome.json,trace.jsonl}`, and a `docker_pull.sh` when the toolchain is pinned in `tools/image-builder/images.toml`. `--verbose` flushes the per-step `VerifyTrace` to stderr for live triage. +- **Real-engine harness paths.** LDAP injection routes through an embedded LDAPv3 BER server, exercised from Java via JNDI `InitialDirContext` and from Python and PHP via pure-stdlib BER clients. XPath injection runs against the live parser in each language: Java `javax.xml.xpath`, PHP `DOMXPath`, JS `xpath` npm, Python `lxml`. `Cap::CRYPTO` lands a `WeakKey` probe across Python, Go, Java, PHP, and Rust that flags sub-2^16 keys produced by non-CSPRNG sources. A new `HeaderSmuggledInWire` oracle predicate catches CRLF smuggling on hand-rolled raw-socket HTTP servers (Python `http.server`, Node `net`, Rust `std::net::TcpListener`) where framework-level CRLF strip cannot intervene. +- **Differential rule v2 and partial confirmations.** A finding confirms when *any* vulnerable payload in the set fires and *every* paired benign control stays clean, replacing the strict pair-wise rule so a single missing control no longer downgrades a confirmable finding. A new `PartiallyConfirmed` verdict marks findings where the sink is reached but the exploit chain does not complete (no marker written, no callback observed), so engine work can ratchet without the tool overstating what it proved. +- **Spec derivation v2.** Every derivation strategy now runs and is scored on flow-step depth, framework binding, cross-file source resolution via `GlobalSummaries`, and payload availability; the highest-scoring candidate wins and the runner-up ranking lands in the trace so engine gaps stay visible. Cross-file seeding walks the call graph (max depth 5) until a `Source` step or framework binding is found. New `EntryKind` adapters auto-recover the entry surface from framework decorators and annotations. + +### Performance + +- **Per-language build pools.** A warm `javac` daemon compiles batched harness sources in one long-lived JVM (Track O headline, Phase 22); Node, PHP, Ruby, Go, Rust, C, and C++ reuse shared module / package / object caches; Python layers a read-only venv per `requirements_hash` with a warmed bytecode cache. Target per-finding harness build: P50 ≤ 200ms hot, ≤ 1.5s cold. Pools self-skip when a toolchain is absent so toolchain-less CI rows stay green. +- **Copy-on-write workdirs.** Per-finding workdir setup uses `clonefile` on macOS and `reflink` / `copy_file_range` on Linux instead of copying every harness file, cutting setup cost to single-digit milliseconds. +- **Cap-routed concurrency lanes.** The verifier worker pool splits into per-cap lanes (`SSRF: 8`, `DESERIALIZE: 2`, `CRYPTO: 1`, and so on) so a slow harness for one cap cannot head-of-line block fast ones. +- **Ship-gate budgets.** Gate 3 holds the with-verify / static-only wall-clock ratio at ≤ 1.5x on `benches/fixtures/`; Gate 6 holds the Java OWASP Benchmark `--verify` run at ≤ 15 min on CI / ≤ 10 min on the dev reference machine. + +### Determinism, policy, telemetry + +- **YAML policy deny list.** `src/policy.rs` is consulted before harness build. Network egress, filesystem writes outside the sandbox root, and process spawns can be denied per-rule; deny decisions land in the trace, redacted via the shared scrubber. +- **Seeded RNG.** `dynamic::rand::SpecRng` is seeded from each `HarnessSpec` hash so two runs of the same spec produce identical payloads. `scripts/check_no_unseeded_rand.sh` audits the tree for unseeded `rand` usage on every CI run. +- **`VerifyTrace` observability.** Every per-step decision (probe selection, payload mutation, oracle check, deny verdict) writes to the trace stream and the repro bundle. +- **Schema-versioned telemetry.** `events.jsonl` carries `schema_version`, `nyx_version`, `corpus_version`, `kind`, and `ts` on every envelope. PII and secret scrubbing runs on every persisted artefact via `src/utils/redact.rs`. +- **`NYX_NO_TELEMETRY=1`** disables event persistence outright. + +### CVE corpus and ground truth + +- **New `Cap` corpora.** Vulnerable + patched fixtures landed for the seven new cap classes (LDAP injection, XPath injection, header injection, open redirect, SSTI, XXE, prototype pollution) plus deserialization, crypto, JSON parsing, unauthorized-id, and data exfiltration. Every cap now carries at least one positive / negative / adversarial / unsupported fixture quad per supported language. +- **OWASP Benchmark v1.2 importer.** `tests/eval_corpus/owasp_gt_convert.py` converts the OWASP Java Benchmark expected-results manifest into Nyx ground truth and lands a 16k-line `owasp_benchmark_v1.2.json` for evaluation. +- **NIST SARD importer.** `tests/eval_corpus/sard_gt_convert.py` converts SARD test cases into the same format so cross-dataset recall numbers stay comparable. +- **Evaluation corpus tooling.** `tests/eval_corpus/run_full.sh` runs the Nyx benchmark, OWASP Benchmark, and NIST SARD evaluation sets and writes `tests/eval_corpus/results.json`. `tests/eval_corpus/report.py` and `tabulate.py` produce the per-cap and per-language summary used to track coverage and accuracy. +- **Real-corpus acceptance gates.** `scripts/m7_ship_gate.sh` adds Gate 6 (Java OWASP Benchmark v1.2), Gate 7 (NodeGoat + Juice Shop), and Gate 8 (RailsGoat, DVWA, DVPWA, gosec, RustSec). Each row enforces the per-`(cap, lang)` budget in `tests/eval_corpus/budget.toml` and publishes per-cap precision / recall / confirmed-rate against a committed ground truth. The corpora are not vendored; each row self-skips unless its `NYX__CORPUS` points at a checkout. +- **Per-spec cryptographic canary.** Every oracle marker is now derived from `BLAKE3(spec_hash || run_nonce)` rather than a fixed literal, so markers are unique per finding, collision-resistant against ambient harness output, and never leak to the host. A compile-time audit rejects any new ad-hoc canary. + +### Engine + +- **DB fast-fail preflight.** `Indexer::init` reads the first 16 bytes of any candidate SQLite file and rejects anything without the standard `SQLite format 3\0` magic. Stops a misnamed JSON / text file from corrupting the index path with a SQLite error halfway through migration. +- **Symbolic-execution coverage.** Symex now recognises a wider set of string operations (`substr`, `replace`, `to_lower`, `to_upper`, `trim`, `strlen`) per the value/transfer pipeline, and the abstract-interpretation framework reasons about interval and prefix/suffix string facts during the dynamic verdict pass. + +### CLI + +- **`nyx scan --verify`** (enabled by default in standard builds) and `--backend {auto,process,docker}` select the dynamic-verification harness. `--no-verify` skips verification for a single run without changing config. +- **`nyx scan --harden {standard,strict}`** picks the process-backend hardening profile. `standard` is no-new-privs plus a memory rlimit on Linux. `strict` layers namespace unshare, chroot to the workdir, and a default-deny seccomp filter on Linux, or wraps the harness with `sandbox-exec` on macOS. +- **Patch-validation CI mode.** `--baseline FILE` reads a previous scan's JSON (or a stripped `.nyx/baseline.json` written by `--baseline-write`) and diffs it against the current scan on `stable_hash`, emitting `New` / `Resolved` / `FlippedConfirmed` / `FlippedNotConfirmed` transitions. `--gate {no-new-confirmed,resolve-all-confirmed}` exits non-zero when the diff violates the policy so CI fails the build instead of merging an unreviewed regression. The stripped baseline carries only `stable_hash`, `dynamic_verdict`, `severity`, `path`, and `rule_id`, so persisting it between scans does not leak source. +- **Repository triage in CI.** `nyx scan` now reads the same `.nyx/triage.json` file written by `nyx serve`. Terminal triage states (`false_positive`, `accepted_risk`, `suppressed`, `fixed`) are hidden from CLI output and excluded from `--fail-on` by default, while `--show-suppressed` includes them with `triage_state` / `triage_note` metadata for JSON, SARIF, and console output. +- **`nyx scan --verify-all-confidence`** drops the Medium cutoff and re-verifies everything. +- **`nyx scan --unsafe-sandbox`** disables hardening (development only, never for CI). +- **`nyx verify-feedback --wrong | --right`** records a correction or confirmation for a finding's verdict in the local telemetry log. +- **`nyx scan --explain-engine`** prints the effective engine configuration and exits without scanning. +- **`nyx surface`** (described above) with `--format {text,json,dot,svg}` and `--build`. +- **`nyx repro` subcommand.** Replays dynamic repro bundles by finding id, + spec hash, or explicit bundle path, with `--docker`, `--print-path`, and + `--list` helpers. The CLI now matches the browser UI's reproduced command + and uses bundle manifests to bridge stable finding ids to spec-hash cache + directories. + +### Frontend + +- **Project target selector in `nyx serve`.** The sidebar now remembers scan roots, lets you switch the active target, and accepts a new project path without restarting the server. `/api/targets` backs the selector, scans can opt into a different `scan_root`, and `nyx scan` / `nyx index build` register the projects they touch so `nyx serve` can pick them up later. +- **Surface page** with ELK auto-layout and the shared node-style palette. +- **Verdict badge** on finding detail, plus a dynamic-verdict section that surfaces the verdict, the payload that triggered it, and a link to the repro bundle. +- **Scan compare** gains a dynamic-verdict diff column so two scans can be compared on what was confirmed versus what was downgraded. + +### License + +- **Internal license grants documentation** at `LICENSE-GRANTS.md`. Grant 1 covers Nyctos derived works. The repo stays GPL-3.0-or-later; the grants document scope of internal product licensing. + +## [0.7.0] - 2026-05-11 + +A focused release that adds seven new vulnerability classes, ships two SSA sidecars for XML and XPath parser hardening, deepens cross-file authorization for FastAPI, trims roughly a thousand auth false positives on Go DAO helpers along with the dominant Hibernate Criteria SQL cluster, and runs a performance pass on the auth extractor, SCCP, and the global summaries map. A `nyx rules list` CLI surfaces the rule registry, the web UI gets a brand-aligned visual refresh, and the CVE corpus grows across Python, PHP, JavaScript, and C. + +### Highlights + +- New caps for LDAP injection, XPath injection, header / CRLF injection, open redirect, server-side template injection, XXE, and prototype pollution, with per-language label rules across all eight supported languages. +- Cross-file FastAPI authorization: `include_router` chains and module-level `APIRouter(dependencies=[…])` now lift onto every attached route, with `Security(..., scopes=[...])` recognised distinctly from `Depends(...)`. +- Type-tracked XML and XPath hardening through two new SSA sidecars: parser bodies that set `secure_processing` / `processEntities: false` / `resolve_entities=False`, and `XPath` instances bound to `setXPathVariableResolver(...)`, are recognised as safe. +- ~957 `go.auth.missing_ownership_check` findings closed on gitea-shaped DAO helpers (id-scalar precision pass), 169 of 216 openmrs `cfg-unguarded-sink` findings closed on Hibernate Criteria-API receivers, joomla and drupal `php.deser.unserialize` closed on `Serializable::unserialize($input)` magic-method bodies. +- `nyx rules list` CLI subcommand, brand-aligned `nyx serve` visual refresh, and regenerated README / docs screenshots and GIFs. + +### 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 per-language sink, sanitizer, and gated-sink rules across JS/TS, Python, Java, PHP, Go, Ruby, Rust, and C/C++. Severity, OWASP 2021 mapping, and human-readable description live in `CAP_RULE_REGISTRY` 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` and `RuleInfo.cap_bits` follow. The serde decoder accepts any unsigned integer width so caches written before the bump still load. SQLite schema bumped from 3 to 4 to force a rescan, since 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 cap class no longer requires a second-table edit. The match requires an exact rule id or a recognised separator (` `, `(`, `.`) so a future `taint-ssrf-allowlist-violation` cannot silently inherit `taint-ssrf`'s 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. + +### Engine + +- **XML-parser configuration tracking.** `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.** `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. 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` (FreeMarker `new Template(...)` / `Configuration.getTemplate`), and `NullPrototypeObject` for JS/TS values produced by `Object.create(null)`. Wired into `constructor_type` for return-type inference and `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 where the dangerous shape is identifiable only by an explicit literal flag, e.g. `jQuery.extend(true, target, src)` deep-merge against Backbone's `Model.extend({proto})`. +- **Two new path-state predicates 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 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.** `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`. 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** at the synthetic `__index_set__` sink (JS/TS, 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 (sibling `if (idx === "__proto__" || ...) return / throw / break;`), and allowlist pattern (ancestor `if (idx === "name" || idx === "id") { obj[idx] = v }`). Walks stop at the enclosing function so closure-captured guards in an outer scope cannot silently authorise inner assignments. +- **Spring MVC `return "redirect:" + tainted` recogniser** (Java). `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 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. `push_node` now walks into the subscript's `object` and classifies its member-expression text, 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`: PHP `encapsed_string` (double-quoted) when every child is a pure-literal segment; boolean literals (`true` / `false`) for the jQuery `extend(true, ...)` `LiteralOnly` gate; leading-string `binary_expression` concat (`"Location: " . $url`, JS/TS `"Location: " + url`) so `dangerous_prefixes` matching activates on partially dynamic concatenations. +- **PHP receiver-text strip** in `helpers::root_receiver_text` drops the leading `$` from `variable_name` nodes so `$smarty->fetch(...)` / `$twig->createTemplate(...)` reconstruct as `Smarty.fetch` / `Environment.createTemplate` for suffix-matcher gates. +- **Gate-callee resolution hardening for member-source rewrites.** When `first_member_label` rewrites a call's `text` to a Source like `req.body`, the gate matcher now reads the call's `function` / `method` / `name` field instead, so `setValue(target, req.body, ...)` matches the `setValue` proto-pollution gate. Whitespace stripped from the function field so multi-line chains still match flat gate matchers. +- **Ruby option-constant lookup in gate activation.** Bare `scope_resolution` / `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. +- **PHP `unary_op_expression` negation recognition.** tree-sitter-php emits `unary_op_expression` for unary `!`; CFG `detect_negation` and condition-chain decomposition now match it, so `if (!validate($x))` no longer carries `condition_negated=false` and the surviving branch is the rejection arm, not the validated one. +- **PHP container kinds.** `declaration_list`, `interface_declaration`, `trait_declaration`, `enum_declaration`, `enum_declaration_list` mapped to `Kind::Block` so methods inside them participate in CFG construction. +- **Go variadic `parameter_declaration` named-field handling** for `collect_param_names`. `name` and `type` named fields read directly so type-segment identifiers no longer pollute the param-name set (`info *PackageInfo` no longer contributes `PackageInfo`). +- **Empty-formals SSA lowering signal.** Per-parameter summary probing now seeds via `BodyMeta.param_destructured_fields`; JS/TS arrow `() => {…}` lowers with `with_params=true` so it is treated as "explicitly zero formals" rather than "no formals info". + +### Authorization + +- **FastAPI cross-file `include_router` dependency tracking.** `auth_analysis/router_facts.rs` captures per-file router declarations (` = X(deps=[…])`) and `.include_router(.)` 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 to parent to child) handled by iterative index walk. Module identity is the file basename without `.py`. Closes the airflow execution-API shape where a child router lives 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(...)])` is pre-walked once per file and merged onto every `@.(...)` route attached in the same file. Closes airflow 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 Login. New scope-tracking boolean threaded through `expand_decorator_calls` and `extract_fastapi_dependencies`. +- **Caller-scope IPA: same-file route-handler-to-helper auth lift.** `apply_caller_scope_propagation` walks every non-route helper unit; if its in-file callers are non-empty AND every caller is itself an authorized route handler (route-level non-Login auth check) or already authorized via this same propagation, the caller's checks lift onto the helper as synthetic `is_route_level=true` `AuthCheck`s. Iterated to a small fixpoint so transitive helper chains (route to mid_helper to leaf_helper) are covered. Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers. Cross-file lifting is not implemented. Closes the dominant FastAPI / Django / Flask "route authenticates via decorator/dependency, then delegates to a private helper that performs the sink" FP shape on sentry / saleor / airflow. +- **Go DAO-helper id-scalar precision pass.** For non-route Go units, a parameter whose declared type is a bounded primitive scalar (`int64`, `uint32`, `string`, `bool`, `byte`, `rune`, `float64`, …) and whose name is id-shaped (`id`, `*Id`, `*_id`, `*ids`) is dropped from `unit.params` before ownership-check evaluation. Real Go HTTP handlers always carry a framework-request-typed param (`*http.Request`, `*gin.Context`, `echo.Context`, `*fiber.Ctx`); per-framework route extractors set `include_id_like_typed=true` so id-shaped path params survive on real routes. Mirrors the existing Python `is_python_id_like_typed_param` filter. Closes ~957 `go.auth.missing_ownership_check` findings on gitea backend DAO helpers (`func GetRunByRepoAndID(ctx, repoID, runID int64)`, `func DeleteRunner(ctx, id int64)`, the entire `models/...` layer where the ownership check sits in the calling route handler) and equivalent shapes in minio / Go ORM codebases. +- **Bare-callee verb-name fallback gate.** `list(...)`, `filter(...)`, `update(...)`, `create_audit_entry(...)`, `update_coding_agent_state(...)` (no receiver dot at all) no longer classify as `DbMutation` / `DbCrossTenantRead` via the loose verb-name fallback. Real ORM/DB calls carry a receiver (`User.find(id)`, `Model.objects.filter`, `repo.save(x)`); a bare `list(events)` is the Python builtin and `filter(fn, xs)` is `Iterable.filter`. New helper `receiver_is_simple_chain(callee)` requires a non-chained receiver dot. The realtime / outbound / cache prefix dispatches still match by chain root. + +### Type-aware sinks and validators + +- **Java JPA / Hibernate Criteria API as structural SQL.** `TypeKind::JpaCriteriaQuery` covers `CriteriaQuery`, `CriteriaUpdate`, `CriteriaDelete`, `Subquery`, `TypedQuery`. `sink_args_jpa_criteria_query_safe` clears `cfg-unguarded-sink` SQL_QUERY when any positional argument to the sink call is JpaCriteriaQuery-typed (receiver excluded; receiver of `session.createQuery(cq)` is the Session/EntityManager channel, never the SQL payload). `cb.createQuery(...)`, `em.getCriteriaBuilder()`, and the JpaCriteriaQuery type chain inferred via constructor / factory return-type hints in `type_facts.rs`. Closes the dominant FP cluster on openmrs (169 of 216 cfg-unguarded-sink), xwiki, and keycloak Hibernate DAO methods. +- **Receiver-side validator registry.** `labels::lookup_receiver_validator(lang, callee)` clears `Cap` from the receiver value (and call equivalents) on success, distinct from `Sanitizer` which clears caps from the return value. Python registers `relative_to => Cap::FILE_IO` so `path.relative_to(base)` drops the file-IO cap on the path. Closes the CVE-2024-23334 patched aiohttp `static_root_path.joinpath(filename).resolve().relative_to(static_root_path)` shape. +- **JS/TS Array-method validator-callback narrowing.** `arr.filter(isSafeIdentifier)`, `arr.find(isValidId)`, `arr.findLast(...)` with a `BooleanTrueIsValid` callback (`isValid…`, `isSafe…`, `hasValid…` and snake-case variants) propagate `validated_must` through the call's return value. Resolves callback name from `info.arg_callees` (call-shape arguments) and SSA `value_defs[v].var_name` (bare-identifier callbacks, the dominant patched-CVE form). Strict-additive: anonymous arrows / opaque identifiers leave existing propagation untouched. `findIndex` / `every` / `some` excluded (scalar return shape). Motivated by CVE-2026-42353. +- **JS/TS ternary-branch source classification.** `let arr = cond ? req.query.lng : "";` previously lowered each branch to a labelless Assign with empty uses; the join phi saw no taint. `lower_ternary_branch` now runs `first_member_label` on the branch AST when no `Source` label is already attached. +- **PHP `fopen` modeled as `Sink(Cap::SSRF)`** (same dual SSRF / LFI shape as `file_get_contents`; fires only on tainted argument). Closes CVE-2026-33486 (roadiz/documents `DownloadedFile::fromUrl` wrapping `fopen($url, 'r')`). +- **PHP `Serializable::unserialize($input)` magic-method passthrough recognition.** The legacy `Serializable` interface contract (deprecated since PHP 8.1) requires the implementation to call `\unserialize($input)` on the formal parameter inside `public function unserialize($x) { ... }`. PHP itself invokes the method when restoring an instance, so the body's call cannot be removed without breaking the interface. `php.deser.unserialize` now suppresses inside this exact shape (method named `unserialize`, single formal, bare-parameter argument). Class-level `Serializable` implementation is the actionable signal (fix is migration to `__serialize` / `__unserialize`). Closes joomla / drupal Serializable-implementing class FPs. +- **SQLAlchemy query-builder chained-call recognition.** `select(X).filter_by(...)`, `query(X).filter(...)`, `select().join().where()` chains now anchor through the chain root primitive when the chain receiver type is opaque. New `db_query_builder_roots` config (Python defaults: `select`, `query`). Closes airflow `session.scalar(select(C).filter_by(conn_id=user_input))` shapes that previously dropped under the chained-call suppression in `classify_sink_class`. +- **Python non-sink container constructor recognition.** Bare-callee `set()` / `dict()` / `list()` / `tuple()` / `frozenset()` / `defaultdict(...)` is treated as a non-sink constructor, so `verified_ids = set(); verified_ids.update(myteams)` does not classify the `.update` call as `DbMutation`. Type-annotation hint form `set[int]` / `dict[str, int]` recognised via PEP 585 generic suffix strip alongside the existing angle-bracket strip. +- **Python `request.match_info` source label** (aiohttp path-parameter source). +- **New Python pattern `py.xss.make_response_format` (Tier B).** Flask `make_response()` reflection. Recognises both bare `make_response(...)` and `flask.make_response(...)`. Closes CVE-2023-6568 (mlflow auth `create_user` reflecting attacker-controlled `Content-Type` header into the response body). + +### Language coverage + +Per-language label rules expanded for the seven new caps. + +- **JavaScript / TypeScript:** ldapjs `LdapClient.search`, `escapeXpath` / `xpathEscape`, `document.evaluate` / 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` opt-in flag: Python `dict.update` / `__dict__.update` proto-pollution gates are off by default because bare `update` overlaps too broadly with `Counter.update` and ordinary state-mutation patterns to ship as a default sink. + +### CVE corpus + +- **C.** CVE-2017-1000117 (git argv injection via `ssh://-oProxyCommand=…`) vulnerable + patched fixtures under `tests/benchmark/cve_corpus/c/CVE-2017-1000117/`. Known remaining gap: array-element taint propagation, `c.cmdi.exec*` AST patterns, and dash-prefix-byte sanitizer recognition. +- **Python.** CVE-2023-6568 (mlflow reflected XSS), CVE-2024-21513 (langchain SQL / Jinja), CVE-2024-23334 (aiohttp static-file path traversal) vulnerable + patched fixtures. +- **PHP.** CVE-2026-33486 (roadiz/documents SSRF) vulnerable + patched fixtures. +- **JavaScript.** CVE-2026-42353 (i18next-http-middleware path traversal) vulnerable + patched fixtures. + +### CLI + +- **`nyx rules list`** 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 `, `--kind `, `--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. `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 cannot drift silently. +- **`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 + +- **Refreshed local web UI visual system** around the mint-cyan Nyx brand: warmer light surfaces, deep green accents, updated severity / confidence colors, tighter typography, smaller radii, denser cards, table, badge, button, header, and sidebar styling, and matched graph / code-viewer colors. +- **Reworked `nyx serve` surfaces** for a more operational layout. Overview uses the refreshed health-score card and chart grid; Scans has a fixed compact table with capped language badges; Scan Detail places summary and timing data side by side; Triage, Rules, Config, Explorer, Finding Detail, Scan Compare, and Debug pages received focused spacing, overflow, and density fixes. +- **Branded asset set** shared between the SPA and the embedded server bundle: PNG favicons, Apple touch icon, sidebar logo image, refreshed SVG favicon, and Rust static handlers for the new `/logo.png` and favicon files. +- **Frontend `RuleListItem` and `RuleDetailView`** carry the new `is_class` flag so the dashboard's Rules page can group cap-class entries separately. +- **Regenerated README and docs screenshots and GIFs** against the new UI at 1600x992, saving raw originals before framing and adding CLI GIF plus combined CLI-to-serve demo GIF capture support. Extended the screenshot capture workflow with mint-led framing copy, optional `nyxscan.dev` asset mirroring, WebP regeneration for mirrored PNGs, and raw `_raw` image / GIF outputs for downstream reuse. + +### Performance + +- **Hoisted `collect_top_level_units` out of the per-extractor loop** in `extract_authorization_model`. Multi-extractor languages (Go gin+echo, JS/TS express+koa+fastify, Python flask+django, Rust axum+actix_web+rocket, Ruby sinatra) had been re-walking the entire AST and rebuilding the `Function`-kind unit set per extractor, then deduping by span. New `AuthExtractor::requires_top_level_units()` opt-out for Spring / Rails which build their own. Was 46% of `extract_authorization_model` wall-clock on the mattermost/server/channels/app subtree. +- **Single `AuthorizationModel` build per file in fused mode.** The diag path and the per-file summary path each ran their own `extract_authorization_model`, duplicating the hoisted unit pass and every framework extractor's AST walk. Auth summaries now extract from the base model (pre var-types, pre helper-lifting) so the persisted per-file summary matches the legacy `extract_auth_summaries_by_key` path bit-for-bit. +- **O(N) shallow value-ref emission in `collect_unit_state`.** The previous per-node `extract_value_refs(node, bytes)` walked the entire subtree on every recursion level (O(N²) per body) even though the recursion below already visits every descendant once. New `append_shallow_value_ref` emits the node's own ref and lets recursion handle the descent. Public callers of `extract_value_refs` (`collect_call`, `collect_condition`, assignment-side extraction) keep the deep walk. Was ~17% + 15% + 11% of wall-clock split across `build_function_unit_with_meta`, `collect_unit_state`, and `extract_value_refs` on mattermost. +- **Per-`ParsedFile` `body_const_facts_cache: OnceCell`.** SSA + const-prop + type-fact build was running 2-3× per body across `run_cfg_analyses_with_lowered`, `run_auth_analyses`, and `collect_file_var_types`. Single-pass cache; gin profile dropped from 13.6% to ~4.5%. +- **SCCP switched from `HashMap` and `HashSet<(BlockId, BlockId)>`** to dense `Vec` per-value lattice and per-destination predecessor `SmallVec<[BlockId; 2]>`. The inner fixed-point loop no longer SipHashes a 64-bit pair for every operand of every phi. Public `ConstPropResult` shape unchanged (one final O(num_values) HashMap conversion). +- **`GlobalSummaries.by_key` switched to `FxHashMap`** (rustc-hash 2.1) from stdlib SipHash. `FuncKey` carries 3 String fields, so any HashMap operation hashes at least 30 bytes; FxHash is ~5× faster on this workload. Seed is fixed (no DoS hardening), fine for an in-process index keyed by program-derived names. +- `large_go_module.go` perf fixture (1493 lines) added to `benches/perf_fixtures/`; `benches/scan_bench.rs` extended with auth-extractor, SCCP, and summary-resolution rows. + +### Fixed (false positives) + +- `Object.create(null)` receivers no longer fire prototype-pollution at the synthetic `__index_set__` sink. Suppression is flow-sensitive 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. +- `setValue(target, req.body, ...)` and similar wrappers no longer gate-match on the rewritten Source `req.body` text. +- Nokogiri / lxml / fast-xml-parser parser bodies hardened with `setFeature` / `processEntities: false` / `XMLParser(resolve_entities=False)` no longer fire `taint-xxe`. +- `XPath` instances bound to `setXPathVariableResolver(...)` no longer fire `taint-xpath-injection` on subsequent `xpath.evaluate(expr, ...)` sinks. +- Inline `if (!url.startsWith("/")) reject` and `if (new URL(url).host !== ALLOWED) reject` open-redirect sanitisers narrow `Cap::OPEN_REDIRECT` on the validated branch instead of falling through to the generic `Comparison` predicate. 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`. +- ~957 gitea backend DAO `go.auth.missing_ownership_check` findings (id-scalar precision pass). +- 169 of 216 openmrs `cfg-unguarded-sink` findings (JpaCriteriaQuery type). Equivalent reductions on xwiki / keycloak Hibernate DAO clusters. +- joomla and drupal `php.deser.unserialize` flagged inside `Serializable::unserialize($input)` magic-method bodies. +- airflow execution-API routes flagged `missing_ownership_check` despite being authorized via cross-file `include_router` chains and module-level `APIRouter(dependencies=[…])` declarations. +- sentry `verified_ids = set(); verified_ids.update(myteams)` flagged as `DbMutation`. +- aiohttp `path.relative_to(static_root_path)` not recognised as a path-traversal validator. +- i18next-http-middleware `arr.filter(utils.isSafeIdentifier)` not narrowing taint on the result. +- `cond ? req.query.lng : ""` ternary lost `Source` label on the truthy branch. +- `if (!validate($x))` rejection-arm narrowing flipped on PHP unary `!`. +- mlflow `make_response(f"Invalid content type: '{content_type}'")` (Tier B pattern). +- Bare-callee verb-name dispatch on Python builtins / locally-defined helpers (`list`, `filter`, `update`, `create_audit_entry`, `update_coding_agent_state`). +- FastAPI `Depends(...)` / `Security(...)` deps declared on a module-level `APIRouter` no longer dropped on every attached route. +- FastAPI `Security(callable, scopes=[...])` no longer downgraded to a Login-only check. + +### Tests + +- New per-cap integration suites: `tests/{xpath_injection,xxe,ssti,prototype_pollution,header_injection,open_redirect,ldap_injection}_tests.rs`, plus `python_proto_pollution_tests.rs` for the env-gated Python form. Per-cap fixture trees under `tests/fixtures///` cover safe, unsafe, and irrelevant-baseline shapes for every supported language. +- Cross-file FastAPI integration test `tests/fastapi_cross_file_include_router_tests.rs` with airflow-shaped fixture tree under `tests/fixtures/auth_cross_file/airflow_execution_api_includes/`. +- New `cfg/cfg_tests.rs` covers ternary-branch CFG lowering shapes. +- New `summary/tests.rs` covers cross-file `include_router` summary persistence and resolution. +- Per-language safe / vuln auth and detector fixtures across Python, Java, Go, PHP, JS, TS. + +### Other + +- Refactor passes across `auth_analysis`, `ssa/const_prop`, `ssa/type_facts`, `summary`, and the per-framework auth extractors (cleaner conditional checks, simpler function signatures, deduplicated assertions). No behaviour change. +- README links to a Simplified Chinese translation (`README.zh-CN.md`). + +## [0.6.1] - 2026-05-03 + +A precision pass on auth and resource analysis plus three fresh CVE corpus pairs, plus a UTF-8 slice panic in the path abstract domain. Closes ~1900 Go auth FPs on gitea-shaped helpers, the mastodon/diaspora private-callback Ruby controller pattern, and a phantom-taint outbreak from JS/TS / Java lambda shorthand in jest-style nested test callbacks. + +### Added + +- Java JDBC raw-SQL sinks. `Statement.execute`, `Statement.executeBatch`, and `Statement.executeLargeUpdate` modeled as `SQL_QUERY` sinks, classified via type-qualified resolution (`DatabaseConnection.execute`) so bare `execute` (Runnable, Executor, HttpClient) does not over-fire. `conn.createStatement()` and `conn.prepareCall()` now infer return type `DatabaseConnection`, so the JDBC chain `Statement s = conn.createStatement(); s.execute(q)` types `s` correctly. Closes GHSA-h8cj-hpmg-636v (Appsmith FilterDataServiceCE.dropTable). Vulnerable + patched Java fixtures added. +- Java/Kotlin `Pattern.matcher(value).matches()` chain recognised as a `ValidationCall` allowlist. Receiver of `.matcher(` must contain `regex` or `pattern`. Validation target is the `.matcher()` argument, not the bare `.matches()` receiver. Branch narrowing applies the `validated_must` to the input variable on the surviving branch. Same GHSA as above (`FILTER_TEMP_TABLE_NAME_PATTERN.matcher(tableName).matches()`). +- Per-parameter SSA summary probe now receives `BodyMeta.param_types`, so `extract_ssa_func_summary` runs a local `analyze_types_with_param_types` pass before extraction. Helper bodies whose sinks resolve only via type-qualified callees (e.g. `DatabaseConnection.execute` for JDBC `Statement.execute`) no longer drop the sink during cross-function summary extraction. Fixes the Appsmith helper `executeDbQuery(query)` that routed SQL through `statement.execute(query)`. +- Short-circuit branch condition CFG nodes now mirror `condition_vars` into `taint.uses`, so `apply_branch_predicates` interns the variable for short-circuit-decomposed validators (`if (x == null || !regex.matcher(x).matches()) throw`). Without this, the per-disjunct cond nodes built via `build_condition_chain` silently no-opped and `x` never reached `validated_must` on the surviving branch. +- Go `goqu.L(s)` and `goqu.Lit(s)` raw-SQL literal builders modeled as `SQL_QUERY` sinks. Safe siblings (`goqu.I` identifier, `goqu.C` column, `goqu.T` table, `goqu.V` parameterised value, `goqu.SUM`, `goqu.COUNT`, …) stay unlabeled. Gin source list extended with the array-returning siblings of the existing scalar helpers: `c.QueryArray`, `c.GetQueryArray`, `c.PostFormArray`, `c.GetPostFormArray`. Closes CVE-2026-41422 (daptin: `c.QueryArray("column")` → `goqu.L(project)` with the loop variable lifted through `for _, project := range columns`). Vulnerable + patched Go corpus pair under `tests/benchmark/cve_corpus/go/CVE-2026-41422/`. +- Go `for ident := range iter` def-use lifting. The `range_clause` child of `for_statement` is now consulted when `left`/`right` aren't direct fields of the `for` node, so taint from the iterable reaches the loop binding. Required for the daptin CVE shape above. +- Java `enhanced_for_statement`, PHP `foreach`, and Ruby `for` def-use lifting, completing the loop forms the Go `range_clause` fix above started. The `Kind::For` def-use arm only knew the JS/Python `left`/`right` pair and Go's `range_clause`; Java carries the binding on `name` and the iterable on `value`, Ruby's `for` on `pattern`/`value`, and PHP's `foreach` keeps both as unnamed children split by the `as` keyword, so none recorded the loop variable as a define and taint on the iterable never reached the binding (`for (Cookie c : req.getCookies()) { … c.getValue() … }` lost the flow at `c`). Each form now folds onto the shared define/use path. Lifts Java OWASP Benchmark recall: path_traversal 0.21 → 0.32, sqli 0.16 → 0.28, cmdi 0.04 → 0.08. +- Iterable-expression classification for the loop forms above. The loop node is classified against its iterable text, so a source-returning iterable (`req.getCookies()`, `req.getParameterValues("v")`, `$_GET['list']`) lands a `Source` on the loop node and the binding inherits its taint, the same rewrite JS/Python `for … of` / `for … in` already had. Subscript iterables (`$_GET['x']`, `params[:list]`) classify on their base object since sources key on the base name, not the index. +- Java iterable-returning request accessors modeled as sources: `getParameterValues`, `getParameterMap`, `getParameterNames`, `getHeaders`, `getHeaderNames`. The `getParameter` / `getHeader` matchers are word-boundary suffix matches and never covered the plural collection variants that feed for-each loops (`for (String s : req.getParameterValues("v"))`). The dominant OWASP Benchmark vulnerable-source shape. +- Rust format-string named-argument lifting (`format!("...{x}...")`, stable since 1.58). Identifiers captured by `{name}` / `{name:fmt-spec}` are pulled into the call's `uses` for known format-style macros: `format`, `print`/`println`, `eprint`/`eprintln`, `write`/`writeln`, `panic`, `format_args`, `assert`/`debug_assert`, `todo`, `unimplemented`, `unreachable`, plus log-crate severity macros (`info`, `warn`, `error`, `debug`, `trace`). Recursive descent through one or two layers of expression wrapping (`format!("{x}").to_owned()`, RHS chained method calls). Without this, taint stopped at the macro boundary. `let q = format!("...{x}...")` carried no `x` because the identifier lives in format-string bytes rather than as a separate AST argument node. Mirrors the Python f-string lifter. +- Rust CVE corpus extended. CVE-2023-42456, CVE-2024-32884, CVE-2025-53549 vulnerable + patched fixtures under `tests/benchmark/cve_corpus/rust/`. +- Java lambda shorthand recognised by `extract_param_meta`. `lambda_expression`'s `parameters` field as a bare `identifier` (`cmd -> …`) or as an `inferred_parameters` wrapper around identifiers (`(a, b) -> …`) was not matching the formal_parameter / spread_parameter kinds in `PARAM_CONFIG`, so the lambda appeared parameterless and the SSA pipeline treated its formals as closure captures. Mirrors the JS/TS arrow shorthand path. + +### Fixed + +- Panic on non-ASCII input to `has_first_char_absolute_check` in the path abstract domain. The 32-byte search window around `[0]` was sliced as `&clause[lo..hi]` (str), which panicked when `hi` landed inside a multi-byte UTF-8 char (e.g. the em dash `—`, bytes 34..37). Switched to `&bytes[lo..hi]` with `windows()` byte-pattern checks; all needles are ASCII so the searches are equivalent. Surfaced by `cargo fuzz` (`scan_bytes` target, `.c` extension path, embedded `—` in a comment near `s[0] == '/'`). Regression test added. + +### Fixed (false positives) + +- `cfg-unguarded-sink` parameter-only trace no longer clears a sink argument whose reaching definition is a loop binding. Once the loop variable resolves to its iterable (the def-use lifting above), a `foreach ($param as $v) { sink($v) }` element looked like a bare `sink($p)` wrapper pass-through and the structural finding was dropped. A loop element over a parameter collection is not wrapper plumbing, so the finding survives for loop-bound sink arguments; literal-keyed arrays stay suppressed through `sink_arg_uses_safe_foreach_key`. Keeps the negative case in `fp_guard_php_foreach_safe_literal_keys` firing. +- Go `unit_has_user_input_evidence` framework-request-name allow-list narrowed for Go. `ctx`, `context`, `info`, `body`, `path`, `payload`, `dto`, `form`, `query` are no longer treated as user-input indicators on Go: in Go these are `context.Context` (cancellation/value-bag from the stdlib) or struct-pointer payload params (`info *PackageInfo`, `opts *FooOptions`), not request bindings. Go HTTP frameworks bind the request to per-framework typed params (`r *http.Request`, `c *gin.Context`, `c echo.Context`, `c *fiber.Ctx`); these arrive at the gate via `RouteHandler` kind or the type-aware param filter below. Stdlib `req` / `request` (the `*http.Request` convention) preserved. Other languages keep the broader allow-list. +- Go param collection drops `ctx context.Context` and `ctx context.CancelFunc` parameters entirely rather than seeding their names into `unit.params`. Tree-sitter-go's `parameter_declaration` exposes `name` and `type` as named fields; descend only into `name` so type-segment identifiers don't pollute the param-name set (`info *PackageInfo` no longer contributes `PackageInfo`). Together with the allow-list narrowing above, closes ~1900 `go.auth.missing_ownership_check` findings on gitea backend helpers whose only "user-input evidence" was the ubiquitous `ctx context.Context` first param. +- Ruby controller method visibility + filter-callback gate. Methods marked `private` (bare `private` directive, targeted `private :foo, :bar`, or `protected`) and Rails filter callback targets (`before_action`, `after_action`, `around_action`, their `prepend_*` / `append_*` / `skip_*` siblings, and the legacy `*_filter` aliases) are no longer emitted as `Function` units. Visibility tracking is class-body source-order with two directive forms (bare toggles default visibility, targeted explicitly marks named methods). Block-form filters (`before_action do … end`) carry no symbol arg and are correctly ignored. Closes mastodon / diaspora `rb.auth.missing_ownership_check` flood on `set_X` row-fetch helpers used as `before_action` callbacks. +- Field-LHS resource acquires no longer counted as local resource leaks at the `apply_assignment` site. `e->name = (char *)e + sizeof(*e)` (sub-buffer alias inside a returned struct) and `mem->buf = ptr` (local-into-field ownership transfer) now mark the RHS local `MOVED` and stop tracking the field as a separately OPEN resource. The parent struct owns the field's lifecycle. Cross-language (distinct from the Go-only `apply_call` field-LHS gate, which is restricted because JS/TS class-field acquires `this.fd = fs.openSync(...)` are the documented expected leak pattern in that path). Closes curl `entry_new` and equivalent C/C++ shapes in openssl / postgres. +- Empty-formals SSA lowering signal. `lower_to_ssa_with_params` now sets `with_params=true` even when `formal_params` is empty, so an arrow `() => {…}` is treated as "explicitly zero formals" rather than "no formals info". External vars in a zero-formal arrow are now correctly tagged as synthetic closure captures, so the JS/TS / Java auto-seed pass cannot mistake a bubbled-up free var (e.g. `userId` lifted from a nested jest test callback) for a real handler formal. Closes 934 phantom taint findings on the outline test suite (`describe("…", () => { test("…", () => { server.post(…) }) })`-shaped fixtures). +- Rust integer-typed values now suppress `Cap::FILE_IO` at the abstract-domain leaf gate (previously HTML_ESCAPE only). An integer's decimal representation is digits with optional leading `-`, never path metacharacters (`/`, `\`, `.`); magnitude is irrelevant. Closes the sudo-rs RUSTSEC-2023-0069 patched FP `let uid: u32 = user.parse()?; path.push(uid.to_string())`. + +## [0.6.0] - 2026-05-02 + +A focused release that splits data-exfiltration off from SSRF and ships sinks for outbound HTTP request bodies across all 10 languages, with calibration tuned so plain user input echoed back upstream does not fire. + +### Added + +- New `taint-data-exfiltration` rule, separate from SSRF. Fires when a Sensitive-tier source (cookie, header, env, file, database, caught exception) reaches the body, headers, or json payload of an outbound HTTP call. Plain user input gets suppressed at emission time so a gateway echoing `req.body` back upstream is not flagged. +- Sinks ship for `fetch` body, `XMLHttpRequest.send`, Python `requests.post` and `httpx.AsyncClient.post`, Java JDK `HttpClient.send` with `BodyPublishers`, OkHttp builder chains, Apache HttpClient `execute`, RestTemplate, WebClient, Go `http.Post` and `http.NewRequest` + `Do`, Rust `reqwest`/`ureq`/`surf`/`hyper` body/json/form/multipart chains, Ruby `Net::HTTP.post` and RestClient, C and C++ `curl_easy_setopt(CURLOPT_POSTFIELDS, ...)` gated by the macro arg. +- Three suppression knobs: + - Sanitizer convention. `logEvent`, `forwardPayload`, `tracker.send`, `analytics.track`, `metrics.report`, `serializeForUpstream` are treated as `Sanitizer(data_exfil)` by default. Add your own with the standard custom-rule path. + - Trusted destination allowlist in `detectors.data_exfil.trusted_destinations`. Matched against the abstract-string domain prefix; a literal or template prefix that begins with one of these entries drops the cap. + - Detector toggle `detectors.data_exfil.enabled = false` strips the cap before emission. Other taint classes are unaffected. +- Calibration. Severity is High for cookie or env sources, Medium for header, file, database, or caught-exception sources. Confidence stays at Medium even with strong corroboration, drops to Low without abstract or symbolic backing, and drops one tier on path-validated flows. SARIF output carries a `properties.data_exfil_field` entry on data-exfil findings, set to the destination object-literal field the leak reached (`body`, `headers`, or `json`). +- Benchmark coverage. 13 vulnerable fixtures across 8 languages under `tests/benchmark/corpus/{lang}/data_exfil/` and 6 paired safe fixtures for the sensitivity gate and sanitizer convention. New `data_exfil` row in the per-class breakdown. Per-class CI floor at P, R, F1 ≥ 0.85 (current baseline is 1.000). +- Backwards taint walk recognises `Cap::DATA_EXFIL` and emits the same rule ID. +- Ruby SSRF coverage. `OpenURI.open_uri` now classified as an SSRF sink (the low-level fetcher that `URI.open` delegates to). Closes the CarrierWave CVE-2021-21288 download path and equivalent gem shapes that route through `OpenURI` directly. +- Ruby chained-call wrapper classification. Statement-level wrappers like `YAML.safe_load(File.read(filename))` and `Marshal.load(File.read(p))` now classify the inner sink for cross-function summary extraction. Without this, the outer call became a non-sink node and the inner sink was lost when the helper was summarised. +- Ruby CVE corpus. Vulnerable + patched fixtures added for CVE-2021-21288 (CarrierWave SSRF) and CVE-2023-38337 (rswag path traversal). +- Lodash `_.template` modeled as a gated `Cap::CODE_EXEC` sink. Activates on the template-string argument; suppresses when arg-1 carries a literal `{ evaluate: false }`. Closes Strapi CVE-2023-22621 (server-side template injection → RCE via `<% … %>` evaluate blocks). Vulnerable + patched fixtures added under `tests/benchmark/cve_corpus/javascript/CVE-2023-22621/`. +- JS/TS gated-sink kwarg extractor falls back to inspecting arg-1 object literals (`fn(x, { evaluate: false })`) when the language has no `keyword_argument` node. Required so the lodash gate can read its options object. +- Lodash double-call form (`_.template(t)(data)`) routes through `find_chained_inner_call` so the outer call's gated-sink rebinding fires. +- Cross-function helper-validation propagation. New `SsaFuncSummary.validated_params_to_return` field records parameter indices whose taint flow to the return value is fully validated by a dominating predicate (regex allowlist, type check, validation call) on every return path. At call sites, each tainted argument passed to a validated position, and the call's own return value, are marked `validated_must` / `validated_may` in the caller's SSA taint state, the same way an inline `if (!regex.test(x)) throw` would. Closes the helper-validator gap behind PayloadCMS CVE-2026-25544 (Drizzle SQL injection in `sanitizeValue`). Vulnerable + patched TypeScript fixtures added. +- Destructured-arg sibling expansion in per-parameter taint summary probing. JS/TS object-pattern formals (`({ column, operator, value }) => …`) now seed every binding sharing the slot, and any sibling reaching `validated_must` counts as the slot being validated. New `BodyMeta.param_destructured_fields` carries sibling lists alongside `params` and `param_types`. JS `PARAM_CONFIG` accepts `assignment_pattern` (default-value formals) and `object_pattern` (destructured formals). +- Regex-allowlist branch narrowing. `.test(value)` / `.match(value)` / `.matches(value)` where the receiver name contains `regex` or `pattern` classifies as a `ValidationCall` and narrows the call's first argument, not the regex receiver. Was also extended to `extract_validation_target` so the surviving branch validates `value`, not the regex object. Motivated by Payload CVE-2026-25544 (`if (!SAFE_STRING_REGEX.test(value)) throw …`). +- TypeScript template-substring (`${fn(arg)}`) call-resolution arity-hint fallback. When CFG lowering drops `arg_uses` but `args` is non-empty, the resolver passes `None` so the unique-name fallback can still pick up the lone candidate. +- Caller-scope-entity exemption in `rs.auth.missing_ownership_check`. `.id` / `.pk` no longer fires when `` is a unit parameter named after a multi-tenant scope primitive: `organization` / `org`, `project`, `team`, `workspace`, `tenant`, `account`, `community`, `group`, `repository` / `repo`, `company`. Other field names (`.name`, `.slug`) still flag, and `user` / `member` / `actor` are deliberately excluded (handled by `is_actor_context_subject`). Closes a flood of FPs in Sentry / Saleor / Discourse / Mastodon-shaped multi-tenant helpers (`get_environments(request, organization)`, `_filter_releases_by_query(qs, organization, …)`). +- Auth value-ref walker recurses into the `value` child of `keyword_argument` / `keyword_arg` / `named_argument` nodes. `Model.objects.filter(organization_id=org.id)` no longer surfaces the kwarg key (`organization_id`) as a bare-identifier user-input subject. The schema column name is fixed at call time. +- Test-decorator denylist for Flask route extraction. `mock.patch`, `mock.patch.object` / `.dict` / `.multiple`, `unittest.mock.*`, `monkeypatch.setattr` / `setenv` / `delattr` / `delenv`, and `pytest.mark.parametrize` no longer collide with `.patch` route registration. Stops every `@mock.patch("…")`-decorated test method from being attached as a Flask PATCH handler and flagged as `missing_ownership_check`. +- Typed-extractor route-level guard injection for axum and actix-web. Handlers registered via attribute macros (`#[get("/path")]`, `#[routes::path(…)]`) or via external service-config builders previously never had their typed-extractor guards seeded. New `apply_typed_extractor_guards_to_units` walks every `Function`-kind unit and injects guard checks from typed-extractor params, complementing the route-walk path that already covered `.route(...)` registration. +- New auth config key `policy_guard_names`. Typed-extractor wrappers that prove route-level capability/policy enforcement (e.g. meilisearch's `GuardedData, _>`) are recognised distinctly from authentication-only wrappers. Matched as last-segment + case-insensitive `starts_with`. Rust default: `["Guarded"]`. Distinct from `login_guard_names` so the pattern doesn't pollute regular call recognition (a function like `guarded_load(..)` is not a login guard). +- Outer-wrapper-aware classification of typed extractors. `GuardedData, Data>` is classified by the outer `GuardedData` (policy-bearing → `AuthCheckKind::Other`), not by whether an inner generic arg substring-matches `auth`. Bare data-only extractors (`Path`, `Query`, `Json`, `Form`, `State`, `Extension`, `Data`) outer-name-match early-return to `None` regardless of inner type tokens. Reference-marker (`&`, `&mut`, `&'a`) and module-path (`std::collections::`) prefixes stripped before matching. +- Project-level web-framework signal in Rust auth analysis. New `FrameworkContext::lang_has_web_framework(lang)` is three-valued: `Some(true)` when manifest names a framework, `Some(false)` when the manifest was inspected and named none, `None` when no manifest was inspected. New `rust_file_imports_web_framework` does a per-file `axum::` / `actix_web::` / `rocket::` / `axum_extra::` import probe (8 KB head). When the project's Cargo.toml is inspected and lists no Rust web framework AND the file does not directly import one, the `context_inputs` and param-name-heuristic arms of `unit_has_user_input_evidence` are suppressed. `RouteHandler` classification (concrete route-registration evidence) still bypasses the gate. Closes a flood of `missing_ownership_check` FPs in non-web Rust crates such as zed-style desktop / GUI codebases where a debug-session handle named `session` would trip `matches_session_context` on `session.update(cx, …)`. Currently Rust-only; other languages keep prior behavior (`None`). +- Rust auth corpus extended with `safe_actix_guarded_data_extractor.rs` and `unsafe_actix_no_guarded_data_extractor.rs` (typed-extractor guard injection); `safe_non_web_rust_project/` and `unsafe_actix_web_project_no_check/` (full Cargo.toml + src/lib.rs project shapes for the framework-signal gate). +- Python auth corpus extended with `vuln_user_id_param_no_auth.py`, `safe_django_orm_caller_scoped_entity.py` (caller-scope-entity exemption), `safe_mock_patch_test_method.py` (test-decorator denylist). +- Go safe corpus extended with `safe_inner_call_close_in_arg.go` (`require.NoError(t, f.Close())` shape), `safe_struct_field_resource_owned_by_struct.go` (field-LHS ownership transfer), and a `vuln_resource_leak_no_close.go` regression guard. + +### Fixed (false positives) + +- C++ `cpp.memory.reinterpret_cast` no longer fires when the target type is well-defined by C++ aliasing rules. Suppressed targets: byte-pointer family (`char*`, `unsigned char*`, `signed char*`, `wchar_t*`, `uint8_t*`, `int8_t*`, `std::byte*`, `byte*`), `void*`, integer round-trip (`uintptr_t`, `intptr_t`, and `std::` variants, no pointer required), and the BSD socket address family (`sockaddr*`, `struct sockaddr*`, `sockaddr_in*`, `sockaddr_in6*`, `sockaddr_un*`, `sockaddr_storage*`). User-defined struct or class pointer targets keep firing. Closes ~70% over-fire on serialization, hashing, IPC, and socket-API code where the cast is the standard-blessed idiom. +- PHP `php.crypto.md5` and `php.crypto.sha1` suppress when the call's consuming context yields a non-cryptographic identifier name. Recognised contexts: assignment LHS (variable, `$obj->property`, `$arr['key']`), array element keys, subscript indices, return statements (resolved to enclosing method or function name with `get` prefix stripped), and method-call arguments where the method is a key/cache/lookup verb (`get`, `set`, `has`, `delete`, `fetch`, `store`, `find`, `getItem`, `setItem`). Names containing a crypto keyword (`password`, `secret`, `token`, `signature`, `hmac`, `digest`, `salt`, `key`) keep firing. Closes ETag generation, cache-key hashing, dedup fingerprint, and `getCacheKey()`-style false positives in real PHP repos (phpmyadmin, nextcloud). +- JS and TS `secrets.fallback_secret` no longer fire on empty-string fallbacks (`process.env.X || ""`). Developers write `|| ""` to satisfy non-undefined string types without committing a real secret. Non-empty literal fallbacks still fire. +- Path-traversal sink suppression accepts canonicalised-and-rooted shapes. New `PathFact::is_path_traversal_safe` predicate clears `Cap::FILE_IO` when the path is dotdot-free and either non-absolute or carries a verified prefix-lock. New `OPAQUE_PREFIX_LOCK` marker records the structural invariant ("rooted under SOME prefix") when the `starts_with`-style guard's argument is a method call, field access, or configured root rather than a string literal. Closes the Ruby `File.expand_path + start_with?(root)` shape (rswag CVE-2023-38337 patched counterpart), the Python `os.path.realpath + .startswith(root)` shape, and the JS `path.resolve + .startsWith(root)` shape. `classify_path_assertion` extended to JS `.startsWith(...)`, Python `.startswith(...)`, Ruby `.start_with?(...)` (paren and paren-less), and Go `strings.HasPrefix(...)`. +- Branch narrowing now flips prefix-lock attachment under condition negation. For `if !target.startsWith(ROOT) { return; }` the lock attaches to the surviving block, not the rejection arm. Rejection-axis narrowing is unchanged because the rejection classifier is text-level and already accounts for leading `!`. +- Go field-LHS resource acquires no longer counted as local resource leaks. `b.cpuprof = os.Create(...)` transfers ownership to the containing struct; closure responsibility belongs to a paired `Stop()` / `Release()` method on the struct's lifecycle. Gated in both `state/transfer.rs::apply_call` and `cfg_analysis/resources.rs::run`. Restricted to Go (`Lang::Go` check). JS/TS class-field acquires (`this.fd = fs.openSync(...)`) keep being tracked because the leak fixtures rely on it. Production trigger: prometheus `cmd/promtool/tsdb.go::startProfiling` cluster (`b.cpuprof`, `b.memprof`, `b.blockprof`, `b.mtxprof`). +- Go inner-call release in argument position. `require.NoError(t, f.Close())`, `errs = append(errs, f.Close())`, JUnit `assertEquals(0, in.read())`: releases that live in argument position now mark the receiver `CLOSED`. Bare-receiver inner calls only (chained-receiver releases stay owned by `chain_proxies`); marks `CLOSED` only with no `DoubleClose` attribution; respects `in_defer` for symmetry. + +### Other + +- Action download script warning for the mutable `latest` tag now references `v0.6.0` instead of `v0.5.0`. + +## [0.5.0] - 2026-04-29 + +The biggest release since launch. The taint engine was rebuilt on top of an SSA IR, cross-file analysis was deepened across the board, and Nyx now ships a local web UI for triaging findings without leaving your machine. + +> Heads-up: false positives or regressions on cross-file flows are possible. Please open an issue with a minimal reproduction if you hit one. + +### Highlights + +- **New SSA-based taint engine.** Block-level worklist analysis over a pruned SSA IR, replacing the legacy BFS engine across all 10 languages. More precise, easier to extend, and the foundation for everything else in this release. +- **Cross-file analysis.** Function summaries (including the new SSA summaries) flow across files via SQLite-backed persistence. Callee bodies can be inlined for context-sensitive analysis (k=1) and walked symbolically across file boundaries. +- **Symbolic execution layer.** Candidate findings are walked symbolically from source to sink, producing concrete attack witnesses, pruning infeasible paths, and (optionally) handing constraints off to Z3. +- **Local web UI (`nyx serve`).** React + Vite frontend for browsing findings, viewing flow paths, and triaging results. Triage decisions persist to `.nyx/triage.json` so they version with your code. +- **Hostile-repo hardening.** Path containment, loopback-only serving, CSRF tokens, bounded artifact reads. Safe to run on untrusted code. +- **Tighter false-positive controls.** Type-aware sink suppression, abstract interpretation (intervals + string prefixes), constraint solving, allowlist and type-check guard recognition, and confidence scoring on every finding. + +### Engine + +- SSA IR with dominance-frontier phi insertion. The optimization pipeline runs constant propagation, branch pruning, copy propagation, alias analysis, DCE, type facts, and points-to in sequence. +- Multi-label classification. A single API can carry both Source and Sink labels (e.g. PHP `file_get_contents`, Java `readObject`). +- Gated sinks. `setAttribute`, `parseFromString`, etc. only activate when the constant attribute argument is dangerous, and only the payload argument is treated as taint-bearing. +- Container taint with per-index precision and bounded points-to. Aliased containers share heap identity correctly. +- Loop-aware analysis: induction-variable pruning, widening at loop heads, bounded unrolling in symex. +- Path-sensitive phi evaluation propagates validation when all tainted predecessors are guarded. +- Per-return-path summaries decompose function effects when paths produce different taint behavior. +- Cross-file SCC fixed-point. Mutually recursive functions across files now reach a joint convergence. +- Demand-driven backwards analysis (off by default) annotates findings with cutoff diagnostics. +- Direction-aware engine notes (`UnderReport`, `OverReport`, `Bail`) flow into confidence scoring, ranking, and the new `--require-converged` strict mode. +- Synthetic field-write inheritance: `u.Path = "/foo"` no longer drops taint carried by other fields of `u`. Fixes Owncast CVE-2023-3188 (SSRF). +- Phantom-Param-aware field suppression skips method/function references that share a base name with a tainted variable. +- Validation err-check narrowing for the two-statement Go idiom `_, err := strconv.Atoi(input); if err != nil { return }`: `input` is marked validated on the surviving `err == nil` branch. +- Go: `strings.Replace` / `strings.ReplaceAll` recognised as a sanitizer when the OLD literal contains a known-dangerous payload (shell metachars, path-traversal, HTML, SQL) and the NEW literal does not reintroduce one. +- Go: literal-strip cap detection extended to shell metachars (`;`, `|`, `&`, `$`, backtick) and SQL metachars (`'`, `"`, `--`). +- Go: `interpreted_string_literal` / `raw_string_literal` handled in tree-sitter so const-string arg extraction works for Go's double-quoted and backtick forms. + +### Symbolic Execution + +- Expression trees (`SymbolicValue`) preserve computation structure through the path walk: integers, strings, binary ops, concatenations, calls, phi merges. +- Witness strings reconstruct concrete attack payloads at sink nodes. +- Bounded multi-path forking with reachability pruning. +- Cross-file: callee summaries are modeled directly, and pre-lowered callee bodies are loaded from SQLite so witnesses can keep walking across files. +- Interprocedural mode: nested frames with full state propagation, transitive descent up to 3 levels, structured cutoff tracking. +- Field-sensitive symbolic heap with bounded fields per object. +- Symbolic string theory: `Substr`, `Replace`, `ToLower`, `ToUpper`, `Trim`, `StrLen` modeled with concrete folding and sanitizer pattern detection. +- Optional Z3 integration (compile-time `smt` feature) for cross-variable constraint solving. + +### Security & Coverage + +- Vulnerability classes added: SSRF (10 languages), deserialization (Python, Ruby, Java, PHP), and `Cap::UNAUTHORIZED_ID` for auth-as-taint (off by default behind config flag). +- Auth analysis: receiver-type sink gating, row-level ownership-equality detection, self-actor recognition (`let user = require_auth()`), sink classification (in-memory vs realtime vs outbound), helper-summary lifting, and SQL JOIN-through-ACL recognition. +- State analysis (resource lifecycle, use-after-close, leaks, unauthed access) is now on by default. RAII-aware for Rust and C++; recognizes Python `with`, Go `defer`, Java try-with-resources. +- Framework rule packs: Express, Flask/Django, Spring/JNDI, Rails. Per-language label depth significantly expanded. +- C/C++ taint depth: output-parameter source propagation, implicit definitions for uninitialized declarations. +- Negative test corpus (30 fixtures) and a 262-case benchmark with CI gates on rule-level Precision/Recall/F1. + +### Detection metrics + +- Aggregate rule-level F1 reaches **0.998** (P=0.995, R=1.000). All real-CVE fixtures fire; only one open FP (`go-safe-009`). +- Go: 98.0% F1 on the 53-case corpus (1 FP / 0 FNs). +- CVE-2023-3188 (owncast SSRF) now detects. + +### CLI & Output + +- `nyx serve`: local web UI on `localhost` only (refuses non-loopback binds). +- `--require-converged` filters out findings where the engine bailed early. +- Analysis-engine toggles graduated from `NYX_*` env vars to first-class flags and `[analysis.engine]` config: `--constraint-solving`, `--abstract-interp`, `--context-sensitive`, `--symex`, `--cross-file-symex`, `--symex-interproc`, `--smt`, `--parse-timeout-ms`. Old env vars still work when Nyx is consumed as a library. +- Confidence (`High`/`Medium`/`Low`) shown on every finding, including console headers. +- Engine notes surfaced in console (`[capped: N notes, over-report]`), JSON (`engine_notes`, `confidence_capped`), and SARIF (`result.properties.loss_direction`). +- Flow paths reconstructed step-by-step with file/line/snippet for each hop. +- Concrete attack witness strings synthesized by the symbolic executor. +- Primary sink locations now point at the callee's real sink line; caller call sites are preserved as flow steps. +- Richer scan progress: explicit stages, timing breakdowns, language counters, skipped/reused file counts. +- Tighter taint-finding deduplication. + +### Hardening + +- Centralized path containment rejects traversal, symlink escapes, and oversized reads across UI, debug, and triage routes. +- `nyx serve` validates `Host` headers, requires per-session CSRF tokens for mutations, and refuses scans outside the original repo root. +- Walker re-validates symlink targets against the scan root. +- Bounded reads on framework manifests and `.nyx/triage.json` imports. +- UI falls back to plain text on pathologically long lines to defeat regex-DoS in syntax highlighting. +- Parser timeout is now configuration-backed with hostile-input regression coverage. + +### Persistence + +- SQLite schema bumped to v2. Anonymous-function identity is now a structural DFS index instead of a byte offset, so inserting a line above an unchanged function no longer invalidates its `FuncKey`. Pre-0.5.0 caches are silently cleared on open; triage data and scan history are preserved. +- Engine-version metadata; persisted summaries and file hashes invalidate on mismatch. +- Stale SSA tables recreate when required columns are missing; deserialization failures log instead of silently dropping rows. + +### Frontend + +- Replaced the legacy `app.js` with a React + Vite + TypeScript SPA. +- Interactive graph workspace for CFG and call-graph views (Graphology + ELK + Sigma) with neighborhood reduction and a full-page inspector. +- Triage UI with database-backed decisions (true positive, false positive, accepted risk, suppressed) and `.nyx/triage.json` round-trip. +- Scan history, rules management, and finding detail panels with evidence and flow visualization. +- Vitest browser-side test suite wired into CI. +- Bumped to React 19, Vite 8, TypeScript 6.0, ESLint 10, `@vitejs/plugin-react` 6, with aligned `@types/react*`. +- `SSEContext`: typed `reconnectTimer` ref as `ReturnType | undefined` to satisfy TS 6's stricter `useRef` overloads. +- `FindingsPage`: included `toast` in `useCallback` deps to avoid stale-closure warnings. +- `tsconfig.json`: dropped `baseUrl`, using a relative `./src/*` path mapping instead. + +### Removed + +- Legacy BFS taint engine, `TaintTransfer`, `TaintState`, and the `NYX_LEGACY` fallback. +- Legacy vanilla-JS frontend (`app.js`). + +## [0.4.0] - 2026-02-25 + +A precision and ergonomics release. Findings are now ranked, lower-noise by default, and easier to triage in CI. + +### Highlights + +- **Attack-surface ranking.** Every finding gets an exploitability score combining severity, analysis kind, evidence strength, and path-validation. Console output shows the score in the header line; `--no-rank` opts out. +- **Low-noise prioritization.** Quality-category findings are excluded by default (`--include-quality` brings them back). High-frequency Quality rules are rolled up per `(file, rule)` with example occurrences. LOW budgets cap noise without ever displacing High/Medium findings. +- **State-model dataflow analysis.** New per-variable resource-lifecycle and auth-level analysis catches use-after-close, double-close, must-leak, may-leak (branch-aware), and unauthenticated-sink access. Opt-in via `scanner.enable_state_analysis`. +- **Inline `nyx:ignore` suppressions** with same-line and next-line directives, comma lists, wildcard suffixes, and string-literal guards across all 10 languages. +- **AST pattern overhaul.** All 10 language pattern files rewritten with consistent metadata, namespaced IDs (`..`), and 30+ new patterns. 11 broken tree-sitter queries fixed. +- **Monotone forward-dataflow taint engine.** Replaced the BFS engine with a proper worklist over a finite lattice. Termination is now guaranteed by lattice height, eliminating BFS-budget bailouts on large files. +- **Path-sensitive taint analysis.** Branch predicates flow with the analysis. Contradictory guards prune infeasible paths; validation calls produce annotated findings without changing severity. +- **Interprocedural call graph.** Whole-program graph with three-valued callee resolution (`Resolved`/`NotFound`/`Ambiguous`), SCC analysis, and topo ordering ready for bottom-up taint propagation. + +### CLI & Output + +- `--severity ` replaces `--high-only`. Supports `HIGH`, `HIGH,MEDIUM`, `>=MEDIUM`. Filtering is now applied at the output stage so taint and CFG findings are correctly downgraded too. +- `--mode ` replaces `--ast-only` and `--cfg-only`. +- `--index ` replaces `--no-index` and `--rebuild-index`. +- `--fail-on ` for CI exit-code gating. +- `--min-score ` for ranking-aware filtering. +- `--show-suppressed` reveals suppressed findings dimmed with `[SUPPRESSED]`. +- `--keep-nonprod-severity` (renamed from `--include-nonprod`). +- `--quiet` mirrors `output.quiet`. +- Console renderer overhauled: severity is the strongest visual anchor, file paths are dim blue, taint flows use `→` arrows, multi-line call chains are normalized. +- Confidence shown alongside score in the header line. +- Pattern-level confidence is now set at the pattern definition site, not heuristically inferred from severity. + +### Breaking + +- Config and data directory renamed from `dev.ecpeter23.nyx` to `nyx`. Existing config and SQLite indexes at the old path won't be picked up. Copy them across or re-run `nyx scan`. +- `Severity::from_str` now returns `Err` for unknown values instead of silently defaulting to Low. + +### Notable Fixes + +- KINDS-map audit across all 10 languages: 89 missing tree-sitter node types added. Switch/case, try/catch/finally, class bodies, lambdas, closures, and namespaces are no longer silently dropped. +- `else_clause` mapping fixed for C, C++, Rust, JS, TS, Python, PHP. Code inside else blocks was being dropped from the CFG. +- Rust `if let` / `while let` taint propagation now works. +- Taint BFS non-termination on large JS files (the BFS engine has since been replaced). +- C++ `popen` pattern ID collision with C. +- Constant-arg sink suppression for AST patterns. + +## [0.3.0] - 2026-02-25 + +Configurability, SARIF, and an aggressive false-positive purge. + +### Highlights + +- **Configurable analysis rules.** Sources, sanitizers, sinks, terminators, and event handlers can be defined per language in `nyx.local` or via `nyx config add-rule`/`add-terminator`. Config rules take priority over built-in rules. +- **`nyx config` CLI subcommand** with `show`, `path`, `add-rule`, `add-terminator`. +- **SARIF 2.1.0 output (`-f sarif`).** Spec-compliant for GitHub Code Scanning, Azure DevOps, and other SARIF consumers. +- **`SourceKind` taint classification.** Findings carry an inferred source kind (`UserInput`, `EnvironmentConfig`, `FileSystem`, `Database`, `Unknown`) and severity is now derived from it instead of being hardcoded to High. +- **Non-prod severity downgrade by default.** Findings in tests, vendor, benchmarks, examples, fixtures, build scripts, and `*.min.js` are downgraded one tier. `--include-nonprod` restores original severity. +- **Resource leak detection** for Python, Ruby, PHP, JavaScript, and TypeScript (file handles, sockets, locks, mysqli, curl, fs streams). +- **Progress bars and quiet mode.** Indicatif-driven progress for discovery, Pass 1, and Pass 2 (auto-hidden in JSON/SARIF/quiet modes). + +### Performance + +- Single fused parse+CFG pass replaces the previous two-parse summary extraction. +- Light-weight dataflow sweep in CFG builder is now O(N) per function instead of O(N²) over the whole file. +- Parallel summary merging via rayon fold/reduce. +- Indexed scans now read and hash each file once instead of up to 4 times. +- SQLite mutex mode relaxed (r2d2 + WAL provides safety without global lock). +- Zero-allocation taint hashing and in-place taint transfer. + +### Notable Fixes + +- One-hop constant-binding suppression: `cmd = "git"; subprocess.run([cmd, ...])` no longer flags. +- Exec-path guards (`which`, `resolve_binary`, `shutil.which`) recognized. +- `signal.connect` / `event.connect` no longer match Python db-connection acquire patterns. +- `threading.Lock()` without `.acquire()` no longer flags as unreleased. +- `FileResponse(f)` / `send_file(f)` recognized as ownership transfer. +- `el.href` no longer matches `location.href` patterns. +- Constant-only sink calls (`subprocess.run(["make","clean"])`) suppressed. +- `std::cout` no longer treated as a sink. +- Break/continue inside loops correctly wires into the loop header/exit, fixing false unreachable-code findings. +- Preprocessor `#ifdef`/`#endif` blocks no longer orphan subsequent code in C/C++. +- `freopen` no longer matches `fopen` acquire patterns. +- Struct-field, linked-list, and global assignment recognized as ownership transfers. + +## [0.2.0] - 2026-02-24 + +The cross-file release. + +- **Two-pass cross-file taint analysis.** Pass 1 extracts `FuncSummary` per function (caps, propagation, callees), Pass 2 runs BFS taint propagation with cross-file callee resolution. +- **CFG analysis engine** with five detectors: unguarded sinks, auth gaps in web handlers, unreachable security code, error fallthrough, resource leaks. +- **Cross-language interop** via explicit `InteropEdge` structs (no false-positive name collisions). +- **Function summaries persisted to SQLite** (`function_summaries` table). +- **Multi-language CFG + taint support** for all 10 languages. +- **Resource leak detection** for C/C++, Go, Rust, and Java. +- **Finding scoring system** combining severity, entry-point proximity, path complexity, taint confirmation, and confidence. +- **Analysis modes**: `Full` (default), `Ast` (`--ast-only`), `Taint` (`--cfg-only`). +- **Cap bitflags expanded**: `ENV_VAR`, `HTML_ESCAPE`, `SHELL_ESCAPE`, `URL_ENCODE`, `JSON_PARSE`, `FILE_IO`. +- Performance: read-once/hash-once via `_from_bytes` variants, lock-free rayon, SQLite WAL + 8 MB cache + 256 MB mmap. +- Tracing instrumentation on all pipeline stages; criterion benchmark suite. + +## [0.2.0-alpha] - 2025-06-28 + +- Experimental intra-procedural CFG + taint analysis for Rust. Builds a CFG, applies dataflow, and flags unsanitised Source → Sink paths (e.g. `env::var` → `Command::new`). +- O(1) node-kind lookup via per-language PHF tables. +- Debug channel `target=cfg` (`RUST_LOG=nyx::cfg=debug`) to inspect generated graphs. +- Fixed Windows release pipeline (PowerShell has no `zip` command). + +## [0.1.1-alpha] - 2025-06-25 + +- Fixed `scan --no-index` not respecting the `max_results` config setting (#1). +- Integration tests covering indexing and scanning pipelines (#3, #4, #5, #8). + +## [0.1.0-alpha] - 2025-06-25 + +Initial alpha release. + +- Multi-language AST pattern scanning via `tree-sitter` for Rust, C/C++, Java, Go, PHP, Python, Ruby, TypeScript, JavaScript. +- `scan` command: filesystem walker, pattern execution, console output. +- `index` command: build, rebuild, and status reporting of SQLite-backed index. +- `list` command: list indexed projects with optional verbosity. +- `clean` command: remove one or all project indexes. +- Configuration system with `nyx.conf` (generated) and `nyx.local` (user overrides). +- Default severity levels: High, Medium, Low. diff --git a/CLA.md b/CLA.md new file mode 100644 index 00000000..205aaa1d --- /dev/null +++ b/CLA.md @@ -0,0 +1,73 @@ +# Nyx Contributor License Agreement + +## Why this exists + +Nyx is an open source project and will always have a fully open-source core available to the community. + +This Contributor License Agreement (CLA) exists to ensure the long-term sustainability of the project. It allows Nyx to evolve over time, including improving, distributing, and potentially offering commercial versions or services that support continued development. + +**You retain ownership of your contributions.** This agreement simply grants the project the rights needed to use and evolve them. + +--- + +Thank you for your interest in contributing to Nyx (the "Project"). This Contributor License Agreement ("Agreement") clarifies the intellectual property rights granted with each Contribution from any person or entity. It is for Your protection as a contributor as well as the protection of the Project and its users. + +By submitting a Contribution to the Project, You accept and agree to the terms below. If You do not agree to these terms, please do not submit Contributions. + +## 1. Definitions + +**"You"** (or **"Your"**) means the individual or legal entity making a Contribution to the Project. For a legal entity, "You" includes the entity and any entity that controls, is controlled by, or is under common control with that entity. + +**"Contribution"** means any work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the Project for inclusion in, or documentation of, the Project. "Submitted" means any form of electronic, verbal, or written communication sent to the Project (including but not limited to pull requests, patches, and issue comments) but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +## 2. Copyright License Grant + +Subject to the terms of this Agreement, You hereby grant to the Project, to any entity that maintains or succeeds it, and to recipients of software distributed by the Project a perpetual, worldwide, non-exclusive, royalty-free, irrevocable copyright license, with the right to sublicense through multiple tiers of sublicensees, to reproduce, prepare derivative works of, publicly display, publicly perform, distribute, and sublicense Your Contribution and such derivative works. + +## 3. Patent License Grant + +Subject to the terms of this Agreement, You hereby grant to the Project, to any entity that maintains or succeeds it, and to recipients of software distributed by the Project a perpetual, worldwide, non-exclusive, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer Your Contribution and any combination of Your Contribution with the Project to which it was submitted. This patent license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution alone or by combination of Your Contribution with the Project. + +If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Project to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Project shall terminate as of the date such litigation is filed. + +## 4. Relicensing Right + +In addition to the licenses granted in Sections 2 and 3, You grant the Project and any entity that maintains or succeeds it the right to relicense Your Contribution, in whole or in part, under terms other than the Project's current license (currently GPL-3.0-or-later), where necessary to support the long-term sustainability, distribution, and evolution of the Project. + +This may include, without limitation: + +1. Dual-licensing the Project under a commercial license; +2. Combining Your Contribution with proprietary components; or +3. Moving the Project to a different open source license. + +This right is irrevocable and may be exercised by the Project's maintainers as part of maintaining and evolving the Project. + +## 5. Moral Rights Waiver + +To the maximum extent permitted by applicable law, You waive, and agree not to assert, any moral rights or similar rights of attribution and integrity that You may have in Your Contribution against the Project, its successors, and recipients of software distributed by the Project. To the extent such rights cannot be waived under applicable law, You agree not to enforce them in a manner that would limit the rights granted under this Agreement. + +## 6. Representations + +You represent that: + +1. Each of Your Contributions is Your original creation, or You otherwise have the legal right to submit it under the terms of this Agreement; +2. To the best of Your knowledge, Your Contribution does not infringe any third party's copyright, patent, trade secret, or other intellectual property rights; and +3. You have the legal authority to enter into this Agreement and to grant the licenses set forth above. + +If any portion of Your Contribution is not Your original creation, You will identify the source and any license or other restriction applicable to that material as part of Your submission. + +## 7. Employer Authorization + +If You are submitting a Contribution on behalf of Your employer, or the Contribution was made within the scope of Your employment, You represent that Your employer has authorized You to make the Contribution and to grant the licenses set forth in this Agreement. If You are unsure, please confirm with Your employer before submitting. + +## 8. No Warranty + +You provide Your Contributions on an "AS IS" basis, without warranties or conditions of any kind, either express or implied, including, without limitation, any warranties of title, non-infringement, merchantability, or fitness for a particular purpose. You are not required to provide support for Your Contributions, except to the extent You desire to provide such support. + +## 9. Copyright Retained + +You retain copyright to Your Contribution. This Agreement grants the licenses set forth above; it does not transfer ownership. Its purpose is to give the Project flexibility to evolve and to relicense the codebase over time without needing to obtain permission from each past contributor on a case-by-case basis. + +## 10. Notice of Changes + +If You become aware of any facts or circumstances that would make any representation in this Agreement inaccurate in any respect, You agree to notify the Project promptly. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..9baa64c7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**opening a private issue** at [https://github.com/elicpeter/nyx/issues/new/choose](). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, +available at +. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/inclusion). + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6dd097fc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,399 @@ +# Contributing to Nyx + +Thank you for your interest in improving Nyx. This guide covers everything you need to contribute effectively. + +User-facing documentation lives at **[elicpeter.github.io/nyx](https://elicpeter.github.io/nyx/)**; the source for those pages is in [`docs/`](docs/). + +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. + +--- + +## Table of Contents + +1. [Development Setup](#development-setup) +2. [Project Layout](#project-layout) +3. [How to Add a New AST Pattern](#how-to-add-a-new-ast-pattern) +4. [How to Add a New Taint Rule](#how-to-add-a-new-taint-rule) +5. [How to Add a New Language](#how-to-add-a-new-language) +6. [Testing](#testing) +7. [Pull Request Guidelines](#pull-request-guidelines) +8. [Bug Reports](#bug-reports) +9. [Feature Requests](#feature-requests) +10. [Release Process](#release-process) + +--- + +## Development Setup + +### Prerequisites + +- **Rust 1.88+** (edition 2024) +- Git +- **Node 20+** — only if you touch the browser UI under `frontend/` (the + `nyx serve` web app). Pure-Rust changes do not need it. + +### Building + +```bash +git clone https://github.com/elicpeter/nyx.git +cd nyx + +cargo build # Debug build +cargo build --release # Release build +cargo install --path . # Install as `nyx` binary +``` + +### Running Quality Checks + +The fastest way to reproduce CI locally is the bundled script — it runs the same +commands CI runs (fmt, Clippy, tests, and the frontend checks): + +```bash +./scripts/check.sh # Mirror CI: fmt + clippy + tests (+ frontend) +./scripts/check.sh --rust-only # Skip the frontend checks +./scripts/fix.sh # Auto-fix: cargo fmt + clippy --fix + prettier/eslint +``` + +Or run the steps individually: + +```bash +cargo test --all-features # Tests, incl. tests/ integration suite +cargo clippy --all-targets --all-features -- -D warnings # Lint, warnings = errors +cargo fmt # Format code +cargo fmt -- --check # Check formatting without modifying +``` + +> **Match CI exactly.** CI lints and tests with `--all-targets --all-features`. +> The older `cargo test --bin nyx` / `cargo clippy --all` commands skip the +> `tests/` integration suite and feature-gated code, so they can pass locally +> while CI fails. Prefer `./scripts/check.sh`. + +> **Note**: The first build downloads and compiles tree-sitter grammars for all 10 languages. Subsequent builds are faster. + +### Benchmarks + +```bash +cargo bench --bench scan_bench +``` + +Benchmark fixtures live in `benches/fixtures/`. Criterion produces HTML reports in `target/criterion/`. + +--- + +## Project Layout + +> **New here?** [`docs/how-it-works.md`](docs/how-it-works.md) walks the analysis +> pipeline end to end (with a diagram), and [`docs/detectors/taint.md`](docs/detectors/taint.md) +> covers the taint engine. The easiest first contribution is usually a new AST +> pattern (see [below](#how-to-add-a-new-ast-pattern)) — small, self-contained, +> and well templated. + +``` +src/ + main.rs CLI entry point + lib.rs Library re-exports (benchmarks, integration tests) + cli.rs Clap command definitions + commands/ Subcommand handlers (scan, index, list, clean, config, serve) + ast.rs Entry points for both passes; tree-sitter parsing + cfg/ CFG construction from AST, type hierarchy + cfg_analysis/ CFG structural detectors + guards.rs Unguarded sink detection (dominator analysis) + auth.rs Auth gap detection + resources.rs Resource leak detection + error_handling.rs Error fallthrough detection + unreachable.rs Unreachable security code detection + rules.rs Guard rules, auth rules, resource pairs + ssa/ SSA IR (lowering, optimization passes, const prop) + taint/ SSA-based taint engine (sole engine since 0.5.0) + mod.rs Facade + JS two-level solve + domain.rs Shared lattice types (VarTaint, Cap, TaintOrigin) + ssa_transfer/ Block-level worklist, k=1 inline cache, gated sinks + backwards.rs Demand-driven backwards taint walk (opt-in) + path_state.rs Predicate tracking and contradiction pruning + state/ + engine.rs Generic monotone dataflow engine (Transfer) + transfer.rs DefaultTransfer: resource lifecycle + auth state + summary/ FuncSummary, SsaFuncSummary, GlobalSummaries, hierarchy index + abstract_interp/ Interval + string prefix/suffix domains + pointer/ Field-sensitive points-to (Steensgaard-style) + symex/ Symbolic execution + witness generation + constraint/ Path-constraint solving (optional Z3 via `smt` feature) + auth_analysis/ Rust auth rule (`rs.auth.missing_ownership_check`) + sink classes + suppress/ Inline `nyx:ignore` directive parsing + labels/ Per-language label rules (one file per language) + patterns/ Per-language AST pattern queries (one file per language) + callgraph.rs Call graph construction (petgraph), SCC, topo sort + database.rs SQLite indexing via r2d2 pool + rank.rs Attack-surface ranking + fmt.rs Console output formatting + output.rs SARIF 2.1 builder + walk.rs Parallel file walker (ignore crate, respects .gitignore) + symbol/ Symbol interning (SymbolId) + server/ `nyx serve` HTTP layer, routes, triage sync + interop.rs Cross-language interop edges + engine_notes.rs Direction-aware engine notes (UnderReport / OverReport / Bail) + evidence.rs Structured evidence emitted with each finding + errors.rs NyxError, NyxResult types + utils/ + config.rs TOML config loading, merging, Config struct +``` + +--- + +## How to Add a New AST Pattern + +AST patterns are the simplest detector to add. Each pattern is a tree-sitter query that matches a structural code construct. + +### Step-by-step + +1. **Pick the language file** under `src/patterns/.rs`. + +2. **Choose the metadata**: + + | Field | Options | Guidelines | + |-------|---------|------------| + | **ID** | `..` | e.g. `py.cmdi.os_popen` | + | **Tier** | `A` or `B` | `A` = presence alone is high-signal; `B` = query includes a heuristic guard | + | **Severity** | `High`, `Medium`, `Low` | High: command exec, deser, banned functions. Medium: SQL concat, reflection, XSS. Low: weak crypto, code quality. | + | **Category** | See `PatternCategory` enum | `CommandExec`, `CodeExec`, `Deserialization`, `SqlInjection`, `PathTraversal`, `Xss`, `Crypto`, `Secrets`, `InsecureTransport`, `Reflection`, `MemorySafety`, `Prototype`, `CodeQuality` | + +3. **Write the tree-sitter query**: + + ```rust + Pattern { + id: "py.cmdi.os_popen", + description: "os.popen() shell command execution", + query: r#"(call + function: (attribute + object: (identifier) @pkg (#eq? @pkg "os") + attribute: (identifier) @fn (#eq? @fn "popen"))) + @vuln"#, + severity: Severity::High, + tier: PatternTier::A, + category: PatternCategory::CommandExec, + }, + ``` + + The query **must** capture a `@vuln` node. That node's span determines the reported location. + +4. **Test it**: + + ```bash + cargo test --bin nyx + ``` + +5. **Update docs**: Add the new rule to `docs/rules/.md`. + +### Tips + +- Use the [tree-sitter playground](https://tree-sitter.github.io/tree-sitter/playground) to develop and test queries. +- Avoid duplicating taint coverage. If the same function is already a labeled sink in `src/labels/.rs`, the AST pattern is still useful for `--mode ast`, but use a distinct ID namespace. The dedup pass prevents exact-duplicate findings at the same location. +- Test with real-world code to check false positive rates before choosing a tier. + +--- + +## How to Add a New Taint Rule + +Taint rules define sources (where untrusted data enters), sinks (where dangerous operations happen), and sanitizers (where data is made safe). + +### Step-by-step + +1. **Open the language file** in `src/labels/.rs`. + +2. **Add an entry** to the `RULES` slice: + + ```rust + LabelRule { + matchers: &["dangerouslySetInnerHTML"], + label: DataLabel::Sink(Cap::HTML_ESCAPE), + }, + ``` + +3. **Choose the right label type**: + + | Type | Purpose | Example | + |------|---------|---------| + | `DataLabel::Source(cap)` | Introduces tainted data | `env::var`, `req.body` | + | `DataLabel::Sanitizer(cap)` | Strips matching capability bits | `html_escape`, `encodeURIComponent` | + | `DataLabel::Sink(cap)` | Dangerous operation requiring sanitization | `eval`, `innerHTML`, `Command::new` | + +4. **Choose capabilities**: + + | Capability | When to use | + |-----------|-------------| + | `Cap::all()` | Sources that produce universally dangerous data | + | `Cap::SHELL_ESCAPE` | Shell command injection sinks/sanitizers | + | `Cap::HTML_ESCAPE` | XSS sinks/sanitizers | + | `Cap::URL_ENCODE` | URL injection sinks/sanitizers | + | `Cap::JSON_PARSE` | JSON parsing sanitizers | + | `Cap::FILE_IO` | File I/O sinks | + | `Cap::FMT_STRING` | Format string sinks | + | `Cap::ENV_VAR` | Environment/config data sources | + +5. **Matcher semantics**: + - Case-insensitive suffix matching by default. + - If a matcher ends with `_`, it acts as a prefix match. + - Multiple matchers in one rule are alternatives (any match triggers the rule). + +### User-defined rules (no code change needed) + +Users can add taint rules via config: + +```toml +[[analysis.languages.javascript.rules]] +matchers = ["dangerouslySetInnerHTML"] +kind = "sink" +cap = "html_escape" +``` + +Or via CLI: + +```bash +nyx config add-rule --lang javascript --matcher dangerouslySetInnerHTML --kind sink --cap html_escape +``` + +--- + +## How to Add a New Language + +Adding a new language requires changes across several modules. Use an existing language (e.g. Go or Python) as a template. + +### Checklist + +1. **Tree-sitter parser**: Add `tree-sitter-` to `Cargo.toml`. + +2. **Language registration**: Register the parser in `ast.rs` (language detection from file extension, parser initialization). + +3. **CFG node kinds**: Create `src/labels/.rs` with a `KINDS` map that maps tree-sitter node types to the internal `Kind` enum (`Block`, `If`, `While`, `For`, `Return`, `CallFn`, `CallMethod`, `Assignment`, etc.). + +4. **Parameter extraction**: Add a `PARAM_CONFIG` constant specifying how to extract function parameters from the AST (field name for parameter list, node type for individual parameters, extraction field for parameter names). + +5. **Label rules**: Add `RULES` (sources, sinks, sanitizers) and `TERMINATORS` to the labels file. + +6. **AST patterns**: Create `src/patterns/.rs` with a `PATTERNS` constant. + +7. **Registry updates**: + - `src/patterns/mod.rs`: add to the `REGISTRY` HashMap + - `src/labels/mod.rs`: add to the `classify()` dispatch + +8. **File extension mapping**: Add the extension in `ast.rs`. + +9. **Tests**: Write unit tests and add test fixtures. + +--- + +## Testing + +### Tests + +Unit tests are inline `#[test]` blocks inside source modules; integration tests +live under `tests/`. Run everything the way CI does: + +```bash +cargo test --all-features +``` + +### What to Test + +- **New AST patterns**: Ensure the tree-sitter query matches the intended construct and does not match safe alternatives. +- **New taint rules**: Verify that source-to-sink flows are detected and that sanitizers properly neutralize findings. +- **New CFG rules**: Test that guard dominance logic correctly suppresses findings when guards are present. +- **Edge cases**: Empty files, files with syntax errors (tree-sitter is error-tolerant), deeply nested structures. + +### Linting + +CI runs Clippy with strict settings. Before submitting: + +```bash +cargo clippy --all-targets --all-features -- -D warnings +``` + +--- + +## Pull Request Guidelines + +First-time contributors are welcome. If you are unsure where to start, open an issue and we can help identify a focused starter task. + +1. **Branch from `master`**. Use descriptive branch names: `feat/add-kotlin-support`, `fix/false-positive-sql-concat`, `docs/update-rule-reference`. + +2. **Keep PRs focused**. One logical change per PR. + +3. **Ensure CI passes** — run `./scripts/check.sh` (mirrors CI), or the steps individually: + ```bash + cargo test --all-features + cargo clippy --all-targets --all-features -- -D warnings + cargo fmt -- --check + ``` + +4. **Commit style**: Use [Conventional Commits](https://www.conventionalcommits.org/). + ``` + feat(patterns): add Python subprocess.Popen pattern + fix(taint): prevent false positive on sanitized innerHTML + docs(rules): update JavaScript rule reference + ``` + +5. **Document new rules**. If you add patterns or taint rules, update the corresponding `docs/rules/.md` page. + +6. **Include test cases** for any new detection rules. + +7. **Disclose material AI assistance** in the PR description if the change was drafted, generated, or substantially refactored by an AI tool. One line is enough. See [AI-POLICY.md](AI-POLICY.md) for the full policy and the bar we hold AI-assisted contributions to. + +--- + +## Bug Reports + +Please [open an issue](https://github.com/elicpeter/nyx/issues) for: + +- **Crashes or panics**: include the backtrace (`RUST_BACKTRACE=1 nyx scan .`) +- **False positives**: include the minimal code snippet, rule ID, and Nyx version +- **False negatives**: describe what you expected Nyx to find and why +- **Documentation errors**: point to the specific page and what's wrong + +--- + +## Feature Requests + +We welcome well-motivated feature proposals. Please describe: + +1. **Problem statement**: what pain point does this solve? +2. **Proposed solution**: high-level description, optionally with pseudo-code. +3. **Alternatives considered**: why existing functionality is not enough. + +--- + +## Release Process + +1. Update version in `Cargo.toml`. +2. Update `CHANGELOG.md` with the new version section. +3. Run full checks: `./scripts/check.sh` (or `cargo test --all-features && cargo clippy --all-targets --all-features -- -D warnings`). +4. Create a git tag: `git tag v0.x.y`. +5. Push tag: `git push origin v0.x.y`. +6. CI builds release binaries and publishes to crates.io. + +--- + +## Security Issues + +Please do **not** open public issues for security-sensitive bugs. See [SECURITY.md](SECURITY.md) for our responsible disclosure process. + +--- + +## License + +### Contributions are released under GPL-3.0-or-later + +By submitting a pull request, patch, or other contribution to Nyx, you agree that your contribution will be released under the [GPL-3.0-or-later](./LICENSE), the same license as the project. + +### Developer Certificate of Origin + +We use the Developer Certificate of Origin (DCO) as a lightweight baseline for contributions. All commits must include a `Signed-off-by:` trailer, which certifies that you wrote the code yourself or otherwise have the right to submit it under the project license. + +Use `git commit -s` to add this automatically. + +### Contributor License Agreement + +Before your first contribution can be merged, you must sign the Nyx [Contributor License Agreement](./CLA.md). + +The CLA does not transfer ownership of your work. You retain copyright to your contributions. It grants Nyx the rights needed to maintain, distribute, and evolve the project over time, including the flexibility to support long-term sustainability through future licensing or commercial offerings. + +If you do not agree to these terms, please do not submit contributions to Nyx. diff --git a/Cargo.lock b/Cargo.lock index 2676bf42..73c82fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,19 +3,28 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.1.3" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "alloca" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] [[package]] name = "android_system_properties" @@ -27,10 +36,16 @@ dependencies = [ ] [[package]] -name = "anstream" -version = "0.6.19" +name = "anes" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -43,39 +58,45 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayref" version = "0.3.9" @@ -89,84 +110,222 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "autocfg" -version = "1.4.0" +name = "assert_cmd" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] [[package]] name = "bitflags" -version = "2.9.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", ] [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", + "regex-automata", "serde", ] [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytesize" -version = "2.0.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", + "serde", "windows-link", ] [[package]] -name = "clap" -version = "4.5.40" +name = "ciborium" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -174,9 +333,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -186,9 +345,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -198,34 +357,59 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "console" -version = "0.15.11" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width", "windows-sys", ] [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation-sys" @@ -233,6 +417,59 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -268,28 +505,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "dashmap" -version = "7.0.0-rc2" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a1e35a65fe0538a60167f0ada6e195ad5d477f6ddae273943596d4a1a5730b" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", - "equivalent", - "hashbrown", + "hashbrown 0.14.5", "lock_api", + "once_cell", "parking_lot_core", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "directories" version = "6.0.0" @@ -313,9 +562,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -329,6 +578,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -341,6 +600,49 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -348,57 +650,170 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "getrandom" -version = "0.2.16" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "rand_core", + "wasip2", + "wasip3", ] [[package]] -name = "globset" -version = "0.4.16" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", ] [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] -name = "hashlink" -version = "0.10.0" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "hashbrown", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", ] [[package]] @@ -414,10 +829,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "iana-time-zone" -version = "0.1.63" +name = "http" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -438,16 +933,22 @@ dependencies = [ ] [[package]] -name = "ignore" -version = "0.4.23" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ignore" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -455,32 +956,67 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -492,77 +1028,126 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.173" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags", "libc", ] [[package]] name = "libsqlite3-sys" -version = "0.34.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ + "cc", "pkg-config", "vcpkg", ] [[package]] -name = "lock_api" -version = "0.4.13" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] -name = "memchr" -version = "2.7.5" +name = "matchit" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -584,27 +1169,51 @@ dependencies = [ ] [[package]] -name = "nyx" -version = "0.1.0" +name = "nyx-scanner" +version = "0.8.0" dependencies = [ + "assert_cmd", + "axum", + "bitflags", "blake3", + "bytes", "bytesize", "chrono", "clap", "console", + "criterion", "crossbeam-channel", "dashmap", "directories", + "glob", + "h2", + "http", "ignore", + "indicatif", "num_cpus", "once_cell", + "parking_lot", + "petgraph", + "phf", + "predicates", + "prost", "r2d2", "r2d2_sqlite", "rayon", + "rmp-serde", "rusqlite", + "rustc-hash", "serde", + "serde_json", + "smallvec", + "tempfile", + "terminal_size", "thiserror", + "tokio", + "tokio-stream", "toml", + "tower", + "tower-http", "tracing", "tracing-subscriber", "tree-sitter", @@ -618,19 +1227,27 @@ dependencies = [ "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-typescript", + "uuid", + "z3", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "option-ext" @@ -639,16 +1256,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "overload" -version = "0.1.1" +name = "page_size" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -656,28 +1277,124 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", + "serde_derive", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "powerfmt" @@ -686,37 +1403,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "predicates" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ - "zerocopy", + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.40" +name = "prost" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "r2d2" @@ -731,9 +1502,9 @@ dependencies = [ [[package]] name = "r2d2_sqlite" -version = "0.30.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06cc23a61faf4643d8b59ed52c27ed434476dd7aa6f39e1eff7d6bbd35985093" +checksum = "f9a289c0a3bf56505c470efa2366e76010f1d892e2492a2f96b223386d63b7e2" dependencies = [ "r2d2", "rusqlite", @@ -742,38 +1513,26 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom 0.4.2", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -781,9 +1540,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -791,73 +1550,87 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "rmp" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] [[package]] name = "rusqlite" -version = "0.36.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ "bitflags", "fallible-iterator", @@ -865,19 +1638,39 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -904,19 +1697,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.219" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -925,23 +1734,47 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", "serde", ] @@ -956,15 +1789,68 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] [[package]] name = "streaming-iterator" @@ -980,9 +1866,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.103" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -990,19 +1876,54 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "2.0.12" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1020,82 +1941,192 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" -version = "0.8.23" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", - "serde", + "serde_core", "serde_spanned", "toml_datetime", - "toml_write", + "toml_parser", + "toml_writer", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1103,9 +2134,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1114,9 +2145,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1145,14 +2176,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -1167,13 +2198,13 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.6" +version = "0.26.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0" +checksum = "4dab76d0b724ba557954125188cf0633a1ca43199ced82d95c7b9c32cc3de1f3" dependencies = [ "cc", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -1181,9 +2212,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3aad8f0129083a59fe8596157552d2bb7148c492d44c21558d68ca1c722707" +checksum = "a9b2eb57a55fed6b00812912e730b7a275cf4fe98bfd6a5d76263d4438371728" dependencies = [ "cc", "tree-sitter-language", @@ -1201,9 +2232,9 @@ dependencies = [ [[package]] name = "tree-sitter-go" -version = "0.23.4" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431" +checksum = "c8560a4d2f835cc0d4d2c2e03cbd0dde2f6114b43bc491164238d333e28b16ea" dependencies = [ "cc", "tree-sitter-language", @@ -1221,9 +2252,9 @@ dependencies = [ [[package]] name = "tree-sitter-javascript" -version = "0.23.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf40bf599e0416c16c125c3cec10ee5ddc7d1bb8b0c60fa5c4de249ad34dc1b1" +checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5" dependencies = [ "cc", "tree-sitter-language", @@ -1231,15 +2262,15 @@ dependencies = [ [[package]] name = "tree-sitter-language" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" [[package]] name = "tree-sitter-php" -version = "0.23.11" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f066e94e9272cfe4f1dcb07a1c50c66097eca648f2d7233d299c8ae9ed8c130c" +checksum = "0d8c17c3ab69052c5eeaa7ff5cd972dd1bc25d1b97ee779fec391ad3b5df5592" dependencies = [ "cc", "tree-sitter-language", @@ -1247,9 +2278,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.23.6" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04" +checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" dependencies = [ "cc", "tree-sitter-language", @@ -1267,9 +2298,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b18034c684a2420722be8b2a91c9c44f2546b631c039edf575ccba8c61be1" +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" dependencies = [ "cc", "tree-sitter-language", @@ -1287,15 +2318,27 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "utf8parse" @@ -1305,11 +2348,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", "rand", "wasm-bindgen", @@ -1327,6 +2370,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1344,45 +2396,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1390,26 +2438,80 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1428,9 +2530,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] @@ -1443,9 +2545,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -1456,9 +2558,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -1467,9 +2569,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -1478,135 +2580,188 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winnow" -version = "0.7.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "memchr", + "wit-bindgen-rust-macro", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "z3" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fc44c9d6bb9fe84c03dfff211cf4c9c8cfefa2de8b803facf7305067d21a23" +dependencies = [ + "log", + "z3-sys", +] + +[[package]] +name = "z3-src" +version = "416.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2af0c6527de39877cf55cb87f233016573eeeb7cf77afdc1469e4b32faef832" +dependencies = [ + "cmake", +] + +[[package]] +name = "z3-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c18b0a91a13522d21b3414847667de2b2056a721a3edcb5b6ee6858352d58db4" +dependencies = [ + "pkg-config", + "z3-src", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5574ae83..e60a97a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,37 +1,167 @@ [package] -name = "nyx" -version = "0.1.0" +name = "nyx-scanner" +version = "0.8.0" edition = "2024" +rust-version = "1.88" +description = "A multi-language static analysis tool for detecting security vulnerabilities" +license = "GPL-3.0-or-later" +authors = ["Eli Peter "] +homepage = "https://nyxsec.dev/scanner" +repository = "https://github.com/elicpeter/nyx" +documentation = "https://nyxsec.dev/docs/nyx/" +keywords = ["security", "vulnerability", "scanner", "static-analysis", "cli"] +categories = ["security", "command-line-utilities", "development-tools", "parser-implementations", "text-processing"] +readme = "README.md" +default-run = "nyx" +include = [ + "/src/**", + "/tools/**", + "/build.rs", + "/Cargo.toml", + "/Cargo.lock", + "/README.md", + "/LICENSE", + "/THIRDPARTY-LICENSES.html", + "/default-nyx.conf", +] + +autoexamples = false + + +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/nyx-{ target }{ archive-suffix }" +pkg-fmt = "zip" +bin-dir = "target/{ target }/release/{ bin }{ binary-ext }" + +# docs.rs builds the `serve` feature (default) so the server module renders. +# `smt` is left off — bundled Z3 takes too long on docs.rs builders, and +# `smt-system-z3` needs a system library that isn't available there. +[package.metadata.docs.rs] +features = ["serve"] +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = ["serve", "dynamic"] +serve = ["dep:axum", "dep:tokio", "dep:tokio-stream", "dep:tower-http"] +smt = ["dep:z3", "z3/bundled"] +smt-system-z3 = ["dep:z3"] +docgen = [] +# Dynamic verification layer: builds harnesses from findings, runs them in a +# sandbox, reports back whether the sink fires. +dynamic = ["dep:bytes", "dep:h2", "dep:http", "dep:prost", "dep:tempfile", "dep:tokio"] +# Phase 19 (Track E.3): the `nyx-image-builder` helper binary that builds +# and pins per-toolchain Docker images. Gated so it does not bloat the +# default `nyx` build with extra TOML-write logic CI-only operators need. +image-builder = [] +# Phase 20 (Track E.4): the firecracker VM backend. Off by default so +# the standard build pulls in zero Firecracker-related code; turning it +# on adds the `firecracker.rs` backend module and exposes +# `SandboxBackend::Firecracker` to callers. When the feature is on but +# the `firecracker` binary is absent on PATH, the backend returns +# `SandboxError::BackendUnavailable(SandboxBackend::Firecracker)` so the +# verifier can route around it cleanly. +firecracker = ["dynamic"] + +[lib] +name = "nyx_scanner" +path = "src/lib.rs" + +[[bin]] +name = "nyx" +path = "src/main.rs" + +[[bin]] +name = "nyx-docgen" +path = "tools/docgen/main.rs" +required-features = ["docgen"] + +[[bin]] +name = "nyx-image-builder" +path = "tools/image-builder/main.rs" +required-features = ["image-builder"] + +[[bench]] +name = "scan_bench" +harness = false + +[[bench]] +name = "dynamic_bench" +harness = false +required-features = [] + +[dev-dependencies] +tempfile = "3.27.0" +criterion = { version = "0.8.2", features = ["html_reports"] } +assert_cmd = "2.2.2" +predicates = "3.1.4" +glob = "0.3.3" +tower = { version = "0.5.3", features = ["util"] } [dependencies] directories = "6.0.0" -clap = { version = "4.5.40", features = ["derive"] } -serde = { version = "1.0.219", features = ["derive"] } -toml = "0.8.23" -tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json", "ansi","time"] } -tracing = "0.1.41" +clap = { version = "4.6.1", features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +rmp-serde = "1.3.1" +toml = "1.1.2" +tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json", "ansi","time"] } +tracing = "0.1.44" num_cpus = "1.17.0" -rusqlite = "0.36.0" -ignore = "0.4.23" -tree-sitter = "0.25.6" -tree-sitter-rust = "0.24.0" -tree-sitter-c = "0.24.1" +rusqlite = { version = "0.39.0", features = ["bundled"] } +r2d2_sqlite = { version = "0.34.0", features = ["bundled"] } +ignore = "0.4.26" +tree-sitter = "0.26.9" +tree-sitter-rust = "0.24.2" +tree-sitter-c = "0.24.2" tree-sitter-cpp = "0.23.4" tree-sitter-java = "0.23.5" tree-sitter-typescript = "0.23.2" -tree-sitter-javascript = "0.23.1" -tree-sitter-go = "0.23.4" -tree-sitter-php = "0.23.11" -tree-sitter-python = "0.23.6" +tree-sitter-javascript = "0.25.0" +tree-sitter-go = "0.25.0" +tree-sitter-php = "0.24.2" +tree-sitter-python = "0.25.0" tree-sitter-ruby = "0.23.1" crossbeam-channel = "0.5.15" -blake3 = "1.8.2" -once_cell = "1.21.3" -console = "0.15.11" -rayon = "1.10.0" -r2d2_sqlite = "0.30.0" +blake3 = "1.8.5" +once_cell = "1.21.4" +console = "0.16.3" +terminal_size = "0.4.4" +rayon = "1.12.0" r2d2 = "0.8.10" -bytesize = "2.0.1" -chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] } -thiserror = "2.0.12" -dashmap = "7.0.0-rc2" +bytesize = "2.3.1" +chrono = { version = "0.4.45", default-features = false, features = ["std", "clock", "serde"] } +thiserror = "2.0.18" +dashmap = "6.2.1" +parking_lot = "0.12.5" +petgraph = { version = "0.8.3", features = ["serde-1"] } +bitflags = "2.12.1" +phf = { version = "0.13.1", features = ["macros"] } +indicatif = "0.18.4" +smallvec = { version = "1.15.1", features = ["serde"] } +rustc-hash = "2.1.2" +uuid = { version = "1.23.2", features = ["v4"] } +axum = { version = "0.8.9", optional = true } +bytes = { version = "1.11.1", optional = true } +h2 = { version = "0.4.14", optional = true } +http = { version = "1.4.1", optional = true } +prost = { version = "0.14.3", optional = true } +tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "signal", "sync", "net", "io-util"], optional = true } +tokio-stream = { version = "0.1.18", features = ["sync"], optional = true } +tower-http = { version = "0.6.11", features = ["cors", "compression-gzip", "trace", "set-header", "limit"], optional = true } +z3 = { version = "0.20.0", optional = true} +tempfile = { version = "3.27.0", optional = true } + +[lints.clippy] +# Allowed project-wide instead of per-file. The vast majority of +# `collapsible_if` hits are `if let Some(x) = .. { if cond { .. } }` patterns +# whose only "fix" is to collapse into a let-chain, which hurts readability on +# the complex extractor expressions throughout the engine. Keeping the decision +# here means the rationale lives in one place and new files inherit it +# automatically rather than re-declaring `#![allow(clippy::collapsible_if)]`. +collapsible_if = "allow" + +[profile.release] +lto = true +codegen-units = 1 +debug = 1 +strip = "none" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..36caec95 --- /dev/null +++ b/LICENSE @@ -0,0 +1,226 @@ + +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. +TERMS AND CONDITIONS +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. +1. Source Code. + +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. +2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. +7. Additional Terms. + +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. +11. Patents. + +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSE-GRANTS.md b/LICENSE-GRANTS.md new file mode 100644 index 00000000..dca6bea5 --- /dev/null +++ b/LICENSE-GRANTS.md @@ -0,0 +1,89 @@ +# Internal License Grants + +This file records dual-licensing grants the copyright holder of Nyx has issued +beyond the public GPL-3.0-or-later release. + +Nyx ships publicly under GPL-3.0-or-later. That license continues to apply to +every public release on GitHub, crates.io, and any other channel. The grants +recorded here are separate, private licenses from the copyright holder to +specific projects. They do not modify the public GPL terms and they are not +transferable to third parties. + +The right to issue these grants is preserved in `CLA.md` Section 4 +(Relicensing Right): + +> [The contributor] grants the Project and any entity that maintains or +> succeeds it the right to relicense Your Contribution, in whole or in part, +> under terms other than the Project's current license (currently +> GPL-3.0-or-later), where necessary to support the long-term sustainability, +> distribution, and evolution of the Project. + +The copyright holder is the sole author of every Contribution to Nyx +(verifiable via `git log`). The CLA covers any future external Contributions. +The copyright holder may therefore grant any party, including projects owned +by the same copyright holder, a license to use Nyx under terms other than +GPL-3.0-or-later, without affecting the public GPL release. + +## How forks are affected + +A third-party fork of nyx-agent that obtains the nyx-agent source under PolyForm +Small Business 1.0.0 (or any successor source-available license) does not +acquire any rights to Nyx beyond the public GPL-3.0-or-later terms. The +internal grant below is project-to-project and non-transferable. Anyone +redistributing a binary that statically or dynamically links the `nyx` crate +must comply with the GPL on the `nyx` portion of the work. GPL is viral +copyleft on distribution. Only the copyright holder may issue further +dual-licensing grants. + +--- + +## Grant Register + +### Grant 1: nyx-agent + +| Field | Value | +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Grantor | Eli Peter, sole copyright holder of Nyx as of the effective date | +| Grantee | The nyx-agent project (`nyx-agent` daemon, web UI, and accompanying tooling). Repository: `nyx-agent` | +| Effective date | 2026-05-17 | +| Scope | All Nyx source code, documentation, fixtures, build artefacts, and binaries (the "Licensed Material") in any version released as of the effective date or thereafter, plus any future modifications the Grantor authors or accepts under the CLA | +| Permitted uses | (a) static or dynamic linking of the Licensed Material into the nyx-agent daemon; (b) modification of the Licensed Material as required for nyx-agent integration; (c) redistribution of the Licensed Material as part of the nyx-agent distribution; (d) sublicensing the Licensed Material to end users of nyx-agent solely under whatever license terms nyx-agent itself is distributed under (currently PolyForm Small Business 1.0.0, or a separately negotiated commercial license) | +| Restrictions | (a) this grant does not modify, supersede, or revoke the public GPL-3.0-or-later release of Nyx; (b) this grant is non-transferable; only the nyx-agent project, owned by the Grantor, may exercise it; (c) any third-party fork of nyx-agent must obtain Nyx under the public GPL terms unless it negotiates a separate grant from the Grantor; (d) attribution of Nyx authorship must be preserved in any redistribution per the CLA's moral-rights waiver | +| Duration | Perpetual and irrevocable, subject only to the Grantee maintaining ownership-or-control by the Grantor. If the nyx-agent project is sold, assigned, or otherwise transferred to a third party, this grant terminates and the new owner must negotiate a separate license | +| Sublicensing of the grant itself | Not permitted. The Grantee may distribute Nyx as part of nyx-agent to end users under nyx-agent's outward terms, but the Grantee may not grant any other project the right to use Nyx outside the public GPL terms | +| Governing law | Same as Nyx CLA | + +--- + +## Adding future grants + +New grants follow the same format as Grant 1. Append a new section +(`### Grant N: `) below the existing entries and commit to +the Nyx repository. Grants are append-only. Revisions land as superseding +entries with their own date, not as edits to the original. + +Grants the Grantor anticipates issuing in the future include: + +- Commercial-license SKU grants to individual customers of nyx-agent that + exceed the PolyForm Small Business threshold. These will be issued + per-customer under a separate Nyx Commercial License contract. +- Stewardship-transition grants if the project is ever handed off (for + example, to a foundation). These would be a single grant to the receiving + entity. + +The Grantor reserves the right to refuse to issue any grant. + +--- + +## What this file is NOT + +- It is not a redistribution license. Third parties cannot rely on it to use + Nyx outside the public GPL terms. +- It is not a Contributor License Agreement. `CLA.md` covers contribution + terms separately. +- It is not a public-facing license file. The canonical public license for + Nyx is `LICENSE` (GPL-3.0-or-later). + +--- + +Copyright (c) 2026 Eli Peter. All rights reserved. diff --git a/README.md b/README.md index 8cbcad06..273f995f 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,302 @@ +
+ NYX +**A local-first security scanner with sandboxed dynamic verification and a browser UI. Scan your repo and triage in your browser, with no cloud and no account.** -# Nyx - Lightweight Multi-Language Vulnerability Scanner +[![crates.io](https://img.shields.io/crates/v/nyx-scanner.svg)](https://crates.io/crates/nyx-scanner) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Rust 1.88+](https://img.shields.io/badge/rust-1.88%2B-orange)](https://www.rust-lang.org) +[![CI](https://img.shields.io/github/actions/workflow/status/elicpeter/nyx/ci.yml?branch=master)](https://github.com/elicpeter/nyx/actions) +[![Docs](https://img.shields.io/badge/docs-nyxscan.dev%2Fdocs-blue)](https://nyxscan.dev/docs/) -Nyx is a lightweight Rust CLI tool for scanning code across multiple programming languages to detect potential vulnerabilities and code quality issues. It works by converting source code to Abstract Syntax Trees (ASTs), analyzing control flow graphs, performing taint analysis, and searching for common vulnerability patterns. +English · [简体中文](./README.zh-CN.md) +
-## Features +

Nyx UI walkthrough: empty Welcome state, kicking off a scan, the populated overview with Health Score, drilling into a HIGH finding's flow visualizer, then the triage flow

-- **Fast and Lightweight**: Written in Rust for optimal performance -- **Multi-Language Support**: Scans code in multiple programming languages -- **AST-Based Analysis**: Uses tree-sitter for accurate code parsing -- **Project Indexing**: Maintains an index to avoid rescanning unchanged files -- **Configurable**: Extensive configuration options for customizing scans -- **Multiple Output Formats**: Supports table, JSON, CSV, and SARIF output formats +--- -## Installation +## Scan locally, browse locally -### From Source +Nyx runs cross-language taint analysis on your repository, then verifies Medium or higher confidence findings by running small sandboxed harnesses against the real code. Results are served to a React UI bound to `127.0.0.1`. You get severity, static evidence, dynamic verdicts, and a step-by-step **flow visualiser** that walks the dataflow from source → sanitizer → sink. Triage decisions persist to `.nyx/triage.json`, which commits alongside your code so the team shares one triage state. ```bash -# Clone the repository -git clone https://github.com/yourusername/nyx.git -cd nyx - -# Build the project -cargo build --release - -# Install the binary -cargo install --path . +cargo install nyx-scanner +nyx scan # runs the analyzer, caches findings in .nyx/ +nyx serve # opens http://localhost:9700 in your browser ``` -## Usage +Everything stays on your machine: loopback-only bind, host-header enforcement, CSRF on every mutation, no remote telemetry, no login. -### Basic Scanning +

Overview dashboard for a small JS app: Health Score C 78 with the five-component breakdown (Severity pressure, Confidence quality, Trend, Triage coverage, Regression resistance), 3 findings detected, OWASP A03 and A02 buckets, confidence distribution and issue category bars, top affected files

+ +--- + +## What's in the UI + +| Page | What it shows | +|---|---| +| **Overview** | Dashboard: finding counts by severity, top offenders, engine profile summary | +| **Findings** | Browsable list with severity badges, triage status, rule filter, language filter | +| **Finding detail** | Flow-path visualiser with numbered steps (source → sanitizer → sink), dynamic verdicts, code snippets, evidence, cross-file markers, triage dropdown | +| **Triage** | Bulk update states (open, investigating, fixed, false_positive, accepted_risk, suppressed), audit trail, import/export JSON | +| **Explorer** | File tree with per-file symbol list and finding overlay | +| **Scans** | Run history, metrics, diff two scans to see what changed | +| **Rules** | Built-in and custom rules per language; add rules from the UI | +| **Config** | Live config editor; reload without restart | + + +`nyx serve` flags: `--port ` (default `9700`), `--host ` (loopback only: `127.0.0.1`, `localhost`, or `::1`), `--no-browser`. See `[server]` in `nyx.conf` for persistent settings, and the [Browser UI guide](https://nyxscan.dev/docs/serve.html) for the page-by-page UI tour and security model. + +--- + +## CLI for CI + +The same engine runs headless for CI pipelines. SARIF output uploads directly to GitHub Code Scanning. + +

nyx scan console output: HIGH taint findings across a JS and Python file with source → sink arrows

```bash -# Scan the current directory -nyx scan +# Fail the job on medium or higher, emit SARIF +nyx scan --format sarif --fail-on MEDIUM > results.sarif -# Scan a specific directory -nyx scan /path/to/project +# Ad-hoc JSON, no index +nyx scan ./server --format json --index off -# Scan with specific output format -nyx scan --format json +# AST patterns only (fastest; skips CFG + taint) +nyx scan --mode ast -# Scan only for high severity issues -nyx scan --high-only +# Engine-depth shortcut: fast | balanced (default) | deep +# `deep` adds symex + demand-driven backwards taint for higher precision at ~2-3× cost +nyx scan --engine-profile deep ``` -### Managing Project Indexes +Forward cross-file taint runs in every profile. Symex and the demand-driven backwards walk are opt-in. Turn them on either via `--engine-profile deep`, or individually (`--symex`, `--backwards-analysis`). See the [CLI reference](https://nyxscan.dev/docs/cli.html#engine-depth-profile) for the full toggle matrix. + +### GitHub Action + +```yaml +- uses: elicpeter/nyx@v0.8.0 + with: + format: sarif + fail-on: MEDIUM +- uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: nyx-results.sarif +``` + +Inputs: `path`, `version`, `format` (`sarif`|`json`|`console`), `fail-on`, `args`, `token`. Outputs: `finding-count`, `sarif-file`, `exit-code`, `nyx-version`. Linux and macOS runners (x86_64, ARM64). + +--- + +## Install + +**Cargo (recommended):** +```bash +cargo install nyx-scanner +``` + +**Pre-built binaries:** Grab the archive for your platform from [Releases](https://github.com/elicpeter/nyx/releases), verify against `SHA256SUMS` (and the detached `SHA256SUMS.asc` GPG signature, when present), unzip, and drop `nyx` on your `PATH`. ```bash -# Build or update index for current project -nyx index build - -# Force rebuild index -nyx index build --force - -# Show index status -nyx index status - -# List all indexed projects -nyx list - -# List all indexed projects with details -nyx list --verbose - -# Remove a project from index -nyx clean project-name - -# Clean all projects -nyx clean --all +# Optional: verify the checksum file's GPG signature (when SHA256SUMS.asc is published) +gpg --verify SHA256SUMS.asc SHA256SUMS +sha256sum -c SHA256SUMS --ignore-missing +unzip nyx-x86_64-unknown-linux-gnu.zip && chmod +x nyx && sudo mv nyx /usr/local/bin/ ``` -## Supported Languages +**From source:** +```bash +git clone https://github.com/elicpeter/nyx.git +cd nyx && cargo build --release +``` -Nyx currently supports scanning code in the following languages: +Requires stable Rust 1.88+. The frontend is compiled and embedded in the binary at build time, so there is no separate install step for `nyx serve`. -- Rust -- C -- C++ -- Java -- Go -- PHP -- Python -- TypeScript -- JavaScript +--- -## How It Works +## Languages -1. **Code Traversal**: Nyx walks through your project's directory structure, respecting ignore files and exclusion patterns. +All 10 languages parse via tree-sitter and run through the full pipeline, but rule depth and engine coverage are uneven. Benchmark F1 on the synthetic corpus at [`tests/benchmark/ground_truth.json`](tests/benchmark/ground_truth.json) is 100% across all ten languages at the last measured baseline (see [`tests/benchmark/RESULTS.md`](tests/benchmark/RESULTS.md)), so F1 alone no longer separates the tiers. Tiering reflects rule depth, gated-sink coverage, and structural idioms the synthetic corpus does not fully stress: -2. **AST Generation**: For each supported file, Nyx uses tree-sitter to parse the code into an Abstract Syntax Tree (AST). +| Tier | Languages | F1 | Use as a CI gate? | +|---|---|---|---| +| **Stable** | Python, JavaScript, TypeScript | 100% | Yes | +| **Beta** | Java, PHP, Ruby, Rust, Go | 100% | Yes, with light FP triage | +| **Preview** | C, C++ | 100% on synthetic corpus | No. STL container flow, builder chains, and inline class member functions are tracked, but deep pointer aliasing and function pointers are not. Pair with clang-tidy or Clang Static Analyzer | -3. **Pattern Matching**: Nyx applies language-specific vulnerability patterns to the AST to identify potential issues. +All real-CVE fixtures fire and the corpus carries zero open FPs at the recorded baseline (P=R=F1=1.000). Per-dimension detail and known blind spots live on the [Language maturity page](https://nyxscan.dev/docs/language-maturity.html). -4. **Control Flow Analysis**: (Planned) Nyx will convert ASTs to control flow graphs for more sophisticated analysis. +### Validated against real CVEs -5. **Taint Analysis**: (Planned) Nyx will track the flow of untrusted data through your application. +The corpus also holds a small set of vulnerable/patched pairs extracted from published advisories, so the benchmark floor is defended by regression protection on demonstrably real bugs rather than just synthetic analogues. Nyx fires on the vulnerable file and emits zero findings on the patched file for each pair. -6. **Reporting**: Issues are reported with severity levels, file locations, and descriptions. +| CVE | Project | Language | Class | +|---|---|---|---| +| [CVE-2023-48022](https://nvd.nist.gov/vuln/detail/CVE-2023-48022) | Ray | Python | Command injection | +| [CVE-2017-18342](https://nvd.nist.gov/vuln/detail/CVE-2017-18342) | PyYAML | Python | Deserialization | +| [CVE-2019-14939](https://nvd.nist.gov/vuln/detail/CVE-2019-14939) | mongo-express | JavaScript | Code execution (`eval`) | +| [CVE-2023-22621](https://nvd.nist.gov/vuln/detail/CVE-2023-22621) | Strapi | JavaScript | Code execution (SSTI) | +| [CVE-2025-64430](https://nvd.nist.gov/vuln/detail/CVE-2025-64430) | Parse Server | JavaScript | SSRF | +| [CVE-2023-26159](https://nvd.nist.gov/vuln/detail/CVE-2023-26159) | follow-redirects | TypeScript | SSRF | +| [GHSA-4x48-cgf9-q33f](https://github.com/advisories/GHSA-4x48-cgf9-q33f) | Novu | TypeScript | SSRF | +| [CVE-2026-25544](https://nvd.nist.gov/vuln/detail/CVE-2026-25544) | Payload CMS | TypeScript | SQL injection | +| [CVE-2022-30323](https://nvd.nist.gov/vuln/detail/CVE-2022-30323) | hashicorp/go-getter | Go | Command injection | +| [CVE-2024-31450](https://nvd.nist.gov/vuln/detail/CVE-2024-31450) | owncast | Go | Path traversal | +| [CVE-2023-3188](https://nvd.nist.gov/vuln/detail/CVE-2023-3188) | owncast | Go | SSRF | +| [CVE-2026-41422](https://github.com/daptin/daptin/security/advisories/GHSA-rw2c-8rfq-gwfv) | daptin | Go | SQL injection | +| [CVE-2015-7501](https://nvd.nist.gov/vuln/detail/CVE-2015-7501) | Apache Commons Collections | Java | Deserialization | +| [CVE-2017-12629](https://nvd.nist.gov/vuln/detail/CVE-2017-12629) | Apache Solr | Java | Command injection | +| [CVE-2022-1471](https://nvd.nist.gov/vuln/detail/CVE-2022-1471) | SnakeYAML | Java | Deserialization | +| [CVE-2022-42889](https://nvd.nist.gov/vuln/detail/CVE-2022-42889) | Apache Commons Text | Java | Code execution | +| [GHSA-h8cj-hpmg-636v](https://github.com/advisories/GHSA-h8cj-hpmg-636v) | Appsmith | Java | SQL injection | +| [CVE-2013-0156](https://nvd.nist.gov/vuln/detail/CVE-2013-0156) | Ruby on Rails | Ruby | Deserialization | +| [CVE-2020-8130](https://nvd.nist.gov/vuln/detail/CVE-2020-8130) | Rake | Ruby | Command injection | +| [CVE-2021-21288](https://nvd.nist.gov/vuln/detail/CVE-2021-21288) | CarrierWave | Ruby | SSRF | +| [CVE-2023-38337](https://nvd.nist.gov/vuln/detail/CVE-2023-38337) | rswag-api | Ruby | Path traversal | +| [CVE-2017-9841](https://nvd.nist.gov/vuln/detail/CVE-2017-9841) | PHPUnit | PHP | Code execution (`eval`) | +| [CVE-2018-15133](https://nvd.nist.gov/vuln/detail/CVE-2018-15133) | Laravel | PHP | Deserialization | +| [CVE-2018-20997](https://nvd.nist.gov/vuln/detail/CVE-2018-20997) | tar-rs | Rust | Path traversal | +| [CVE-2022-36113](https://nvd.nist.gov/vuln/detail/CVE-2022-36113) | cargo | Rust | Path traversal | +| [CVE-2024-24576](https://nvd.nist.gov/vuln/detail/CVE-2024-24576) | Rust stdlib | Rust | Command injection | +| [CVE-2023-42456](https://rustsec.org/advisories/RUSTSEC-2023-0069.html) | sudo-rs | Rust | Path traversal | +| [CVE-2024-32884](https://rustsec.org/advisories/RUSTSEC-2024-0335.html) | gitoxide | Rust | Command injection | +| [CVE-2025-53549](https://rustsec.org/advisories/RUSTSEC-2025-0043.html) | matrix-rust-sdk | Rust | SQL injection | +| [CVE-2016-3714](https://nvd.nist.gov/vuln/detail/CVE-2016-3714) | ImageMagick (ImageTragick) | C | Command injection | +| [CVE-2019-18634](https://nvd.nist.gov/vuln/detail/CVE-2019-18634) | sudo (pwfeedback) | C | Memory safety | +| [CVE-2019-13132](https://nvd.nist.gov/vuln/detail/CVE-2019-13132) | ZeroMQ libzmq | C++ | Memory safety | +| [CVE-2022-1941](https://nvd.nist.gov/vuln/detail/CVE-2022-1941) | Protocol Buffers | C++ | Memory safety | +| [CVE-2025-69662](https://nvd.nist.gov/vuln/detail/CVE-2025-69662) | geopandas | Python | SQL injection | +| [CVE-2026-33626](https://nvd.nist.gov/vuln/detail/CVE-2026-33626) | LMDeploy | Python | SSRF | + +Fixtures live under [`tests/benchmark/cve_corpus/`](tests/benchmark/cve_corpus/) with upstream attribution headers. + + + +--- + +## How it works + +Two passes over the filesystem, with an optional SQLite index to skip unchanged files: + +```mermaid +flowchart LR + Repo["Repository files"] --> Pass1["Pass 1 per file
tree-sitter, CFG, SSA"] + Pass1 --> Summaries["Function summaries
sources, sinks, sanitizers, points-to"] + Summaries --> Index["SQLite index
optional incremental cache"] + Index --> Pass2["Pass 2 cross-file
global summaries, k=1 inline, SCC fixpoint"] + Pass2 --> Rank["Rank and dedupe
severity, evidence, exploitability"] + Rank --> Verify["Dynamic verification
sandboxed harnesses, verdicts"] + Verify --> Output["Console, JSON, SARIF
and browser UI"] +``` + +1. **Pass 1**: parse each file via tree-sitter, build an intra-procedural CFG (petgraph), lower to pruned SSA (Cytron phi insertion over dominance frontiers), and export per-function summaries (source/sanitizer/sink caps, taint transforms, points-to, callees). +2. **Summary merge**: union all per-file summaries into a `GlobalSummaries` map. +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, verify, emit**: findings are scored by severity × evidence strength × source-kind exploitability. Medium or higher confidence findings are dynamically verified by default, then results are emitted to console, JSON, SARIF, and the browser UI. + +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://nyxscan.dev/docs/detectors.html). + +--- + +## Verify findings dynamically + +Static analysis says a sink is reachable. Dynamic verification tries to prove it. With `--verify` (on by default), Nyx builds a small harness around each Medium-or-higher finding, runs it in a sandbox against a curated payload corpus, and stamps a verdict onto the finding. + +```bash +nyx scan --verify # build + run a harness per finding (default) +nyx scan --no-verify # static analysis only, for fast local loops +``` + +A finding is **Confirmed** only when an attacker-controlled payload fires the sink *and* a paired benign control stays clean. That differential rule, plus behavioral oracles (a template that renders `49`, a deserializer that resolves a gadget class, a redirect that leaves the origin), keeps the verifier from confirming on an echoed string. Sinks behind a recognized guard demote to `ConfirmedWithKnownGuard`; sinks reached without a completed exploit chain land as `PartiallyConfirmed`. + +Coverage spans 18 verifiable capability classes and 120+ registered adapters across all ten languages (Flask, Django, Express, NestJS, Spring, Rails, Laravel, Gin, Axum, and more), with per-language build pools and copy-on-write workdirs to keep the per-finding cost low. Confirmed findings write a hermetic repro bundle with a `reproduce.sh`. Runs are deterministic: every payload is seeded from the spec hash. + +```bash +# CI: fail the build if a new Confirmed finding appears vs. a baseline +nyx scan --baseline .nyx/baseline.json --gate no-new-confirmed +``` + +Backends: Docker (preferred, network-blocked by default) or an in-process runner with `--harden {standard,strict}`. Full matrix, oracle list, and limitations: [Dynamic verification](https://nyxscan.dev/docs/dynamic.html). + +--- ## Configuration -Nyx uses a configuration system with defaults that can be overridden by a user-specific configuration file. The configuration file is located at: - -- Linux/macOS: `~/.config/nyx/nyx.local` -- Windows: `C:\Users\\AppData\Roaming\ecpeter23\nyx\config\nyx.local` - -Example configuration: +Config merges `nyx.conf` (defaults) and `nyx.local` (your overrides) from the platform config directory (`~/.config/nyx/` on Linux, `~/Library/Application Support/nyx/` on macOS, `%APPDATA%\elicpeter\nyx\config\` on Windows). ```toml [scanner] +mode = "full" # full | ast | cfg | taint min_severity = "Medium" -follow_symlinks = true -[output] -default_format = "json" -color_output = true +[server] +host = "127.0.0.1" +port = 9700 +open_browser = true -[performance] -worker_threads = 8 +# Project-specific sanitizer +[[analysis.languages.javascript.rules]] +matchers = ["escapeHtml"] +kind = "sanitizer" +cap = "html_escape" ``` -## License +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://nyxscan.dev/docs/configuration.html). Run `nyx rules list` to browse the registry from the terminal. -[Add your license information here] +--- + +## Status + +Under active development. APIs, detector behavior, and configuration options may change between releases. Rule-level F1 on the synthetic corpus is the CI regression floor; per-language detail lives in [`tests/benchmark/RESULTS.md`](tests/benchmark/RESULTS.md). + +Taint analysis is interprocedural. Persisted per-function SSA summaries carry per-return-path transforms and parameter-granularity points-to, and call-graph SCCs (including SCCs that span files) iterate to a joint fixed-point. The default `balanced` profile also runs k=1 context-sensitive inlining for intra-file callees. Symex (with cross-file and interprocedural frames) and the demand-driven backwards walk are opt-in. Enable them individually with `--symex` and `--backwards-analysis`, or together with `--engine-profile deep`. + +Limitations: +- Interprocedural precision is bounded rather than unlimited. Context-sensitive inlining is k=1 with a callee body-size cap, and SCC fixed-point has an iteration cap. When the engine hits a bound it falls back to summaries and records an `engine_note` on the finding. +- Cross-language calls (FFI, subprocess, WASM) are not traversed. Each language is analysed independently. +- Several language features are not modeled: macros, most dynamic dispatch, aliased imports, reflection. +- C/C++ are preview tier. STL container flow, builder chains, and inline class member functions are tracked now; deep pointer aliasing and function pointers are not. A clean report should not be read as a clean audit. Pair with a clang-based tool before using as a hard CI gate. +- Results may contain false positives or false negatives; manual review is expected. + +--- + +## Documentation + +Browse the full docs site at **[nyxscan.dev/docs](https://nyxscan.dev/docs/)**. + +- [Quick Start](https://nyxscan.dev/docs/quickstart.html) · [CLI Reference](https://nyxscan.dev/docs/cli.html) · [Installation](https://nyxscan.dev/docs/installation.html) +- [`nyx serve`](https://nyxscan.dev/docs/serve.html) · [Output Formats](https://nyxscan.dev/docs/output.html) · [Configuration](https://nyxscan.dev/docs/configuration.html) · [Dynamic verification](https://nyxscan.dev/docs/dynamic.html) +- [How it works](https://nyxscan.dev/docs/how-it-works.html) · [Detectors](https://nyxscan.dev/docs/detectors.html) ([Taint](https://nyxscan.dev/docs/detectors/taint.html), [CFG](https://nyxscan.dev/docs/detectors/cfg.html), [State](https://nyxscan.dev/docs/detectors/state.html), [AST Patterns](https://nyxscan.dev/docs/detectors/patterns.html)) +- [Rule Reference](https://nyxscan.dev/docs/rules.html) · [Language Maturity](https://nyxscan.dev/docs/language-maturity.html) · [Advanced Analysis](https://nyxscan.dev/docs/advanced-analysis.html) · [Auth Analysis](https://nyxscan.dev/docs/auth.html) + +--- ## Contributing -[Add contribution guidelines here] +Contributions are welcome. + +Nyx is open source and will always have a fully open-source core. To support long-term development and keep the project sustainable, contributors may be asked to sign a Contributor License Agreement before their first merged contribution. + +Run `sh scripts/check.sh` before submitting. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full guide, including how to add rules and support new languages. Open an issue for crashes, panics, or suspicious results; attach a minimal snippet and the Nyx version. + +--- + +## AI Disclosure + +- **Engine code** (taint, SSA, CFG, call graph, abstract interp, symbolic exec): predominantly human-written. AI was used selectively for refactors and boilerplate, with all merges human-reviewed. +- **Docs and most of this README**: AI-generated from the code and hand-edited. Report doc/code drift as a bug. +- **Test fixtures and `expected.yaml` files**: AI-assisted drafting, human-audited before landing. +- **Frontend UI** (React app): built with AI assistance, human-reviewed. + +As with any static analyzer, validate findings against your own corpus before using Nyx as a CI gate. + +--- + +## License + +GNU General Public License v3.0 or later (GPL-3.0-or-later). The optional `smt` feature bundles Z3 (MIT-licensed); distributors of binaries built with `--features smt` should include Z3's license in their attribution. Full text in [LICENSE](./LICENSE); third-party dependencies in [THIRDPARTY-LICENSES.html](./THIRDPARTY-LICENSES.html). diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 00000000..22d2c5cd --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,276 @@ +
+ NYX + +**本地优先的安全扫描器,带沙箱动态验证和浏览器 UI。在本地扫描代码仓库并在浏览器中分诊处理,无需云端、无需账号。** + +[![crates.io](https://img.shields.io/crates/v/nyx-scanner.svg)](https://crates.io/crates/nyx-scanner) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Rust 1.88+](https://img.shields.io/badge/rust-1.88%2B-orange)](https://www.rust-lang.org) +[![CI](https://img.shields.io/github/actions/workflow/status/elicpeter/nyx/ci.yml?branch=master)](https://github.com/elicpeter/nyx/actions) +[![Docs](https://img.shields.io/badge/docs-nyxscan.dev%2Fdocs-blue)](https://nyxscan.dev/docs/) + +[English](./README.md) · 简体中文 +
+ +

Nyx UI 演示:从空欢迎页开始扫描,查看含健康分的总览页,钻入一条 HIGH 级发现的流可视化,再到分诊流程

+ +--- + +## 本地扫描,本地浏览 + +Nyx 在你的代码仓库上运行跨语言污点分析,然后对中高置信度发现运行小型沙箱 harness,验证真实代码里 source 到 sink 的流是否会触发。结果通过绑定到 `127.0.0.1` 的 React UI 提供给你。你会看到严重等级、静态证据、动态验证结果,以及分步**流可视化**,从源 → 净化器 → 汇逐步呈现数据流。分诊决策持久化在 `.nyx/triage.json` 中,与代码一同提交,团队共享同一份分诊状态。 + +```bash +cargo install nyx-scanner +nyx scan # 运行分析器,把发现缓存到 .nyx/ +nyx serve # 在浏览器中打开 http://localhost:9700 +``` + +一切都留在你本地:仅回环绑定、强制 host 头校验、所有变更操作均带 CSRF、无远程遥测、无登录。 + +

一个小型 JS 应用的总览仪表盘:健康分 C 78,五项分量分解(严重度压力、置信度质量、趋势、分诊覆盖、回归抗性),3 条发现,OWASP A03 与 A02 类别,置信度分布与问题类别条形图,受影响最多的文件

+ +--- + +## UI 中包含什么 + +| 页面 | 显示内容 | +|---|---| +| **总览** | 仪表盘:按严重等级分类的发现计数、热点文件、引擎画像摘要 | +| **发现** | 可浏览列表,含严重度徽章、分诊状态、规则筛选、语言筛选 | +| **发现详情** | 流路径可视化,带编号步骤(源 → 净化器 → 汇)、动态验证结果、代码片段、证据、跨文件标记、分诊下拉框 | +| **分诊** | 批量更新状态(open、investigating、fixed、false_positive、accepted_risk、suppressed),审计日志,JSON 导入/导出 | +| **资源管理器** | 文件树,含每个文件的符号列表与发现叠加层 | +| **扫描** | 历史记录、指标,对比两次扫描查看差异 | +| **规则** | 各语言的内置与自定义规则;可在 UI 中添加规则 | +| **配置** | 实时配置编辑器;无需重启即可重载 | + + +`nyx serve` 参数:`--port `(默认 `9700`)、`--host `(仅回环:`127.0.0.1`、`localhost`、`::1`)、`--no-browser`。持久化设置见 `nyx.conf` 的 `[server]` 段,分页面 UI 介绍与安全模型详见 [Browser UI 指南](https://nyxscan.dev/docs/serve.html)。 + +--- + +## 用于 CI 的 CLI + +同一个引擎可以无头运行用于 CI 流水线。SARIF 输出可直接上传到 GitHub Code Scanning。 + +

nyx scan 终端输出:JS 与 Python 文件中的 HIGH 级污点发现及 source → sink 箭头

+ +```bash +# 在 medium 及以上等级让 CI 失败,并输出 SARIF +nyx scan --format sarif --fail-on MEDIUM > results.sarif + +# 临时 JSON,无索引 +nyx scan ./server --format json --index off + +# 仅 AST 模式(最快;跳过 CFG + 污点) +nyx scan --mode ast + +# 引擎深度快捷方式:fast | balanced(默认) | deep +# `deep` 增加 symex 与按需后向污点,精度更高,开销约 2-3 倍 +nyx scan --engine-profile deep +``` + +正向跨文件污点在所有画像下都会运行。Symex 与按需后向遍历是可选项,可通过 `--engine-profile deep` 一次性开启,或单独开启(`--symex`、`--backwards-analysis`)。完整开关矩阵见 [CLI 参考](https://nyxscan.dev/docs/cli.html#engine-depth-profile)。 + +### GitHub Action + +```yaml +- uses: elicpeter/nyx@v0.8.0 + with: + format: sarif + fail-on: MEDIUM +- uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: nyx-results.sarif +``` + +输入:`path`、`version`、`format`(`sarif`|`json`|`console`)、`fail-on`、`args`、`token`。输出:`finding-count`、`sarif-file`、`exit-code`、`nyx-version`。支持 Linux 与 macOS runner(x86_64、ARM64)。 + +--- + +## 安装 + +**Cargo(推荐):** +```bash +cargo install nyx-scanner +``` + +**预编译二进制:** 从 [Releases](https://github.com/elicpeter/nyx/releases) 下载对应平台的归档包,对照 `SHA256SUMS`(以及随附的 `SHA256SUMS.asc` GPG 签名,如有提供)校验,解压并把 `nyx` 放到 `PATH` 中。 + +```bash +# 可选:校验校验文件的 GPG 签名(当 SHA256SUMS.asc 已发布时) +gpg --verify SHA256SUMS.asc SHA256SUMS +sha256sum -c SHA256SUMS --ignore-missing +unzip nyx-x86_64-unknown-linux-gnu.zip && chmod +x nyx && sudo mv nyx /usr/local/bin/ +``` + +**从源码编译:** +```bash +git clone https://github.com/elicpeter/nyx.git +cd nyx && cargo build --release +``` + +需要 stable Rust 1.88+。前端会在编译期被打包嵌入二进制中,因此 `nyx serve` 没有单独的安装步骤。 + +--- + +## 语言支持 + +全部 10 种语言都通过 tree-sitter 解析并跑完整流水线,但规则深度与引擎覆盖并不均衡。在 [`tests/benchmark/ground_truth.json`](tests/benchmark/ground_truth.json) 的合成语料上,所有十种语言在最近一次基线测量中 F1 均为 100%(见 [`tests/benchmark/RESULTS.md`](tests/benchmark/RESULTS.md)),因此 F1 已无法单独区分梯度。分级反映规则深度、门控汇覆盖、以及合成语料未充分覆盖的结构性惯用法: + +| 梯度 | 语言 | F1 | 适合用作 CI 门禁吗? | +|---|---|---|---| +| **稳定** | Python、JavaScript、TypeScript | 100% | 适合 | +| **Beta** | Java、PHP、Ruby、Rust、Go | 100% | 适合,需轻度 FP 分诊 | +| **预览** | C、C++ | 合成语料 100% | 不适合。已跟踪 STL 容器流、builder 链、内联类成员函数;尚未覆盖深度指针别名与函数指针。建议与 clang-tidy 或 Clang Static Analyzer 搭配使用 | + +所有真实 CVE 用例均触发,语料在记录基线下无未关闭的 FP(P=R=F1=1.000)。各维度详情与已知盲区见 [语言成熟度页面](https://nyxscan.dev/docs/language-maturity.html)。 + +### 通过真实 CVE 验证 + +语料中还包含一小批从公开公告中提取的「漏洞 / 已修复」配对,因此基准下限不仅由合成的同形测例守护,还由对真实 bug 的回归保护守护。每个配对 Nyx 都在漏洞文件上触发、在已修复文件上零发现。 + +| CVE | 项目 | 语言 | 类别 | +|---|---|---|---| +| [CVE-2023-48022](https://nvd.nist.gov/vuln/detail/CVE-2023-48022) | Ray | Python | 命令注入 | +| [CVE-2017-18342](https://nvd.nist.gov/vuln/detail/CVE-2017-18342) | PyYAML | Python | 反序列化 | +| [CVE-2019-14939](https://nvd.nist.gov/vuln/detail/CVE-2019-14939) | mongo-express | JavaScript | 代码执行(`eval`) | +| [CVE-2023-22621](https://nvd.nist.gov/vuln/detail/CVE-2023-22621) | Strapi | JavaScript | 代码执行(SSTI) | +| [CVE-2025-64430](https://nvd.nist.gov/vuln/detail/CVE-2025-64430) | Parse Server | JavaScript | SSRF | +| [CVE-2023-26159](https://nvd.nist.gov/vuln/detail/CVE-2023-26159) | follow-redirects | TypeScript | SSRF | +| [GHSA-4x48-cgf9-q33f](https://github.com/advisories/GHSA-4x48-cgf9-q33f) | Novu | TypeScript | SSRF | +| [CVE-2026-25544](https://nvd.nist.gov/vuln/detail/CVE-2026-25544) | Payload CMS | TypeScript | SQL 注入 | +| [CVE-2022-30323](https://nvd.nist.gov/vuln/detail/CVE-2022-30323) | hashicorp/go-getter | Go | 命令注入 | +| [CVE-2024-31450](https://nvd.nist.gov/vuln/detail/CVE-2024-31450) | owncast | Go | 路径穿越 | +| [CVE-2023-3188](https://nvd.nist.gov/vuln/detail/CVE-2023-3188) | owncast | Go | SSRF | +| [CVE-2026-41422](https://github.com/daptin/daptin/security/advisories/GHSA-rw2c-8rfq-gwfv) | daptin | Go | SQL 注入 | +| [CVE-2015-7501](https://nvd.nist.gov/vuln/detail/CVE-2015-7501) | Apache Commons Collections | Java | 反序列化 | +| [CVE-2017-12629](https://nvd.nist.gov/vuln/detail/CVE-2017-12629) | Apache Solr | Java | 命令注入 | +| [CVE-2022-1471](https://nvd.nist.gov/vuln/detail/CVE-2022-1471) | SnakeYAML | Java | 反序列化 | +| [CVE-2022-42889](https://nvd.nist.gov/vuln/detail/CVE-2022-42889) | Apache Commons Text | Java | 代码执行 | +| [GHSA-h8cj-hpmg-636v](https://github.com/advisories/GHSA-h8cj-hpmg-636v) | Appsmith | Java | SQL 注入 | +| [CVE-2013-0156](https://nvd.nist.gov/vuln/detail/CVE-2013-0156) | Ruby on Rails | Ruby | 反序列化 | +| [CVE-2020-8130](https://nvd.nist.gov/vuln/detail/CVE-2020-8130) | Rake | Ruby | 命令注入 | +| [CVE-2021-21288](https://nvd.nist.gov/vuln/detail/CVE-2021-21288) | CarrierWave | Ruby | SSRF | +| [CVE-2023-38337](https://nvd.nist.gov/vuln/detail/CVE-2023-38337) | rswag-api | Ruby | 路径穿越 | +| [CVE-2017-9841](https://nvd.nist.gov/vuln/detail/CVE-2017-9841) | PHPUnit | PHP | 代码执行(`eval`) | +| [CVE-2018-15133](https://nvd.nist.gov/vuln/detail/CVE-2018-15133) | Laravel | PHP | 反序列化 | +| [CVE-2018-20997](https://nvd.nist.gov/vuln/detail/CVE-2018-20997) | tar-rs | Rust | 路径穿越 | +| [CVE-2022-36113](https://nvd.nist.gov/vuln/detail/CVE-2022-36113) | cargo | Rust | 路径穿越 | +| [CVE-2024-24576](https://nvd.nist.gov/vuln/detail/CVE-2024-24576) | Rust stdlib | Rust | 命令注入 | +| [CVE-2023-42456](https://rustsec.org/advisories/RUSTSEC-2023-0069.html) | sudo-rs | Rust | 路径穿越 | +| [CVE-2024-32884](https://rustsec.org/advisories/RUSTSEC-2024-0335.html) | gitoxide | Rust | 命令注入 | +| [CVE-2025-53549](https://rustsec.org/advisories/RUSTSEC-2025-0043.html) | matrix-rust-sdk | Rust | SQL 注入 | +| [CVE-2016-3714](https://nvd.nist.gov/vuln/detail/CVE-2016-3714) | ImageMagick (ImageTragick) | C | 命令注入 | +| [CVE-2019-18634](https://nvd.nist.gov/vuln/detail/CVE-2019-18634) | sudo (pwfeedback) | C | 内存安全 | +| [CVE-2019-13132](https://nvd.nist.gov/vuln/detail/CVE-2019-13132) | ZeroMQ libzmq | C++ | 内存安全 | +| [CVE-2022-1941](https://nvd.nist.gov/vuln/detail/CVE-2022-1941) | Protocol Buffers | C++ | 内存安全 | +| [CVE-2025-69662](https://nvd.nist.gov/vuln/detail/CVE-2025-69662) | geopandas | Python | SQL 注入 | +| [CVE-2026-33626](https://nvd.nist.gov/vuln/detail/CVE-2026-33626) | LMDeploy | Python | SSRF | + +用例文件位于 [`tests/benchmark/cve_corpus/`](tests/benchmark/cve_corpus/),并附上游归属头注释。 + +--- + +## 工作原理 + +对文件系统进行两遍扫描,可选用 SQLite 索引跳过未变更文件: + +1. **Pass 1**:用 tree-sitter 解析每个文件,构建过程内 CFG(petgraph),下降到剪枝后的 SSA(在支配边界上做 Cytron phi 插入),并导出每函数摘要(source/sanitizer/sink 能力位、污点变换、指向集、被调集合)。 +2. **摘要合并**:将每文件摘要并集合并为 `GlobalSummaries` 映射。 +3. **Pass 2**:在跨文件上下文与有限上下文敏感(文件内被调用 k=1 内联,SCC 不动点上限 64 次迭代,超过内联体大小阈值的被调用走摘要回退)下重新分析每个文件。正向数据流工作表通过 SSA 格传播污点,保证收敛。调用图 SCC 迭代到不动点(在上限内),使相互递归函数能拿到准确摘要。 +4. **排序、去重、动态验证、输出**:按 严重度 × 证据强度 × 源类可利用性 打分。默认构建会对中高置信度发现做动态验证,然后输出到控制台、JSON、SARIF 和浏览器 UI。 + +检测器家族:污点(跨文件 source→sink,含 SQLi、XSS、命令/代码执行、反序列化、SSRF、路径穿越、格式串、加密、LDAP 注入、XPath 注入、HTTP 头/响应拆分、开放重定向、服务端模板注入、XXE、原型污染、数据外泄、以及 auth 折入的能力位类规则)、CFG 结构(鉴权缺失、未守卫汇、资源泄漏)、状态模型(use-after-close、double-close、must-leak、unauthed-access)、AST 模式(tree-sitter 结构匹配)。完整检测器文档:[Detectors](https://nyxscan.dev/docs/detectors.html)。 + +--- + +## 动态验证 + +静态分析说明 source 到 sink 可达。动态验证会尝试证明这条路径在真实代码里会触发。默认构建开启该功能,`nyx scan` 会为中高置信度发现生成 harness,在沙箱中用 curated payload 运行,并把结果写入 `evidence.dynamic_verdict`。 + +```bash +nyx scan --verify # 默认行为的显式写法 +nyx scan --no-verify # 只跑静态分析,适合本地快速循环 +``` + +`Confirmed` 只有在攻击 payload 触发 sink 且对应的良性 control 保持干净时才会出现。`NotConfirmed` 表示 harness 跑完但没有触发,不等于发现已关闭。完整能力矩阵、后端与限制见 [Dynamic verification](https://nyxscan.dev/docs/dynamic.html)。 + +--- + +## 配置 + +配置由 `nyx.conf`(默认值)与 `nyx.local`(你的覆写)合并而成,从平台配置目录读取(Linux 为 `~/.config/nyx/`,macOS 为 `~/Library/Application Support/nyx/`,Windows 为 `%APPDATA%\elicpeter\nyx\config\`)。 + +```toml +[scanner] +mode = "full" # full | ast | cfg | taint +min_severity = "Medium" + +[server] +host = "127.0.0.1" +port = 9700 +open_browser = true + +# 项目专属净化器 +[[analysis.languages.javascript.rules]] +matchers = ["escapeHtml"] +kind = "sanitizer" +cap = "html_escape" +``` + +或交互式添加规则:`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`。完整 schema:[Configuration](https://nyxscan.dev/docs/configuration.html)。运行 `nyx rules list` 可在终端浏览注册表。 + +--- + +## 状态 + +正在积极开发中。API、检测器行为、配置项可能在版本间发生变化。合成语料上的规则级 F1 是 CI 回归下限;分语言详情见 [`tests/benchmark/RESULTS.md`](tests/benchmark/RESULTS.md)。 + +污点分析是过程间的。持久化的每函数 SSA 摘要带有按返回路径的变换与参数粒度的指向集,调用图 SCC(包括跨文件 SCC)迭代到联合不动点。默认 `balanced` 画像还会对文件内被调用做 k=1 上下文敏感内联。Symex(含跨文件与过程间帧)以及按需后向遍历是可选项。可分别用 `--symex` 与 `--backwards-analysis` 单独开启,或通过 `--engine-profile deep` 一并开启。 + +局限: +- 过程间精度是有界而非无限的。上下文敏感内联为 k=1 且有被调用体大小上限,SCC 不动点有迭代上限。引擎触达上限时回退到摘要,并在发现上记录 `engine_note`。 +- 不跨语言追踪调用(FFI、子进程、WASM)。每种语言独立分析。 +- 几项语言特性未建模:宏、大多数动态分派、别名导入、反射。 +- C/C++ 处于预览梯度。当前已跟踪 STL 容器流、builder 链、内联类成员函数;深度指针别名与函数指针未跟踪。干净报告不应被理解为干净审计。在作为硬性 CI 门禁之前,请与基于 clang 的工具搭配使用。 +- 结果可能含误报或漏报;预期需要人工复核。 + +--- + +## 文档 + +完整文档站点:**[nyxscan.dev/docs](https://nyxscan.dev/docs/)**。 + +- [Quick Start](https://nyxscan.dev/docs/quickstart.html) · [CLI Reference](https://nyxscan.dev/docs/cli.html) · [Installation](https://nyxscan.dev/docs/installation.html) +- [`nyx serve`](https://nyxscan.dev/docs/serve.html) · [Output Formats](https://nyxscan.dev/docs/output.html) · [Configuration](https://nyxscan.dev/docs/configuration.html) +- [How it works](https://nyxscan.dev/docs/how-it-works.html) · [Detectors](https://nyxscan.dev/docs/detectors.html)([Taint](https://nyxscan.dev/docs/detectors/taint.html)、[CFG](https://nyxscan.dev/docs/detectors/cfg.html)、[State](https://nyxscan.dev/docs/detectors/state.html)、[AST Patterns](https://nyxscan.dev/docs/detectors/patterns.html)) +- [Rule Reference](https://nyxscan.dev/docs/rules.html) · [Language Maturity](https://nyxscan.dev/docs/language-maturity.html) · [Advanced Analysis](https://nyxscan.dev/docs/advanced-analysis.html) · [Auth Analysis](https://nyxscan.dev/docs/auth.html) + +--- + +## 参与贡献 + +欢迎贡献。 + +Nyx 是开源项目,并将永远保有完全开源的核心。为了支持长期开发并使项目可持续,贡献者在首次合入前可能会被要求签署 Contributor License Agreement。 + +提交前请运行 `sh scripts/check.sh`。完整指南(包括如何添加规则与支持新语言)见 [`CONTRIBUTING.md`](CONTRIBUTING.md)。崩溃、panic 或可疑结果请提 issue,附最小复现片段与 Nyx 版本号。 + +--- + +## AI 披露 + +- **引擎代码**(taint、SSA、CFG、调用图、抽象解释、符号执行):以人工编写为主。AI 仅用于有选择的重构与样板代码,所有合入均经人工审阅。 +- **文档与本 README 的大部分内容**:由 AI 基于代码生成并经人工编辑。文档与代码漂移请作为 bug 上报。 +- **测试用例与 `expected.yaml` 文件**:AI 协助起草,落库前经人工审核。 +- **前端 UI**(React 应用):在 AI 协助下构建,经人工审阅。 + +与任何静态分析器一样,在把 Nyx 用作 CI 门禁前,请基于你自己的语料验证发现。 + +--- + +## 许可证 + +GNU General Public License v3.0 或更高版本(GPL-3.0-or-later)。可选的 `smt` 特性会捆绑 Z3(MIT 许可);分发以 `--features smt` 构建的二进制时,应在归属信息中包含 Z3 的许可证。完整文本见 [LICENSE](./LICENSE);第三方依赖见 [THIRDPARTY-LICENSES.html](./THIRDPARTY-LICENSES.html)。 diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md new file mode 100644 index 00000000..194cd90c --- /dev/null +++ b/RELEASE_CHECKLIST.md @@ -0,0 +1,94 @@ +# Release checklist: 0.8.0 (dynamic verification) + +Maintainer-facing gate for cutting `0.8.0`. The release ships the dynamic +verifier (Tracks J through S of `.pitboss/play/plan.md`). Sign-off requires +every row below green, and every CI matrix row green for at least three +consecutive runs on `master`. + +Legend: `[x]` verified locally on the dev reference machine, `[ ]` confirmed +by CI (must hold for three consecutive runs before tagging). + +## Cross-cutting invariants + +- [x] `cargo check --no-default-features --features serve` green. +- [x] `cargo check --features dynamic` green. +- [x] `cargo nextest run --features dynamic` green: 6545 passed, 0 failed, 16 skipped. +- [x] Determinism: every payload RNG seeds from `spec.spec_hash`; oracle canaries derive from `BLAKE3(spec_hash || run_nonce)`. `scripts/check_no_unseeded_rand.sh` audits the tree. +- [x] Observability: each new code path emits a `VerifyTrace` event and a typed `Inconclusive` / `Unsupported` reason. +- [x] Security: every sink-under-test routes through `src/dynamic/policy.rs` deny rules; no phase weakened the seccomp / `.sb` profile sets. +- [ ] Performance: default `nyx scan` (no `--verify`) latency does not regress. + +## Ship gates (`scripts/m7_ship_gate.sh`) + +- [x] Gate 1: static-only scan green on `tests/benchmark/corpus`. +- [x] Gate 2: `cargo nextest run --features dynamic` green (covers Gate 4 + Gate 5 binaries). +- [x] Gate 3: with-verify / static-only wall-clock ratio <= 1.5x on `benches/fixtures/`. +- [x] Gate 4: SARIF schema validation on every dynamic verdict variant. +- [x] Gate 5: layering boundary test green. +- [ ] Gate 6: Java OWASP Benchmark v1.2 `--verify` acceptance (wall-clock <= 15 min CI, per-cap precision >= 0.85 / recall >= 0.40, per-`(cap, lang)` budget). Self-skips without `NYX_OWASP_CORPUS`. +- [ ] Gate 7: NodeGoat + Juice Shop acceptance. Self-skips without `NYX_NODEGOAT_CORPUS` / `NYX_JUICESHOP_CORPUS`. +- [ ] Gate 8: RailsGoat / DVWA / DVPWA / gosec / RustSec acceptance. Self-skips without the matching `NYX_*_CORPUS`. + +Gates 6 through 8 run against real corpora that are not vendored into the repo. +They are enforced in the `eval` workflow with the corpora cached on the CI +runner. Locally they self-skip with a clear message. + +## CI matrix rows (must be green three runs running) + +`ci.yml`: +- [ ] frontend, rustfmt, clippy-stable, cargo-deny, unused-deps, third-party-licenses +- [ ] docs-fresh (`nyx-docgen` output committed), rustdoc +- [ ] rust-beta-build, msrv +- [ ] rust-stable-test-linux-without-docker, rust-stable-test-linux-with-docker (`cargo nextest run --all-features`) + +`dynamic.yml` (each runs `cargo nextest run --features dynamic`): +- [ ] linux-process-only +- [ ] linux-with-docker +- [ ] macos + +`eval.yml`: +- [ ] owasp (Gate 6) +- [ ] jsts matrix: nodegoat, juiceshop (Gate 7) +- [ ] polyglot matrix: railsgoat, dvwa, dvpwa, gosec, rustsec (Gate 8) + +## Docs and metadata + +- [x] `Cargo.toml` version bumped to `0.8.0`; `Cargo.lock` regenerated. +- [x] `docs/dynamic.md` rewritten: cap x lang matrix, framework adapter table, oracle table, performance budgets, limitations. +- [x] `README.md` dynamic verification section + docs link. +- [x] `CHANGELOG.md` `[0.8.0]` entry covers Tracks J through S. +- [x] Stray version strings updated (README GitHub Action pin, telemetry doc example). + +## Known limitations carried into 0.8.0 + +These are documented in `docs/dynamic.md` and accepted for the MVP. They are +not release blockers, but the release notes should not overstate the verifier. + +- **Guarded-sink over-confirmation (resolved on `dynamic`).** The synthesized + harness now drives the finding's enclosing entry function when one is + derivable, routing the payload to the tainted parameter, so a guard that + lives in the caller (a `Object.create(null)` merge target, an allowlisting + `resolveClass`, a const-name check before `Marshal.load`) runs first and + participates in the verdict. The build-time entry-vs-sink choice is recorded + on the verify trace as `entry_invocation`. When no enclosing entry can be + derived the harness falls back to driving the sink directly, which can still + over-confirm a guard it never executes. On the in-house fixture set the + verify scan now confirms the 8 genuine vulnerabilities and reads + `NotConfirmed` on all 4 negative-control files. +- **In-house confirmed rate is modest.** A `--verify` scan of + `tests/dynamic_fixtures` (process backend) lands 8 Confirmed / 15 + NotConfirmed / 115 Inconclusive / 137 Unsupported of 275. The Unsupported + bulk is `SoundOracleUnavailable` (ENV_VAR / SHELL_ESCAPE / URL_ENCODE source + and sanitizer caps, correct by design); the Inconclusive bulk is + `SpecDerivationFailed` on benign and scaffolding fixtures with no derivable + flow. The authoritative confirmed / precision / recall numbers come from the + real-corpus gates (6 through 8), which require the corpora. +- **Real-corpus gates unverified locally.** Gates 6 through 8 self-skip without + `NYX_*_CORPUS`. The >= 40% confirmed and >= 0.85 precision targets are + enforced only in the `eval` workflow. + +## Tag + +- [ ] Three consecutive green CI runs on `master` confirmed. +- [ ] Real-corpus gates (6 through 8) green in the `eval` workflow with corpora wired. +- [ ] `git tag v0.8.0` and push; `release-build.yml` publishes the binaries and `SHA256SUMS`. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..3eae9653 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,23 @@ +# Roadmap + +## Now: recall and precision on real codebases + +The current focus is straightforward. Run Nyx against real open-source repositories and real CVEs, then close the gap between what it finds and what it should find. + +That means: + +- **Recall.** Pick CVEs with public fixes. Reproduce them on the vulnerable commit. If Nyx misses, figure out why (missing source, missing sink, lost flow across a call, dropped at a sanitizer that was not actually a sanitizer) and fix the underlying analysis, not the fixture. +- **Precision.** Triage the noise on large repos (phpMyAdmin, Nextcloud, and others). Each false positive gets reduced to a pattern: receiver-type gate, non-crypto context for `md5`/`sha1`, type-safe sink suppression, etc. Land the gate, re-run the corpus, confirm the count drops without taking real bugs with it. +- **Corpus discipline.** Every fix lands with a fixture (positive or negative) and a corpus row. Rule-level F1 on `tests/benchmark/corpus/` is the scoreboard. CI floors only ratchet up. + +The scanner internals (SSA, cross-file summaries, abstract interpretation, symbolic execution, auth analysis) are in place. They get refined in service of the recall/precision work, not extended for their own sake. + +## Later: dynamic capability + +Static analysis confirms a flow exists. Dynamic execution confirms it fires. The plan is a local sandbox that picks up entry points Nyx already identifies, builds a harness, injects a payload, and watches for the crash or shell. Pairs naturally with fuzzing (libFuzzer, cargo-fuzz, go-fuzz, HTTP) where the static engine picks the targets. + +Not started. Lands after the static side is honest on real corpora. + +## Later still: reasoning layer + +Embeddings for cross-codebase pattern similarity. LLM-assisted detection for logic bugs that resist taint modeling. Automated exploit refinement loops. All speculative until the foundation is solid. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..fedd4cda --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,88 @@ +# Security Policy + +## Reporting a vulnerability + +Report privately. Do not open a public GitHub issue for a security bug. + +Use [GitHub Security Advisories](https://github.com/elicpeter/nyx/security/advisories/new) to file a private report. Only the maintainers see it. + +Include: + +- Affected version (`nyx --version`) and OS +- Reproduction steps or a minimal PoC +- Impact (RCE, file read or write, sandbox escape, auth bypass in `nyx serve`, etc.) +- Whether you have a fix in mind + +You'll get an acknowledgement within 3 business days, and a status update every 7 days until the issue is closed. + +## Scope + +In scope: bugs that let untrusted input reach the Nyx process and cause harm. + +- Code execution in the scanner: parser exploits, deserialization, command injection in helpers, custom-rule sandbox escape. +- Path traversal or arbitrary file access outside the target repo. +- `nyx serve` issues: auth bypass, host-header bypass, CSRF on mutating routes, XSS in the UI, cross-origin access from a non-loopback origin. +- Memory safety bugs in any unsafe Rust we introduce. +- Tampering with `.nyx/` triage state from outside the user's repo. +- Supply chain issues affecting published `nyx-scanner` crates or release artifacts. + +Out of scope: + +- False positives or missed detections in scan output. File a regular GitHub issue with the rule ID and a fixture. +- Findings Nyx reports against your own code. That's the scanner working, not a Nyx vulnerability. +- Anything requiring physical or local-account access to the user's machine. +- Self-XSS and missing security headers on `127.0.0.1` endpoints. The UI is loopback-only. +- Performance pathologies on hostile input (a 50 GB file, deeply nested grammars). We harden where we can. +- Issues only reachable by a user editing their own `nyx.conf` to weaken defaults. + +## Supported versions + +| Version | Status | +|---------|-----------------------| +| 0.7.x | Supported | +| 0.6.x | Critical fixes only | +| < 0.6 | End of life | + +The project follows [Semantic Versioning](https://semver.org) once it reaches 1.0.0. Until then, breaking changes can land in any minor release. + +## Severity + +We use [CVSS 3.1](https://www.first.org/cvss/v3.1/specification-document) to rate reports. + +| Severity | Examples | +|----------|-----------------------------------------------------------------------------------------------| +| Critical | Unauthenticated RCE in `nyx serve`, custom-rule sandbox escape during a default scan | +| High | Auth bypass against `nyx serve`, arbitrary file write outside the repo | +| Medium | Stored XSS in the UI, CSRF on a mutating route, host-header bypass | +| Low | Information disclosure with no privilege change, log-injection, denial of service via input | + +## Disclosure + +Coordinated disclosure. + +1. We confirm the report and assign severity. +2. We request a CVE through GitHub or MITRE. +3. A fix is developed on a private branch, with backports to supported lines if needed. +4. A new release ships on crates.io and a public advisory goes out. +5. The reporter is credited in the advisory and the changelog, unless they ask to stay anonymous. + +Target window from report to fix is 90 days. If you need to publish on a shorter timeline, tell us in the report and we'll work toward it. + +## Safe harbor + +Good-faith security research is welcome. We won't pursue legal action against researchers who: + +- Report privately and give a reasonable window before publishing. +- Test against their own installations, not third-party deployments running Nyx. +- Avoid data destruction, account takeover, and service disruption. +- Stop and reach out if a test starts to affect data or systems they don't own. + +If you're not sure whether a test is in scope, ask first. + +## Bounty + +There is no paid bug bounty program. Credit, a thank-you in the advisory, and a mention in the changelog are what we offer today. + +## Security model recap + +Nyx runs locally. The browser UI binds to `127.0.0.1` by default, requires a matching `Host` header, and uses a CSRF token on every mutating request. There is no login, no telemetry, and no remote control plane. If you find a way around any of those defaults, that's a security issue and we want to hear about it. diff --git a/THIRDPARTY-LICENSES.html b/THIRDPARTY-LICENSES.html new file mode 100644 index 00000000..73b982c9 --- /dev/null +++ b/THIRDPARTY-LICENSES.html @@ -0,0 +1,6498 @@ + + + + + + + +
+
+

Third Party Licenses

+

This page lists the licenses of the projects used in cargo-about.

+
+ +

Overview of licenses:

+ + +

All license text:

+
    +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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 2023 Jacob Pratt et al.
    +
    +   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
    +
    +       http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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 2024 Jacob Pratt et al.
    +
    +   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
    +
    +       http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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
    +
    +       http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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 (c) Microsoft Corporation.
    +
    +   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
    +
    +       http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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 2023 The Fuchsia Authors
    +
    +   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
    +
    +       http://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.
    +
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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
    +
    +       http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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
    +
    +       http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                     Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   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
    +
    +       http://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.
    +
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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 (c) 2016 Alex Crichton
    +Copyright (c) 2017 The Tokio Authors
    +
    +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
    +
    +	http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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 2017 http-rs authors
    +
    +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
    +
    +	http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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 2020 Andrew Straw
    +
    +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
    +
    +	http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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
    +
    +	http://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.
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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
    +
    +	http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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
    +
    +    http://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.
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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
    +
    +   http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     https://www.apache.org/licenses/
    +
    +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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     https://www.apache.org/licenses/
    +
    +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
    +
    +	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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
                                  Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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
    +
    +	http://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.
    +
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Apache License
    +Version 2.0, January 2004
    +http://www.apache.org/licenses/
    +
    +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
    +
    +http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Apache License
    +Version 2.0, January 2004
    +http://www.apache.org/licenses/
    +
    +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
    +
    +http://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.
    +
    +
  • +
  • +

    Apache License 2.0

    +

    Used by:

    + +
    Rust-chrono is dual-licensed under The MIT License [1] and
    +Apache 2.0 License [2]. Copyright (c) 2014--2026, Kang Seonghoon and
    +contributors.
    +
    +Nota Bene: This is same as the Rust Project's own license.
    +
    +
    +[1]: <http://opensource.org/licenses/MIT>, which is reproduced below:
    +
    +~~~~
    +The MIT License (MIT)
    +
    +Copyright (c) 2014, Kang Seonghoon.
    +
    +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
    +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.
    +~~~~
    +
    +
    +[2]: <http://www.apache.org/licenses/LICENSE-2.0>, which is reproduced below:
    +
    +~~~~
    +                              Apache License
    +                        Version 2.0, January 2004
    +                     http://www.apache.org/licenses/
    +
    +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
    +
    +	http://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.
    +~~~~
    +
    +
    +
  • +
  • +

    BSD 2-Clause "Simplified" License

    +

    Used by:

    + +
    Copyright (c) 2015 David Roundy <roundyd@physics.oregonstate.edu>
    +All rights reserved.
    +
    +Redistribution and use in source and binary forms, with or without
    +modification, are permitted provided that the following conditions are
    +met:
    +
    +1. Redistributions of source code must retain the above copyright
    +   notice, this list of conditions and the following disclaimer.
    +
    +2. Redistributions in binary form must reproduce the above copyright
    +   notice, this list of conditions and the following disclaimer in the
    +   documentation and/or other materials provided with the
    +   distribution.
    +
    +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    +
    +
  • +
  • +

    BSD 3-Clause "New" or "Revised" License

    +

    Used by:

    + +
    BSD 3-Clause License
    +
    +Copyright (c) 2013, Julien Schmidt
    +All rights reserved.
    +
    +Redistribution and use in source and binary forms, with or without
    +modification, are permitted provided that the following conditions are met:
    +
    +1. Redistributions of source code must retain the above copyright notice, this
    +   list of conditions and the following disclaimer.
    +
    +2. Redistributions in binary form must reproduce the above copyright notice,
    +   this list of conditions and the following disclaimer in the documentation
    +   and/or other materials provided with the distribution.
    +
    +3. Neither the name of the copyright holder nor the names of its
    +   contributors may be used to endorse or promote products derived from
    +   this software without specific prior written permission.
    +
    +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    +
    +
  • +
  • +

    GNU General Public License v3.0 only

    +

    Used by:

    + +
    +GNU GENERAL PUBLIC LICENSE
    +
    +Version 3, 29 June 2007
    +
    +Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
    +
    +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
    +Preamble
    +
    +The GNU General Public License is a free, copyleft license for software and other kinds of works.
    +
    +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
    +
    +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
    +
    +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
    +
    +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
    +
    +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
    +
    +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
    +
    +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
    +
    +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
    +
    +The precise terms and conditions for copying, distribution and modification follow.
    +TERMS AND CONDITIONS
    +0. Definitions.
    +
    +“This License” refers to version 3 of the GNU General Public License.
    +
    +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
    +
    +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
    +
    +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
    +
    +A “covered work” means either the unmodified Program or a work based on the Program.
    +
    +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
    +
    +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
    +
    +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
    +1. Source Code.
    +
    +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
    +
    +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
    +
    +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
    +
    +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
    +
    +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
    +
    +The Corresponding Source for a work in source code form is that same work.
    +2. Basic Permissions.
    +
    +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
    +
    +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
    +
    +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
    +3. Protecting Users' Legal Rights From Anti-Circumvention Law.
    +
    +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
    +
    +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
    +4. Conveying Verbatim Copies.
    +
    +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
    +
    +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
    +5. Conveying Modified Source Versions.
    +
    +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
    +
    +    a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
    +    b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
    +    c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
    +    d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
    +
    +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
    +6. Conveying Non-Source Forms.
    +
    +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
    +
    +    a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
    +    b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
    +    c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
    +    d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
    +    e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
    +
    +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
    +
    +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
    +
    +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
    +
    +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
    +
    +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
    +
    +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
    +7. Additional Terms.
    +
    +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
    +
    +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
    +
    +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
    +
    +    a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
    +    b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
    +    c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
    +    d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
    +    e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
    +    f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
    +
    +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
    +
    +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
    +
    +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
    +8. Termination.
    +
    +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
    +
    +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
    +
    +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
    +
    +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
    +9. Acceptance Not Required for Having Copies.
    +
    +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
    +10. Automatic Licensing of Downstream Recipients.
    +
    +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
    +
    +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
    +
    +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
    +11. Patents.
    +
    +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
    +
    +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
    +
    +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
    +
    +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
    +
    +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
    +
    +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
    +
    +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
    +
    +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
    +12. No Surrender of Others' Freedom.
    +
    +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
    +13. Use with the GNU Affero General Public License.
    +
    +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
    +14. Revised Versions of this License.
    +
    +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
    +
    +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
    +
    +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
    +
    +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
    +15. Disclaimer of Warranty.
    +
    +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
    +16. Limitation of Liability.
    +
    +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
    +17. Interpretation of Sections 15 and 16.
    +
    +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
    +
    +END OF TERMS AND CONDITIONS
    +How to Apply These Terms to Your New Programs
    +
    +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
    +
    +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
    +
    +    <one line to give the program's name and a brief idea of what it does.>
    +    Copyright (C) <year>  <name of author>
    +
    +    This program is free software: you can redistribute it and/or modify
    +    it under the terms of the GNU General Public License as published by
    +    the Free Software Foundation, either version 3 of the License, or
    +    (at your option) any later version.
    +
    +    This program is distributed in the hope that it will be useful,
    +    but WITHOUT ANY WARRANTY; without even the implied warranty of
    +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    +    GNU General Public License for more details.
    +
    +    You should have received a copy of the GNU General Public License
    +    along with this program.  If not, see <https://www.gnu.org/licenses/>.
    +
    +Also add information on how to contact you by electronic and paper mail.
    +
    +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
    +
    +    <program>  Copyright (C) <year>  <name of author>
    +    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    +    This is free software, and you are welcome to redistribute it
    +    under certain conditions; type `show c' for details.
    +
    +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
    +
    +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
    +
    +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2014 Carl Lerche and other MIO contributors
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2014 The rusqlite developers
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2014-2020 Optimal Computing (NZ) Ltd
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2014-2026 Sean McArthur
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2017 h2 authors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2018 Carl Lerche
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 Carl Lerche
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 Eliza Weisman
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 Eliza Weisman
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 Tokio Contributors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 Tower Contributors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019 axum Contributors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019-2021 Tower Contributors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019-2024 Sean McArthur & Hyper Contributors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2019-2025 Sean McArthur & Hyper Contributors
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) 2023-2025 Sean McArthur
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    Copyright (c) Individual contributors
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2017 Evgeny Safronov
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2019 Acrimon
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2019 Yoshua Wuyts
    +Copyright (c) Tokio Contributors
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2019–2025 axum Contributors
    +
    +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 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.
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2021 Adel Prokurov
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2022 Ibraheem Ahmed
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) 2024 Benjamin Sago, Fabio Valentini
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) <year> <copyright holders>
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    MIT License
    +
    +Copyright (c) Tokio Contributors
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2014 Max Brunsfeld
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2014-2022 Steven Fackler, Yuki Okushi
    +
    +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 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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2015 Andrew Gallant
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2015 Danny Guo
    +Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
    +Copyright (c) 2018 Akash Kurdekar
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2016 Max Brunsfeld
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2017 Andrew Gallant
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com>
    +
    +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
    +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.
    +
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2017 Josh Vera, GitHub
    +Copyright (c) 2019 Max Brunsfeld, Amaan Qureshi, Christian Frøystad, Caleb White
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2017 Maxim Sokolov
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2018 Max Brunsfeld
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2019 Simon Heath
    +
    +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
    +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.
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2014 Benjamin Sago
    +Copyright (c) 2021-2022 The Nushell Project Developers
    +
    +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
    +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.
    +
    +
  • +
  • +

    MIT License

    +

    Used by:

    + +
    The MIT License (MIT)
    +
    +Copyright (c) 2015 Jovansonlee Cesar
    +
    +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
    +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.
    +
    +
  • +
  • +

    Mozilla Public License 2.0

    +

    Used by:

    + +
    Mozilla Public License Version 2.0
    +==================================
    +
    +1. Definitions
    +--------------
    +
    +1.1. "Contributor"
    +    means each individual or legal entity that creates, contributes to
    +    the creation of, or owns Covered Software.
    +
    +1.2. "Contributor Version"
    +    means the combination of the Contributions of others (if any) used
    +    by a Contributor and that particular Contributor's Contribution.
    +
    +1.3. "Contribution"
    +    means Covered Software of a particular Contributor.
    +
    +1.4. "Covered Software"
    +    means Source Code Form to which the initial Contributor has attached
    +    the notice in Exhibit A, the Executable Form of such Source Code
    +    Form, and Modifications of such Source Code Form, in each case
    +    including portions thereof.
    +
    +1.5. "Incompatible With Secondary Licenses"
    +    means
    +
    +    (a) that the initial Contributor has attached the notice described
    +        in Exhibit B to the Covered Software; or
    +
    +    (b) that the Covered Software was made available under the terms of
    +        version 1.1 or earlier of the License, but not also under the
    +        terms of a Secondary License.
    +
    +1.6. "Executable Form"
    +    means any form of the work other than Source Code Form.
    +
    +1.7. "Larger Work"
    +    means a work that combines Covered Software with other material, in 
    +    a separate file or files, that is not Covered Software.
    +
    +1.8. "License"
    +    means this document.
    +
    +1.9. "Licensable"
    +    means having the right to grant, to the maximum extent possible,
    +    whether at the time of the initial grant or subsequently, any and
    +    all of the rights conveyed by this License.
    +
    +1.10. "Modifications"
    +    means any of the following:
    +
    +    (a) any file in Source Code Form that results from an addition to,
    +        deletion from, or modification of the contents of Covered
    +        Software; or
    +
    +    (b) any new file in Source Code Form that contains any Covered
    +        Software.
    +
    +1.11. "Patent Claims" of a Contributor
    +    means any patent claim(s), including without limitation, method,
    +    process, and apparatus claims, in any patent Licensable by such
    +    Contributor that would be infringed, but for the grant of the
    +    License, by the making, using, selling, offering for sale, having
    +    made, import, or transfer of either its Contributions or its
    +    Contributor Version.
    +
    +1.12. "Secondary License"
    +    means either the GNU General Public License, Version 2.0, the GNU
    +    Lesser General Public License, Version 2.1, the GNU Affero General
    +    Public License, Version 3.0, or any later versions of those
    +    licenses.
    +
    +1.13. "Source Code Form"
    +    means the form of the work preferred for making modifications.
    +
    +1.14. "You" (or "Your")
    +    means an individual or a legal entity exercising rights under this
    +    License. For legal entities, "You" includes any entity that
    +    controls, is controlled by, or is under common control with You. For
    +    purposes of this definition, "control" means (a) the power, direct
    +    or indirect, to cause the direction or management of such entity,
    +    whether by contract or otherwise, or (b) ownership of more than
    +    fifty percent (50%) of the outstanding shares or beneficial
    +    ownership of such entity.
    +
    +2. License Grants and Conditions
    +--------------------------------
    +
    +2.1. Grants
    +
    +Each Contributor hereby grants You a world-wide, royalty-free,
    +non-exclusive license:
    +
    +(a) under intellectual property rights (other than patent or trademark)
    +    Licensable by such Contributor to use, reproduce, make available,
    +    modify, display, perform, distribute, and otherwise exploit its
    +    Contributions, either on an unmodified basis, with Modifications, or
    +    as part of a Larger Work; and
    +
    +(b) under Patent Claims of such Contributor to make, use, sell, offer
    +    for sale, have made, import, and otherwise transfer either its
    +    Contributions or its Contributor Version.
    +
    +2.2. Effective Date
    +
    +The licenses granted in Section 2.1 with respect to any Contribution
    +become effective for each Contribution on the date the Contributor first
    +distributes such Contribution.
    +
    +2.3. Limitations on Grant Scope
    +
    +The licenses granted in this Section 2 are the only rights granted under
    +this License. No additional rights or licenses will be implied from the
    +distribution or licensing of Covered Software under this License.
    +Notwithstanding Section 2.1(b) above, no patent license is granted by a
    +Contributor:
    +
    +(a) for any code that a Contributor has removed from Covered Software;
    +    or
    +
    +(b) for infringements caused by: (i) Your and any other third party's
    +    modifications of Covered Software, or (ii) the combination of its
    +    Contributions with other software (except as part of its Contributor
    +    Version); or
    +
    +(c) under Patent Claims infringed by Covered Software in the absence of
    +    its Contributions.
    +
    +This License does not grant any rights in the trademarks, service marks,
    +or logos of any Contributor (except as may be necessary to comply with
    +the notice requirements in Section 3.4).
    +
    +2.4. Subsequent Licenses
    +
    +No Contributor makes additional grants as a result of Your choice to
    +distribute the Covered Software under a subsequent version of this
    +License (see Section 10.2) or under the terms of a Secondary License (if
    +permitted under the terms of Section 3.3).
    +
    +2.5. Representation
    +
    +Each Contributor represents that the Contributor believes its
    +Contributions are its original creation(s) or it has sufficient rights
    +to grant the rights to its Contributions conveyed by this License.
    +
    +2.6. Fair Use
    +
    +This License is not intended to limit any rights You have under
    +applicable copyright doctrines of fair use, fair dealing, or other
    +equivalents.
    +
    +2.7. Conditions
    +
    +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
    +in Section 2.1.
    +
    +3. Responsibilities
    +-------------------
    +
    +3.1. Distribution of Source Form
    +
    +All distribution of Covered Software in Source Code Form, including any
    +Modifications that You create or to which You contribute, must be under
    +the terms of this License. You must inform recipients that the Source
    +Code Form of the Covered Software is governed by the terms of this
    +License, and how they can obtain a copy of this License. You may not
    +attempt to alter or restrict the recipients' rights in the Source Code
    +Form.
    +
    +3.2. Distribution of Executable Form
    +
    +If You distribute Covered Software in Executable Form then:
    +
    +(a) such Covered Software must also be made available in Source Code
    +    Form, as described in Section 3.1, and You must inform recipients of
    +    the Executable Form how they can obtain a copy of such Source Code
    +    Form by reasonable means in a timely manner, at a charge no more
    +    than the cost of distribution to the recipient; and
    +
    +(b) You may distribute such Executable Form under the terms of this
    +    License, or sublicense it under different terms, provided that the
    +    license for the Executable Form does not attempt to limit or alter
    +    the recipients' rights in the Source Code Form under this License.
    +
    +3.3. Distribution of a Larger Work
    +
    +You may create and distribute a Larger Work under terms of Your choice,
    +provided that You also comply with the requirements of this License for
    +the Covered Software. If the Larger Work is a combination of Covered
    +Software with a work governed by one or more Secondary Licenses, and the
    +Covered Software is not Incompatible With Secondary Licenses, this
    +License permits You to additionally distribute such Covered Software
    +under the terms of such Secondary License(s), so that the recipient of
    +the Larger Work may, at their option, further distribute the Covered
    +Software under the terms of either this License or such Secondary
    +License(s).
    +
    +3.4. Notices
    +
    +You may not remove or alter the substance of any license notices
    +(including copyright notices, patent notices, disclaimers of warranty,
    +or limitations of liability) contained within the Source Code Form of
    +the Covered Software, except that You may alter any license notices to
    +the extent required to remedy known factual inaccuracies.
    +
    +3.5. Application of Additional Terms
    +
    +You may choose to offer, and to charge a fee for, warranty, support,
    +indemnity or liability obligations to one or more recipients of Covered
    +Software. However, You may do so only on Your own behalf, and not on
    +behalf of any Contributor. You must make it absolutely clear that any
    +such warranty, support, indemnity, or liability obligation is offered by
    +You alone, and You hereby agree to indemnify every Contributor for any
    +liability incurred by such Contributor as a result of warranty, support,
    +indemnity or liability terms You offer. You may include additional
    +disclaimers of warranty and limitations of liability specific to any
    +jurisdiction.
    +
    +4. Inability to Comply Due to Statute or Regulation
    +---------------------------------------------------
    +
    +If it is impossible for You to comply with any of the terms of this
    +License with respect to some or all of the Covered Software due to
    +statute, judicial order, or regulation then You must: (a) comply with
    +the terms of this License to the maximum extent possible; and (b)
    +describe the limitations and the code they affect. Such description must
    +be placed in a text file included with all distributions of the Covered
    +Software under this License. Except to the extent prohibited by statute
    +or regulation, such description must be sufficiently detailed for a
    +recipient of ordinary skill to be able to understand it.
    +
    +5. Termination
    +--------------
    +
    +5.1. The rights granted under this License will terminate automatically
    +if You fail to comply with any of its terms. However, if You become
    +compliant, then the rights granted under this License from a particular
    +Contributor are reinstated (a) provisionally, unless and until such
    +Contributor explicitly and finally terminates Your grants, and (b) on an
    +ongoing basis, if such Contributor fails to notify You of the
    +non-compliance by some reasonable means prior to 60 days after You have
    +come back into compliance. Moreover, Your grants from a particular
    +Contributor are reinstated on an ongoing basis if such Contributor
    +notifies You of the non-compliance by some reasonable means, this is the
    +first time You have received notice of non-compliance with this License
    +from such Contributor, and You become compliant prior to 30 days after
    +Your receipt of the notice.
    +
    +5.2. If You initiate litigation against any entity by asserting a patent
    +infringement claim (excluding declaratory judgment actions,
    +counter-claims, and cross-claims) alleging that a Contributor Version
    +directly or indirectly infringes any patent, then the rights granted to
    +You by any and all Contributors for the Covered Software under Section
    +2.1 of this License shall terminate.
    +
    +5.3. In the event of termination under Sections 5.1 or 5.2 above, all
    +end user license agreements (excluding distributors and resellers) which
    +have been validly granted by You or Your distributors under this License
    +prior to termination shall survive termination.
    +
    +************************************************************************
    +*                                                                      *
    +*  6. Disclaimer of Warranty                                           *
    +*  -------------------------                                           *
    +*                                                                      *
    +*  Covered Software is provided under this License on an "as is"       *
    +*  basis, without warranty of any kind, either expressed, implied, or  *
    +*  statutory, including, without limitation, warranties that the       *
    +*  Covered Software is free of defects, merchantable, fit for a        *
    +*  particular purpose or non-infringing. The entire risk as to the     *
    +*  quality and performance of the Covered Software is with You.        *
    +*  Should any Covered Software prove defective in any respect, You     *
    +*  (not any Contributor) assume the cost of any necessary servicing,   *
    +*  repair, or correction. This disclaimer of warranty constitutes an   *
    +*  essential part of this License. No use of any Covered Software is   *
    +*  authorized under this License except under this disclaimer.         *
    +*                                                                      *
    +************************************************************************
    +
    +************************************************************************
    +*                                                                      *
    +*  7. Limitation of Liability                                          *
    +*  --------------------------                                          *
    +*                                                                      *
    +*  Under no circumstances and under no legal theory, whether tort      *
    +*  (including negligence), contract, or otherwise, shall any           *
    +*  Contributor, or anyone who distributes Covered Software as          *
    +*  permitted above, be liable to You for any direct, indirect,         *
    +*  special, incidental, or consequential damages of any character      *
    +*  including, without limitation, damages for lost profits, loss of    *
    +*  goodwill, work stoppage, computer failure or malfunction, or any    *
    +*  and all other commercial damages or losses, even if such party      *
    +*  shall have been informed of the possibility of such damages. This   *
    +*  limitation of liability shall not apply to liability for death or   *
    +*  personal injury resulting from such party's negligence to the       *
    +*  extent applicable law prohibits such limitation. Some               *
    +*  jurisdictions do not allow the exclusion or limitation of           *
    +*  incidental or consequential damages, so this exclusion and          *
    +*  limitation may not apply to You.                                    *
    +*                                                                      *
    +************************************************************************
    +
    +8. Litigation
    +-------------
    +
    +Any litigation relating to this License may be brought only in the
    +courts of a jurisdiction where the defendant maintains its principal
    +place of business and such litigation shall be governed by laws of that
    +jurisdiction, without reference to its conflict-of-law provisions.
    +Nothing in this Section shall prevent a party's ability to bring
    +cross-claims or counter-claims.
    +
    +9. Miscellaneous
    +----------------
    +
    +This License represents the complete agreement concerning the subject
    +matter hereof. If any provision of this License is held to be
    +unenforceable, such provision shall be reformed only to the extent
    +necessary to make it enforceable. Any law or regulation which provides
    +that the language of a contract shall be construed against the drafter
    +shall not be used to construe this License against a Contributor.
    +
    +10. Versions of the License
    +---------------------------
    +
    +10.1. New Versions
    +
    +Mozilla Foundation is the license steward. Except as provided in Section
    +10.3, no one other than the license steward has the right to modify or
    +publish new versions of this License. Each version will be given a
    +distinguishing version number.
    +
    +10.2. Effect of New Versions
    +
    +You may distribute the Covered Software under the terms of the version
    +of the License under which You originally received the Covered Software,
    +or under the terms of any subsequent version published by the license
    +steward.
    +
    +10.3. Modified Versions
    +
    +If you create software not governed by this License, and you want to
    +create a new license for such software, you may create and use a
    +modified version of this License if you rename the license and remove
    +any references to the name of the license steward (except to note that
    +such modified license differs from this License).
    +
    +10.4. Distributing Source Code Form that is Incompatible With Secondary
    +Licenses
    +
    +If You choose to distribute Source Code Form that is Incompatible With
    +Secondary Licenses under the terms of this version of the License, the
    +notice described in Exhibit B of this License must be attached.
    +
    +Exhibit A - Source Code Form License Notice
    +-------------------------------------------
    +
    +  This Source Code Form is subject to the terms of the Mozilla Public
    +  License, v. 2.0. If a copy of the MPL was not distributed with this
    +  file, You can obtain one at https://mozilla.org/MPL/2.0/.
    +
    +If it is not possible or desirable to put the notice in a particular
    +file, then You may include the notice in a location (such as a LICENSE
    +file in a relevant directory) where a recipient would be likely to look
    +for such a notice.
    +
    +You may add additional accurate notices of copyright ownership.
    +
    +Exhibit B - "Incompatible With Secondary Licenses" Notice
    +---------------------------------------------------------
    +
    +  This Source Code Form is "Incompatible With Secondary Licenses", as
    +  defined by the Mozilla Public License, v. 2.0.
    +
    +
  • +
  • +

    Unicode License v3

    +

    Used by:

    + +
    UNICODE LICENSE V3
    +
    +COPYRIGHT AND PERMISSION NOTICE
    +
    +Copyright © 1991-2023 Unicode, Inc.
    +
    +NOTICE TO USER: Carefully read the following legal agreement. BY
    +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
    +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
    +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
    +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
    +
    +Permission is hereby granted, free of charge, to any person obtaining a
    +copy of data files and any associated documentation (the "Data Files") or
    +software and any associated documentation (the "Software") to deal in the
    +Data Files or Software without restriction, including without limitation
    +the rights to use, copy, modify, merge, publish, distribute, and/or sell
    +copies of the Data Files or Software, and to permit persons to whom the
    +Data Files or Software are furnished to do so, provided that either (a)
    +this copyright and permission notice appear with all copies of the Data
    +Files or Software, or (b) this copyright and permission notice appear in
    +associated Documentation.
    +
    +THE DATA FILES AND SOFTWARE ARE 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 OF
    +THIRD PARTY RIGHTS.
    +
    +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
    +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
    +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
    +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
    +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
    +FILES OR SOFTWARE.
    +
    +Except as contained in this notice, the name of a copyright holder shall
    +not be used in advertising or otherwise to promote the sale, use or other
    +dealings in these Data Files or Software without prior written
    +authorization of the copyright holder.
    +
    +
  • +
  • +

    zlib License

    +

    Used by:

    + +
    Copyright (c) 2024 Orson Peters
    +
    +This software is provided 'as-is', without any express or implied warranty. In
    +no event will the authors be held liable for any damages arising from the use of
    +this software.
    +
    +Permission is granted to anyone to use this software for any purpose, including
    +commercial applications, and to alter it and redistribute it freely, subject to
    +the following restrictions:
    +
    +1. The origin of this software must not be misrepresented; you must not claim
    +    that you wrote the original software. If you use this software in a product,
    +    an acknowledgment in the product documentation would be appreciated but is
    +    not required.
    +
    +2. Altered source versions must be plainly marked as such, and must not be
    +    misrepresented as being the original software.
    +
    +3. This notice may not be removed or altered from any source distribution.
    +
  • +
+
+ + + + diff --git a/about.hbs b/about.hbs new file mode 100644 index 00000000..699b3b04 --- /dev/null +++ b/about.hbs @@ -0,0 +1,70 @@ + + + + + + + +
+
+

Third Party Licenses

+

This page lists the licenses of the projects used in cargo-about.

+
+ +

Overview of licenses:

+
    + {{#each overview}} +
  • {{name}} ({{count}})
  • + {{/each}} +
+ +

All license text:

+ +
+ + + diff --git a/about.toml b/about.toml new file mode 100644 index 00000000..4304a25b --- /dev/null +++ b/about.toml @@ -0,0 +1,80 @@ +# Pin the target triples scanned so `cargo about generate` produces the +# same output regardless of host OS. Must match the release build matrix +# in .github/workflows/release-build.yml — otherwise the CI diff step +# (third-party-licenses) will fail on platform-specific crates like +# linux-raw-sys, android_system_properties, etc. +targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", +] + +accepted = [ + # --- Apache / MIT / BSD / permissive --- + "Apache-2.0", + "MIT", + "MIT-0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "zlib-acknowledgement", + "BSL-1.0", + "NCSA", + "PostgreSQL", + "curl", + "BlueOak-1.0.0", + "X11", + "HPND", + "TCL", + "ICU", + "Info-ZIP", + + # --- Unicode / data / specs --- + "Unicode-DFS-2016", + "Unicode-3.0", + + # --- compression / libs --- + "bzip2-1.0.6", + "Libpng", + "libpng-2.0", + "IJG", + "FTL", + + # --- public domain style --- + "CC0-1.0", + "Unlicense", + "0BSD", + + # --- weak copyleft (GPL-compatible) --- + "MPL-2.0", + "LGPL-3.0", + "EPL-2.0", + + # --- GPL family --- + "GPL-3.0", + "GPL-2.0", + + # --- Python / PSF --- + "PSF-2.0", + "Python-2.0", + "Python-2.0.1", + + # --- Artistic / Perl --- + "Artistic-2.0", + + # --- LLVM / clang --- + "Apache-2.0 WITH LLVM-exception", + + # --- data / ML --- + "CDLA-Permissive-2.0", + + # --- fonts --- + "OFL-1.1", + + # --- Creative Commons (code-safe ones) --- + "CC-BY-3.0", + "CC-BY-4.0", +] \ No newline at end of file diff --git a/action-scripts/download.sh b/action-scripts/download.sh new file mode 100755 index 00000000..7ba62964 --- /dev/null +++ b/action-scripts/download.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="elicpeter/nyx" +VERSION="${NYX_VERSION:-latest}" +INSTALL_DIR="${RUNNER_TOOL_CACHE:-/tmp}/nyx" + +# Optional: pin a GPG key fingerprint here (40-char, no spaces) or set +# NYX_GPG_FINGERPRINT in the calling env to require GPG-signed SHA256SUMS. +# Empty ⇒ GPG verification is skipped (SHA256 + SLSA attestation still run). +PINNED_GPG_FINGERPRINT="${NYX_GPG_FINGERPRINT:-}" + +# ── Detect runner OS and architecture ───────────────────────────────────────── +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "${OS}-${ARCH}" in + Linux-x86_64) TARGET="x86_64-unknown-linux-gnu" ;; + Linux-aarch64) TARGET="aarch64-unknown-linux-gnu" ;; + Darwin-x86_64) TARGET="x86_64-apple-darwin" ;; + Darwin-arm64) TARGET="aarch64-apple-darwin" ;; + *) + echo "::error::Unsupported platform: ${OS} ${ARCH}" + exit 1 + ;; +esac + +# ── Resolve "latest" to an actual release tag ──────────────────────────────── +if [[ "$VERSION" == "latest" ]]; then + echo "::warning::version: latest follows a mutable tag. Pin to a specific release (e.g. v0.7.0) for supply-chain safety." + API_URL="https://api.github.com/repos/${REPO}/releases/latest" + CURL_ARGS=(-fsSL) + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + CURL_ARGS+=(-H "Authorization: token ${GITHUB_TOKEN}") + fi + RELEASE_JSON="$(curl "${CURL_ARGS[@]}" "$API_URL")" + VERSION="$(echo "$RELEASE_JSON" | grep -o '"tag_name":\s*"[^"]*"' | head -1 | cut -d'"' -f4)" + if [[ -z "$VERSION" ]]; then + echo "::error::Failed to resolve latest release tag from ${API_URL}" + exit 1 + fi + echo "Resolved latest version: ${VERSION}" +fi + +# ── Download the release asset into an isolated staging dir ────────────────── +ASSET_NAME="nyx-${TARGET}.zip" +RELEASE_BASE="https://github.com/${REPO}/releases/download/${VERSION}" +DOWNLOAD_URL="${RELEASE_BASE}/${ASSET_NAME}" +STAGING="$(mktemp -d)" +trap 'rm -rf "$STAGING"' EXIT + +CURL_COMMON=(-fsSL) +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + CURL_COMMON+=(-H "Authorization: token ${GITHUB_TOKEN}") +fi + +echo "Downloading nyx ${VERSION} for ${TARGET}..." +curl "${CURL_COMMON[@]}" -o "${STAGING}/${ASSET_NAME}" "$DOWNLOAD_URL" + +# SHA256SUMS is required — the whole release signing chain hinges on it. +echo "Downloading SHA256SUMS..." +curl "${CURL_COMMON[@]}" -o "${STAGING}/SHA256SUMS" "${RELEASE_BASE}/SHA256SUMS" + +# SHA256SUMS.asc is optional (GPG signing was wired up mid-0.x); fetch it if +# present so we can attempt signature verification. +SIG_PATH="" +if curl "${CURL_COMMON[@]}" -o "${STAGING}/SHA256SUMS.asc" "${RELEASE_BASE}/SHA256SUMS.asc" 2>/dev/null; then + SIG_PATH="${STAGING}/SHA256SUMS.asc" +fi + +# ── Mandatory: verify the binary's SHA256 matches SHA256SUMS ───────────────── +( + cd "$STAGING" + # --ignore-missing: SHA256SUMS lists every platform archive; we only have one. + if ! sha256sum --ignore-missing -c SHA256SUMS >/dev/null 2>&1; then + echo "::error::SHA256 verification failed for ${ASSET_NAME}. Release may be tampered." + echo "Expected (from SHA256SUMS):" + grep -F "${ASSET_NAME}" SHA256SUMS || true + echo "Actual:" + sha256sum "${ASSET_NAME}" || true + exit 1 + fi +) +echo "::notice::SHA256 checksum verified for ${ASSET_NAME}." + +# ── Best-effort: GPG verify SHA256SUMS.asc against a pinned fingerprint ────── +# Trust model: only accept a signature from a fingerprint we have pinned. A +# signature from any other key is treated as a failure, not a success. If no +# fingerprint is pinned, GPG verification is skipped (SHA256+SLSA still run). +if [[ -n "$SIG_PATH" ]]; then + if [[ -z "$PINNED_GPG_FINGERPRINT" ]]; then + echo "::warning::SHA256SUMS.asc found but no GPG fingerprint pinned. Set NYX_GPG_FINGERPRINT (40-char, no spaces) to enforce GPG verification." + elif ! command -v gpg >/dev/null 2>&1; then + echo "::warning::gpg not installed on runner; skipping SHA256SUMS.asc verification." + else + # Fetch the pinned key from keys.openpgp.org into an ephemeral keyring. + GNUPGHOME="$(mktemp -d)" + export GNUPGHOME + chmod 700 "$GNUPGHOME" + trap 'rm -rf "$STAGING" "$GNUPGHOME"' EXIT + if ! gpg --batch --keyserver hkps://keys.openpgp.org \ + --recv-keys "$PINNED_GPG_FINGERPRINT" >/dev/null 2>&1; then + echo "::error::Failed to fetch GPG key ${PINNED_GPG_FINGERPRINT} from keys.openpgp.org." + exit 1 + fi + # --status-fd 1 gives machine-readable output; VALIDSIG + the pinned fpr + # is the only accept condition. + GPG_STATUS="$(gpg --batch --status-fd 1 --verify \ + "$SIG_PATH" "${STAGING}/SHA256SUMS" 2>/dev/null || true)" + if ! grep -q "^\[GNUPG:\] VALIDSIG ${PINNED_GPG_FINGERPRINT} " <<<"$GPG_STATUS"; then + echo "::error::GPG signature on SHA256SUMS does not match pinned fingerprint ${PINNED_GPG_FINGERPRINT}." + echo "$GPG_STATUS" + exit 1 + fi + echo "::notice::GPG signature verified against ${PINNED_GPG_FINGERPRINT}." + fi +else + echo "::warning::SHA256SUMS.asc not published for ${VERSION}; relying on SHA256 + SLSA only." +fi + +# ── Best-effort: SLSA build-provenance attestation (Sigstore) ──────────────── +# gh attestation verify ships with the gh CLI (preinstalled on GH-hosted +# runners) and validates attestations produced by actions/attest-build- +# provenance against the Sigstore public-good transparency log. Unlike GPG +# this requires no pre-shared key and is the preferred trust root. +if command -v gh >/dev/null 2>&1; then + if gh attestation verify "${STAGING}/${ASSET_NAME}" --repo "${REPO}" >/dev/null 2>&1; then + echo "::notice::SLSA build provenance verified for ${ASSET_NAME}." + else + echo "::warning::gh attestation verify failed or no attestation present for ${VERSION}. (Expected for releases predating attest-build-provenance.)" + fi +else + echo "::warning::gh CLI not available; skipping SLSA attestation verification." +fi + +# ── Extract and install ────────────────────────────────────────────────────── +mkdir -p "$INSTALL_DIR" +# The zip stores target/{TARGET}/release/nyx — use -j to flatten paths +unzip -o -j "${STAGING}/${ASSET_NAME}" "*/nyx" -d "$INSTALL_DIR" +chmod +x "${INSTALL_DIR}/nyx" + +# ── Add to PATH for subsequent steps ───────────────────────────────────────── +echo "${INSTALL_DIR}" >> "$GITHUB_PATH" + +# ── Verify and set output ──────────────────────────────────────────────────── +INSTALLED_VERSION="$("${INSTALL_DIR}/nyx" --version 2>&1 | head -1 || echo "unknown")" +echo "nyx-version=${INSTALLED_VERSION}" >> "$GITHUB_OUTPUT" +echo "Installed nyx: ${INSTALLED_VERSION} (${TARGET})" diff --git a/action-scripts/run.sh b/action-scripts/run.sh new file mode 100755 index 00000000..74a8c914 --- /dev/null +++ b/action-scripts/run.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -uo pipefail +# Note: NOT -e — we capture nyx's exit code manually. + +# ── Build the nyx command ──────────────────────────────────────────────────── +FORMAT="${INPUT_FORMAT:-sarif}" +ARGS=("scan" "${INPUT_PATH:-.}" "--quiet" "--format" "$FORMAT") + +if [[ -n "${INPUT_FAIL_ON:-}" ]]; then + ARGS+=("--fail-on" "$INPUT_FAIL_ON") +fi + +# Append raw user args (word-split is intentional here) +if [[ -n "${INPUT_ARGS:-}" ]]; then + read -ra EXTRA <<< "$INPUT_ARGS" + ARGS+=("${EXTRA[@]}") +fi + +# ── Execute the scan ───────────────────────────────────────────────────────── +OUTDIR="${RUNNER_TEMP:-/tmp}" +SARIF_FILE="" +NYX_EXIT=0 + +echo "::group::nyx scan" +echo "Running: nyx ${ARGS[*]}" + +case "$FORMAT" in + sarif) + SARIF_FILE="${OUTDIR}/nyx-results.sarif" + nyx "${ARGS[@]}" > "$SARIF_FILE" || NYX_EXIT=$? + ;; + json) + nyx "${ARGS[@]}" > "${OUTDIR}/nyx-results.json" || NYX_EXIT=$? + ;; + *) + nyx "${ARGS[@]}" || NYX_EXIT=$? + ;; +esac + +echo "::endgroup::" + +# ── Count findings ─────────────────────────────────────────────────────────── +count_findings() { + python3 -c " +import json, sys +try: + data = json.load(open(sys.argv[1])) + fmt = sys.argv[2] + if fmt == 'sarif': + runs = data.get('runs', []) + print(len(runs[0].get('results', [])) if runs else 0) + else: + print(len(data) if isinstance(data, list) else 0) +except Exception: + print(0) +" "$1" "$2" 2>/dev/null || echo "0" +} + +FINDING_COUNT="unknown" +case "$FORMAT" in + sarif) + if [[ -f "$SARIF_FILE" ]]; then + FINDING_COUNT="$(count_findings "$SARIF_FILE" sarif)" + fi + ;; + json) + if [[ -f "${OUTDIR}/nyx-results.json" ]]; then + FINDING_COUNT="$(count_findings "${OUTDIR}/nyx-results.json" json)" + fi + ;; +esac + +# ── Set outputs ────────────────────────────────────────────────────────────── +echo "exit-code=${NYX_EXIT}" >> "$GITHUB_OUTPUT" +echo "finding-count=${FINDING_COUNT}" >> "$GITHUB_OUTPUT" +if [[ -n "$SARIF_FILE" ]]; then + echo "sarif-file=${SARIF_FILE}" >> "$GITHUB_OUTPUT" +fi + +# ── Summary ────────────────────────────────────────────────────────────────── +if [[ "$NYX_EXIT" -eq 0 ]]; then + echo "::notice::Nyx scan completed. Findings: ${FINDING_COUNT}" +else + echo "::warning::Nyx scan found issues meeting threshold. Findings: ${FINDING_COUNT}" +fi + +exit "$NYX_EXIT" diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..6e4d1f46 --- /dev/null +++ b/action.yml @@ -0,0 +1,68 @@ +name: 'Nyx Security Scanner' +description: 'Run the Nyx multi-language vulnerability scanner on your codebase. Supports Linux and macOS runners (x86_64 and ARM64).' +author: 'Eli Peter' + +branding: + icon: 'shield' + color: 'purple' + +inputs: + path: + description: 'Directory to scan' + required: false + default: '.' + version: + description: 'Nyx release tag (e.g. v0.7.0). "latest" is accepted but discouraged, pinning to a specific tag protects against upstream compromise.' + required: false + default: 'v0.7.0' + format: + description: 'Output format: sarif, json, or console' + required: false + default: 'sarif' + fail-on: + description: 'Exit non-zero if findings meet this severity threshold: HIGH, MEDIUM, or LOW' + required: false + default: '' + args: + description: 'Additional CLI arguments (e.g. "--severity >=MEDIUM --profile ci")' + required: false + default: '' + token: + description: 'GitHub token for release download (avoids rate limits)' + required: false + default: ${{ github.token }} + +outputs: + finding-count: + description: 'Number of findings detected' + value: ${{ steps.scan.outputs.finding-count }} + sarif-file: + description: 'Path to SARIF results file (empty if format is not sarif)' + value: ${{ steps.scan.outputs.sarif-file }} + exit-code: + description: 'Nyx exit code (0 = clean, 1 = threshold breached)' + value: ${{ steps.scan.outputs.exit-code }} + nyx-version: + description: 'Installed nyx version' + value: ${{ steps.install.outputs.nyx-version }} + +runs: + using: 'composite' + steps: + - name: Install nyx + id: install + shell: bash + env: + NYX_VERSION: ${{ inputs.version }} + GITHUB_TOKEN: ${{ inputs.token }} + run: ${{ github.action_path }}/action-scripts/download.sh + + - name: Run nyx scan + id: scan + shell: bash + env: + INPUT_PATH: ${{ inputs.path }} + INPUT_FORMAT: ${{ inputs.format }} + INPUT_FAIL_ON: ${{ inputs.fail-on }} + INPUT_ARGS: ${{ inputs.args }} + run: ${{ github.action_path }}/action-scripts/run.sh diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..fecb6e78 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/nyx-readme-header.png b/assets/nyx-readme-header.png new file mode 100644 index 00000000..a692d763 Binary files /dev/null and b/assets/nyx-readme-header.png differ diff --git a/assets/nyx-readme-header.svg b/assets/nyx-readme-header.svg new file mode 100644 index 00000000..f1b55a3b --- /dev/null +++ b/assets/nyx-readme-header.svg @@ -0,0 +1,24 @@ + + NYX + NYX security scanner. + + + + + + + + + + + + + diff --git a/assets/nyx-wordmark.svg b/assets/nyx-wordmark.svg new file mode 100644 index 00000000..d37fc586 --- /dev/null +++ b/assets/nyx-wordmark.svg @@ -0,0 +1,10 @@ + + nyx + diff --git a/assets/screenshots/cli-scan.gif b/assets/screenshots/cli-scan.gif new file mode 100644 index 00000000..5c002bae Binary files /dev/null and b/assets/screenshots/cli-scan.gif differ diff --git a/assets/screenshots/cli-scan.png b/assets/screenshots/cli-scan.png new file mode 100644 index 00000000..91e43979 Binary files /dev/null and b/assets/screenshots/cli-scan.png differ diff --git a/assets/screenshots/cli-scan_raw.gif b/assets/screenshots/cli-scan_raw.gif new file mode 100644 index 00000000..fdd9a9fc Binary files /dev/null and b/assets/screenshots/cli-scan_raw.gif differ diff --git a/assets/screenshots/cli-scan_raw.png b/assets/screenshots/cli-scan_raw.png new file mode 100644 index 00000000..ef68b052 Binary files /dev/null and b/assets/screenshots/cli-scan_raw.png differ diff --git a/assets/screenshots/demo-combo.gif b/assets/screenshots/demo-combo.gif new file mode 100644 index 00000000..fb9f394c Binary files /dev/null and b/assets/screenshots/demo-combo.gif differ diff --git a/assets/screenshots/demo.gif b/assets/screenshots/demo.gif new file mode 100644 index 00000000..382f767b Binary files /dev/null and b/assets/screenshots/demo.gif differ diff --git a/assets/screenshots/demo_raw.gif b/assets/screenshots/demo_raw.gif new file mode 100644 index 00000000..8e0a3d3d Binary files /dev/null and b/assets/screenshots/demo_raw.gif differ diff --git a/assets/screenshots/docs/cli-configshow.png b/assets/screenshots/docs/cli-configshow.png new file mode 100644 index 00000000..7e6d28f0 Binary files /dev/null and b/assets/screenshots/docs/cli-configshow.png differ diff --git a/assets/screenshots/docs/cli-configshow_raw.png b/assets/screenshots/docs/cli-configshow_raw.png new file mode 100644 index 00000000..e6f378ed Binary files /dev/null and b/assets/screenshots/docs/cli-configshow_raw.png differ diff --git a/assets/screenshots/docs/cli-explain-engine.png b/assets/screenshots/docs/cli-explain-engine.png new file mode 100644 index 00000000..ce90f8f8 Binary files /dev/null and b/assets/screenshots/docs/cli-explain-engine.png differ diff --git a/assets/screenshots/docs/cli-explain-engine_raw.png b/assets/screenshots/docs/cli-explain-engine_raw.png new file mode 100644 index 00000000..13eeee69 Binary files /dev/null and b/assets/screenshots/docs/cli-explain-engine_raw.png differ diff --git a/assets/screenshots/docs/cli-failon.png b/assets/screenshots/docs/cli-failon.png new file mode 100644 index 00000000..91e43979 Binary files /dev/null and b/assets/screenshots/docs/cli-failon.png differ diff --git a/assets/screenshots/docs/cli-failon_raw.png b/assets/screenshots/docs/cli-failon_raw.png new file mode 100644 index 00000000..ef68b052 Binary files /dev/null and b/assets/screenshots/docs/cli-failon_raw.png differ diff --git a/assets/screenshots/docs/cli-idxstatus.png b/assets/screenshots/docs/cli-idxstatus.png new file mode 100644 index 00000000..38853548 Binary files /dev/null and b/assets/screenshots/docs/cli-idxstatus.png differ diff --git a/assets/screenshots/docs/cli-idxstatus_raw.png b/assets/screenshots/docs/cli-idxstatus_raw.png new file mode 100644 index 00000000..376ca8d7 Binary files /dev/null and b/assets/screenshots/docs/cli-idxstatus_raw.png differ diff --git a/assets/screenshots/docs/cli-rollup-tail.png b/assets/screenshots/docs/cli-rollup-tail.png new file mode 100644 index 00000000..83050fe7 Binary files /dev/null and b/assets/screenshots/docs/cli-rollup-tail.png differ diff --git a/assets/screenshots/docs/serve-config.png b/assets/screenshots/docs/serve-config.png new file mode 100644 index 00000000..0a1e1b19 Binary files /dev/null and b/assets/screenshots/docs/serve-config.png differ diff --git a/assets/screenshots/docs/serve-config_raw.png b/assets/screenshots/docs/serve-config_raw.png new file mode 100644 index 00000000..5d27260e Binary files /dev/null and b/assets/screenshots/docs/serve-config_raw.png differ diff --git a/assets/screenshots/docs/serve-explorer.png b/assets/screenshots/docs/serve-explorer.png new file mode 100644 index 00000000..a769ff70 Binary files /dev/null and b/assets/screenshots/docs/serve-explorer.png differ diff --git a/assets/screenshots/docs/serve-explorer_raw.png b/assets/screenshots/docs/serve-explorer_raw.png new file mode 100644 index 00000000..c3daaf73 Binary files /dev/null and b/assets/screenshots/docs/serve-explorer_raw.png differ diff --git a/assets/screenshots/docs/serve-finding-detail.png b/assets/screenshots/docs/serve-finding-detail.png new file mode 100644 index 00000000..69a65180 Binary files /dev/null and b/assets/screenshots/docs/serve-finding-detail.png differ diff --git a/assets/screenshots/docs/serve-finding-detail_raw.png b/assets/screenshots/docs/serve-finding-detail_raw.png new file mode 100644 index 00000000..5cb2eb1e Binary files /dev/null and b/assets/screenshots/docs/serve-finding-detail_raw.png differ diff --git a/assets/screenshots/docs/serve-findings-list.png b/assets/screenshots/docs/serve-findings-list.png new file mode 100644 index 00000000..d8e88250 Binary files /dev/null and b/assets/screenshots/docs/serve-findings-list.png differ diff --git a/assets/screenshots/docs/serve-findings-list_raw.png b/assets/screenshots/docs/serve-findings-list_raw.png new file mode 100644 index 00000000..29ee7f61 Binary files /dev/null and b/assets/screenshots/docs/serve-findings-list_raw.png differ diff --git a/assets/screenshots/docs/serve-overview.png b/assets/screenshots/docs/serve-overview.png new file mode 100644 index 00000000..4d86004f Binary files /dev/null and b/assets/screenshots/docs/serve-overview.png differ diff --git a/assets/screenshots/docs/serve-overview_raw.png b/assets/screenshots/docs/serve-overview_raw.png new file mode 100644 index 00000000..eec47b6b Binary files /dev/null and b/assets/screenshots/docs/serve-overview_raw.png differ diff --git a/assets/screenshots/docs/serve-rules.png b/assets/screenshots/docs/serve-rules.png new file mode 100644 index 00000000..18b28ad5 Binary files /dev/null and b/assets/screenshots/docs/serve-rules.png differ diff --git a/assets/screenshots/docs/serve-rules_raw.png b/assets/screenshots/docs/serve-rules_raw.png new file mode 100644 index 00000000..15d6229f Binary files /dev/null and b/assets/screenshots/docs/serve-rules_raw.png differ diff --git a/assets/screenshots/docs/serve-scan-detail.png b/assets/screenshots/docs/serve-scan-detail.png new file mode 100644 index 00000000..a0d9420b Binary files /dev/null and b/assets/screenshots/docs/serve-scan-detail.png differ diff --git a/assets/screenshots/docs/serve-scan-detail_raw.png b/assets/screenshots/docs/serve-scan-detail_raw.png new file mode 100644 index 00000000..0e25c072 Binary files /dev/null and b/assets/screenshots/docs/serve-scan-detail_raw.png differ diff --git a/assets/screenshots/docs/serve-scans.png b/assets/screenshots/docs/serve-scans.png new file mode 100644 index 00000000..b422c53c Binary files /dev/null and b/assets/screenshots/docs/serve-scans.png differ diff --git a/assets/screenshots/docs/serve-scans_raw.png b/assets/screenshots/docs/serve-scans_raw.png new file mode 100644 index 00000000..8ada75ef Binary files /dev/null and b/assets/screenshots/docs/serve-scans_raw.png differ diff --git a/assets/screenshots/docs/serve-triage.png b/assets/screenshots/docs/serve-triage.png new file mode 100644 index 00000000..4c8e91e7 Binary files /dev/null and b/assets/screenshots/docs/serve-triage.png differ diff --git a/assets/screenshots/docs/serve-triage_raw.png b/assets/screenshots/docs/serve-triage_raw.png new file mode 100644 index 00000000..0b72b704 Binary files /dev/null and b/assets/screenshots/docs/serve-triage_raw.png differ diff --git a/assets/screenshots/explorer.png b/assets/screenshots/explorer.png new file mode 100644 index 00000000..a83f2f59 Binary files /dev/null and b/assets/screenshots/explorer.png differ diff --git a/assets/screenshots/finding-detail.png b/assets/screenshots/finding-detail.png new file mode 100644 index 00000000..ec9459d0 Binary files /dev/null and b/assets/screenshots/finding-detail.png differ diff --git a/assets/screenshots/overview.png b/assets/screenshots/overview.png new file mode 100644 index 00000000..4d86004f Binary files /dev/null and b/assets/screenshots/overview.png differ diff --git a/assets/screenshots/triage.png b/assets/screenshots/triage.png new file mode 100644 index 00000000..e711acd8 Binary files /dev/null and b/assets/screenshots/triage.png differ diff --git a/benches/dynamic_bench.rs b/benches/dynamic_bench.rs new file mode 100644 index 00000000..34fec934 --- /dev/null +++ b/benches/dynamic_bench.rs @@ -0,0 +1,686 @@ +//! Dynamic verification benchmarks (§8.4). +//! +//! Tracks the per-scan cost anchors: +//! +//! 1. `harness_build_cold` — fresh workdir, spec → BuiltHarness (source gen + disk write). +//! 2. `harness_build_warm` — same spec, workdir already staged (file write skipped). +//! 3. `sandbox_run_payload` — single payload run via process backend against +//! sqli_positive.py (subprocess + settrace overhead, no networking). +//! 4. `docker_image_build` — cold image pull/build for the python:3-slim base. +//! 5. `docker_exec_warm` — `docker exec` into a running container (no cold start). +//! 6. `docker_payload_cost` — per-payload sandbox cost via docker backend end-to-end. +//! 7. `composite_chain_reverify_dispatch` — `reverify_top_chains` on a +//! synthetic 3-member chain with no member diags. Measures the no-derive +//! dispatch path (chain_step_specs miss, early-exit build/run loops, +//! Inconclusive verdict allocation, severity downgrade). +//! 8. `composite_chain_reverify_stub_confirmed` — same chain shape, stubbed +//! reverifier returning `Confirmed`. Measures the apply-verdict happy path +//! (no severity bucket change). +//! 9. `composite_chain_reverify_top_n_slice` — 5-chain slice with `top_n=3`. +//! Measures the slice traversal cost so a regression that walks the full +//! slice instead of the prefix is visible. +//! 10. `composite_chain_reverify_replay_stable` — same chain shape as +//! `stub_confirmed`, but with `VerifyOptions::replay_stable_check=true` +//! and a stub that stamps `replay_stable=Some(true)`. Anchors the +//! apply-verdict allocation cost when the telemetry stability field +//! is populated; a regression that adds per-chain work behind the +//! replay opt-in (e.g. an extra run_chain_steps call leaking out of +//! the live path into the stub layer) shows up here. +//! +//! Wall-clock budget anchors for the composite reverify path: the live +//! process backend stays under 400ms per 3-member chain, the docker +//! backend under 1500ms. Those live-run numbers are covered by the +//! `flask_eval_chain_reverify_populates_dynamic_verdict` integration +//! test in `tests/chain_emission_e2e.rs`; the microbenches here anchor +//! the dispatch + verdict-application overhead so regressions on the +//! API-shape half land in the criterion baseline. +//! +//! Baselines committed to `benches/dynamic_bench_baseline.json`. +//! Run: `cargo bench --features dynamic -- dynamic` +//! +//! Docker benchmarks are no-ops when docker is unavailable (skipped, not failed). + +use criterion::{Criterion, criterion_group, criterion_main}; + +#[cfg(feature = "dynamic")] +use nyx_scanner::dynamic::spec::{ + EntryKind, HarnessSpec, JavaToolchain, PayloadSlot, SpecDerivationStrategy, +}; +#[cfg(feature = "dynamic")] +use nyx_scanner::labels::Cap; +#[cfg(feature = "dynamic")] +use nyx_scanner::symbol::Lang; + +#[cfg(feature = "dynamic")] +fn make_rust_sqli_spec() -> HarnessSpec { + HarnessSpec { + finding_id: "bench_rust_0001".into(), + entry_file: "tests/dynamic_fixtures/rust/sqli_positive.rs".into(), + entry_name: "run".into(), + entry_kind: nyx_scanner::dynamic::spec::EntryKind::Function, + lang: Lang::Rust, + toolchain_id: "rust-stable".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::SQL_QUERY, + constraint_hints: vec![], + sink_file: "tests/dynamic_fixtures/rust/sqli_positive.rs".into(), + sink_line: 18, + spec_hash: "benchrustsqli0001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + } +} + +#[cfg(feature = "dynamic")] +fn make_sqli_spec() -> HarnessSpec { + HarnessSpec { + finding_id: "bench0000000001".into(), + entry_file: "tests/dynamic_fixtures/python/sqli_positive.py".into(), + entry_name: "login".into(), + entry_kind: EntryKind::Function, + lang: Lang::Python, + toolchain_id: "python-3".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::SQL_QUERY, + constraint_hints: vec![], + sink_file: "tests/dynamic_fixtures/python/sqli_positive.py".into(), + sink_line: 7, + spec_hash: "benchsqli000001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + } +} + +#[cfg(feature = "dynamic")] +fn bench_harness_build_cold(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_sqli_spec(); + c.bench_function("harness_build_cold", |b| { + b.iter(|| { + let workdir = std::env::temp_dir() + .join("nyx-harness") + .join(&spec.spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + harness::build(&spec).expect("harness build") + }); + }); +} + +#[cfg(feature = "dynamic")] +fn bench_harness_build_warm(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_sqli_spec(); + harness::build(&spec).expect("harness pre-stage"); + c.bench_function("harness_build_warm", |b| { + b.iter(|| harness::build(&spec).expect("harness build warm")); + }); +} + +#[cfg(feature = "dynamic")] +fn bench_sandbox_run_payload(c: &mut Criterion) { + use nyx_scanner::dynamic::corpus::payloads_for; + use nyx_scanner::dynamic::harness; + use nyx_scanner::dynamic::sandbox::{self, SandboxOptions}; + + let spec = make_sqli_spec(); + let harness = harness::build(&spec).expect("harness build"); + let payloads = payloads_for(Cap::SQL_QUERY); + let payload = payloads + .iter() + .find(|p| !p.is_benign) + .expect("sqli payload"); + let opts = SandboxOptions { + timeout: std::time::Duration::from_secs(10), + ..SandboxOptions::default() + }; + + c.bench_function("sandbox_run_payload", |b| { + b.iter(|| sandbox::run(&harness, payload.bytes, &opts).expect("sandbox run")); + }); +} + +#[cfg(feature = "dynamic")] +fn docker_available() -> bool { + std::process::Command::new("docker") + .arg("info") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +/// Cold docker image pull/build. +/// +/// Measures the time to ensure `python:3-slim` is present locally. On a +/// warm cache this is just an inspect call (sub-second). On a cold host it +/// includes the pull from the registry. +/// +/// Registers a labelled noop measurement when Docker is absent so criterion's +/// output is never empty for this slot. +#[cfg(feature = "dynamic")] +fn bench_docker_image_build(c: &mut Criterion) { + if !docker_available() { + c.bench_function("docker_image_build_no_docker", |b| b.iter(|| ())); + return; + } + c.bench_function("docker_image_build", |b| { + b.iter(|| { + // `docker pull` is idempotent and fast when image is already local. + let _ = std::process::Command::new("docker") + .args(["pull", "python:3-slim"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + }); + }); +} + +/// Warm `docker exec` reuse benchmark. +/// +/// Starts a single container before the benchmark loop and measures the cost +/// of each `docker exec` call (no cold-start amortisation visible here — that +/// is visible by comparing this vs `bench_docker_payload_cost`). +#[cfg(feature = "dynamic")] +fn bench_docker_exec_warm(c: &mut Criterion) { + if !docker_available() { + eprintln!("bench_docker_exec_warm: docker unavailable, skipping"); + return; + } + // Start a long-lived container for the benchmark. + let container = "nyx-bench-exec-warm"; + let _ = std::process::Command::new("docker") + .args([ + "run", + "-d", + "--rm", + "--name", + container, + "--cap-drop=ALL", + "--security-opt", + "no-new-privileges:true", + "--network", + "none", + "python:3-slim", + "sleep", + "300", + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + + c.bench_function("docker_exec_warm", |b| { + b.iter(|| { + let _ = std::process::Command::new("docker") + .args(["exec", container, "python3", "-c", "pass"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + }); + }); + + let _ = std::process::Command::new("docker") + .args(["stop", container]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); +} + +/// Per-payload sandbox cost via docker backend end-to-end. +/// +/// Measures the complete path: harness already built + docker backend + +/// process the sqli_positive fixture. The first call includes container +/// start; subsequent calls show exec-reuse cost. +/// +/// Registers a labelled noop measurement when Docker is absent so criterion's +/// output is never empty for this slot. +#[cfg(feature = "dynamic")] +fn bench_docker_payload_cost(c: &mut Criterion) { + if !docker_available() { + c.bench_function("docker_payload_cost_no_docker", |b| b.iter(|| ())); + return; + } + use nyx_scanner::dynamic::corpus::payloads_for; + use nyx_scanner::dynamic::harness; + use nyx_scanner::dynamic::sandbox::{self, SandboxBackend, SandboxOptions}; + + let spec = make_sqli_spec(); + let built = harness::build(&spec).expect("harness build"); + let payloads = payloads_for(Cap::SQL_QUERY); + let payload = payloads + .iter() + .find(|p| !p.is_benign) + .expect("sqli payload"); + let opts = SandboxOptions { + timeout: std::time::Duration::from_secs(30), + backend: SandboxBackend::Docker, + ..SandboxOptions::default() + }; + + c.bench_function("docker_payload_cost", |b| { + b.iter(|| { + let _ = sandbox::run(&built, payload.bytes, &opts); + }); + }); +} + +/// Rust harness build (source gen + disk write, no compilation). +/// +/// Measures only `harness::build()` — staging files to the workdir. +/// The expensive `cargo build --release` step is NOT included here +/// (that is the province of an integration benchmark, not this microbench). +#[cfg(feature = "dynamic")] +fn bench_rust_harness_build_cold(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_rust_sqli_spec(); + c.bench_function("rust_harness_build_cold", |b| { + b.iter(|| { + let workdir = std::env::temp_dir() + .join("nyx-harness") + .join(&spec.spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + harness::build(&spec).expect("harness build") + }); + }); +} + +#[cfg(feature = "dynamic")] +fn make_js_sqli_spec() -> HarnessSpec { + HarnessSpec { + finding_id: "bench_js_0001".into(), + entry_file: "tests/dynamic_fixtures/js/sqli_positive.js".into(), + entry_name: "login".into(), + entry_kind: nyx_scanner::dynamic::spec::EntryKind::Function, + lang: Lang::JavaScript, + toolchain_id: "node-20".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::SQL_QUERY, + constraint_hints: vec![], + sink_file: "tests/dynamic_fixtures/js/sqli_positive.js".into(), + sink_line: 8, + spec_hash: "benchjssqli000001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + } +} + +#[cfg(feature = "dynamic")] +fn make_go_sqli_spec() -> HarnessSpec { + HarnessSpec { + finding_id: "bench_go_0001".into(), + entry_file: "tests/dynamic_fixtures/go/sqli_positive.go".into(), + entry_name: "Login".into(), + entry_kind: nyx_scanner::dynamic::spec::EntryKind::Function, + lang: Lang::Go, + toolchain_id: "go-1.21".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::SQL_QUERY, + constraint_hints: vec![], + sink_file: "tests/dynamic_fixtures/go/sqli_positive.go".into(), + sink_line: 12, + spec_hash: "benchgosqli000001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + } +} + +#[cfg(feature = "dynamic")] +fn make_java_sqli_spec() -> HarnessSpec { + HarnessSpec { + finding_id: "bench_java_0001".into(), + entry_file: "tests/dynamic_fixtures/java/sqli_positive.java".into(), + entry_name: "login".into(), + entry_kind: nyx_scanner::dynamic::spec::EntryKind::Function, + lang: Lang::Java, + toolchain_id: "java-21".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::SQL_QUERY, + constraint_hints: vec![], + sink_file: "tests/dynamic_fixtures/java/sqli_positive.java".into(), + sink_line: 9, + spec_hash: "benchjavasqli00001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + } +} + +#[cfg(feature = "dynamic")] +fn make_php_sqli_spec() -> HarnessSpec { + HarnessSpec { + finding_id: "bench_php_0001".into(), + entry_file: "tests/dynamic_fixtures/php/sqli_positive.php".into(), + entry_name: "login".into(), + entry_kind: nyx_scanner::dynamic::spec::EntryKind::Function, + lang: Lang::Php, + toolchain_id: "php-8".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::SQL_QUERY, + constraint_hints: vec![], + sink_file: "tests/dynamic_fixtures/php/sqli_positive.php".into(), + sink_line: 9, + spec_hash: "benchphpsqli000001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + } +} + +/// JS harness build (source gen + disk write). +#[cfg(feature = "dynamic")] +fn bench_js_harness_build_cold(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_js_sqli_spec(); + c.bench_function("js_harness_build_cold", |b| { + b.iter(|| { + let workdir = std::env::temp_dir() + .join("nyx-harness") + .join(&spec.spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + harness::build(&spec).expect("JS harness build") + }); + }); +} + +/// Go harness build (source gen + disk write, no compilation). +#[cfg(feature = "dynamic")] +fn bench_go_harness_build_cold(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_go_sqli_spec(); + c.bench_function("go_harness_build_cold", |b| { + b.iter(|| { + let workdir = std::env::temp_dir() + .join("nyx-harness") + .join(&spec.spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + harness::build(&spec).expect("Go harness build") + }); + }); +} + +/// Java harness build (source gen + disk write, no compilation). +#[cfg(feature = "dynamic")] +fn bench_java_harness_build_cold(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_java_sqli_spec(); + c.bench_function("java_harness_build_cold", |b| { + b.iter(|| { + let workdir = std::env::temp_dir() + .join("nyx-harness") + .join(&spec.spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + harness::build(&spec).expect("Java harness build") + }); + }); +} + +/// PHP harness build (source gen + disk write). +#[cfg(feature = "dynamic")] +fn bench_php_harness_build_cold(c: &mut Criterion) { + use nyx_scanner::dynamic::harness; + let spec = make_php_sqli_spec(); + c.bench_function("php_harness_build_cold", |b| { + b.iter(|| { + let workdir = std::env::temp_dir() + .join("nyx-harness") + .join(&spec.spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + harness::build(&spec).expect("PHP harness build") + }); + }); +} + +#[cfg(feature = "dynamic")] +fn mk_chain_member(hash: u64, idx: usize) -> nyx_scanner::chain::FindingRef { + use nyx_scanner::surface::SourceLocation; + nyx_scanner::chain::FindingRef { + finding_id: format!("bench-chain-member-{idx}"), + stable_hash: hash, + location: SourceLocation::new("bench/synthetic.py", (idx as u32) + 1, 1), + rule_id: "taint-unsanitised-flow".into(), + cap_bits: 0, + } +} + +#[cfg(feature = "dynamic")] +fn mk_synthetic_chain(hash: u64, members: usize) -> nyx_scanner::chain::ChainFinding { + use nyx_scanner::chain::{ChainFinding, ChainSeverity, ChainSink, ImpactCategory}; + ChainFinding { + stable_hash: hash, + members: (0..members) + .map(|i| mk_chain_member(hash.wrapping_add(i as u64 + 1), i)) + .collect(), + sink: ChainSink { + file: "bench/synthetic.py".into(), + line: 99, + col: 1, + function_name: "sink".into(), + cap_bits: 0, + }, + implied_impact: ImpactCategory::Rce, + severity: ChainSeverity::Critical, + score: 100.0, + dynamic_verdict: None, + reverify_reason: None, + } +} + +#[cfg(feature = "dynamic")] +struct BenchConfirmedReverifier; + +#[cfg(feature = "dynamic")] +impl nyx_scanner::chain::CompositeReverifier for BenchConfirmedReverifier { + fn reverify( + &self, + _chain: &nyx_scanner::chain::ChainFinding, + _member_diags: &[nyx_scanner::commands::scan::Diag], + _surface: &nyx_scanner::surface::SurfaceMap, + opts: &nyx_scanner::dynamic::verify::VerifyOptions, + ) -> nyx_scanner::evidence::VerifyResult { + // Mirror `DefaultCompositeReverifier::reverify`'s replay-stable + // stamping shape so the apply-verdict allocation cost matches + // the live path when the opt-in is on. The stub does not + // re-run any work (it has none to re-run) but the resulting + // `VerifyResult` populates `replay_stable=Some(true)` so + // downstream sites that branch on the field exercise the same + // path they would for a real Confirmed-with-stable run. + let replay_stable = if opts.replay_stable_check { + Some(true) + } else { + None + }; + nyx_scanner::evidence::VerifyResult { + finding_id: "bench".into(), + status: nyx_scanner::evidence::VerifyStatus::Confirmed, + triggered_payload: None, + reason: None, + inconclusive_reason: None, + detail: None, + attempts: vec![], + toolchain_match: None, + differential: None, + replay_stable, + wrong: None, + hardening_outcome: None, + } + } +} + +/// Phase 26 dispatch-cost anchor: synthetic 3-member chain with no +/// matching member diags. The reverifier walks chain_step_specs (3 +/// HashMap misses → 3 NoFlowSteps errors), the build loop sees zero +/// derived specs and exits early, the run loop sees zero built steps +/// and exits early. The composed VerifyResult is allocated and applied +/// via `apply_dynamic_verdict` (Inconclusive → severity downgrade). +/// +/// This is the no-toolchain-dep dispatch overhead — a regression here +/// signals a hot-path allocation introduced into the reverify pipeline. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_dispatch(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions::default(); + + c.bench_function("composite_chain_reverify_dispatch", |b| { + b.iter(|| { + let mut chains = [mk_synthetic_chain(0xC1A1, 3)]; + let _ = reverify::reverify_top_chains(&mut chains, &[], &surface, &opts, 1); + }); + }); +} + +/// Phase 26 stub-reverifier happy-path anchor: synthetic 3-member +/// chain driven through `reverify_top_chains_with` + a stubbed +/// reverifier returning `Confirmed`. Measures the apply-verdict path +/// when the verdict does NOT trigger a severity downgrade, so the +/// `ChainReverifyResult` allocation + `chain.apply_dynamic_verdict` +/// transition cost is exercised independent of the verdict-side +/// allocation in the dispatch bench. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_stub_confirmed(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions::default(); + let reverifier = BenchConfirmedReverifier; + + c.bench_function("composite_chain_reverify_stub_confirmed", |b| { + b.iter(|| { + let mut chains = [mk_synthetic_chain(0xC2A2, 3)]; + let _ = reverify::reverify_top_chains_with( + &mut chains, + &[], + &surface, + &opts, + 1, + &reverifier, + ); + }); + }); +} + +/// Phase 26 top-N slice anchor: 5-chain slice with `top_n=3`. Asserts +/// (by way of regression) that the reverify pass never walks past the +/// top-N prefix. The fan-in is the per-chain dispatch cost times three; +/// a regression that drops the `bound = top_n.min(chains.len())` cap +/// would show up as a ~5/3 increase in this bench. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_top_n_slice(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions::default(); + let reverifier = BenchConfirmedReverifier; + + c.bench_function("composite_chain_reverify_top_n_slice", |b| { + b.iter(|| { + let mut chains: [nyx_scanner::chain::ChainFinding; 5] = [ + mk_synthetic_chain(0xC301, 3), + mk_synthetic_chain(0xC302, 3), + mk_synthetic_chain(0xC303, 3), + mk_synthetic_chain(0xC304, 3), + mk_synthetic_chain(0xC305, 3), + ]; + let _ = reverify::reverify_top_chains_with( + &mut chains, + &[], + &surface, + &opts, + 3, + &reverifier, + ); + }); + }); +} + +/// Phase 26 replay-stable anchor: same 3-member synthetic chain as +/// `stub_confirmed`, driven through `reverify_top_chains_with` with +/// `VerifyOptions::replay_stable_check=true`. The `BenchConfirmedReverifier` +/// stub honours the opt-in by stamping `replay_stable=Some(true)` on +/// the returned `VerifyResult`, exercising the apply-verdict path with +/// the telemetry stability field populated. +/// +/// Purpose: anchor the cost of the replay-stable apply path so a +/// regression that leaks a real `run_chain_steps` invocation into the +/// stubbed verifier layer (or that allocates extra state behind the +/// `replay_stable_check` toggle in `chain::reverify::apply_one`) shows +/// up immediately against the `stub_confirmed` baseline. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_replay_stable(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions { + replay_stable_check: true, + ..VerifyOptions::default() + }; + let reverifier = BenchConfirmedReverifier; + + c.bench_function("composite_chain_reverify_replay_stable", |b| { + b.iter(|| { + let mut chains = [mk_synthetic_chain(0xC4A3, 3)]; + let _ = reverify::reverify_top_chains_with( + &mut chains, + &[], + &surface, + &opts, + 1, + &reverifier, + ); + }); + }); +} + +#[cfg(feature = "dynamic")] +#[allow(dead_code)] +fn bench_noop(_c: &mut Criterion) {} + +// When dynamic feature is off, provide a stub so the binary still links. +#[cfg(not(feature = "dynamic"))] +fn bench_noop(c: &mut Criterion) { + c.bench_function("dynamic_disabled_noop", |b| b.iter(|| ())); +} + +#[cfg(feature = "dynamic")] +criterion_group!( + dynamic, + bench_harness_build_cold, + bench_harness_build_warm, + bench_sandbox_run_payload, + bench_docker_image_build, + bench_docker_exec_warm, + bench_docker_payload_cost, + bench_rust_harness_build_cold, + bench_js_harness_build_cold, + bench_go_harness_build_cold, + bench_java_harness_build_cold, + bench_php_harness_build_cold, + bench_composite_chain_reverify_dispatch, + bench_composite_chain_reverify_stub_confirmed, + bench_composite_chain_reverify_top_n_slice, + bench_composite_chain_reverify_replay_stable, +); + +#[cfg(not(feature = "dynamic"))] +criterion_group!(dynamic, bench_noop); + +criterion_main!(dynamic); diff --git a/benches/dynamic_bench_baseline.json b/benches/dynamic_bench_baseline.json new file mode 100644 index 00000000..3a5985cf --- /dev/null +++ b/benches/dynamic_bench_baseline.json @@ -0,0 +1,26 @@ +{ + "schema": 1, + "note": "ASPIRATIONAL placeholder — values were hand-typed, not captured from a real bench run. Regenerate with: benches/regen_baseline.sh (requires --features dynamic and python3 on PATH). Commit the updated file to establish a real regression reference for M3+.", + "benchmarks": { + "harness_build_cold": { + "mean_ns": 800000, + "stddev_ns": 120000, + "description": "Fresh workdir; spec → BuiltHarness including source gen + disk write." + }, + "harness_build_warm": { + "mean_ns": 180000, + "stddev_ns": 30000, + "description": "Workdir already staged; file write skipped by dst.exists() guard." + }, + "sandbox_run_payload": { + "mean_ns": 120000000, + "stddev_ns": 15000000, + "description": "Single process-backend run with sqli payload; includes python3 startup + settrace." + } + }, + "regression_thresholds": { + "harness_build_cold": 2.0, + "harness_build_warm": 2.0, + "sandbox_run_payload": 1.5 + } +} diff --git a/benches/fixtures/sample.c b/benches/fixtures/sample.c new file mode 100644 index 00000000..bac1257d --- /dev/null +++ b/benches/fixtures/sample.c @@ -0,0 +1,31 @@ +#include +#include +#include + +char* get_env_value(void) { + return getenv("SECRET"); +} + +void execute_command(const char* cmd) { + system(cmd); +} + +void safe_flow(void) { + char* val = get_env_value(); + if (val != NULL) { + printf("Value: %s\n", val); + } +} + +void unsafe_flow(void) { + char* val = get_env_value(); + if (val != NULL) { + execute_command(val); + } +} + +int main(void) { + safe_flow(); + unsafe_flow(); + return 0; +} diff --git a/benches/fixtures/sample.cpp b/benches/fixtures/sample.cpp new file mode 100644 index 00000000..9a1c16e8 --- /dev/null +++ b/benches/fixtures/sample.cpp @@ -0,0 +1,28 @@ +#include +#include +#include + +std::string get_env_value() { + const char* val = std::getenv("APP_SECRET"); + return val ? std::string(val) : ""; +} + +void execute_command(const std::string& cmd) { + std::system(cmd.c_str()); +} + +void safe_flow() { + std::string val = get_env_value(); + std::cout << "Value: " << val << std::endl; +} + +void unsafe_flow() { + std::string val = get_env_value(); + execute_command(val); +} + +int main() { + safe_flow(); + unsafe_flow(); + return 0; +} diff --git a/benches/fixtures/sample.go b/benches/fixtures/sample.go new file mode 100644 index 00000000..26cebc3b --- /dev/null +++ b/benches/fixtures/sample.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "html" +) + +func getEnv() string { + return os.Getenv("APP_SECRET") +} + +func sanitizeHTML(input string) string { + return html.EscapeString(input) +} + +func runCommand(cmd string) { + exec.Command("sh", "-c", cmd).Run() +} + +func safeFlow() { + val := getEnv() + clean := sanitizeHTML(val) + fmt.Println(clean) +} + +func unsafeFlow() { + val := getEnv() + runCommand(val) +} + +func main() { + safeFlow() + unsafeFlow() +} diff --git a/benches/fixtures/sample.java b/benches/fixtures/sample.java new file mode 100644 index 00000000..60b1e65c --- /dev/null +++ b/benches/fixtures/sample.java @@ -0,0 +1,31 @@ +import java.io.IOException; + +public class Sample { + public static String getEnv() { + return System.getenv("DB_PASSWORD"); + } + + public static String sanitize(String input) { + return input.replaceAll("[<>&]", ""); + } + + public static void executeCommand(String cmd) throws IOException { + Runtime.getRuntime().exec(cmd); + } + + public static void safeFlow() throws IOException { + String val = getEnv(); + String clean = sanitize(val); + System.out.println(clean); + } + + public static void unsafeFlow() throws IOException { + String val = getEnv(); + executeCommand(val); + } + + public static void main(String[] args) throws IOException { + safeFlow(); + unsafeFlow(); + } +} diff --git a/benches/fixtures/sample.js b/benches/fixtures/sample.js new file mode 100644 index 00000000..6f81836e --- /dev/null +++ b/benches/fixtures/sample.js @@ -0,0 +1,35 @@ +const { execSync } = require("child_process"); + +function getUserInput() { + return process.env.USER_INPUT || ""; +} + +function sanitizeHtml(input) { + return input.replace(/[<>&"']/g, ""); +} + +function renderPage(data) { + document.innerHTML = data; +} + +function safeRender() { + const input = getUserInput(); + const clean = sanitizeHtml(input); + renderPage(clean); +} + +function unsafeRender() { + const input = getUserInput(); + renderPage(input); +} + +function runShell(cmd) { + execSync(cmd); +} + +function unsafeExec() { + const input = getUserInput(); + runShell(input); +} + +module.exports = { safeRender, unsafeRender, unsafeExec }; diff --git a/benches/fixtures/sample.php b/benches/fixtures/sample.php new file mode 100644 index 00000000..99774b8e --- /dev/null +++ b/benches/fixtures/sample.php @@ -0,0 +1,27 @@ + String { + env::var("APP_CONFIG").unwrap_or_default() +} + +fn sanitize_shell(input: &str) -> String { + shell_escape::unix::escape(input.into()).to_string() +} + +fn run_command(cmd: &str) { + Command::new("sh") + .arg("-c") + .arg(cmd) + .status() + .expect("failed to execute"); +} + +fn safe_run() { + let config = get_config(); + let clean = sanitize_shell(&config); + run_command(&clean); +} + +fn unsafe_run() { + let config = get_config(); + run_command(&config); +} + +fn main() { + safe_run(); + unsafe_run(); +} diff --git a/benches/fixtures/sample.ts b/benches/fixtures/sample.ts new file mode 100644 index 00000000..7ab5891f --- /dev/null +++ b/benches/fixtures/sample.ts @@ -0,0 +1,30 @@ +import { execSync } from "child_process"; + +function getUserInput(): string { + return process.env.USER_INPUT || ""; +} + +function sanitizeHtml(input: string): string { + return input.replace(/[<>&"']/g, ""); +} + +function renderPage(data: string): void { + document.body.innerHTML = data; +} + +function runCommand(cmd: string): void { + execSync(cmd); +} + +function safeRender(): void { + const input = getUserInput(); + const clean = sanitizeHtml(input); + renderPage(clean); +} + +function unsafeExec(): void { + const input = getUserInput(); + runCommand(input); +} + +export { safeRender, unsafeExec }; diff --git a/benches/fixtures/state_bench.c b/benches/fixtures/state_bench.c new file mode 100644 index 00000000..fe6339ce --- /dev/null +++ b/benches/fixtures/state_bench.c @@ -0,0 +1,61 @@ +#include +#include +#include + +/* Clean open/close — no findings expected */ +void clean_usage(void) { + FILE *f = fopen("data.txt", "r"); + char buf[256]; + fread(buf, 1, 256, f); + fclose(f); +} + +/* Resource leak — fopen without fclose */ +void leaky_function(void) { + FILE *f = fopen("log.txt", "w"); + fprintf(f, "hello"); +} + +/* Use after close */ +void use_after_close(void) { + FILE *f = fopen("tmp.txt", "r"); + fclose(f); + char buf[64]; + fread(buf, 1, 64, f); +} + +/* Branch leak — closed on one path only */ +void branch_leak(int cond) { + FILE *f = fopen("x.txt", "r"); + if (cond) { + fclose(f); + } +} + +/* Multiple handles — both properly closed */ +void multi_handle(void) { + FILE *a = fopen("a.txt", "r"); + FILE *b = fopen("b.txt", "w"); + fclose(a); + fclose(b); +} + +/* Double close */ +void double_close(void) { + FILE *f = fopen("d.txt", "r"); + fclose(f); + fclose(f); +} + +/* Malloc/free — clean */ +void malloc_clean(void) { + char *p = malloc(1024); + memset(p, 0, 1024); + free(p); +} + +/* Malloc leak — never freed */ +void malloc_leak(void) { + char *p = malloc(512); + memset(p, 0, 512); +} diff --git a/benches/perf_fixtures/large_go_module.go b/benches/perf_fixtures/large_go_module.go new file mode 100644 index 00000000..04dcb36c --- /dev/null +++ b/benches/perf_fixtures/large_go_module.go @@ -0,0 +1,1493 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. +// +// Source: gin-gonic/gin context.go (MIT-licensed); copied verbatim as a +// realistic perfhunt fixture covering many function bodies (~147 fns, +// ~1.5k lines) so per-body analysis caching is exercised at scale. + +package gin + +import ( + "errors" + "fmt" + "io" + "io/fs" + "log" + "maps" + "math" + "mime/multipart" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/gin-contrib/sse" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" +) + +// Content-Type MIME of the most common data formats. +const ( + MIMEJSON = binding.MIMEJSON + MIMEHTML = binding.MIMEHTML + MIMEXML = binding.MIMEXML + MIMEXML2 = binding.MIMEXML2 + MIMEPlain = binding.MIMEPlain + MIMEPOSTForm = binding.MIMEPOSTForm + MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm + MIMEYAML = binding.MIMEYAML + MIMEYAML2 = binding.MIMEYAML2 + MIMETOML = binding.MIMETOML + MIMEPROTOBUF = binding.MIMEPROTOBUF + MIMEBSON = binding.MIMEBSON +) + +// BodyBytesKey indicates a default body bytes key. +const BodyBytesKey = "_gin-gonic/gin/bodybyteskey" + +// ContextKey is the key that a Context returns itself for. +const ContextKey = "_gin-gonic/gin/contextkey" + +type ContextKeyType int + +const ContextRequestKey ContextKeyType = 0 + +// abortIndex represents a typical value used in abort functions. +const abortIndex int8 = math.MaxInt8 >> 1 + +// Context is the most important part of gin. It allows us to pass variables between middleware, +// manage the flow, validate the JSON of a request and render a JSON response for example. +type Context struct { + writermem responseWriter + Request *http.Request + Writer ResponseWriter + + Params Params + handlers HandlersChain + index int8 + fullPath string + + engine *Engine + params *Params + skippedNodes *[]skippedNode + + // This mutex protects Keys map. + mu sync.RWMutex + + // Keys is a key/value pair exclusively for the context of each request. + Keys map[any]any + + // Errors is a list of errors attached to all the handlers/middlewares who used this context. + Errors errorMsgs + + // Accepted defines a list of manually accepted formats for content negotiation. + Accepted []string + + // queryCache caches the query result from c.Request.URL.Query(). + queryCache url.Values + + // formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH, + // or PUT body parameters. + formCache url.Values + + // SameSite allows a server to define a cookie attribute making it impossible for + // the browser to send this cookie along with cross-site requests. + sameSite http.SameSite +} + +/************************************/ +/********** CONTEXT CREATION ********/ +/************************************/ + +func (c *Context) reset() { + c.Writer = &c.writermem + c.Params = c.Params[:0] + c.handlers = nil + c.index = -1 + + c.fullPath = "" + c.Keys = nil + c.Errors = c.Errors[:0] + c.Accepted = nil + c.queryCache = nil + c.formCache = nil + c.sameSite = 0 + *c.params = (*c.params)[:0] + *c.skippedNodes = (*c.skippedNodes)[:0] +} + +// Copy returns a copy of the current context that can be safely used outside the request's scope. +// This has to be used when the context has to be passed to a goroutine. +func (c *Context) Copy() *Context { + cp := Context{ + writermem: c.writermem, + Request: c.Request, + engine: c.engine, + } + + cp.writermem.ResponseWriter = nil + cp.Writer = &cp.writermem + cp.index = abortIndex + cp.handlers = nil + cp.fullPath = c.fullPath + + cKeys := c.Keys + c.mu.RLock() + cp.Keys = maps.Clone(cKeys) + c.mu.RUnlock() + + cParams := c.Params + cp.Params = make([]Param, len(cParams)) + copy(cp.Params, cParams) + + return &cp +} + +// HandlerName returns the main handler's name. For example if the handler is "handleGetUsers()", +// this function will return "main.handleGetUsers". +func (c *Context) HandlerName() string { + return nameOfFunction(c.handlers.Last()) +} + +// HandlerNames returns a list of all registered handlers for this context in descending order, +// following the semantics of HandlerName() +func (c *Context) HandlerNames() []string { + hn := make([]string, 0, len(c.handlers)) + for _, val := range c.handlers { + if val == nil { + continue + } + hn = append(hn, nameOfFunction(val)) + } + return hn +} + +// Handler returns the main handler. +func (c *Context) Handler() HandlerFunc { + return c.handlers.Last() +} + +// FullPath returns a matched route full path. For not found routes +// returns an empty string. +// +// router.GET("/user/:id", func(c *gin.Context) { +// c.FullPath() == "/user/:id" // true +// }) +func (c *Context) FullPath() string { + return c.fullPath +} + +/************************************/ +/*********** FLOW CONTROL ***********/ +/************************************/ + +// Next should be used only inside middleware. +// It executes the pending handlers in the chain inside the calling handler. +// See example in GitHub. +func (c *Context) Next() { + c.index++ + for c.index < safeInt8(len(c.handlers)) { + if c.handlers[c.index] != nil { + c.handlers[c.index](c) + } + c.index++ + } +} + +// IsAborted returns true if the current context was aborted. +func (c *Context) IsAborted() bool { + return c.index >= abortIndex +} + +// Abort prevents pending handlers from being called. Note that this will not stop the current handler. +// Let's say you have an authorization middleware that validates that the current request is authorized. +// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers +// for this request are not called. +func (c *Context) Abort() { + c.index = abortIndex +} + +// AbortWithStatus calls `Abort()` and writes the headers with the specified status code. +// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401). +func (c *Context) AbortWithStatus(code int) { + c.Status(code) + c.Writer.WriteHeaderNow() + c.Abort() +} + +// AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally. +// This method stops the chain, writes the status code and return a JSON body without escaping. +// It also sets the Content-Type as "application/json". +func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) { + c.Abort() + c.PureJSON(code, jsonObj) +} + +// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. +// This method stops the chain, writes the status code and return a JSON body. +// It also sets the Content-Type as "application/json". +func (c *Context) AbortWithStatusJSON(code int, jsonObj any) { + c.Abort() + c.JSON(code, jsonObj) +} + +// AbortWithError calls `AbortWithStatus()` and `Error()` internally. +// This method stops the chain, writes the status code and pushes the specified error to `c.Errors`. +// See Context.Error() for more details. +func (c *Context) AbortWithError(code int, err error) *Error { + c.AbortWithStatus(code) + return c.Error(err) +} + +/************************************/ +/********* ERROR MANAGEMENT *********/ +/************************************/ + +// Error attaches an error to the current context. The error is pushed to a list of errors. +// It's a good idea to call Error for each error that occurred during the resolution of a request. +// A middleware can be used to collect all the errors and push them to a database together, +// print a log, or append it in the HTTP response. +// Error will panic if err is nil. +func (c *Context) Error(err error) *Error { + if err == nil { + panic("err is nil") + } + + var parsedError *Error + ok := errors.As(err, &parsedError) + if !ok { + parsedError = &Error{ + Err: err, + Type: ErrorTypePrivate, + } + } + + c.Errors = append(c.Errors, parsedError) + return parsedError +} + +/************************************/ +/******** METADATA MANAGEMENT********/ +/************************************/ + +// Set is used to store a new key/value pair exclusively for this context. +// It also lazy initializes c.Keys if it was not used previously. +func (c *Context) Set(key any, value any) { + c.mu.Lock() + defer c.mu.Unlock() + if c.Keys == nil { + c.Keys = make(map[any]any) + } + + c.Keys[key] = value +} + +// Get returns the value for the given key, ie: (value, true). +// If the value does not exist it returns (nil, false) +func (c *Context) Get(key any) (value any, exists bool) { + c.mu.RLock() + defer c.mu.RUnlock() + value, exists = c.Keys[key] + return +} + +// MustGet returns the value for the given key if it exists, otherwise it panics. +func (c *Context) MustGet(key any) any { + if value, exists := c.Get(key); exists { + return value + } + panic(fmt.Sprintf("key %v does not exist", key)) +} + +func getTyped[T any](c *Context, key any) (res T) { + if val, ok := c.Get(key); ok && val != nil { + res, _ = val.(T) + } + return +} + +// GetString returns the value associated with the key as a string. +func (c *Context) GetString(key any) string { + return getTyped[string](c, key) +} + +// GetBool returns the value associated with the key as a boolean. +func (c *Context) GetBool(key any) bool { + return getTyped[bool](c, key) +} + +// GetInt returns the value associated with the key as an integer. +func (c *Context) GetInt(key any) int { + return getTyped[int](c, key) +} + +// GetInt8 returns the value associated with the key as an integer 8. +func (c *Context) GetInt8(key any) int8 { + return getTyped[int8](c, key) +} + +// GetInt16 returns the value associated with the key as an integer 16. +func (c *Context) GetInt16(key any) int16 { + return getTyped[int16](c, key) +} + +// GetInt32 returns the value associated with the key as an integer 32. +func (c *Context) GetInt32(key any) int32 { + return getTyped[int32](c, key) +} + +// GetInt64 returns the value associated with the key as an integer 64. +func (c *Context) GetInt64(key any) int64 { + return getTyped[int64](c, key) +} + +// GetUint returns the value associated with the key as an unsigned integer. +func (c *Context) GetUint(key any) uint { + return getTyped[uint](c, key) +} + +// GetUint8 returns the value associated with the key as an unsigned integer 8. +func (c *Context) GetUint8(key any) uint8 { + return getTyped[uint8](c, key) +} + +// GetUint16 returns the value associated with the key as an unsigned integer 16. +func (c *Context) GetUint16(key any) uint16 { + return getTyped[uint16](c, key) +} + +// GetUint32 returns the value associated with the key as an unsigned integer 32. +func (c *Context) GetUint32(key any) uint32 { + return getTyped[uint32](c, key) +} + +// GetUint64 returns the value associated with the key as an unsigned integer 64. +func (c *Context) GetUint64(key any) uint64 { + return getTyped[uint64](c, key) +} + +// GetFloat32 returns the value associated with the key as a float32. +func (c *Context) GetFloat32(key any) float32 { + return getTyped[float32](c, key) +} + +// GetFloat64 returns the value associated with the key as a float64. +func (c *Context) GetFloat64(key any) float64 { + return getTyped[float64](c, key) +} + +// GetTime returns the value associated with the key as time. +func (c *Context) GetTime(key any) time.Time { + return getTyped[time.Time](c, key) +} + +// GetDuration returns the value associated with the key as a duration. +func (c *Context) GetDuration(key any) time.Duration { + return getTyped[time.Duration](c, key) +} + +// GetError returns the value associated with the key as an error. +func (c *Context) GetError(key any) error { + return getTyped[error](c, key) +} + +// GetIntSlice returns the value associated with the key as a slice of integers. +func (c *Context) GetIntSlice(key any) []int { + return getTyped[[]int](c, key) +} + +// GetInt8Slice returns the value associated with the key as a slice of int8 integers. +func (c *Context) GetInt8Slice(key any) []int8 { + return getTyped[[]int8](c, key) +} + +// GetInt16Slice returns the value associated with the key as a slice of int16 integers. +func (c *Context) GetInt16Slice(key any) []int16 { + return getTyped[[]int16](c, key) +} + +// GetInt32Slice returns the value associated with the key as a slice of int32 integers. +func (c *Context) GetInt32Slice(key any) []int32 { + return getTyped[[]int32](c, key) +} + +// GetInt64Slice returns the value associated with the key as a slice of int64 integers. +func (c *Context) GetInt64Slice(key any) []int64 { + return getTyped[[]int64](c, key) +} + +// GetUintSlice returns the value associated with the key as a slice of unsigned integers. +func (c *Context) GetUintSlice(key any) []uint { + return getTyped[[]uint](c, key) +} + +// GetUint8Slice returns the value associated with the key as a slice of uint8 integers. +func (c *Context) GetUint8Slice(key any) []uint8 { + return getTyped[[]uint8](c, key) +} + +// GetUint16Slice returns the value associated with the key as a slice of uint16 integers. +func (c *Context) GetUint16Slice(key any) []uint16 { + return getTyped[[]uint16](c, key) +} + +// GetUint32Slice returns the value associated with the key as a slice of uint32 integers. +func (c *Context) GetUint32Slice(key any) []uint32 { + return getTyped[[]uint32](c, key) +} + +// GetUint64Slice returns the value associated with the key as a slice of uint64 integers. +func (c *Context) GetUint64Slice(key any) []uint64 { + return getTyped[[]uint64](c, key) +} + +// GetFloat32Slice returns the value associated with the key as a slice of float32 numbers. +func (c *Context) GetFloat32Slice(key any) []float32 { + return getTyped[[]float32](c, key) +} + +// GetFloat64Slice returns the value associated with the key as a slice of float64 numbers. +func (c *Context) GetFloat64Slice(key any) []float64 { + return getTyped[[]float64](c, key) +} + +// GetStringSlice returns the value associated with the key as a slice of strings. +func (c *Context) GetStringSlice(key any) []string { + return getTyped[[]string](c, key) +} + +// GetErrorSlice returns the value associated with the key as a slice of errors. +func (c *Context) GetErrorSlice(key any) []error { + return getTyped[[]error](c, key) +} + +// GetStringMap returns the value associated with the key as a map of interfaces. +func (c *Context) GetStringMap(key any) map[string]any { + return getTyped[map[string]any](c, key) +} + +// GetStringMapString returns the value associated with the key as a map of strings. +func (c *Context) GetStringMapString(key any) map[string]string { + return getTyped[map[string]string](c, key) +} + +// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. +func (c *Context) GetStringMapStringSlice(key any) map[string][]string { + return getTyped[map[string][]string](c, key) +} + +// Delete deletes the key from the Context's Key map, if it exists. +// This operation is safe to be used by concurrent go-routines +func (c *Context) Delete(key any) { + c.mu.Lock() + defer c.mu.Unlock() + if c.Keys != nil { + delete(c.Keys, key) + } +} + +/************************************/ +/************ INPUT DATA ************/ +/************************************/ + +// Param returns the value of the URL param. +// It is a shortcut for c.Params.ByName(key) +// +// router.GET("/user/:id", func(c *gin.Context) { +// // a GET request to /user/john +// id := c.Param("id") // id == "john" +// // a GET request to /user/john/ +// id := c.Param("id") // id == "/john/" +// }) +func (c *Context) Param(key string) string { + return c.Params.ByName(key) +} + +// AddParam adds param to context and +// replaces path param key with given value for e2e testing purposes +// Example Route: "/user/:id" +// AddParam("id", 1) +// Result: "/user/1" +func (c *Context) AddParam(key, value string) { + c.Params = append(c.Params, Param{Key: key, Value: value}) +} + +// Query returns the keyed url query value if it exists, +// otherwise it returns an empty string `("")`. +// It is shortcut for `c.Request.URL.Query().Get(key)` +// +// GET /path?id=1234&name=Manu&value= +// c.Query("id") == "1234" +// c.Query("name") == "Manu" +// c.Query("value") == "" +// c.Query("wtf") == "" +func (c *Context) Query(key string) (value string) { + value, _ = c.GetQuery(key) + return +} + +// DefaultQuery returns the keyed url query value if it exists, +// otherwise it returns the specified defaultValue string. +// See: Query() and GetQuery() for further information. +// +// GET /?name=Manu&lastname= +// c.DefaultQuery("name", "unknown") == "Manu" +// c.DefaultQuery("id", "none") == "none" +// c.DefaultQuery("lastname", "none") == "" +func (c *Context) DefaultQuery(key, defaultValue string) string { + if value, ok := c.GetQuery(key); ok { + return value + } + return defaultValue +} + +// GetQuery is like Query(), it returns the keyed url query value +// if it exists `(value, true)` (even when the value is an empty string), +// otherwise it returns `("", false)`. +// It is shortcut for `c.Request.URL.Query().Get(key)` +// +// GET /?name=Manu&lastname= +// ("Manu", true) == c.GetQuery("name") +// ("", false) == c.GetQuery("id") +// ("", true) == c.GetQuery("lastname") +func (c *Context) GetQuery(key string) (string, bool) { + if values, ok := c.GetQueryArray(key); ok { + return values[0], ok + } + return "", false +} + +// QueryArray returns a slice of strings for a given query key. +// The length of the slice depends on the number of params with the given key. +func (c *Context) QueryArray(key string) (values []string) { + values, _ = c.GetQueryArray(key) + return +} + +func (c *Context) initQueryCache() { + if c.queryCache == nil { + if c.Request != nil && c.Request.URL != nil { + c.queryCache = c.Request.URL.Query() + } else { + c.queryCache = url.Values{} + } + } +} + +// GetQueryArray returns a slice of strings for a given query key, plus +// a boolean value whether at least one value exists for the given key. +func (c *Context) GetQueryArray(key string) (values []string, ok bool) { + c.initQueryCache() + values, ok = c.queryCache[key] + return +} + +// QueryMap returns a map for a given query key. +func (c *Context) QueryMap(key string) (dicts map[string]string) { + dicts, _ = c.GetQueryMap(key) + return +} + +// GetQueryMap returns a map for a given query key, plus a boolean value +// whether at least one value exists for the given key. +func (c *Context) GetQueryMap(key string) (map[string]string, bool) { + c.initQueryCache() + return getMapFromFormData(c.queryCache, key) +} + +// PostForm returns the specified key from a POST urlencoded form or multipart form +// when it exists, otherwise it returns an empty string `("")`. +func (c *Context) PostForm(key string) (value string) { + value, _ = c.GetPostForm(key) + return +} + +// DefaultPostForm returns the specified key from a POST urlencoded form or multipart form +// when it exists, otherwise it returns the specified defaultValue string. +// See: PostForm() and GetPostForm() for further information. +func (c *Context) DefaultPostForm(key, defaultValue string) string { + if value, ok := c.GetPostForm(key); ok { + return value + } + return defaultValue +} + +// GetPostForm is like PostForm(key). It returns the specified key from a POST urlencoded +// form or multipart form when it exists `(value, true)` (even when the value is an empty string), +// otherwise it returns ("", false). +// For example, during a PATCH request to update the user's email: +// +// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com" +// email= --> ("", true) := GetPostForm("email") // set email to "" +// --> ("", false) := GetPostForm("email") // do nothing with email +func (c *Context) GetPostForm(key string) (string, bool) { + if values, ok := c.GetPostFormArray(key); ok { + return values[0], ok + } + return "", false +} + +// PostFormArray returns a slice of strings for a given form key. +// The length of the slice depends on the number of params with the given key. +func (c *Context) PostFormArray(key string) (values []string) { + values, _ = c.GetPostFormArray(key) + return +} + +func (c *Context) initFormCache() { + if c.formCache == nil { + c.formCache = make(url.Values) + req := c.Request + if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + if !errors.Is(err, http.ErrNotMultipart) { + debugPrint("error on parse multipart form array: %v", err) + } + } + c.formCache = req.PostForm + } +} + +// GetPostFormArray returns a slice of strings for a given form key, plus +// a boolean value whether at least one value exists for the given key. +func (c *Context) GetPostFormArray(key string) (values []string, ok bool) { + c.initFormCache() + values, ok = c.formCache[key] + return +} + +// PostFormMap returns a map for a given form key. +func (c *Context) PostFormMap(key string) (dicts map[string]string) { + dicts, _ = c.GetPostFormMap(key) + return +} + +// GetPostFormMap returns a map for a given form key, plus a boolean value +// whether at least one value exists for the given key. +func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { + c.initFormCache() + return getMapFromFormData(c.formCache, key) +} + +// getMapFromFormData return a map which satisfies conditions. +// It parses from data with bracket notation like "key[subkey]=value" into a map. +func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) { + d := make(map[string]string) + found := false + keyLen := len(key) + + for k, v := range m { + if len(k) < keyLen+3 { // key + "[" + at least one char + "]" + continue + } + + if k[:keyLen] != key || k[keyLen] != '[' { + continue + } + + if j := strings.IndexByte(k[keyLen+1:], ']'); j > 0 { + found = true + d[k[keyLen+1:keyLen+1+j]] = v[0] + } + } + + return d, found +} + +// FormFile returns the first file for the provided form key. +func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { + if c.Request.MultipartForm == nil { + if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + return nil, err + } + } + f, fh, err := c.Request.FormFile(name) + if err != nil { + return nil, err + } + f.Close() + return fh, err +} + +// MultipartForm is the parsed multipart form, including file uploads. +func (c *Context) MultipartForm() (*multipart.Form, error) { + err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory) + return c.Request.MultipartForm, err +} + +// SaveUploadedFile uploads the form file to specific dst. +func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error { + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + var mode os.FileMode = 0o750 + if len(perm) > 0 { + mode = perm[0] + } + dir := filepath.Dir(dst) + if err = os.MkdirAll(dir, mode); err != nil { + return err + } + if err = os.Chmod(dir, mode); err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, src) + return err +} + +// Bind checks the Method and Content-Type to select a binding engine automatically, +// Depending on the "Content-Type" header different bindings are used, for example: +// +// "application/json" --> JSON binding +// "application/xml" --> XML binding +// +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. +// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. +func (c *Context) Bind(obj any) error { + b := binding.Default(c.Request.Method, c.ContentType()) + return c.MustBindWith(obj, b) +} + +// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON). +func (c *Context) BindJSON(obj any) error { + return c.MustBindWith(obj, binding.JSON) +} + +// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML). +func (c *Context) BindXML(obj any) error { + return c.MustBindWith(obj, binding.XML) +} + +// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query). +func (c *Context) BindQuery(obj any) error { + return c.MustBindWith(obj, binding.Query) +} + +// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML). +func (c *Context) BindYAML(obj any) error { + return c.MustBindWith(obj, binding.YAML) +} + +// BindTOML is a shortcut for c.MustBindWith(obj, binding.TOML). +func (c *Context) BindTOML(obj any) error { + return c.MustBindWith(obj, binding.TOML) +} + +// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain). +func (c *Context) BindPlain(obj any) error { + return c.MustBindWith(obj, binding.Plain) +} + +// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). +func (c *Context) BindHeader(obj any) error { + return c.MustBindWith(obj, binding.Header) +} + +// BindUri binds the passed struct pointer using binding.Uri. +// It will abort the request with HTTP 400 if any error occurs. +func (c *Context) BindUri(obj any) error { + if err := c.ShouldBindUri(obj); err != nil { + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck + return err + } + return nil +} + +// MustBindWith binds the passed struct pointer using the specified binding engine. +// It will abort the request with HTTP 400 if any error occurs. +// See the binding package. +func (c *Context) MustBindWith(obj any, b binding.Binding) error { + err := c.ShouldBindWith(obj, b) + if err != nil { + var maxBytesErr *http.MaxBytesError + + // Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error + // https://github.com/goccy/go-json/issues/485 + // https://github.com/bytedance/sonic/issues/800 + switch { + case errors.As(err, &maxBytesErr): + c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck + default: + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck + } + return err + } + return nil +} + +// ShouldBind checks the Method and Content-Type to select a binding engine automatically, +// Depending on the "Content-Type" header different bindings are used, for example: +// +// "application/json" --> JSON binding +// "application/xml" --> XML binding +// +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. +// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid. +func (c *Context) ShouldBind(obj any) error { + b := binding.Default(c.Request.Method, c.ContentType()) + return c.ShouldBindWith(obj, b) +} + +// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON). +// +// Example: +// +// POST /user +// Content-Type: application/json +// +// Request Body: +// { +// "name": "Manu", +// "age": 20 +// } +// +// type User struct { +// Name string `json:"name"` +// Age int `json:"age"` +// } +// +// var user User +// if err := c.ShouldBindJSON(&user); err != nil { +// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) +// return +// } +// c.JSON(http.StatusOK, user) +func (c *Context) ShouldBindJSON(obj any) error { + return c.ShouldBindWith(obj, binding.JSON) +} + +// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). +// It works like ShouldBindJSON but binds the request body as XML data. +func (c *Context) ShouldBindXML(obj any) error { + return c.ShouldBindWith(obj, binding.XML) +} + +// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). +// It works like ShouldBindJSON but binds query parameters from the URL. +func (c *Context) ShouldBindQuery(obj any) error { + return c.ShouldBindWith(obj, binding.Query) +} + +// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). +// It works like ShouldBindJSON but binds the request body as YAML data. +func (c *Context) ShouldBindYAML(obj any) error { + return c.ShouldBindWith(obj, binding.YAML) +} + +// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML). +// It works like ShouldBindJSON but binds the request body as TOML data. +func (c *Context) ShouldBindTOML(obj any) error { + return c.ShouldBindWith(obj, binding.TOML) +} + +// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain). +// It works like ShouldBindJSON but binds plain text data from the request body. +func (c *Context) ShouldBindPlain(obj any) error { + return c.ShouldBindWith(obj, binding.Plain) +} + +// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). +// It works like ShouldBindJSON but binds values from HTTP headers. +func (c *Context) ShouldBindHeader(obj any) error { + return c.ShouldBindWith(obj, binding.Header) +} + +// ShouldBindUri binds the passed struct pointer using the specified binding engine. +// It works like ShouldBindJSON but binds parameters from the URI. +func (c *Context) ShouldBindUri(obj any) error { + m := make(map[string][]string, len(c.Params)) + for _, v := range c.Params { + m[v.Key] = []string{v.Value} + } + return binding.Uri.BindUri(m, obj) +} + +// ShouldBindWith binds the passed struct pointer using the specified binding engine. +// See the binding package. +func (c *Context) ShouldBindWith(obj any, b binding.Binding) error { + return b.Bind(c.Request, obj) +} + +// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request +// body into the context, and reuse when it is called again. +// +// NOTE: This method reads the body before binding. So you should use +// ShouldBindWith for better performance if you need to call only once. +func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error) { + var body []byte + if cb, ok := c.Get(BodyBytesKey); ok { + if cbb, ok := cb.([]byte); ok { + body = cbb + } + } + if body == nil { + body, err = io.ReadAll(c.Request.Body) + if err != nil { + return err + } + c.Set(BodyBytesKey, body) + } + return bb.BindBody(body, obj) +} + +// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON). +func (c *Context) ShouldBindBodyWithJSON(obj any) error { + return c.ShouldBindBodyWith(obj, binding.JSON) +} + +// ShouldBindBodyWithXML is a shortcut for c.ShouldBindBodyWith(obj, binding.XML). +func (c *Context) ShouldBindBodyWithXML(obj any) error { + return c.ShouldBindBodyWith(obj, binding.XML) +} + +// ShouldBindBodyWithYAML is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML). +func (c *Context) ShouldBindBodyWithYAML(obj any) error { + return c.ShouldBindBodyWith(obj, binding.YAML) +} + +// ShouldBindBodyWithTOML is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML). +func (c *Context) ShouldBindBodyWithTOML(obj any) error { + return c.ShouldBindBodyWith(obj, binding.TOML) +} + +// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain). +func (c *Context) ShouldBindBodyWithPlain(obj any) error { + return c.ShouldBindBodyWith(obj, binding.Plain) +} + +// ClientIP implements one best effort algorithm to return the real client IP. +// It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. +// If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-IP]). +// If the headers are not syntactically valid OR the remote IP does not correspond to a trusted proxy, +// the remote IP (coming from Request.RemoteAddr) is returned. +func (c *Context) ClientIP() string { + // Check if we're running on a trusted platform, continue running backwards if error + if c.engine.TrustedPlatform != "" { + // Developers can define their own header of Trusted Platform or use predefined constants + if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" { + return addr + } + } + + // Legacy "AppEngine" flag + if c.engine.AppEngine { + log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine' instead.`) + if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" { + return addr + } + } + + var ( + trusted bool + remoteIP net.IP + ) + // If gin is listening a unix socket, always trust it. + localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr) + if ok && strings.HasPrefix(localAddr.Network(), "unix") { + trusted = true + } + + // Fallback + if !trusted { + // It also checks if the remoteIP is a trusted proxy or not. + // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks + // defined by Engine.SetTrustedProxies() + remoteIP = net.ParseIP(c.RemoteIP()) + if remoteIP == nil { + return "" + } + trusted = c.engine.isTrustedProxy(remoteIP) + } + + if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil { + for _, headerName := range c.engine.RemoteIPHeaders { + headerValue := strings.Join(c.Request.Header.Values(headerName), ",") + ip, valid := c.engine.validateHeader(headerValue) + if valid { + return ip + } + } + } + return remoteIP.String() +} + +// RemoteIP parses the IP from Request.RemoteAddr, normalizes and returns the IP (without the port). +func (c *Context) RemoteIP() string { + ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)) + if err != nil { + return "" + } + return ip +} + +// ContentType returns the Content-Type header of the request. +func (c *Context) ContentType() string { + return filterFlags(c.requestHeader("Content-Type")) +} + +// IsWebsocket returns true if the request headers indicate that a websocket +// handshake is being initiated by the client. +func (c *Context) IsWebsocket() bool { + if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") && + strings.EqualFold(c.requestHeader("Upgrade"), "websocket") { + return true + } + return false +} + +func (c *Context) requestHeader(key string) string { + return c.Request.Header.Get(key) +} + +/************************************/ +/******** RESPONSE RENDERING ********/ +/************************************/ + +// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. +// Uses http.StatusContinue constant for better code clarity. +func bodyAllowedForStatus(status int) bool { + switch { + case status >= http.StatusContinue && status < http.StatusOK: + return false + case status == http.StatusNoContent: + return false + case status == http.StatusNotModified: + return false + } + return true +} + +// Status sets the HTTP response code. +func (c *Context) Status(code int) { + c.Writer.WriteHeader(code) +} + +// Header is an intelligent shortcut for c.Writer.Header().Set(key, value). +// It writes a header in the response. +// If value == "", this method removes the header `c.Writer.Header().Del(key)` +func (c *Context) Header(key, value string) { + if value == "" { + c.Writer.Header().Del(key) + return + } + c.Writer.Header().Set(key, value) +} + +// GetHeader returns value from request headers. +func (c *Context) GetHeader(key string) string { + return c.requestHeader(key) +} + +// GetRawData returns stream data. +func (c *Context) GetRawData() ([]byte, error) { + if c.Request.Body == nil { + return nil, errors.New("cannot read nil body") + } + return io.ReadAll(c.Request.Body) +} + +// SetSameSite with cookie +func (c *Context) SetSameSite(samesite http.SameSite) { + c.sameSite = samesite +} + +// SetCookie adds a Set-Cookie header to the ResponseWriter's headers. +// The provided cookie must have a valid Name. Invalid cookies may be +// silently dropped. +func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { + if path == "" { + path = "/" + } + http.SetCookie(c.Writer, &http.Cookie{ + Name: name, + Value: url.QueryEscape(value), + MaxAge: maxAge, + Path: path, + Domain: domain, + SameSite: c.sameSite, + Secure: secure, + HttpOnly: httpOnly, + }) +} + +// SetCookieData adds a Set-Cookie header to the ResponseWriter's headers. +// It accepts a pointer to http.Cookie structure for more flexibility in setting cookie attributes. +// The provided cookie must have a valid Name. Invalid cookies may be silently dropped. +func (c *Context) SetCookieData(cookie *http.Cookie) { + if cookie.Path == "" { + cookie.Path = "/" + } + if cookie.SameSite == http.SameSiteDefaultMode { + cookie.SameSite = c.sameSite + } + http.SetCookie(c.Writer, cookie) +} + +// Cookie returns the named cookie provided in the request or +// ErrNoCookie if not found. And return the named cookie is unescaped. +// If multiple cookies match the given name, only one cookie will +// be returned. +func (c *Context) Cookie(name string) (string, error) { + cookie, err := c.Request.Cookie(name) + if err != nil { + return "", err + } + val, _ := url.QueryUnescape(cookie.Value) + return val, nil +} + +// Render writes the response headers and calls render.Render to render data. +func (c *Context) Render(code int, r render.Render) { + c.Status(code) + + if !bodyAllowedForStatus(code) { + r.WriteContentType(c.Writer) + c.Writer.WriteHeaderNow() + return + } + + if err := r.Render(c.Writer); err != nil { + // Pushing error to c.Errors + _ = c.Error(err) + c.Abort() + } +} + +// HTML renders the HTTP template specified by its file name. +// It also updates the HTTP code and sets the Content-Type as "text/html". +// See http://golang.org/doc/articles/wiki/ +func (c *Context) HTML(code int, name string, obj any) { + instance := c.engine.HTMLRender.Instance(name, obj) + c.Render(code, instance) +} + +// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body. +// It also sets the Content-Type as "application/json". +// WARNING: we recommend using this only for development purposes since printing pretty JSON is +// more CPU and bandwidth consuming. Use Context.JSON() instead. +func (c *Context) IndentedJSON(code int, obj any) { + c.Render(code, render.IndentedJSON{Data: obj}) +} + +// SecureJSON serializes the given struct as Secure JSON into the response body. +// Default prepends "while(1)," to response body if the given struct is array values. +// It also sets the Content-Type as "application/json". +func (c *Context) SecureJSON(code int, obj any) { + c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj}) +} + +// JSONP serializes the given struct as JSON into the response body. +// It adds padding to response body to request data from a server residing in a different domain than the client. +// It also sets the Content-Type as "application/javascript". +func (c *Context) JSONP(code int, obj any) { + callback := c.DefaultQuery("callback", "") + if callback == "" { + c.Render(code, render.JSON{Data: obj}) + return + } + c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) +} + +// JSON serializes the given struct as JSON into the response body. +// It also sets the Content-Type as "application/json". +func (c *Context) JSON(code int, obj any) { + c.Render(code, render.JSON{Data: obj}) +} + +// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string. +// It also sets the Content-Type as "application/json". +func (c *Context) AsciiJSON(code int, obj any) { + c.Render(code, render.AsciiJSON{Data: obj}) +} + +// PureJSON serializes the given struct as JSON into the response body. +// PureJSON, unlike JSON, does not replace special html characters with their unicode entities. +func (c *Context) PureJSON(code int, obj any) { + c.Render(code, render.PureJSON{Data: obj}) +} + +// XML serializes the given struct as XML into the response body. +// It also sets the Content-Type as "application/xml". +func (c *Context) XML(code int, obj any) { + c.Render(code, render.XML{Data: obj}) +} + +// PDF writes the given PDF binary data into the response body. +// It also sets the Content-Type as "application/pdf". +func (c *Context) PDF(code int, data []byte) { + c.Render(code, render.PDF{Data: data}) +} + +// YAML serializes the given struct as YAML into the response body. +func (c *Context) YAML(code int, obj any) { + c.Render(code, render.YAML{Data: obj}) +} + +// TOML serializes the given struct as TOML into the response body. +func (c *Context) TOML(code int, obj any) { + c.Render(code, render.TOML{Data: obj}) +} + +// ProtoBuf serializes the given struct as ProtoBuf into the response body. +func (c *Context) ProtoBuf(code int, obj any) { + c.Render(code, render.ProtoBuf{Data: obj}) +} + +// BSON serializes the given struct as BSON into the response body. +func (c *Context) BSON(code int, obj any) { + c.Render(code, render.BSON{Data: obj}) +} + +// String writes the given string into the response body. +func (c *Context) String(code int, format string, values ...any) { + c.Render(code, render.String{Format: format, Data: values}) +} + +// Redirect returns an HTTP redirect to the specific location. +func (c *Context) Redirect(code int, location string) { + c.Render(-1, render.Redirect{ + Code: code, + Location: location, + Request: c.Request, + }) +} + +// Data writes some data into the body stream and updates the HTTP code. +func (c *Context) Data(code int, contentType string, data []byte) { + c.Render(code, render.Data{ + ContentType: contentType, + Data: data, + }) +} + +// DataFromReader writes the specified reader into the body stream and updates the HTTP code. +func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) { + c.Render(code, render.Reader{ + Headers: extraHeaders, + ContentType: contentType, + ContentLength: contentLength, + Reader: reader, + }) +} + +// File writes the specified file into the body stream in an efficient way. +func (c *Context) File(filepath string) { + http.ServeFile(c.Writer, c.Request, filepath) +} + +// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way. +func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { + defer func(old string) { + c.Request.URL.Path = old + }(c.Request.URL.Path) + + c.Request.URL.Path = filepath + + http.FileServer(fs).ServeHTTP(c.Writer, c.Request) +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +// FileAttachment writes the specified file into the body stream in an efficient way +// On the client side, the file will typically be downloaded with the given filename +func (c *Context) FileAttachment(filepath, filename string) { + if isASCII(filename) { + c.Writer.Header().Set("Content-Disposition", `attachment; filename="`+escapeQuotes(filename)+`"`) + } else { + c.Writer.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) + } + http.ServeFile(c.Writer, c.Request, filepath) +} + +// SSEvent writes a Server-Sent Event into the body stream. +func (c *Context) SSEvent(name string, message any) { + c.Render(-1, sse.Event{ + Event: name, + Data: message, + }) +} + +// Stream sends a streaming response and returns a boolean +// indicates "Is client disconnected in middle of stream" +func (c *Context) Stream(step func(w io.Writer) bool) bool { + w := c.Writer + clientGone := w.CloseNotify() + for { + select { + case <-clientGone: + return true + default: + keepOpen := step(w) + w.Flush() + if !keepOpen { + return false + } + } + } +} + +/************************************/ +/******** CONTENT NEGOTIATION *******/ +/************************************/ + +// Negotiate contains all negotiations data. +type Negotiate struct { + Offered []string + HTMLName string + HTMLData any + JSONData any + XMLData any + YAMLData any + Data any + TOMLData any + PROTOBUFData any + BSONData any +} + +// Negotiate calls different Render according to acceptable Accept format. +func (c *Context) Negotiate(code int, config Negotiate) { + switch c.NegotiateFormat(config.Offered...) { + case binding.MIMEJSON: + data := chooseData(config.JSONData, config.Data) + c.JSON(code, data) + + case binding.MIMEHTML: + data := chooseData(config.HTMLData, config.Data) + c.HTML(code, config.HTMLName, data) + + case binding.MIMEXML: + data := chooseData(config.XMLData, config.Data) + c.XML(code, data) + + case binding.MIMEYAML, binding.MIMEYAML2: + data := chooseData(config.YAMLData, config.Data) + c.YAML(code, data) + + case binding.MIMETOML: + data := chooseData(config.TOMLData, config.Data) + c.TOML(code, data) + + case binding.MIMEPROTOBUF: + data := chooseData(config.PROTOBUFData, config.Data) + c.ProtoBuf(code, data) + + case binding.MIMEBSON: + data := chooseData(config.BSONData, config.Data) + c.BSON(code, data) + + default: + c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck + } +} + +// NegotiateFormat returns an acceptable Accept format. +func (c *Context) NegotiateFormat(offered ...string) string { + assert1(len(offered) > 0, "you must provide at least one offer") + + if c.Accepted == nil { + c.Accepted = parseAccept(c.requestHeader("Accept")) + } + if len(c.Accepted) == 0 { + return offered[0] + } + for _, accepted := range c.Accepted { + for _, offer := range offered { + // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, + // therefore we can just iterate over the string without casting it into []rune + i := 0 + for ; i < len(accepted) && i < len(offer); i++ { + if accepted[i] == '*' || offer[i] == '*' { + return offer + } + if accepted[i] != offer[i] { + break + } + } + if i == len(accepted) { + return offer + } + } + } + return "" +} + +// SetAccepted sets Accept header data. +func (c *Context) SetAccepted(formats ...string) { + c.Accepted = formats +} + +/************************************/ +/***** GOLANG.ORG/X/NET/CONTEXT *****/ +/************************************/ + +// hasRequestContext returns whether c.Request has Context and fallback. +func (c *Context) hasRequestContext() bool { + hasFallback := c.engine != nil && c.engine.ContextWithFallback + hasRequestContext := c.Request != nil && c.Request.Context() != nil + return hasFallback && hasRequestContext +} + +// Deadline returns that there is no deadline (ok==false) when c.Request has no Context. +func (c *Context) Deadline() (deadline time.Time, ok bool) { + if !c.hasRequestContext() { + return + } + return c.Request.Context().Deadline() +} + +// Done returns nil (chan which will wait forever) when c.Request has no Context. +func (c *Context) Done() <-chan struct{} { + if !c.hasRequestContext() { + return nil + } + return c.Request.Context().Done() +} + +// Err returns nil when c.Request has no Context. +func (c *Context) Err() error { + if !c.hasRequestContext() { + return nil + } + return c.Request.Context().Err() +} + +// Value returns the value associated with this context for key, or nil +// if no value is associated with key. Successive calls to Value with +// the same key returns the same result. +func (c *Context) Value(key any) any { + if key == ContextRequestKey { + return c.Request + } + if key == ContextKey { + return c + } + if keyAsString, ok := key.(string); ok { + if val, exists := c.Get(keyAsString); exists { + return val + } + } + if !c.hasRequestContext() { + return nil + } + return c.Request.Context().Value(key) +} diff --git a/benches/regen_baseline.sh b/benches/regen_baseline.sh new file mode 100755 index 00000000..af33e079 --- /dev/null +++ b/benches/regen_baseline.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Regenerate benches/dynamic_bench_baseline.json from a real cargo bench run. +# +# Usage: +# bash benches/regen_baseline.sh +# +# Requirements: +# - python3 on PATH +# - cargo (nightly or stable with edition 2024) +# - Criterion's JSON output (criterion feature already in dev-deps) +# +# The script runs the dynamic bench group, parses Criterion's estimates JSON, +# and overwrites dynamic_bench_baseline.json with real numbers. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +BASELINE_FILE="${SCRIPT_DIR}/dynamic_bench_baseline.json" + +echo "Running cargo bench --features dynamic -- dynamic ..." +cargo bench --manifest-path "${REPO_ROOT}/Cargo.toml" \ + --features dynamic \ + -- dynamic \ + 2>&1 | tee /tmp/nyx_bench_raw.txt + +# Criterion writes estimates to target/criterion///estimates.json. +# Extract mean_ns for each tracked benchmark. +extract_ns() { + local path="$1" + if [[ -f "${path}" ]]; then + python3 -c " +import json, sys +d = json.load(open('${path}')) +mean = d['mean']['point_estimate'] +stddev = (d['std_dev']['point_estimate']) if 'std_dev' in d else 0 +print(int(mean), int(stddev)) +" + else + echo "0 0" + fi +} + +TARGET="${REPO_ROOT}/target/criterion" + +read COLD_MEAN COLD_STDDEV < <(extract_ns "${TARGET}/harness_build_cold/default/estimates.json") +read WARM_MEAN WARM_STDDEV < <(extract_ns "${TARGET}/harness_build_warm/default/estimates.json") +read RUN_MEAN RUN_STDDEV < <(extract_ns "${TARGET}/sandbox_run_payload/default/estimates.json") + +MACHINE="$(uname -m) / $(uname -s)" +NYX_VER="$(cargo metadata --manifest-path "${REPO_ROOT}/Cargo.toml" --no-deps --format-version 1 \ + | python3 -c "import json,sys; d=json.load(sys.stdin); print(next(p['version'] for p in d['packages'] if p['name']=='nyx-scanner'))")" +DATE="$(date +%Y-%m-%d)" + +cat > "${BASELINE_FILE}" < = rx.into_iter().flatten().collect(); + let mut diags = Vec::new(); + for path in &paths { + if let Ok(mut d) = + nyx_scanner::ast::run_rules_on_file(path, &cfg, None, Some(&fixtures)) + { + diags.append(&mut d); + } + } + diags + }); + }); +} + +fn bench_full_scan(c: &mut Criterion) { + let fixtures = Path::new(FIXTURES).canonicalize().expect("fixtures dir"); + let mut cfg = Config::default(); + cfg.scanner.mode = AnalysisMode::Full; + cfg.performance.worker_threads = Some(1); + cfg.performance.channel_multiplier = 1; + cfg.performance.batch_size = 64; + + c.bench_function("full_scan", |b| { + b.iter(|| { + let (rx, handle) = nyx_scanner::walk::spawn_file_walker(&fixtures, &cfg); + if let Err(err) = handle.join() { + panic!("walker panicked: {err:#?}"); + } + let paths: Vec<_> = rx.into_iter().flatten().collect(); + + // Pass 1: extract summaries + let mut all_sums = Vec::new(); + for path in &paths { + if let Ok(sums) = nyx_scanner::ast::extract_summaries_from_file(path, &cfg) { + all_sums.extend(sums); + } + } + let root_str = fixtures.to_string_lossy(); + let global = nyx_scanner::summary::merge_summaries(all_sums, Some(&root_str)); + + // Pass 2: full analysis + let mut diags = Vec::new(); + for path in &paths { + if let Ok(mut d) = + nyx_scanner::ast::run_rules_on_file(path, &cfg, Some(&global), Some(&fixtures)) + { + diags.append(&mut d); + } + } + diags + }); + }); +} + +fn bench_full_scan_with_state(c: &mut Criterion) { + let fixtures = Path::new(FIXTURES).canonicalize().expect("fixtures dir"); + let mut cfg = Config::default(); + cfg.scanner.mode = AnalysisMode::Full; + cfg.scanner.enable_state_analysis = true; + cfg.performance.worker_threads = Some(1); + cfg.performance.channel_multiplier = 1; + cfg.performance.batch_size = 64; + + c.bench_function("full_scan_with_state", |b| { + b.iter(|| { + let (rx, handle) = nyx_scanner::walk::spawn_file_walker(&fixtures, &cfg); + if let Err(err) = handle.join() { + panic!("walker panicked: {err:#?}"); + } + let paths: Vec<_> = rx.into_iter().flatten().collect(); + + // Pass 1: extract summaries + let mut all_sums = Vec::new(); + for path in &paths { + if let Ok(sums) = nyx_scanner::ast::extract_summaries_from_file(path, &cfg) { + all_sums.extend(sums); + } + } + let root_str = fixtures.to_string_lossy(); + let global = nyx_scanner::summary::merge_summaries(all_sums, Some(&root_str)); + + // Pass 2: full analysis with state + let mut diags = Vec::new(); + for path in &paths { + if let Ok(mut d) = + nyx_scanner::ast::run_rules_on_file(path, &cfg, Some(&global), Some(&fixtures)) + { + diags.append(&mut d); + } + } + diags + }); + }); +} + +fn bench_single_file_parse_and_cfg(c: &mut Criterion) { + let fixture = Path::new(FIXTURES).join("sample.rs"); + let fixture = fixture.canonicalize().expect("sample.rs fixture"); + let cfg = Config::default(); + + c.bench_function("single_file_parse_cfg", |b| { + b.iter(|| { + nyx_scanner::ast::extract_summaries_from_file(&fixture, &cfg) + .expect("extract summaries") + }); + }); +} + +fn bench_state_analysis_only(c: &mut Criterion) { + let fixture = Path::new(FIXTURES) + .join("state_bench.c") + .canonicalize() + .expect("state_bench.c fixture"); + let mut cfg = Config::default(); + cfg.scanner.mode = AnalysisMode::Full; + cfg.scanner.enable_state_analysis = true; + + // Parse and build CFG once (outside benchmark loop) + let (file_cfg, lang) = nyx_scanner::ast::build_cfg_for_file(&fixture, &cfg) + .expect("build cfg") + .expect("supported language"); + let source_bytes = std::fs::read(&fixture).expect("read fixture"); + let top = file_cfg.toplevel(); + + c.bench_function("state_analysis_only", |b| { + b.iter(|| { + nyx_scanner::state::run_state_analysis( + &top.graph, + top.entry, + lang, + &source_bytes, + &file_cfg.summaries, + None, + true, + &[], + &[], + &std::collections::HashSet::new(), + None, + None, + ) + }); + }); +} + +fn bench_classify(c: &mut Criterion) { + c.bench_function("classify_hit", |b| { + b.iter(|| nyx_scanner::labels::classify("rust", "std::env::var", None)); + }); + + c.bench_function("classify_miss", |b| { + b.iter(|| nyx_scanner::labels::classify("rust", "some_random_function", None)); + }); +} + +/// Per-file fused analysis throughput on a realistic ~1.5k-line Go module +/// (gin context.go, ~147 fns). Guards the +/// `ParsedFile::body_const_facts_cache` optimization that collapses the +/// 2-3× per-body re-lowering that previously dominated `analyse_file_fused` +/// (~14% of wall-clock on the gin-scan profile). Regressions here mean +/// per-body work is being recomputed across passes again. +fn bench_analyse_file_fused_large_go(c: &mut Criterion) { + let fixture = Path::new("benches/perf_fixtures/large_go_module.go") + .canonicalize() + .expect("perf fixture"); + let bytes = std::fs::read(&fixture).expect("read fixture"); + let mut cfg = Config::default(); + cfg.scanner.mode = AnalysisMode::Full; + cfg.scanner.enable_state_analysis = true; + cfg.performance.worker_threads = Some(1); + + // One-shot diagnostic: count `build_body_const_facts` calls per fused + // analysis so a regression that removes the per-file cache surfaces here + // (expected ~148 calls on this fixture; pre-cache was ~444). + nyx_scanner::cfg_analysis::BUILD_BODY_CONST_FACTS_CALLS + .store(0, std::sync::atomic::Ordering::Relaxed); + let _ = nyx_scanner::ast::analyse_file_fused(&bytes, &fixture, &cfg, None, None) + .expect("warmup analyse"); + let calls = nyx_scanner::cfg_analysis::BUILD_BODY_CONST_FACTS_CALLS + .load(std::sync::atomic::Ordering::Relaxed); + eprintln!("[diag] build_body_const_facts calls per analyse_file_fused: {calls}"); + + c.bench_function("analyse_file_fused_large_go", |b| { + b.iter(|| { + nyx_scanner::ast::analyse_file_fused(&bytes, &fixture, &cfg, None, None) + .expect("analyse_file_fused") + }); + }); +} + +/// Per-file `extract_authorization_model` throughput on the realistic +/// ~1.5k-line Go fixture (gin context.go). Guards the +/// `extract_authorization_model` orchestrator hoist that pulled the +/// shared `collect_top_level_units` AST walk out of every supporting +/// extractor's `extract()` (one walk per file instead of one per +/// matching extractor). On Go files both `EchoExtractor` and +/// `GinExtractor` match by default — pre-hoist this bench measured the +/// AST being walked twice; regressions here mean the hoist has been +/// broken or a new Go extractor was added that re-walks the tree. +fn bench_extract_authorization_model_go(c: &mut Criterion) { + use tree_sitter::Parser; + + let fixture = Path::new("benches/perf_fixtures/large_go_module.go") + .canonicalize() + .expect("perf fixture"); + let bytes = std::fs::read(&fixture).expect("read fixture"); + + let mut parser = Parser::new(); + let go_lang: tree_sitter::Language = tree_sitter_go::LANGUAGE.into(); + parser.set_language(&go_lang).expect("set go grammar"); + let tree = parser.parse(&bytes, None).expect("parse fixture"); + + let cfg = Config::default(); + let rules = nyx_scanner::auth_analysis::config::build_auth_rules(&cfg, "go"); + + c.bench_function("extract_authorization_model_go", |b| { + b.iter(|| { + nyx_scanner::auth_analysis::extract::extract_authorization_model( + "go", + cfg.framework_ctx.as_ref(), + &tree, + &bytes, + &fixture, + &rules, + None, + ) + }); + }); +} + +/// Per-file shared-vs-double `extract_authorization_model` cost on a +/// realistic Go fixture (gin context.go). Pre-fix +/// `analyse_file_fused` called `extract_authorization_model` twice per +/// file (once for diagnostics via `run_auth_analysis`, once for +/// per-file summary keying via `extract_auth_summaries_by_key`). This +/// bench records the **shared-model path** only (extract once, derive +/// both summaries + diagnostics) so a regression that re-introduces +/// the double-call surfaces as a ≥1.7× slowdown here. +fn bench_extract_authorization_model_shared_go(c: &mut Criterion) { + use tree_sitter::Parser; + + let fixture = Path::new("benches/perf_fixtures/large_go_module.go") + .canonicalize() + .expect("perf fixture"); + let bytes = std::fs::read(&fixture).expect("read fixture"); + + let mut parser = Parser::new(); + let go_lang: tree_sitter::Language = tree_sitter_go::LANGUAGE.into(); + parser.set_language(&go_lang).expect("set go grammar"); + let tree = parser.parse(&bytes, None).expect("parse fixture"); + + let cfg = Config::default(); + let rules = nyx_scanner::auth_analysis::config::build_auth_rules(&cfg, "go"); + + c.bench_function("extract_authorization_model_shared_go", |b| { + b.iter(|| { + // Mirror `analyse_file_fused`: extract once, derive both + // per-file summaries (cheap iter over units) AND run the + // full diagnostic pipeline against the same model. + let model = nyx_scanner::auth_analysis::extract::extract_authorization_model( + "go", + cfg.framework_ctx.as_ref(), + &tree, + &bytes, + &fixture, + &rules, + None, + ); + let summaries = nyx_scanner::auth_analysis::extract_auth_summaries_from_model( + &model, "go", &fixture, None, + ); + let diags = nyx_scanner::auth_analysis::run_auth_analysis_with_model( + model, &tree, "go", &fixture, &rules, None, None, None, + ); + (summaries, diags) + }); + }); +} + +/// Per-file `collect_top_level_units` cost on a realistic Go fixture +/// (gin context.go, ~147 functions). Targets the inner per-function +/// AST-walk path: `collect_top_level_units` → +/// `build_function_unit_with_meta` → `collect_unit_state` (recursive +/// per-AST-node walk that emits per-node value-refs). +/// +/// Pre-fix (2026-05-04 perfhunt session-0009) `collect_unit_state` +/// called `extract_value_refs(node, bytes)` at every AST node, and that +/// helper recursively walked the node's full subtree. Combined with +/// the recursion below, every descendant got walked once for each of +/// its ancestors — total work O(N²) per function body. The fix +/// replaced that call with an O(1)-per-node `append_shallow_value_ref` +/// helper. A regression that re-introduces the deep walk surfaces +/// here as a ≥2× slowdown. +fn bench_collect_top_level_units_go(c: &mut Criterion) { + use tree_sitter::Parser; + + let fixture = Path::new("benches/perf_fixtures/large_go_module.go") + .canonicalize() + .expect("perf fixture"); + let bytes = std::fs::read(&fixture).expect("read fixture"); + + let mut parser = Parser::new(); + let go_lang: tree_sitter::Language = tree_sitter_go::LANGUAGE.into(); + parser.set_language(&go_lang).expect("set go grammar"); + let tree = parser.parse(&bytes, None).expect("parse fixture"); + + let cfg = Config::default(); + let rules = nyx_scanner::auth_analysis::config::build_auth_rules(&cfg, "go"); + + c.bench_function("collect_top_level_units_go", |b| { + b.iter(|| { + let mut model = nyx_scanner::auth_analysis::model::AuthorizationModel::default(); + nyx_scanner::auth_analysis::extract::common::collect_top_level_units( + tree.root_node(), + &bytes, + &rules, + &mut model, + ); + model + }); + }); +} + +/// SCCP throughput on every SSA body lowered from the gin context.go +/// fixture. Targets `nyx_scanner::ssa::const_prop::const_propagate` +/// directly, isolating it from the surrounding `optimize_ssa` pass and +/// the full-fused per-file analysis. +/// +/// Pre-fix (2026-05-04 perfhunt) `const_propagate` stored its lattice in +/// `HashMap` and walked +/// `inst_uses(inst).contains(&val)` for every block re-evaluation in the +/// SSA worklist — both shapes paid `SipHash` cost on every operand, and +/// the `inst_uses` factory allocated a fresh `Vec` on every +/// call. Switching the lattice + executable-edge maps to dense +/// `Vec`-indexed storage and the use-check to a zero-allocation +/// predicate cut `const_propagate` self-time roughly in half on the +/// large-Go fixture. A regression that re-introduces the hash-keyed +/// inner loop will surface here as a ≥1.4× slowdown. +fn bench_const_propagate_large_go(c: &mut Criterion) { + use nyx_scanner::ssa; + + let fixture = Path::new("benches/perf_fixtures/large_go_module.go") + .canonicalize() + .expect("perf fixture"); + let cfg_obj = Config::default(); + let (file_cfg, _lang) = nyx_scanner::ast::build_cfg_for_file(&fixture, &cfg_obj) + .expect("build cfg") + .expect("supported language"); + + // Lower every body once outside the bench loop so we measure only + // SCCP cost. The collected `(SsaBody, Cfg)` pairs are the input to + // the inner loop. + let mut bodies: Vec = Vec::new(); + for body in &file_cfg.bodies { + // Use `body.meta.name` as the scope filter so the SSA lowering + // pulls only this function's nodes; `scope_all=true` is reserved + // for the synthetic top-level body where `name` is None. + let scope = body.meta.name.as_deref(); + let scope_all = scope.is_none(); + match ssa::lower_to_ssa(&body.graph, body.entry, scope, scope_all) { + Ok(ssa_body) => bodies.push(ssa_body), + Err(_) => continue, + } + } + eprintln!( + "[diag] const_propagate bench: {} bodies lowered", + bodies.len() + ); + + c.bench_function("const_propagate_large_go", |b| { + b.iter(|| { + let mut total_values = 0usize; + for body in &bodies { + let result = ssa::const_prop::const_propagate(body); + total_values += result.values.len(); + } + total_values + }); + }); +} + +/// `GlobalSummaries::lookup_same_lang` cost on a populated index. The +/// inner loop hashes `(Lang, String)` once per call, then `FuncKey` once +/// per candidate via `by_key.get(k)`. Pre-fix the four secondary +/// indices used `std::collections::HashMap` (SipHash). Post-fix +/// (2026-05-04 perfhunt session-0015) they use `rustc_hash::FxHashMap`, +/// trading DoS hardening (irrelevant for in-process program-keyed +/// indices) for ~5x faster hashing on the 30+ byte 3-string `FuncKey` +/// hash workload. A regression that re-introduces SipHash would +/// surface here as a ≥3x slowdown. +fn bench_global_summaries_lookup_same_lang_go(c: &mut Criterion) { + let fixture = Path::new("benches/perf_fixtures/large_go_module.go") + .canonicalize() + .expect("perf fixture"); + let cfg = Config::default(); + + let summaries = + nyx_scanner::ast::extract_summaries_from_file(&fixture, &cfg).expect("extract summaries"); + let names: Vec = summaries.iter().map(|s| s.name.clone()).collect(); + let global = nyx_scanner::summary::merge_summaries(summaries, None); + let lang = nyx_scanner::symbol::Lang::Go; + + eprintln!("[diag] lookup_same_lang bench: {} names", names.len()); + + c.bench_function("global_summaries_lookup_same_lang_go", |b| { + b.iter(|| { + let mut total = 0usize; + for name in &names { + total += global.lookup_same_lang(lang, name).len(); + } + total + }); + }); +} + +criterion_group!( + benches, + bench_ast_only_scan, + bench_full_scan, + bench_full_scan_with_state, + bench_single_file_parse_and_cfg, + bench_state_analysis_only, + bench_classify, + bench_analyse_file_fused_large_go, + bench_extract_authorization_model_go, + bench_extract_authorization_model_shared_go, + bench_collect_top_level_units_go, + bench_const_propagate_large_go, + bench_global_summaries_lookup_same_lang_go, +); +criterion_main!(benches); diff --git a/book.toml b/book.toml new file mode 100644 index 00000000..3980d261 --- /dev/null +++ b/book.toml @@ -0,0 +1,22 @@ +[book] +title = "Nyx" +authors = ["Eli Peter"] +description = " Multi-language static analysis with cross-file taint tracking. Scan your repo, triage findings in your browser, commit triage state with your code. No cloud, no account." +language = "en" +src = "docs" + +[output.html] +default-theme = "navy" +preferred-dark-theme = "navy" +git-repository-url = "https://github.com/elicpeter/nyx" +edit-url-template = "https://github.com/elicpeter/nyx/edit/master/{path}" +site-url = "/nyx/" +additional-css = ["docs/mermaid.css"] +additional-js = ["docs/mermaid-init.js"] + +[output.html.fold] +enable = true +level = 1 + +[output.html.search] +enable = true diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..54959ac3 --- /dev/null +++ b/build.rs @@ -0,0 +1,436 @@ +use std::collections::BTreeMap; +use std::path::Path; +use std::process::Command; + +fn main() { + // Phase 17 (Track E.1): always emit the seccomp policy table to + // OUT_DIR. Gated runtime via `#[cfg(target_os = "linux")]`, but the + // codegen runs on every host so `cargo check` on macOS still emits + // the file (the include never actually compiles on non-Linux). + emit_seccomp_policy(); + + // Phase 19 (Track E.3): emit the IMAGE_DIGESTS table from + // tools/image-builder/images.toml. The runtime side (src/dynamic/ + // toolchain.rs) `include!`s the generated file unconditionally so + // every host build has the same pinned-digest catalogue. + emit_image_digests(); + + // Only relevant when the serve feature is active. + if std::env::var("CARGO_FEATURE_SERVE").is_err() { + return; + } + + let dist_dir = Path::new("src/server/assets/dist"); + let index_html = dist_dir.join("index.html"); + + // Re-run build.rs only when dist output is missing/changed + println!("cargo:rerun-if-changed=src/server/assets/dist/index.html"); + + if index_html.exists() { + // Dist already built, nothing to do + return; + } + + // Dist missing, try to build frontend + let frontend_dir = Path::new("frontend"); + if !frontend_dir.join("package.json").exists() { + emit_placeholder_and_warn(dist_dir); + return; + } + + // Run npm install + build + println!("cargo:warning=Frontend dist not found, running npm install && npm run build..."); + let npm_install = Command::new("npm") + .arg("install") + .current_dir(frontend_dir) + .status(); + + match npm_install { + Ok(s) if s.success() => {} + _ => { + emit_placeholder_and_warn(dist_dir); + return; + } + } + + let npm_build = Command::new("npm") + .arg("run") + .arg("build") + .current_dir(frontend_dir) + .status(); + + match npm_build { + Ok(s) if s.success() => { + println!("cargo:warning=Frontend built successfully."); + } + _ => { + emit_placeholder_and_warn(dist_dir); + } + } +} + +fn emit_placeholder_and_warn(dist_dir: &Path) { + // Create minimal placeholder files so compilation succeeds + std::fs::create_dir_all(dist_dir).ok(); + std::fs::write( + dist_dir.join("index.html"), + "

Frontend not built

Run: cd frontend && npm install && npm run build

", + ) + .ok(); + std::fs::write(dist_dir.join("app.js"), "// frontend not built\n").ok(); + std::fs::write(dist_dir.join("style.css"), "/* frontend not built */\n").ok(); + println!( + "cargo:warning=Node.js/npm not available — wrote placeholder frontend assets. Run 'cd frontend && npm install && npm run build' for the real UI." + ); +} + +// ── Phase 17 (Track E.1) — seccomp policy codegen ──────────────────────────── + +const SECCOMP_POLICY_PATH: &str = "src/dynamic/sandbox/seccomp/seccomp_policy.toml"; + +/// Cap-name → Cap bit value table. Mirrors the `bitflags!` block in +/// `src/labels/mod.rs`. Keep in sync when adding/removing `Cap` +/// constants. +const CAP_BIT_FOR_NAME: &[(&str, u32)] = &[ + ("ENV_VAR", 1 << 0), + ("HTML_ESCAPE", 1 << 1), + ("SHELL_ESCAPE", 1 << 2), + ("URL_ENCODE", 1 << 3), + ("JSON_PARSE", 1 << 4), + ("FILE_IO", 1 << 5), + ("FMT_STRING", 1 << 6), + ("SQL_QUERY", 1 << 7), + ("DESERIALIZE", 1 << 8), + ("SSRF", 1 << 9), + ("CODE_EXEC", 1 << 10), + ("CRYPTO", 1 << 11), + ("UNAUTHORIZED_ID", 1 << 12), + ("DATA_EXFIL", 1 << 13), + ("LDAP_INJECTION", 1 << 14), + ("XPATH_INJECTION", 1 << 15), + ("HEADER_INJECTION", 1 << 16), + ("OPEN_REDIRECT", 1 << 17), + ("SSTI", 1 << 18), + ("XXE", 1 << 19), + ("PROTOTYPE_POLLUTION", 1 << 20), +]; + +fn emit_seccomp_policy() { + println!("cargo:rerun-if-changed={}", SECCOMP_POLICY_PATH); + + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR must be set by cargo"); + let out_path = Path::new(&out_dir).join("seccomp_policy.rs"); + + // Read the policy file; on missing file (e.g. fresh checkout on a + // foreign target), emit empty tables so compilation still succeeds. + let toml_text = match std::fs::read_to_string(SECCOMP_POLICY_PATH) { + Ok(s) => s, + Err(_) => { + std::fs::write( + &out_path, + "pub static BASE: &[&str] = &[];\npub static CAP: &[(u32, &[&str])] = &[];\n", + ) + .expect("write empty seccomp policy stub"); + return; + } + }; + + let parsed = parse_seccomp_toml(&toml_text); + + let mut out = String::new(); + out.push_str("// generated by build.rs from seccomp_policy.toml — do not edit\n\n"); + + // Base allowlist. + out.push_str("pub static BASE: &[&str] = &[\n"); + for name in &parsed.base { + out.push_str(&format!(" \"{}\",\n", escape(name))); + } + out.push_str("];\n\n"); + + // Per-cap allowlists. + out.push_str("pub static CAP: &[(u32, &[&str])] = &[\n"); + for (cap_name, allow) in &parsed.caps { + let bit = CAP_BIT_FOR_NAME + .iter() + .find(|(n, _)| *n == cap_name.as_str()) + .map(|(_, b)| *b) + .unwrap_or_else(|| { + panic!( + "seccomp_policy.toml references unknown Cap '{cap_name}' — \ + add it to CAP_BIT_FOR_NAME in build.rs first" + ) + }); + out.push_str(&format!(" (0x{bit:08x}_u32, &[\n")); + for name in allow { + out.push_str(&format!(" \"{}\",\n", escape(name))); + } + out.push_str(" ]),\n"); + } + out.push_str("];\n"); + + std::fs::write(&out_path, out).expect("write seccomp policy table"); +} + +#[derive(Default)] +struct SeccompPolicy { + base: Vec, + caps: BTreeMap>, +} + +/// Tiny line-oriented TOML parser scoped to the shape used by +/// `seccomp_policy.toml`: +/// +/// [base] +/// allow = ["read", "write", ...] +/// +/// [cap.SQL_QUERY] +/// allow = [ +/// "fdatasync", +/// ... +/// ] +/// +/// Comments (`#`) and blank lines are skipped. Multi-line array bodies +/// are accumulated until the closing `]`. +fn parse_seccomp_toml(src: &str) -> SeccompPolicy { + let mut policy = SeccompPolicy::default(); + let mut current_section: Option = None; + let mut accumulating_array: Option = None; + let mut array_buf = String::new(); + + for raw_line in src.lines() { + let line = strip_comment(raw_line).trim(); + if line.is_empty() { + continue; + } + + if let Some(_key) = accumulating_array.as_ref() { + array_buf.push_str(line); + array_buf.push('\n'); + if line.contains(']') { + let key = accumulating_array.take().unwrap(); + let values = parse_string_array(&array_buf); + store_allow(&mut policy, current_section.as_deref(), &key, values); + array_buf.clear(); + } + continue; + } + + if let Some(section) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + current_section = Some(section.to_string()); + continue; + } + + if let Some((key, rest)) = line.split_once('=') { + let key = key.trim().to_string(); + let rest = rest.trim(); + if rest.starts_with('[') && rest.contains(']') { + let values = parse_string_array(rest); + store_allow(&mut policy, current_section.as_deref(), &key, values); + } else if rest.starts_with('[') { + accumulating_array = Some(key); + array_buf.push_str(rest); + array_buf.push('\n'); + } + continue; + } + } + + policy +} + +fn strip_comment(line: &str) -> &str { + let mut in_string = false; + let bytes = line.as_bytes(); + for (i, &b) in bytes.iter().enumerate() { + match b { + b'"' => in_string = !in_string, + b'#' if !in_string => return &line[..i], + _ => {} + } + } + line +} + +fn parse_string_array(src: &str) -> Vec { + // Find every "..." run between the first `[` and the last `]`. + let start = src.find('[').map(|i| i + 1).unwrap_or(0); + let end = src.rfind(']').unwrap_or(src.len()); + let body = &src[start..end]; + let mut out = Vec::new(); + let mut chars = body.chars().peekable(); + while let Some(c) = chars.next() { + if c == '"' { + let mut s = String::new(); + for c2 in chars.by_ref() { + if c2 == '"' { + break; + } + s.push(c2); + } + out.push(s); + } + } + out +} + +fn store_allow(policy: &mut SeccompPolicy, section: Option<&str>, key: &str, values: Vec) { + if key != "allow" { + return; + } + match section { + Some("base") => policy.base = values, + Some(other) => { + if let Some(cap_name) = other.strip_prefix("cap.") { + policy.caps.insert(cap_name.to_string(), values); + } + } + None => {} + } +} + +fn escape(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + +// ── Phase 19 (Track E.3) — image digest codegen ────────────────────────────── + +const IMAGE_CATALOGUE_PATH: &str = "tools/image-builder/images.toml"; + +/// Parse `tools/image-builder/images.toml` and emit two tables to +/// `$OUT_DIR/image_digests.rs`: +/// +/// pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = …; +/// pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = …; +/// +/// `IMAGE_DIGESTS` keys are toolchain IDs (`python-3.11`, …) and values are +/// `@sha256:…` strings ready to hand to `docker pull`. An empty digest +/// in `images.toml` is treated as "not yet pinned" and the entry is omitted +/// from `IMAGE_DIGESTS`; `IMAGE_BASES` always carries the unpinned reference +/// so `docker.rs` can fall back to a tag pull when no digest is recorded. +fn emit_image_digests() { + println!("cargo:rerun-if-changed={}", IMAGE_CATALOGUE_PATH); + + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR must be set by cargo"); + let out_path = Path::new(&out_dir).join("image_digests.rs"); + + let toml_text = match std::fs::read_to_string(IMAGE_CATALOGUE_PATH) { + Ok(s) => s, + Err(_) => { + // Missing catalogue (fresh checkout without the file) — emit + // empty maps so the runtime include still compiles. + std::fs::write( + &out_path, + "/// generated empty IMAGE_DIGESTS — images.toml missing\n\ + pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = \ + phf::phf_map! {};\n\ + pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = \ + phf::phf_map! {};\n", + ) + .expect("write empty image digests stub"); + return; + } + }; + + let entries = parse_image_catalogue(&toml_text); + + let mut out = String::new(); + out.push_str("// generated by build.rs from tools/image-builder/images.toml — do not edit\n\n"); + + // IMAGE_DIGESTS: only entries with a non-empty digest survive. + out.push_str( + "pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = phf::phf_map! {\n", + ); + for e in &entries { + if e.digest.is_empty() { + continue; + } + let pinned = format!("{}@{}", e.base, e.digest); + out.push_str(&format!( + " \"{}\" => \"{}\",\n", + escape(&e.toolchain_id), + escape(&pinned), + )); + } + out.push_str("};\n\n"); + + // IMAGE_BASES: every entry, digest stripped. Used by docker.rs when no + // digest is pinned yet so a `docker pull ` is still possible. + out.push_str( + "pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = phf::phf_map! {\n", + ); + for e in &entries { + out.push_str(&format!( + " \"{}\" => \"{}\",\n", + escape(&e.toolchain_id), + escape(&e.base), + )); + } + out.push_str("};\n"); + + std::fs::write(&out_path, out).expect("write image_digests.rs"); +} + +#[derive(Default)] +struct ImageEntry { + toolchain_id: String, + base: String, + digest: String, +} + +/// Tiny TOML parser scoped to the `[[image]] toolchain_id = …` shape used +/// by `images.toml`. Only the three fields we consume here are extracted; +/// the rest of each entry (`toolchain`, `packages`) is ignored. +fn parse_image_catalogue(src: &str) -> Vec { + let mut entries: Vec = Vec::new(); + let mut current: Option = None; + + for raw_line in src.lines() { + let line = strip_comment(raw_line).trim(); + if line.is_empty() { + continue; + } + + if line == "[[image]]" { + if let Some(prev) = current.take() + && !prev.toolchain_id.is_empty() + { + entries.push(prev); + } + current = Some(ImageEntry::default()); + continue; + } + + if line.starts_with("[[") || line.starts_with('[') { + // Any other section ends accumulation. + if let Some(prev) = current.take() + && !prev.toolchain_id.is_empty() + { + entries.push(prev); + } + continue; + } + + let Some(slot) = current.as_mut() else { + continue; + }; + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + let value = value.trim().trim_matches('"').trim_matches('\''); + match key { + "toolchain_id" => slot.toolchain_id = value.to_owned(), + "base" => slot.base = value.to_owned(), + "digest" => slot.digest = value.to_owned(), + _ => {} + } + } + + if let Some(prev) = current.take() + && !prev.toolchain_id.is_empty() + { + entries.push(prev); + } + + entries +} diff --git a/default-nyx.conf b/default-nyx.conf index 9a6d32e7..49a14c38 100644 --- a/default-nyx.conf +++ b/default-nyx.conf @@ -8,12 +8,20 @@ [scanner] +## Analysis mode: full | ast | cfg | taint +## full = AST analyses + CFG + state + taint +## ast = AST analyses only (tree-sitter patterns + auth analysis; no CFG/taint/state) +## cfg = CFG + state + taint only (no AST patterns) +## taint = taint-focused CFG analysis only (no AST patterns, no state findings) +mode = "full" + ## Minimum severity level to include in the report -## Possible values: Low | Medium | High | Critical +## Possible values: Low | Medium | High min_severity = "Low" -## Maximum file size to scan (MiB); null = unlimited -max_file_size_mb = null +## Maximum file size to scan (MiB); null = unlimited. +## Raise or set to `null` when scanning a trusted codebase with large generated files or bundles. +max_file_size_mb = 16 ## File extensions to ignore completely excluded_extensions = [ @@ -30,7 +38,7 @@ excluded_directories = [ ## Individual files to ignore completely excluded_files = [] -## Honour global ignore file (e.g. ~/.config/nyx/ignore) +## Honour global ignore file (e.g. ~/.config/nyx/ignore) (RESERVED) read_global_ignore = false ## Honour .gitignore / .hgignore, etc. @@ -48,43 +56,112 @@ follow_symlinks = false ## Scan hidden files (dot-files) scan_hidden_files = false +## Enable state-model dataflow analysis (resource lifecycle + auth state). +## Detects use-after-close, double-close, resource leaks, and unauthed access. +## Requires mode = "full" or "cfg" (or explicit taint/state-capable scans). Default: on. +enable_state_analysis = true + +## Enable AST-based authorization analysis for supported web frameworks. +## Produces `.auth.*` findings such as admin-route, ownership, token, +## and stale-auth checks. Runs only when AST analysis is active: +## mode = "full" or "ast" => auth analysis runs +## mode = "cfg" or "taint" => auth analysis is skipped +## Per-language auth overrides live under [analysis.languages..auth]. +enable_auth_analysis = true + +## Run dynamic verification on Medium/High confidence findings after static analysis. +## Default builds include this support. Use --no-verify or set this false for +## fast static-only scans, or when building with --no-default-features. +verify = true + +## Also verify Low-confidence findings. Slower; intended for payload tuning. +verify_all_confidence = false + +## Dynamic sandbox backend: auto | docker | process | firecracker +## auto uses Docker when available, otherwise the process backend. +verify_backend = "auto" + +## Process-backend hardening profile: standard | strict +harden_profile = "standard" + +## Catch per-file panics during analysis and continue the scan. +## When false (default), a panic in one file's analyser aborts the whole +## scan — useful for catching engine bugs loudly in development. +## When true, the poisoned file is skipped with a warning; the rest of +## the scan proceeds. Enable when running against untrusted input. +# enable_panic_recovery = false + [database] -## Where to store the SQLite database (empty = default path) +## Custom SQLite database path (empty = platform default) (RESERVED) path = "" -## Number of days to keep database files; 0 = no cleanup (UNIMPLEMENTED) +## Number of days to keep database files; 0 = no cleanup (RESERVED) auto_cleanup_days = 30 -## Maximum database size in MiB; 0 = no limit (UNIMPLEMENTED) +## Maximum database size in MiB; 0 = no limit (RESERVED) max_db_size_mb = 1024 -## Run VACUUM on startup (UNIMPLEMENTED) +## Run VACUUM on startup vacuum_on_startup = false [output] -## Output format — only "console" exists for now +## Default output format: console | json | sarif +## Used when --format is not specified on the command line. default_format = "console" -## Suppress all console output (UNIMPLEMENTED) +## Suppress all human-readable status output (stderr) quiet = false +## Enable attack-surface ranking (sort findings by exploitability score) +attack_surface_ranking = true + ## Cap the number of issues shown; null = unlimited max_results = null +## Minimum attack-surface score to include; null = no minimum +## Findings below this threshold are dropped after ranking. +## Requires attack_surface_ranking to be enabled. +min_score = null + +## Minimum confidence level to include in output; null = no minimum +## Values: "low", "medium", "high" +# min_confidence = "medium" + +## Include Quality-category findings (excluded by default). +## Quality findings (e.g. unwrap, expect, panic) are noise-heavy and hidden +## unless this is set to true or --include-quality is passed. +include_quality = false + +## Show all findings: disables category filtering, rollups, and LOW budgets. +## Equivalent to --all on the command line. +show_all = false + +## Maximum total LOW findings to show (rollups count as 1). +max_low = 20 + +## Maximum LOW findings per file (rollups count as 1). +max_low_per_file = 1 + +## Maximum LOW findings per rule (rollups count as 1). +max_low_per_rule = 10 + +## Number of example locations stored in rollup findings. +rollup_examples = 5 + [performance] -## Maximum search depth; null = unlimited (UNIMPLEMENTED) +## Maximum search depth; null = unlimited max_depth = null -## Minimum depth for reported entries; null = none (UNIMPLEMENTED) +## Minimum depth for reported entries; null = none (RESERVED) min_depth = null -## Stop traversing into matching directories +## Stop traversing into matching directories (RESERVED) prune = false ## Worker threads; null or 0 = auto @@ -96,8 +173,213 @@ batch_size = 100 ## Channel capacity multiplier (capacity = threads × this) channel_multiplier = 4 -## Timeout on individual files (seconds); null = none (UNIMPLEMENTED) +## Maximum stack size for Rayon threads (bytes) +rayon_thread_stack_size = 8388608 # 8 MiB + +## Timeout on individual files (seconds); null = none (RESERVED) scan_timeout_secs = null -## Maximum memory to use in MiB; 0 = no limit (UNIMPLEMENTED) +## Maximum memory to use in MiB; 0 = no limit (RESERVED) memory_limit_mb = 512 + + +[server] + +## Enable the local web UI server (nyx serve) +enabled = true + +## Host to bind to (localhost only by default for security) +host = "127.0.0.1" + +## Port for the web UI +port = 9700 + +## Open browser automatically when serve starts +open_browser = true + +## Auto-reload UI when scan results change +auto_reload = true + +## Persist scan runs for history view +persist_runs = true + +## Maximum number of saved runs +max_saved_runs = 50 + +## Auto-sync triage decisions to .nyx/triage.json in the project root. +## When enabled, triage changes are written to this file so they can be +## committed to git and shared with your team. +triage_sync = true + + +[runs] + +## Persist scan run history to disk +persist = false + +## Maximum number of runs to keep +max_runs = 100 + +## Save scan logs with each run +save_logs = false + +## Save stdout capture with each run +save_stdout = false + +## Save code snippets in findings +save_code_snippets = true + + +# ─── Scan Profiles ────────────────────────────────────────────────── +# Named presets that override scan-related config. +# Activate with --profile on the command line. +# +# Built-in profiles: quick, full, ci, taint_only, conservative_large_repo. +# Override a built-in by defining [profiles.] here. +# +# [profiles.quick] +# mode = "ast" +# min_severity = "Medium" +# +# [profiles.ci] +# mode = "full" +# min_severity = "Medium" +# quiet = true +# default_format = "sarif" + + +# ─── Analysis engine toggles ──────────────────────────────────────── +# Release-grade switches for optional analysis passes. Every field has a +# matching CLI flag (e.g. --no-symex / --backwards-analysis), which takes +# precedence over the config value for a single run. The listed env vars +# override both config and CLI when set to "0" or "false". +# +# For a shortcut that sets the full stack in one shot, use +# `nyx scan --engine-profile {fast,balanced,deep}`. The profile applies +# before individual toggles, so you can mix (e.g. `--engine-profile fast +# --backwards-analysis`). See `docs/cli.md` for profile contents. +# +# To print the resolved engine config for a given invocation without +# running a scan, pass `--explain-engine`. + +[analysis.engine] + +## Path-constraint solving (prunes infeasible paths in taint). +## Default: on. CLI: --constraint-solving / --no-constraint-solving. +## env: NYX_CONSTRAINT=0 disables. +constraint_solving = true + +## Abstract interpretation (interval / string domains). +## Default: on. CLI: --abstract-interp / --no-abstract-interp. +## env: NYX_ABSTRACT_INTERP=0 disables. +abstract_interpretation = true + +## k=1 context-sensitive callee inlining for intra-file calls. +## Default: on. CLI: --context-sensitive / --no-context-sensitive. +## env: NYX_CONTEXT_SENSITIVE=0 disables. +context_sensitive = true + +## Demand-driven backwards taint analysis. Adds a second pass from +## candidate sinks back toward sources to recover flows the forward +## solver gave up on. Default: off because it adds scan time on large +## repos. CLI: --backwards-analysis / --no-backwards-analysis. +## env: NYX_BACKWARDS=1 enables. +backwards_analysis = false + +## Per-file tree-sitter parse timeout (ms). 0 disables the cap. +## CLI: --parse-timeout-ms. env: NYX_PARSE_TIMEOUT_MS. +parse_timeout_ms = 10000 + +[analysis.engine.symex] + +## Run the symex pipeline after taint. Produces witness strings and +## symbolic verdicts; disable only if you want raw taint output. +## Default: on. CLI: --symex / --no-symex. env: NYX_SYMEX=0 disables. +enabled = true + +## Persist and consult cross-file SSA bodies so symex can reason about +## callees defined in other files. Adds index/DB work on pass 1. +## Default: on. CLI: --cross-file-symex / --no-cross-file-symex. +## env: NYX_CROSS_FILE_SYMEX=0 disables. +cross_file = true + +## Intra-file interprocedural symex (k >= 2 via frame stack). +## Default: on. CLI: --symex-interproc / --no-symex-interproc. +## env: NYX_SYMEX_INTERPROC=0 disables. +interprocedural = true + +## Use the SMT backend when nyx was built with the `smt` feature. +## Ignored when the feature is off. +## Default: on. CLI: --smt / --no-smt. env: NYX_SMT=0 disables. +smt = true + + +# ─── Detector knobs ────────────────────────────────────────────────── +# Per-detector class suppression and enablement. These knobs target +# common false-positive classes that show up on legitimate forwarding +# pipelines (telemetry / analytics / metrics dispatch). +# +# [detectors.data_exfil] +# +# # Toggle the entire `taint-data-exfiltration` detector class. Set to +# # false on projects whose architecture routes user-derived payloads +# # through trusted forwarding boundaries by design. +# enabled = true +# +# # URL prefixes treated as trusted destinations. Outbound calls whose +# # destination argument has a static prefix (proven by the abstract +# # string domain or visible as a literal) matching one of these entries +# # have `Cap::DATA_EXFIL` dropped before event emission. Mirrors the +# # SSRF prefix-lock semantics. Use full origins or origin-prefixed +# # paths (e.g. "https://api.internal/") so partial matches across +# # unrelated hosts cannot occur. +# trusted_destinations = [ +# "https://api.internal/", +# "https://telemetry.", +# ] + + +# ─── Per-language analysis rules ───────────────────────────────────── + +# [analysis.languages.javascript.auth] +# enabled = true +# admin_path_patterns = ["/admin/"] +# admin_guard_names = ["requireAdmin", "isAdmin", "adminOnly"] +# login_guard_names = ["requireLogin", "authenticate", "requireAuth"] +# authorization_check_names = ["checkMembership", "hasWorkspaceMembership", "checkOwnership"] +# mutation_indicator_names = ["update", "delete", "create", "archive", "publish", "addMembership"] +# read_indicator_names = ["find", "findById", "get", "list"] +# token_lookup_names = ["findByToken"] +# token_expiry_fields = ["expires_at", "expiresAt"] +# token_recipient_fields = ["email", "recipient_email", "recipientEmail"] +# Auth-analysis rule IDs use language-normalized prefixes: +# javascript + typescript => js.auth.* +# python => py.auth.* ruby => rb.auth.* rust => rs.auth.* +# TypeScript inherits [analysis.languages.javascript.auth] by default; add an +# optional [analysis.languages.typescript.auth] block only for TS-specific +# overlays. These settings affect auth analysis only in "full" or "ast" mode. +# Add custom sources, sanitizers, sinks, terminators, and event handlers. +# Each language is keyed under [analysis.languages.] where slug is +# one of: rust, javascript, typescript, python, go, java, c, cpp, php, ruby. +# +# Example: recognise `escapeHtml` as an HTML sanitizer in JavaScript: +# +# [analysis.languages.javascript] +# event_handlers = ["addEventListener"] +# terminators = ["process.exit"] +# +# [[analysis.languages.javascript.rules]] +# matchers = ["escapeHtml"] +# kind = "sanitizer" +# cap = "html_escape" +# +# [[analysis.languages.javascript.rules]] +# matchers = ["location.href", "window.location.href"] +# kind = "sink" +# cap = "url_encode" +# +# Valid `kind` values: "source", "sanitizer", "sink" +# Valid `cap` values: "env_var", "html_escape", "shell_escape", +# "url_encode", "json_parse", "file_io", +# "fmt_string", "sql_query", "deserialize", +# "ssrf", "code_exec", "crypto", "all" diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..644d9a1e --- /dev/null +++ b/deny.toml @@ -0,0 +1,68 @@ +[licenses] +allow = [ + # --- Apache / MIT / BSD / permissive --- + "Apache-2.0", + "MIT", + "MIT-0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "zlib-acknowledgement", + "BSL-1.0", + "NCSA", + "PostgreSQL", + "curl", + "BlueOak-1.0.0", + "X11", + "HPND", + "TCL", + "ICU", + "Info-ZIP", + + # --- Unicode / data / specs --- + "Unicode-DFS-2016", + "Unicode-3.0", + + # --- compression / libs --- + "bzip2-1.0.6", + "libpng-2.0", + "IJG", + "FTL", + + # --- public domain style --- + "CC0-1.0", + "Unlicense", + "0BSD", + + # --- weak copyleft (GPL-compatible) --- + "MPL-2.0", + "LGPL-3.0", + "EPL-2.0", + + # --- GPL family --- + "GPL-3.0", + "GPL-3.0-or-later", + "GPL-2.0", + + # --- Python / PSF --- + "PSF-2.0", + "Python-2.0", + "Python-2.0.1", + + # --- Artistic / Perl --- + "Artistic-2.0", + + # --- LLVM / clang --- + "Apache-2.0 WITH LLVM-exception", + + # --- data / ML --- + "CDLA-Permissive-2.0", + + # --- fonts --- + "OFL-1.1", + + # --- Creative Commons (code-safe ones) --- + "CC-BY-3.0", + "CC-BY-4.0", +] \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000..a23549b2 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,35 @@ +# Summary + +# Getting started + +- [Quickstart](quickstart.md) +- [Installation](installation.md) + +# Using nyx + +- [CLI reference](cli.md) +- [Browser UI](serve.md) +- [Dynamic verification](dynamic.md) +- [Configuration](configuration.md) +- [Output formats](output.md) + +# Coverage + +- [Language maturity](language-maturity.md) +- [Rules](rules.md) +- [Auth analysis](auth.md) + +# Under the hood + +- [How it works](how-it-works.md) +- [Advanced analysis](advanced-analysis.md) +- [Detectors](detectors.md) + - [Patterns](detectors/patterns.md) + - [CFG](detectors/cfg.md) + - [State](detectors/state.md) + - [Taint](detectors/taint.md) + +# Project + +- [Roadmap](roadmap.md) +- [Changelog](changelog.md) diff --git a/docs/advanced-analysis.md b/docs/advanced-analysis.md new file mode 100644 index 00000000..d52c27d6 --- /dev/null +++ b/docs/advanced-analysis.md @@ -0,0 +1,340 @@ +# Advanced Analysis + +Nyx layers several analysis passes on top of the core SSA taint engine. +Most are switchable via config (`[analysis.engine]` in `nyx.conf` / +`nyx.local`), a matching CLI flag pair, or, as a last-resort override for +library users with no CLI entry point, a `NYX_*` environment variable. The +five precision-tuning passes (abstract interpretation, context sensitivity, +symbolic execution, constraint solving, field-sensitive points-to) are +**on by default** because the benchmark numbers in +[language-maturity.md](language-maturity.md) are measured with them on. +The demand-driven backwards walk and hierarchy fan-out sit alongside but +are not user-toggleable in the same way. + +See [`Configuration`](configuration.md#analysisengine) for the full config +surface and CLI flag table. This page explains what each pass does, why it +helps, how to disable it, and what it does not cover. + +--- + +## Abstract interpretation + +**What it does.** Propagates interval and string abstract domains through the +SSA worklist alongside taint. Integer values carry `[lo, hi]` bounds; +string values carry a prefix and suffix (plus a bit domain for known-zero / +known-one bits). Values are joined at merge points and widened at loop +heads so the worklist always terminates. + +**Why it helps.** Lets Nyx suppress some findings that are obviously safe +given the abstract value; a proven-bounded integer does not flow into a +SQL sink as an injection risk; an SSRF sink whose URL prefix is locked to a +trusted host stays quiet. This turns a large class of FPs on numeric and +locked-prefix paths into true negatives. + +**Path traversal.** The path domain accepts canonicalised-and-rooted +shapes via `PathFact::is_path_traversal_safe`: a path that is +dotdot-free and either non-absolute or carries a verified prefix-lock has +its `Cap::FILE_IO` cleared. When the lock argument is a string literal +the lock prefix is recorded directly; when it is a method call, field +access, or configured root, an `OPAQUE_PREFIX_LOCK` marker captures the +structural invariant ("rooted under SOME prefix") instead. This closes +the Ruby `File.expand_path + start_with?(root)`, Python +`os.path.realpath + .startswith(root)`, and JS +`path.resolve + .startsWith(root)` shapes. `classify_path_assertion` +recognises JS `.startsWith(...)`, Python `.startswith(...)`, Ruby +`.start_with?(...)` (paren and paren-less), and Go `strings.HasPrefix(...)`. +Branch narrowing flips lock attachment under condition negation +(`if !target.startsWith(ROOT) { return; }` attaches the lock to the +surviving block, not the rejection arm). + +**How to turn it off.** + +| Surface | Value | +|---|---| +| Config | `abstract_interpretation = false` under `[analysis.engine]` | +| CLI flag | `--no-abstract-interp` | +| Env var (legacy) | `NYX_ABSTRACT_INTERP=0` | + +**Limitations.** The interval domain is 64-bit signed; very wide or +overflow-producing arithmetic degrades to `⊤` (unbounded). String prefix / +suffix tracking is concat-only; it does not model reordering, reversal, or +character-level regex constraints. Loop widening deliberately drops +changing bounds rather than chasing fixpoints. + +**Source**: [`src/abstract_interp/`](https://github.com/elicpeter/nyx/tree/master/src/abstract_interp/). + +--- + +## Context-sensitive analysis + +**What it does.** Adds k=1 call-site-sensitive taint propagation for +intra-file callees. When a function is invoked, Nyx reanalyzes the callee +body with the actual per-argument taint signature of the call site, +producing call-site-specific return taint. Results are cached by +`(function_name, ArgTaintSig)` so repeated calls with the same signature +are free. + +**Why it helps.** A helper called once with a tainted argument and once +with a sanitized argument produces two different findings; without k=1 +sensitivity, the conservative union of both call sites would be applied +to the sanitized call, producing a spurious finding there. + +**How to turn it off.** + +| Surface | Value | +|---|---| +| Config | `context_sensitive = false` under `[analysis.engine]` | +| CLI flag | `--no-context-sensitive` | +| Env var (legacy) | `NYX_CONTEXT_SENSITIVE=0` | + +**Limitations.** Intra-file only. Cross-file callees are resolved via +summaries (see `src/summary/`) rather than re-inlined. Depth is capped at +k=1 to prevent cache blow-up and re-entrancy; higher k would require a +different cache key design. Callee bodies larger than the internal +`MAX_INLINE_BLOCKS` threshold fall back to the summary path. Cache keys +hash per-argument `Cap` bits but not source-origin identity, so two +callers with identical caps but different origins share cached +origin-attribution. + +**Helper-validator propagation.** SSA summaries carry a +`validated_params_to_return` field listing parameter indices whose +taint flow to the return value is fully validated by a dominating +predicate (regex allowlist, type check, validation call) on every +return path. At call sites, each tainted argument passed to a +validated position, and the call's own return value, are marked +`validated_must` / `validated_may` in the caller's SSA taint state, +the same way an inline `if (!regex.test(x)) throw …` would validate +the surviving branch. Sound because the summary is recorded only when +the parameter's name is in `validated_must` at *every* return block; a +normal-returning call therefore proves the validating arm. JS/TS +object-pattern formals (`({ column, operator, value }) => …`) seed +every destructured sibling in the per-parameter probe, so flow through +any of them counts toward the slot being validated. + +**Source**: [`src/taint/ssa_transfer/`](https://github.com/elicpeter/nyx/tree/master/src/taint/ssa_transfer/) +(`ArgTaintSig`, `InlineCache`, `inline_analyse_callee`, +`propagate_validated_params_to_return`). + +--- + +## Field-sensitive points-to + +**What it does.** Runs a Steensgaard-style alias analysis that interns field +accesses as their own abstract locations. `c.mu` becomes `Field(c, mu)`, +distinct from `c` itself; a write to `obj.cache` and a read from +`obj.cache` in different methods both land on the same abstract location; +subscript reads and writes (`arr[i]`, `map[k] = v`) lower to synthetic +`__index_get__` / `__index_set__` calls so the engine can model them +through the same container store/load primitives used for STL containers, +Python lists, JS arrays, and similar. + +**Why it helps.** It splits a class of false positives that the +whole-variable taint model produced. Before this pass, `obj.field = +tainted; sink(obj.other_field)` would taint `obj` as a whole and fire on +the safe field; the receiver-type / sub-field distinction is also what +lets the resource-lifecycle pass attribute a `c.mu.Lock()` to the lock +field rather than to its container. Cross-method field flow (writer in +one method, reader in another) shows up only when fields have stable +identity independent of the parent value. + +**How to turn it off.** + +| Surface | Value | +|---|---| +| Env var | `NYX_POINTER_ANALYSIS=0` | + +The pass is **on by default**. The env-var override exists so you can +compare against the pre-pointer baseline. + +**Limitations.** This is not a general escape analysis. Function pointers +and arbitrary indirect calls still resolve to no callee, and deep alias +chains through `*p` / `p->field` in C/C++ are not tracked beyond the +direct field case. The points-to set per value is capped at +`--max-pointsto` (default 32); when truncation happens, an engine note +records the precision loss. + +**Source**: [`src/pointer/`](https://github.com/elicpeter/nyx/tree/master/src/pointer/). + +--- + +## Hierarchy fan-out for virtual dispatch + +**What it does.** Builds a per-language type-hierarchy index in pass 1 +(extends, implements, impl-for, includes; the exact construct depends on +the language) and uses it in pass 2 to widen method-call resolution. When +a call's receiver is statically typed as a super-class, trait, or +interface, the resolver returns every concrete implementer it has seen +in the codebase rather than just the first match. + +**Why it helps.** Without it, a call like `repository.findById(id)` where +`repository` is typed as the interface gets resolved against whatever the +single-result resolver finds first; if the matching implementer is in +another file the call effectively goes opaque. With the hierarchy, the +taint engine sees the union of every implementer's transform and the +flow shows up regardless of which file holds the concrete class. + +**Limitations.** Fan-out is capped at 8 implementers per call site; over +that, the tail is silently dropped (a debug log records the cap hit) and +the call is treated as a non-deterministic union of the kept +implementers. Languages that use structural / implicit interface +satisfaction (Go) are deliberately skipped because per-file extraction +is intractable; those calls fall back to the single-result resolver. The +extractor covers Java, Rust, TS/JS/TSX, Python, Ruby, PHP, and C++. + +**Source**: [`src/cfg/hierarchy.rs`](https://github.com/elicpeter/nyx/blob/master/src/cfg/hierarchy.rs) +and [`src/summary/mod.rs`](https://github.com/elicpeter/nyx/blob/master/src/summary/mod.rs) +(`TypeHierarchyIndex`, `resolve_callee_widened`). + +--- + +## Symbolic execution + +**What it does.** Builds a symbolic expression tree per tainted SSA value, +generates a witness string for each taint finding (the concrete-looking +shape of the dangerous value at the sink), and detects sanitization +patterns that the taint engine alone would miss. Supports string +operations (`trim`, `replace`, `toLower`, `substring`, `strlen`, …), +arithmetic, concatenation, phi nodes, and opaque calls. + +**Why it helps.** Raises finding quality. A taint finding with a rendered +witness like `"SELECT * FROM t WHERE id=" + userInput` is substantially +easier to triage than one without. Also powers some confidence-gating for +downstream display. + +**How to turn it off.** + +| Surface | Value | +|---|---| +| Config | `symex.enabled = false` under `[analysis.engine]` | +| CLI flag | `--no-symex` | +| Env var (legacy) | `NYX_SYMEX=0` | + +Two nested switches refine the scope without disabling symex entirely: + +| Setting | CLI | Env | Default | Effect | +|---|---|---|---|---| +| `symex.cross_file` | `--no-cross-file-symex` | `NYX_CROSS_FILE_SYMEX=0` | on | Consult cross-file SSA bodies so symex can reason about callees defined in other files | +| `symex.interprocedural` | `--no-symex-interproc` | `NYX_SYMEX_INTERPROC=0` | on | Intra-file interprocedural symex (k ≥ 2 via frame stack) | + +**Limitations.** Expression trees are bounded at `MAX_EXPR_DEPTH=32`; +deeper expressions degrade to `Unknown` rather than growing unboundedly. +Sanitizer detection is informational: string-replace sanitizer patterns +are reported as witness metadata, not used to clear taint. + +**Source**: [`src/symex/`](https://github.com/elicpeter/nyx/tree/master/src/symex/). + +--- + +## Demand-driven analysis + +**What it does.** After the forward pass-2 taint analysis finishes, runs a +*backwards* walk from each sink's tainted SSA operands. The walk follows +reverse SSA-edge transfer (phi fan-out, `Assign` operand-fanout, `Call` +body-expansion or arg-fanout) until it reaches a taint source, proves +the flow infeasible via an accumulated path predicate, or exhausts its +budget. Each forward finding is then annotated with the aggregate verdict: + +- `backwards-confirmed`; a matching source was reached. Finding picks + up a small confidence boost and the note appears in + `evidence.symbolic.cutoff_notes`. +- `backwards-infeasible`; every walk proved the flow unreachable. + Finding is capped to Low confidence and a user-readable limiter is + attached. +- `backwards-budget-exhausted`; the walk hit `BACKWARDS_VALUE_BUDGET` + without a verdict. Recorded as a limiter so operators can see when + the pass could not keep up. +- Inconclusive outcomes are a no-op: the forward finding is untouched. + +Because the backwards walk can consult `GlobalSummaries.bodies_by_key` +(populated by the cross-file callee body persistence layer) it closes +across file boundaries; when a callee body is not loadable the walk +falls back to fanning out over the call's arguments so local reach-back +is still possible. + +**Why it helps.** Inverts the analysis direction so budget follows +questions the scanner actually cares about; "does any source reach +*this* sink?"; instead of proving every potential source-to-sink +path. Corroborated findings are a stronger signal than forward-only +ones, and proven-infeasible flows provide a principled way to lower +confidence on forward false positives without silently dropping them. + +**How to turn it on.** Defaults off so the benchmark floor is preserved +while the pass stabilises. + +| Surface | Value | +|---|---| +| Config | `backwards_analysis = true` under `[analysis.engine]` | +| CLI flag | `--backwards-analysis` / `--no-backwards-analysis` | +| Env var (legacy) | `NYX_BACKWARDS=1` | + +**Limitations.** Reverse call-graph expansion stops at `ReachedParam`; the walk +terminates at function parameters rather than crossing back into callers. +Path-constraint pruning is conservative: only the accumulated +`PredicateSummary` bits are consulted, not the full symbolic predicate stack. +Depth-bounded at k=2 for +cross-function body expansion. See `DEFAULT_BACKWARDS_DEPTH`, +`BACKWARDS_VALUE_BUDGET`, and `MAX_BACKWARDS_CALLEE_BLOCKS` in +`src/taint/backwards.rs` for the exact bounds. + +**Cap parity.** The walk treats `DemandState.caps` as opaque bitflags, +every cap defined in `src/labels/mod.rs` round-trips identically through +the demand transfer. Including `Cap::DATA_EXFIL` (bit 13): a +`taint-data-exfiltration` forward finding receives `backwards-confirmed` +exactly like a `taint-unsanitised-flow` SQL/CMD/SSRF finding when its +demand walk reaches a Sensitive source. The cap-routing logic in +`src/ast.rs` then surfaces the rule id correctly regardless of which +direction confirmed the flow. See +`tests/backwards_analysis_tests.rs::demand_driven_suite` (the +`data_exfil` sub-case) and +`taint::backwards::tests::driver_walks_data_exfil_source_to_sink` for +the regression guards. + +**Source**: [`src/taint/backwards.rs`](https://github.com/elicpeter/nyx/blob/master/src/taint/backwards.rs). + +--- + +## Constraint solving + +**What it does.** Collects path constraints at each branch in SSA and +propagates them alongside taint. Prunes paths whose accumulated constraint +set is unsatisfiable; a taint flow guarded by `if x < 0 && x > 10` is +dropped rather than surfaced. Optionally delegates the satisfiability +check to Z3 when Nyx is built with the `smt` Cargo feature. + +**Why it helps.** Removes a class of FPs rooted in clearly-infeasible +control-flow combinations. Without path constraints, a taint flow that +only occurs when mutually-exclusive branches are simultaneously taken can +still produce a finding. + +**How to turn it off.** + +| Surface | Value | +|---|---| +| Config | `constraint_solving = false` under `[analysis.engine]` | +| CLI flag | `--no-constraint-solving` | +| Env var (legacy) | `NYX_CONSTRAINT=0` | + +The SMT backend is a separate switch: + +| Setting | CLI | Env | Default | Effect | +|---|---|---|---|---| +| `symex.smt` | `--no-smt` | `NYX_SMT=0` | on when built with `smt` feature | Delegate satisfiability checks to Z3; ignored if Nyx was built without `smt` | + +**Limitations.** The default path-constraint domain is syntactic; +trivially-inconsistent pairs are caught without an SMT solver, but richer +algebraic unsatisfiability requires the `smt` feature (Z3). Without `smt`, +Nyx ships a lightweight satisfiability check that catches literal +contradictions but not deeper reasoning. + +**Source**: [`src/constraint/`](https://github.com/elicpeter/nyx/tree/master/src/constraint/). + +--- + +## Combining the switches + +The defaults (all on) are the configuration Nyx is benchmarked against. +Turning any switch off trades precision for speed and may move findings +relative to the published baseline; CI regression gates assume defaults. +If you need a minimal-overhead scan (for very large repositories or a +pre-commit fast path), the AST-only scan mode (`--mode ast`) skips CFG, +taint, and all four advanced passes entirely and is the right tool. diff --git a/docs/assets b/docs/assets new file mode 120000 index 00000000..ec2e4be2 --- /dev/null +++ b/docs/assets @@ -0,0 +1 @@ +../assets \ No newline at end of file diff --git a/docs/auth.md b/docs/auth.md new file mode 100644 index 00000000..dead84f8 --- /dev/null +++ b/docs/auth.md @@ -0,0 +1,143 @@ +# Auth analysis + +**Rust is the stable target.** Python and Go have shipped precision work as of 0.7.0 (FastAPI cross-file dependencies, Go DAO-helper filtering, same-file caller-scope IPA) and are usable on real codebases. Ruby, Java, JavaScript, and TypeScript have rule scaffolding in [`src/auth_analysis/config.rs`](https://github.com/elicpeter/nyx/blob/master/src/auth_analysis/config.rs) but no benchmark corpus yet; treat findings there as preview. + +## What it catches + +The Rust rule is `rs.auth.missing_ownership_check`. It fires when a request handler reaches a privileged operation that takes a scoped identifier (`*_id`, row reference, scoped resource) without a preceding ownership or membership check. + +Concretely, it looks for these patterns of authorization in the function body and flags the call when none are present: + +- A call to a recognised authorization helper. Defaults: `check_ownership`, `has_ownership`, `require_ownership`, `ensure_ownership`, `is_owner`, `authorize`, `verify_access`, `has_permission`, `can_access`, `can_manage`, plus `*_membership` and `require_{group,org,workspace,tenant,team}_member` variants. Extend in `[analysis.languages.rust]`. +- An ownership-equality check on a row reference: `if owner_id != user.id { return 403 }` or any `field_id != self_actor` shape. The check writes `AuthCheck` evidence back to the row-fetch arguments via `AnalysisUnit.row_field_vars`. +- A self-actor reference: `let user = require_auth(...).await?` followed by use of `user.id`, `user.user_id`, `user.uid`. The actor is recognised from typed extractor params (`Extension`, `CurrentUser`, etc.) and from typed helper bindings. +- A typed extractor wrapper that proves route-level capability/policy enforcement: meilisearch-style `GuardedData, _>`. Recognised by outer wrapper name (last segment, case-insensitive `starts_with`) so `GuardedData, Data>` is classified by the outer `GuardedData`, not by whether an inner generic arg substring-matches `auth`. Configured via `policy_guard_names` (Rust default: `["Guarded"]`). Distinct from authentication-only wrappers so the pattern doesn't pollute regular call recognition. +- A SQL query that joins through an ACL table or filters by `user_id` predicate. Detected without a SQL parser via [`sql_semantics.rs`](https://github.com/elicpeter/nyx/blob/master/src/auth_analysis/sql_semantics.rs); the authorized result variable propagates through `let row = ...prepare(LIT)...`, `for row in result`, `let id = row.get(...)`. +- A helper-summary lift: handler calls `validate_target(db, widget_id, user.id)` whose body contains a `require_*_member` call. Cross-function summaries are merged at fixed-point (capped at 4 iterations). + +Handlers registered through attribute macros (`#[get("/path")]`, `#[routes::path(…)]`) or external service-config builders are also walked for typed-extractor guards, complementing the `.route(...)` registration path. + +## Caller-scope-entity exemption + +`.id` / `.pk` is not flagged when `` is a unit parameter named after a multi-tenant scope primitive: `organization` / `org`, `project`, `team`, `workspace`, `tenant`, `account`, `community`, `group`, `repository` / `repo`, `company`. The argument represents the caller's scope, not a user-controlled target, so internal helpers like `def get_environments(request, organization): Environment.objects.filter(organization_id=organization.id, …)` inherit the caller's authorization. Other field names (`.name`, `.slug`) still flag, and `user` / `member` / `actor` are deliberately excluded; those are handled by the actor-context recogniser. + +## Project-level web-framework gate (Rust) + +In Rust, the `context_inputs` and param-name arms of the user-input heuristic are gated by a project-level web-framework signal. The signal is three-valued: + +- `Some(true)`: the project's `Cargo.toml` names `axum`, `actix-web`, or `rocket`, OR the file directly imports one (`axum::`, `actix_web::`, `rocket::`, `axum_extra::`). Heuristics stay on. +- `Some(false)`: `Cargo.toml` was inspected and named no web framework, AND the file does not directly import one. Heuristics off; only `RouteHandler` classification (concrete route-registration evidence) survives. +- `None`: no detection ran (single-file scan with no project root). Heuristics on; behavior unchanged. + +This avoids a class of FPs in non-web Rust crates where a debug-session handle named `session` would trip on `session.update(cx, …)`-style desktop-app code. Other languages keep prior behavior; the gate is currently Rust-only. + +## Python: FastAPI cross-file dependencies + +FastAPI's `include_router` chain is resolved across files. A child router declared in `routes/task_instances.py` and attached on a parent in `routes/__init__.py` inherits the parent's `dependencies=[...]`. + +- Module-level `router = APIRouter(dependencies=[Security(...)])` is pre-walked once per file and merged onto every `@.(...)` route attached in the same file. +- `.include_router(.)` edges are captured per file in pass 1, persisted into `GlobalSummaries::router_facts_by_module`, and lifted onto the active file's `AuthorizationModel::cross_file_router_deps` at pass 2 entry. Transitive lifts (grandparent to parent to child) iterate to fixpoint. +- `Security(callable, scopes=[...])` is recognised distinctly from `Depends(callable)` and promotes the synthetic `AuthCheck` to `AuthCheckKind::Other` (route-level scope-checked authorization). Bare `Depends(callable)` is still a Login-only check. + +Module identity is the file basename without `.py`. This is sufficient for airflow-style `task_instances.router` naming; a project with two files of the same name in different subtrees will currently collide. + +## Go: DAO-helper id-scalar precision pass + +For non-route Go units, a parameter whose declared type is a bounded primitive scalar (`int64`, `uint32`, `string`, `bool`, `byte`, `rune`, `float64`, etc.) and whose name is id-shaped (`id`, `*Id`, `*_id`, `*ids`) is dropped from `unit.params` before ownership-check evaluation. + +Real Go HTTP handlers always carry a framework-request-typed param (`*http.Request`, `*gin.Context`, `echo.Context`, `*fiber.Ctx`); per-framework route extractors set `include_id_like_typed=true` so id-shaped path params survive on real routes. The filter only fires when the unit was not classified as a route handler, so helpers like `func GetRunByRepoAndID(ctx, repoID, runID int64)` are recognised as DAO callees and the ownership check is expected at the calling route handler, not inside the helper. + +## Same-file caller-scope IPA + +When a private helper is called only from authorized route handlers in the same file, the caller's auth checks lift onto the helper as synthetic `is_route_level=true` `AuthCheck` entries. + +- Iterated to a small fixpoint so transitive chains (route to mid_helper to leaf_helper) are covered. +- Refuses to authorize helpers with no in-file caller, helpers called from a mix of authorized and unauthorized callers, and helpers called only from un-lifted helpers. +- Cross-file caller-scope lifting is not implemented yet. + +This closes the FastAPI / Django / Flask shape where a route authenticates via decorator or dependency, then delegates to a private helper that performs the sink. + +## Sink classification + +The same call name can be safe on a local collection and dangerous on a database. The detector categorises each candidate sink before deciding whether to flag: + +| Class | Examples | Default treatment | +|---|---|---| +| `InMemoryLocal` | `map.insert`, `set.insert`, `vec.push` on tracked local | Never a sink | +| `RealtimePublish` | `realtime.publish_to_group`, `pubsub.send` | Sink unless ownership is established for the channel scope | +| `OutboundNetwork` | `http.post`, `reqwest::Client::post` | Sink unless a sanitiser is on the path | +| `CacheCrossTenant` | `redis.set`, `memcached.set` with scoped keys | Sink unless tenant is checked | +| `DbMutation` | `db.insert`, `repo.save` with scoped IDs | Sink unless ownership is established | +| `DbCrossTenantRead` | `db.query` returning rows from a tenant scope | Sink unless ACL-join or tenant predicate is present | + +Receiver type drives the classification when SSA type facts are available, so `client.send(...)` correctly resolves through the receiver's inferred type. + +## What it can't catch + +- **Non-Rust frameworks**, in practice. Scaffolding exists; coverage doesn't. +- **Type-system authorization.** A typestate pattern that makes unauthenticated handlers fail to compile (`fn endpoint(user: AuthenticatedUser)`) is invisible. This is mostly fine because the type system already enforced the check, but the rule won't credit it. +- **Authorization performed only via macros** that the AST doesn't expose as a recognisable call. +- **Cross-async-boundary actor binding.** If the handler awaits `let user = require_auth(...).await?` and then spawns a task that uses `user.id` after a `tokio::spawn`, the spawn body is treated as a separate scope. + +## The taint-based variant + +A second rule, `rs.auth.missing_ownership_check.taint`, folds the same logic into the SSA/taint engine using the `Cap::UNAUTHORIZED_ID` capability (bit 12). Request-bound handler parameters seed `UNAUTHORIZED_ID` into taint state; ownership checks act as sanitizers that strip the cap; sinks that take scoped IDs require it absent. + +This path is **off by default** while the standalone analyser carries the stable signal. Enable both: + +```toml +[scanner] +enable_auth_as_taint = true +``` + +Run them together; if both fire for the same site, treat it as the same finding (the taint variant carries fuller flow evidence). + +## Tuning + +### Add a project-specific authorization helper + +```toml +[[analysis.languages.rust.rules]] +matchers = ["require_subscription", "ensure_paid_seat"] +kind = "sanitizer" +cap = "unauthorized_id" +``` + +The same rule recognised in the standalone analyser also strips `Cap::UNAUTHORIZED_ID` for the taint-based variant. + +### Add a project-specific typed-extractor policy wrapper + +```toml +[analysis.languages.rust.auth] +policy_guard_names = ["MyAppGuarded", "PolicyExtractor"] +``` + +Matched as last-segment + case-insensitive `starts_with` (so a single entry `"Guarded"` covers `Guarded`, `GuardedData`, `GuardedRoute`). Distinct from `login_guard_names` and `admin_guard_names`. + +### Recognised actor names + +Recognised by default: `user.id`, `user.user_id`, `user.uid`, `session.user_id`, `current_user.id`, plus typed extractor parameters with `CurrentUser`, `SessionUser`, `AuthUser`, `Extension<...>` shapes. To add a custom binding pattern, file an issue or add a fixture; the heuristic lives in [`src/auth_analysis/extract/common.rs`](https://github.com/elicpeter/nyx/blob/master/src/auth_analysis/extract/common.rs) under the `*self_actor*` helpers (`collect_self_actor_binding`, `collect_typed_extractor_self_actor`, `is_self_actor_type_text`). + +### Suppress + +Inline: + +```rust +db.insert(widget_id, value)?; // nyx:ignore rs.auth.missing_ownership_check +``` + +Or filter by severity / confidence in CI: + +```bash +nyx scan . --severity ">=MEDIUM" --min-confidence medium +``` + +## In the UI + +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. + +

Nyx finding detail: numbered source → call → sink walk with a How to fix panel and an inline evidence object

+ +## Benchmark corpus + +The Rust auth corpus at [`tests/benchmark/corpus/rust/auth/`](https://github.com/elicpeter/nyx/tree/master/tests/benchmark/corpus/rust/auth/) covers the recognised authorization patterns, true-positive controls, typed-extractor guard injection, and the project-level web-framework gate (full-Cargo.toml fixtures under `safe_non_web_rust_project/` and `unsafe_actix_web_project_no_check/`). Per-row metrics live under the Rust auth row in `tests/benchmark/RESULTS.md`. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..bdde652d --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +{{#include ../CHANGELOG.md}} diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..b20bfb6f --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,475 @@ +# CLI Reference + +## Global + +``` +nyx [COMMAND] +nyx --version +nyx --help +``` + +--- + +## `nyx scan` + +Run a security scan on a directory. + +``` +nyx scan [PATH] [OPTIONS] +``` + +**PATH** defaults to `.` (current directory). + +### Analysis Mode + +| Flag | Default | Description | +|------|---------|-------------| +| `--mode ` | `full` | Analysis mode: `full`, `ast`, `cfg`, or `taint` | + +| Mode | What runs | +|------|-----------| +| `full` | AST patterns + CFG structural analysis + taint analysis | +| `ast` | AST patterns only (fastest, no CFG or taint) | +| `cfg` / `taint` | CFG + taint analysis only (no AST patterns) | + +**Deprecated aliases**: `--ast-only` (use `--mode ast`), `--cfg-only` (use `--mode cfg`), `--all-targets` (use `--mode full`). + +### Index Control + +| Flag | Default | Description | +|------|---------|-------------| +| `--index ` | `auto` | Index behavior: `auto`, `off`, or `rebuild` | + +| Index Mode | Behavior | +|------------|----------| +| `auto` | Use existing index if available; build if missing | +| `off` | Skip indexing, scan filesystem directly | +| `rebuild` | Force rebuild index before scanning | + +**Deprecated aliases**: `--no-index` (use `--index off`), `--rebuild-index` (use `--index rebuild`). + +### Output + +| Flag | Default | Description | +|------|---------|-------------| +| `-f, --format ` | `console` | Output format: `console`, `json`, or `sarif` | +| `--quiet` | off | Suppress status messages (stderr), including the Preview-tier banner for C/C++ scans | +| `--no-rank` | off | Disable attack-surface ranking | +| `--no-state` | off | Disable state-model analysis (resource lifecycle + auth state). Overrides `scanner.enable_state_analysis` | + +### Profiles + +| Flag | Default | Description | +|------|---------|-------------| +| `--profile ` | *(none)* | Apply a named scan profile. Built-ins: `quick`, `full`, `ci`, `taint_only`, `conservative_large_repo`. User-defined profiles override built-ins with the same name. CLI flags still take precedence over profile values | + +### Filtering + +| Flag | Default | Description | +|------|---------|-------------| +| `--severity ` | *(none)* | Filter findings by severity | +| `--min-score ` | *(none)* | Drop findings with rank score below N | +| `--min-confidence ` | *(none)* | Drop findings below this confidence level (`low`, `medium`, `high`) | +| `--require-converged` | off | Drop findings whose engine provenance notes indicate widening (over-report) or analysis bail. Keeps `under-report` findings (emitted flow is still real). Intended for strict CI gates. | +| `--fail-on ` | *(none)* | Exit code 1 if any finding >= this severity | +| `--show-suppressed` | off | Show inline-suppressed findings (dimmed, tagged `[SUPPRESSED]`) | +| `--keep-nonprod-severity` | off | Don't downgrade severity for test/vendor paths | +| `--all` | off | Disable category filtering, rollups, and LOW budgets. Shows everything | +| `--include-quality` | off | Include Quality-category findings (hidden by default) | +| `--max-low ` | `20` | Maximum total LOW findings to show | +| `--max-low-per-file ` | `1` | Maximum LOW findings per file | +| `--max-low-per-rule ` | `10` | Maximum LOW findings per rule | +| `--rollup-examples ` | `5` | Number of example locations in rollup findings | +| `--show-instances ` | *(none)* | Expand all instances of a specific rule (bypass rollup) | + +`nyx scan` automatically reads `.nyx/triage.json` from the scan root when the +file exists. Terminal triage states written by `nyx serve` (`false_positive`, +`accepted_risk`, `suppressed`, and `fixed`) are hidden from CLI output and do +not trigger `--fail-on` by default. Use `--show-suppressed` to include them in +console, JSON, or SARIF output with their `triage_state` and optional +`triage_note`. + +**Severity expression formats**: + +```bash +--severity HIGH # Only high +--severity "HIGH,MEDIUM" # High or medium +--severity ">=MEDIUM" # Medium and above (high + medium) +--severity ">= low" # All severities (case-insensitive) +``` + +**Deprecated aliases**: `--high-only` (use `--severity HIGH`), `--include-nonprod` (use `--keep-nonprod-severity`). + +`--fail-on` returns a non-zero exit code when the threshold trips, so CI jobs fail without further wiring: + +

nyx scan with --fail-on HIGH against a small fixture: three HIGH taint findings printed, followed by exit=1 from the shell

+ +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: + +

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

+ +### Analysis Engine Toggles + +Override the corresponding `[analysis.engine]` values in `nyx.conf` for a single run. All default **on**; pass the `--no-*` variant to disable. + +| Pair | Config field | Effect when disabled | +|------|---|---| +| `--constraint-solving` / `--no-constraint-solving` | `constraint_solving` | Skip path-constraint solving; infeasible paths no longer pruned | +| `--abstract-interp` / `--no-abstract-interp` | `abstract_interpretation` | Skip interval / string / bit abstract domains | +| `--context-sensitive` / `--no-context-sensitive` | `context_sensitive` | Treat intra-file callees insensitively (summary-only) | +| `--symex` / `--no-symex` | `symex.enabled` | Skip the symex pipeline; no symbolic verdicts or witnesses | +| `--cross-file-symex` / `--no-cross-file-symex` | `symex.cross_file` | Skip extracting / consulting cross-file SSA bodies | +| `--symex-interproc` / `--no-symex-interproc` | `symex.interprocedural` | Cap symex frame stack at the entry function | +| `--smt` / `--no-smt` | `symex.smt` | Skip the SMT backend (still a no-op without the `smt` feature) | +| `--backwards-analysis` / `--no-backwards-analysis` | `backwards_analysis` | Demand-driven backwards taint walk from sinks (default **off**) | +| `--parse-timeout-ms ` | `parse_timeout_ms` | Per-file tree-sitter parse timeout (ms); `0` disables the cap | + +### Lattice-width Caps + +Two caps bound the width of taint origin sets and points-to sets per SSA value. When a set would exceed the cap, entries are truncated deterministically and an engine note (`OriginsTruncated` / `PointsToTruncated`) is recorded on affected findings so you can see when precision was lost. + +| Flag | Default | Description | +|------|---------|-------------| +| `--max-origins ` | `32` | Max taint origins retained per lattice value. Raise on very wide codebases where truncation is observed; lower only when lattice width is a measured bottleneck. Also set via `NYX_MAX_ORIGINS` | +| `--max-pointsto ` | `32` | Max abstract heap objects retained per points-to set. Raise on factory-heavy codebases where truncation is observed. Also set via `NYX_MAX_POINTSTO` | + +See [configuration.md](configuration.md#analysisengine) for the full schema. + +### Engine-Depth Profile + +Individual engine toggles are fine-grained but hard to remember in combination. The `--engine-profile` shortcut sets the whole stack in one shot, and individual flags are layered on top after the profile is applied. + +| Profile | Backwards | Symex | Abstract-interp | Context-sensitive | +|---------|-----------|-------|-----------------|-------------------| +| `fast` | off | off | off | off | +| `balanced` (default) | off | off | on | on | +| `deep` | on | on (cross-file + interprocedural) | on | on | + +All three profiles build the AST, CFG, and SSA lattice and run forward taint; the columns above show which additional analyses each profile enables. SMT (`symex.smt`) is always off unless Nyx was built with `--features smt`. + +Individual flags override the profile. For example, `--engine-profile fast --backwards-analysis` runs the fast stack but with backwards analysis on. + +### Explain Effective Engine + +`--explain-engine` prints the resolved engine configuration (profile + config + CLI overrides + env-var fallbacks) to stdout and exits without scanning. Useful for sanity-checking a CI invocation. + +```bash +nyx scan --engine-profile deep --no-smt --explain-engine +``` + +

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

+ +### Dynamic verification + +Available in default builds, or in custom builds with `--features dynamic`. See [dynamic.md](dynamic.md) for the full pipeline and verdict semantics. + +| Flag | Default | Description | +|------|---------|-------------| +| `--verify` | on | Enable dynamic verification (default when built with `dynamic`). Conflicts with `--no-verify` | +| `--no-verify` | off | Skip verification for this run. Useful for fast static-only scans without editing config | +| `--verify-all-confidence` | off | Also verify findings below `Confidence >= Medium`. Slower; intended for payload tuning | +| `--backend ` | `auto` | Sandbox backend: `auto` (docker if available, else process), `docker` (required), `process` (in-process runner) | +| `--unsafe-sandbox` | off | Force the process backend. Equivalent to `--backend process`. Cannot combine with `--backend docker` | +| `--harden ` | `standard` | Process-backend lockdown: `standard` (no-new-privs + rlimit on Linux) or `strict` (namespaces + chroot + seccomp on Linux; `sandbox-exec` on macOS) | +| `--verbose` | off | Flush the per-finding `VerifyTrace` to stderr after each verdict. Same stream that lands in `expected/trace.jsonl` in the repro bundle | + +### Baseline / patch validation + +| Flag | Default | Description | +|------|---------|-------------| +| `--baseline ` | *(none)* | Read a prior scan's JSON (or a stripped `.nyx/baseline.json`) and diff it against this scan on `stable_hash`. Reports `New` / `Resolved` / `FlippedConfirmed` / `FlippedNotConfirmed` transitions | +| `--baseline-write ` | *(none)* | After scanning, write a stripped baseline (only `stable_hash`, `dynamic_verdict`, `severity`, `path`, `rule_id`; no source). Safe to commit | +| `--gate ` | *(none)* | CI gate to enforce when `--baseline` is active. `no-new-confirmed` exits 2 on any new Confirmed finding; `resolve-all-confirmed` exits 2 if any baseline-Confirmed finding is not fully resolved | + +### Examples + +```bash +# Basic scan +nyx scan + +# Scan specific path, JSON output +nyx scan ./server --format json + +# CI gate: fail on medium+, SARIF output +nyx scan . --format sarif --fail-on medium > results.sarif + +# Fast AST-only scan, no index +nyx scan . --mode ast --index off + +# High-severity only, quiet mode +nyx scan . --severity HIGH --quiet + +# Only findings scoring 50 or above +nyx scan . --min-score 50 + +# Only medium+ confidence findings +nyx scan . --min-confidence medium + +# Show everything (no filtering, no rollups) +nyx scan . --all + +# Include quality findings but keep rollups and budgets +nyx scan . --include-quality + +# See all unwrap findings expanded +nyx scan . --include-quality --show-instances rs.quality.unwrap + +# Allow more LOW findings +nyx scan . --max-low 50 --max-low-per-file 5 +``` + +--- + +## `nyx repro` + +Replay a dynamic repro bundle for a confirmed finding. + +``` +nyx repro (--finding | --spec-hash | --bundle ) [OPTIONS] +``` + +Nyx writes repro bundles under the platform cache directory and keys them by +`spec_hash`. The browser UI and scan output show `finding_id`, so +`--finding` scans cached bundle manifests and replays the newest match. + +| Flag | Description | +|------|-------------| +| `--finding ` | Find the newest cached bundle whose manifest carries this stable finding ID | +| `--spec-hash ` | Replay an exact cache bundle by spec hash | +| `--bundle ` | Replay an explicit bundle directory | +| `--docker` | Run the bundle's Docker replay path (`./reproduce.sh --docker`) | +| `--print-path` | Print the resolved bundle path and exit without replaying | +| `--list` | With `--finding`, list all matching cached bundles newest first | + +Examples: + +```bash +nyx repro --finding b9caa35df2213040 +nyx repro --finding b9caa35df2213040 --docker +nyx repro --finding b9caa35df2213040 --print-path +nyx repro --spec-hash 8bca7f8e0311d6c9 +nyx repro --bundle /path/to/repro/8bca7f8e0311d6c9 +``` + +Exit codes mirror `reproduce.sh`: `0` pass, `1` replay mismatch, `2` Docker +unavailable, `3` process-backend toolchain mismatch. Any other script exit is +passed through. + +--- + +## `nyx index` + +Manage the SQLite file index. + +### `nyx index build` + +``` +nyx index build [PATH] [--force] +``` + +Build or update the index for the given path (default: `.`). + +| Flag | Description | +|------|-------------| +| `-f, --force` | Force full rebuild, ignoring cached file hashes | + +### `nyx index status` + +``` +nyx index status [PATH] +``` + +Display index statistics (file count, size, last modified) for the given path. + +

nyx index status output: project name, index path under the platform config dir, exists/size/modified fields

+ +--- + +## `nyx list` + +``` +nyx list [-v] +``` + +List all indexed projects. + +| Flag | Description | +|------|-------------| +| `-v, --verbose` | Show detailed information per project | + +--- + +## `nyx clean` + +``` +nyx clean [PROJECT] [--all] +``` + +Remove index data. + +| Argument/Flag | Description | +|---------------|-------------| +| `PROJECT` | Project name or path to clean | +| `--all` | Clean all indexed projects | + +--- + +## `nyx surface` + +Print the project's attack-surface map. + +``` +nyx surface [PATH] [--format ] [--build] +``` + +Loads the `SurfaceMap` persisted by the most recent indexed scan when available; otherwise runs the per-language framework probes against the on-disk source to produce an entry-points-only map. Pass `--build` to force a full inline build (pass-1 summary extraction + call-graph construction) on an unscanned project, which adds `DataStore` / `ExternalService` / `DangerousLocal` nodes the entry-points-only fallback omits. + +| Flag | Default | Description | +|------|---------|-------------| +| `--format ` | `text` | Output format: `text` (indented tree), `json` (canonical SurfaceMap), `dot` (Graphviz source), or `svg` (spawns `dot` locally) | +| `--build` | off | Force a full SurfaceMap build inline when no indexed scan exists. Same cost as `nyx index build` | + +Pipe `dot` output through `dot -Tsvg` for a renderable graph, or use `--format svg` for a one-step render when graphviz is installed. + +--- + +## `nyx serve` + +Start the local browser UI for browsing scan results. + +``` +nyx serve [PATH] [OPTIONS] +``` + +**PATH** defaults to `.` (current directory). The server binds to a loopback address only and refuses non-loopback hosts at startup. + +| Flag | Default | Description | +|------|---------|-------------| +| `-p, --port ` | *(from config)* | Port to bind to (overrides `[server].port`) | +| `--host ` | *(from config)* | Host to bind to (overrides `[server].host`) | +| `--no-browser` | off | Skip opening the browser automatically | + +See [serve.md](serve.md) for the UI tour, route map, and CSRF / host-header behaviour. + +--- + +## `nyx verify-feedback` + +Record a correction or confirmation against a dynamic-verifier verdict. Requires `--features dynamic`. + +``` +nyx verify-feedback [--wrong | --right] [--upload] +``` + +| Argument/Flag | Description | +|---------------|-------------| +| `FINDING_ID` | Stable 16-char hex id shown in `nyx scan --verify` output | +| `--wrong ` | Mark the verdict wrong and record the reason. Conflicts with `--right` | +| `--right` | Confirm the verdict. Conflicts with `--wrong` | +| `--upload` | Reserved; uploading to Nyx telemetry is not yet implemented | + +Feedback is written to the local telemetry log under the platform cache dir. + +--- + +## `nyx config` + +Manage configuration. + +### `nyx config show` + +Print the effective merged configuration as TOML. Useful for sanity-checking what the scanner is actually using after `nyx.conf` and `nyx.local` merge: + +

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

+ +### `nyx config path` + +Print the configuration directory path. + +### `nyx config add-rule` + +``` +nyx config add-rule --lang --matcher --kind --cap +``` + +Add a custom taint rule. Written to `nyx.local`. + +| Flag | Values | +|------|--------| +| `--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`, `data_exfil`, `ldap_injection`, `xpath_injection`, `header_injection`, `open_redirect`, `ssti`, `xxe`, `prototype_pollution`, `all` | + +### `nyx config add-terminator` + +``` +nyx config add-terminator --lang --name +``` + +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 ] [--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. + +--- + +## Environment variables + +Runtime behaviour: + +| Variable | Description | +|----------|-------------| +| `RUST_LOG` | Set tracing verbosity (e.g. `RUST_LOG=debug nyx scan .`) | +| `NO_COLOR` | Disable ANSI color output | + +Engine toggles (legacy, still honored; prefer CLI flags or `[analysis.engine]` config): + +| Variable | Matches | +|---|---| +| `NYX_CONSTRAINT` | `--constraint-solving` | +| `NYX_ABSTRACT_INTERP` | `--abstract-interp` | +| `NYX_CONTEXT_SENSITIVE` | `--context-sensitive` | +| `NYX_SYMEX`, `NYX_CROSS_FILE_SYMEX`, `NYX_SYMEX_INTERPROC` | `--symex` and friends | +| `NYX_SMT` | `--smt` (no-op without the `smt` feature) | +| `NYX_BACKWARDS` | `--backwards-analysis` | +| `NYX_PARSE_TIMEOUT_MS` | `--parse-timeout-ms` | +| `NYX_MAX_ORIGINS`, `NYX_MAX_POINTSTO` | `--max-origins`, `--max-pointsto` | diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..af81cc8f --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,471 @@ +# Configuration + +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`: + +

Nyx config page: General settings, Triage Sync toggle, Sources panel with language/matcher/capability dropdowns and a per-language matcher table

+ +## File Locations + +| Platform | Directory | +|----------|-----------| +| Linux | `~/.config/nyx/` | +| macOS | `~/Library/Application Support/nyx/` | +| Windows | `%APPDATA%\elicpeter\nyx\config\` | + +Run `nyx config path` to see the exact directory on your system. + +## File Precedence + +1. **`nyx.conf`**: default config (auto-created from built-in template on first run) +2. **`nyx.local`**: user overrides (loaded on top of defaults) + +Both files are optional. CLI flags take precedence over both. + +## Merge Strategy + +| Type | Behavior | +|------|----------| +| Scalars (`mode`, `min_severity`, booleans) | User value wins | +| Arrays (`excluded_extensions`, `excluded_directories`, `excluded_files`) | Union + deduplicate | +| Analysis rules | Per-language union with deduplication | +| Profiles | User profile with same name fully replaces built-in | +| Server / Runs | User value wins (full section override) | + +Example: +```toml +# nyx.conf (default): +excluded_extensions = ["jpg", "png", "exe"] + +# nyx.local (user): +excluded_extensions = ["foo", "jpg"] + +# Effective result: +# ["exe", "foo", "jpg", "png"] (sorted, deduped union) +``` + +--- + +## Full Schema + +### `[scanner]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `mode` | `"full"` \| `"ast"` \| `"cfg"` \| `"taint"` | `"full"` | Analysis mode | +| `min_severity` | `"Low"` \| `"Medium"` \| `"High"` | `"Low"` | Minimum severity to report | +| `max_file_size_mb` | int \| null | 16 | Max file size in MiB; null = unlimited. Default is a safe ceiling for untrusted repos; lift explicitly when scanning trusted codebases with large generated files | +| `excluded_extensions` | [string] | `["jpg", "png", "gif", "mp4", ...]` | File extensions to skip | +| `excluded_directories` | [string] | `["node_modules", ".git", "target", ...]` | Directories to skip | +| `excluded_files` | [string] | `[]` | Specific files to skip | +| `read_global_ignore` | bool | `false` | Honor global ignore file (RESERVED) | +| `read_vcsignore` | bool | `true` | Honor `.gitignore` / `.hgignore` | +| `require_git_to_read_vcsignore` | bool | `true` | Require `.git` dir to apply gitignore | +| `one_file_system` | bool | `false` | Don't cross filesystem boundaries | +| `follow_symlinks` | bool | `false` | Follow symbolic links | +| `scan_hidden_files` | bool | `false` | Scan dot-files | +| `include_nonprod` | bool | `false` | Keep original severity for test/vendor paths | +| `enable_state_analysis` | bool | `true` | Enable resource lifecycle + auth state analysis. Detects use-after-close, double-close, resource leaks (per-function scope), and unauthenticated access. Requires `mode = "full"` or `mode = "taint"`. | +| `enable_auth_analysis` | bool | `true` | Enable auth-state analysis within the state engine. When false, only resource lifecycle findings (leak, use-after-close, double-close) are produced. | +| `enable_panic_recovery` | bool | `false` | Catch per-file analysis panics as warnings and continue. When false, a panic aborts the scan, preserving the loud-fail behaviour for users debugging engine bugs. | +| `enable_auth_as_taint` | bool | `false` | Fold auth analysis into the SSA/taint engine via `Cap::UNAUTHORIZED_ID`. Off while the standalone path still carries stable detection. | +| `verify` | bool | `true` | Run dynamic verification on each `Confidence >= Medium` finding after the static pass. Included in default builds; custom `--no-default-features` builds need `--features dynamic`. CLI overrides: `--verify` / `--no-verify`. | +| `verify_all_confidence` | bool | `false` | Extend dynamic verification to findings below `Confidence::Medium`. Intended for corpus-building, not production scans. CLI: `--verify-all-confidence`. | +| `verify_backend` | string | `"auto"` | Sandbox backend for dynamic verification. `"auto"` picks docker when available else process; `"docker"` requires docker; `"process"` runs in-process (same as `--unsafe-sandbox`). | +| `harden_profile` | string | `"standard"` | Process-backend hardening profile. `"standard"` engages `PR_SET_NO_NEW_PRIVS` + `setrlimit(RLIMIT_AS)` on Linux; `"strict"` adds namespace unshare, chroot to workdir, and a default-deny seccomp filter on Linux, plus `sandbox-exec` wrapping on macOS keyed off the finding's expected cap. | + +### `[database]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `path` | string | `""` | Custom SQLite DB path; empty = platform default (RESERVED) | +| `auto_cleanup_days` | int | `30` | Days to keep DB files (RESERVED) | +| `max_db_size_mb` | int | `1024` | Maximum DB size in MiB (RESERVED) | +| `vacuum_on_startup` | bool | `false` | Run VACUUM before indexed scans | + +### `[output]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `default_format` | `"console"` \| `"json"` \| `"sarif"` | `"console"` | Default output format (used when `--format` is not specified) | +| `quiet` | bool | `false` | Suppress status messages | +| `max_results` | int \| null | null | Cap number of findings; null = unlimited | +| `attack_surface_ranking` | bool | `true` | Enable attack-surface ranking | +| `min_score` | int \| null | null | Minimum rank score to include; null = no minimum | +| `min_confidence` | string \| null | null | Minimum confidence level (`"low"`, `"medium"`, `"high"`); null = no minimum | +| `include_quality` | bool | `false` | Include Quality-category findings (hidden by default) | +| `show_all` | bool | `false` | Disable category filtering, rollups, and LOW budgets | +| `max_low` | int | `20` | Maximum total LOW findings to show (rollups count as 1) | +| `max_low_per_file` | int | `1` | Maximum LOW findings per file (rollups count as 1) | +| `max_low_per_rule` | int | `10` | Maximum LOW findings per rule (rollups count as 1) | +| `rollup_examples` | int | `5` | Number of example locations stored in rollup findings | + +### `[performance]` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `max_depth` | int \| null | null | Max filesystem traversal depth; null = unlimited | +| `min_depth` | int \| null | null | Min depth for reported entries (RESERVED) | +| `prune` | bool | `false` | Stop traversing into matching directories (RESERVED) | +| `worker_threads` | int \| null | null | Worker thread count; null/0 = auto-detect | +| `batch_size` | int | `100` | Files per index batch | +| `channel_multiplier` | int | `4` | Channel capacity = threads x multiplier | +| `rayon_thread_stack_size` | int | `8388608` | Rayon thread stack size in bytes (8 MiB) | +| `scan_timeout_secs` | int \| null | null | Per-file timeout in seconds (RESERVED) | +| `memory_limit_mb` | int | `512` | Max memory in MiB (RESERVED) | + +### `[server]` + +Configuration for the local web UI (`nyx serve`). + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | bool | `true` | Whether the serve command is enabled | +| `host` | string | `"127.0.0.1"` | Host to bind to (localhost by default) | +| `port` | int | `9700` | Port for the web UI | +| `open_browser` | bool | `true` | Open browser automatically on serve | +| `auto_reload` | bool | `true` | Auto-reload UI when scan results change | +| `persist_runs` | bool | `true` | Persist scan runs for history view | +| `max_saved_runs` | int | `50` | Maximum number of saved runs | +| `triage_sync` | bool | `true` | Auto-sync triage decisions to `.nyx/triage.json` in the project root so changes can be committed to git. | + +### `[runs]` + +Configuration for scan run persistence and history. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `persist` | bool | `false` | Persist scan run history to disk | +| `max_runs` | int | `100` | Maximum number of runs to keep | +| `save_logs` | bool | `false` | Save scan logs with each run | +| `save_stdout` | bool | `false` | Save stdout capture with each run | +| `save_code_snippets` | bool | `true` | Save code snippets in findings | + +### `[profiles.]` + +Named scan presets that override scan-related config. Activate with `--profile `. + +All fields are optional; omitted fields inherit from the base config. + +| Field | Type | Description | +|-------|------|-------------| +| `mode` | string | Analysis mode | +| `min_severity` | string | Minimum severity | +| `max_file_size_mb` | int | Max file size in MiB | +| `include_nonprod` | bool | Keep original severity for test/vendor | +| `enable_state_analysis` | bool | Enable state analysis | +| `default_format` | string | Output format | +| `quiet` | bool | Suppress status output | +| `attack_surface_ranking` | bool | Enable ranking | +| `max_results` | int | Max findings | +| `min_score` | int | Min rank score | +| `show_all` | bool | Show all findings | +| `include_quality` | bool | Include quality findings | +| `worker_threads` | int | Worker thread count | +| `max_depth` | int | Max traversal depth | + +**Built-in profiles:** + +| Name | Description | +|------|-------------| +| `quick` | AST-only, medium+ severity | +| `full` | Full analysis with state analysis enabled | +| `ci` | Full analysis, medium+ severity, quiet, SARIF output | +| `taint_only` | Taint analysis only | +| `conservative_large_repo` | AST-only, high severity, 5 MiB file limit, depth 10 | + +User-defined profiles with the same name as a built-in will override it. + +### `[analysis.engine]` + +Release-grade switches for the optional analysis passes. Each toggle has a +matching CLI flag (pair of `--foo` / `--no-foo`) that overrides the config +value for a single run. These used to be `NYX_*` environment variables +(`NYX_CONSTRAINT`, `NYX_ABSTRACT_INTERP`, `NYX_SYMEX`, `NYX_CROSS_FILE_SYMEX`, +`NYX_SYMEX_INTERPROC`, `NYX_CONTEXT_SENSITIVE`, `NYX_BACKWARDS`, +`NYX_PARSE_TIMEOUT_MS`, `NYX_SMT`); those env vars are still honored as a +fallback default when nyx is used as a library (no CLI entry point), but the +config/CLI surface is the stable path. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `constraint_solving` | bool | `true` | Path-constraint solving (prunes infeasible paths in taint) | +| `abstract_interpretation` | bool | `true` | Interval / string / bit abstract domains carried through the SSA worklist | +| `context_sensitive` | bool | `true` | k=1 context-sensitive callee inlining for intra-file calls | +| `backwards_analysis` | bool | `false` | Demand-driven backwards taint walk from sinks (adds scan time; default off) | +| `parse_timeout_ms` | int | `10000` | Per-file tree-sitter parse timeout; `0` disables the cap | +| `max_origins` | int | `32` | Maximum taint origins retained per lattice value. Excess origins are dropped deterministically (sorted by source location) and an `OriginsTruncated` engine note is recorded. CLI: `--max-origins`. | +| `max_pointsto` | int | `32` | Maximum abstract heap objects retained per intra-procedural points-to set. Excess objects are dropped and a `PointsToTruncated` engine note is recorded. CLI: `--max-pointsto`. | + +**`[analysis.engine.symex]`** sub-section: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | bool | `true` | Run the symex pipeline after taint; adds witness strings and symbolic verdicts | +| `cross_file` | bool | `true` | Persist / consult cross-file SSA bodies so symex can reason about callees defined in other files | +| `interprocedural` | bool | `true` | Intra-file interprocedural symex (k ≥ 2 via frame stack) | +| `smt` | bool | `true` | Use the SMT backend when nyx is built with the `smt` feature; ignored otherwise | + +CLI flag map (each pair is `--enable / --no-enable`): + +| Config field | CLI flags | +|---|---| +| `constraint_solving` | `--constraint-solving` / `--no-constraint-solving` | +| `abstract_interpretation` | `--abstract-interp` / `--no-abstract-interp` | +| `context_sensitive` | `--context-sensitive` / `--no-context-sensitive` | +| `backwards_analysis` | `--backwards-analysis` / `--no-backwards-analysis` | +| `parse_timeout_ms` | `--parse-timeout-ms ` | +| `symex.enabled` | `--symex` / `--no-symex` | +| `symex.cross_file` | `--cross-file-symex` / `--no-cross-file-symex` | +| `symex.interprocedural` | `--symex-interproc` / `--no-symex-interproc` | +| `symex.smt` | `--smt` / `--no-smt` | +| `max_origins` | `--max-origins ` | +| `max_pointsto` | `--max-pointsto ` | + +**Engine-depth profile shortcut**: instead of flipping individual toggles, pass `--engine-profile {fast,balanced,deep}` to set the whole stack at once. Individual flags override the profile, so `--engine-profile fast --backwards-analysis` runs the fast stack with backwards analysis on. See `docs/cli.md` for the exact toggle matrix. + +**Explain effective engine**: pass `--explain-engine` to print the resolved engine configuration (profile + config + CLI overrides) and exit without scanning. + +### `[chain]` + +Bounded-DFS path search across taint findings. Emits multi-step attack chains when several findings link through shared SSA values or call edges. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `max_depth` | int | `4` | Maximum per-finding hops in a single chain path. | +| `min_score` | float | `9.5` | Score threshold; chains below this value are dropped. | +| `reverify_top_n` | int | `5` | Only the top-N chains by score are eligible for composite dynamic re-verification. `0` disables composite re-verification. | + +### `[telemetry]` + +Sampling policy for the on-disk event log written by dynamic verification (`~/.cache/nyx/dynamic/events.jsonl`). Confirmed and Inconclusive verdicts are calibration-critical and kept by default; other verdict statuses can be downsampled to bound log growth. Decisions are seeded by `spec_hash` for determinism. See `docs/dynamic.md` for the on-disk schema and `NYX_NO_TELEMETRY=1` opt-out. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `keep_all_confirmed` | bool | `true` | Always retain `Confirmed` verdicts. | +| `keep_all_inconclusive` | bool | `true` | Always retain `Inconclusive` verdicts. | +| `sample_rate_other` | float | `1.0` | Retention probability for verdicts not covered by the keep-all flags. `1.0` keeps everything, `0.0` drops everything. | + +### `[detectors.data_exfil]` + +Per-project tuning for the `taint-data-exfiltration` rule. All fields are optional. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | bool | `true` | Set `false` to strip `Cap::DATA_EXFIL` from sink caps before emission. No `taint-data-exfiltration` finding reaches the report. Other taint classes are not affected. | +| `trusted_destinations` | [string] | `[]` | URL prefixes that drop `Cap::DATA_EXFIL` on the call site. Matched against the abstract-string domain prefix of the destination arg, so a literal URL or a template literal with a static prefix both work. Use full origins or origin-pinned paths and include the trailing `/`, otherwise `https://api.` matches `https://api.evil.example.com/` too. | + +```toml +[detectors.data_exfil] +enabled = true +trusted_destinations = [ + "https://api.internal/", + "https://telemetry.example.com/", +] +``` + +For the sanitizer convention, source sensitivity gate, and per-language sink coverage, see [Detectors / Taint / DATA_EXFIL](detectors/taint.md#data_exfil-suppression-layers). + +### `[analysis.languages.]` + +Per-language custom rules. `` is one of: `rust`, `javascript`, `typescript`, `python`, `go`, `java`, `c`, `cpp`, `php`, `ruby`. + +| Field | Type | Description | +|-------|------|-------------| +| `rules` | array of rule objects | Custom label rules | +| `terminators` | [string] | Functions that terminate execution | +| `event_handlers` | [string] | Event handler function names | + +**Rule object**: + +```toml +[[analysis.languages.javascript.rules]] +matchers = ["escapeHtml"] +kind = "sanitizer" # "source" | "sanitizer" | "sink" +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" | "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 + +### Minimal override (`nyx.local`) + +```toml +[scanner] +min_severity = "Medium" + +[output] +default_format = "json" +max_results = 100 +``` + +### CI-optimized + +```toml +[scanner] +mode = "full" +min_severity = "Medium" +excluded_directories = ["node_modules", ".git", "target", "vendor", "dist"] + +[output] +quiet = true +default_format = "sarif" + +[performance] +worker_threads = 4 +``` + +### Using a scan profile + +```bash +# Use a built-in profile +nyx scan --profile ci + +# CLI flags still override profile values +nyx scan --profile ci --format json +``` + +### Custom profile + +```toml +[profiles.security_audit] +mode = "full" +min_severity = "Low" +enable_state_analysis = true +show_all = true +``` + +### Custom rules for a Node.js project + +```toml +[analysis.languages.javascript] +terminators = ["process.exit", "abort"] +event_handlers = ["addEventListener"] + +[[analysis.languages.javascript.rules]] +matchers = ["escapeHtml", "sanitizeInput"] +kind = "sanitizer" +cap = "html_escape" + +[[analysis.languages.javascript.rules]] +matchers = ["dangerouslySetInnerHTML"] +kind = "sink" +cap = "html_escape" + +[[analysis.languages.javascript.rules]] +matchers = ["getRequestBody", "readUserInput"] +kind = "source" +cap = "all" +``` + +### Adding rules via CLI + +```bash +# Add a sanitizer +nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape + +# Add a terminator +nyx config add-terminator --lang javascript --name process.exit + +# Verify +nyx config show +``` + +--- + +## Config Validation + +Config is validated after loading and merging. Validation checks include: + +- Server port must be 1 to 65535 +- Server host must not be empty +- `max_saved_runs` must be > 0 when `persist_runs` is true +- `max_runs` must be > 0 when `persist` is true +- `batch_size` and `channel_multiplier` must be > 0 +- `rollup_examples` must be > 0 +- Profile names must be alphanumeric with underscores only + +Invalid config produces structured error messages identifying the section, field, and issue. + +--- + +## State Analysis + +State analysis detects resource lifecycle violations (use-after-close, double-close, resource leaks) and unauthenticated access patterns. It is **enabled by default**. + +To disable: + +```toml +[scanner] +enable_state_analysis = false +``` + +State analysis requires `mode = "full"` or `mode = "taint"`. It has no effect in `mode = "ast"`. + +**Tradeoffs**: +- Additional per-function state-machine pass adds some scan time +- May produce findings that require domain knowledge to evaluate (e.g., whether a resource handle is intentionally left open) +- Most useful for C, C++, Rust, Go, and Java where acquire/release patterns are common + +--- + +## Upgrading + +### Engine-version mismatch is handled automatically + +Nyx stores the scanner's `CARGO_PKG_VERSION` in the project index database. +When the version recorded in the DB differs from the running binary, or the +row is missing entirely, every cached summary, SSA body, and file-hash row +is wiped on the next open. The next scan rebuilds the index against the new +engine. No flag is needed; CI pipelines keep working across upgrades. + +The rebuild is logged at `info` level: + +``` +engine version changed (), rebuilding index +``` + +If you see this once per upgrade it is working as intended. If you see it on +every scan, the metadata row is not being persisted; file an issue. + +### Forcing a reindex + +Use `--index rebuild` to throw away the current project's cached summaries +and re-run pass 1 against the current rules. Useful after editing +`nyx.local` rules, after an upgrade that changed label definitions without +changing the engine version, or when you want a known-clean baseline: + +```bash +nyx scan --index rebuild . +``` + +This clears the current project's rows in `files`, `function_summaries`, +`ssa_function_summaries`, and `ssa_function_bodies`; other projects sharing +the same DB directory are untouched. + +### Recovering from a corrupt database + +If the `.sqlite` file itself is damaged (e.g. from a killed scan or full +disk) and `nyx scan` fails to open it, delete the file and let the next +scan recreate it: + +```bash +rm "$(nyx config path)"/.sqlite* +``` + +On the next scan Nyx builds a fresh index from scratch. + +--- + +## Reserved Fields + +Some config fields are defined but not yet implemented. They are marked `(RESERVED)` in the default config and accept values without effect. Config files stay forward-compatible: settings start having an effect when the feature ships, with no edit needed. diff --git a/docs/detectors.md b/docs/detectors.md new file mode 100644 index 00000000..7a2019dc --- /dev/null +++ b/docs/detectors.md @@ -0,0 +1,102 @@ +# Detectors + +Nyx ships four independent detector families. They run together in `--mode full`, the default. Findings are merged, deduplicated, ranked, and printed in one result set. + +| Family | Rule prefix | Looks at | What it finds | +|---|---|---|---| +| [Taint analysis](detectors/taint.md) | `taint-*` | Cross-file dataflow | Unsanitized data flowing source to sink | +| [CFG structural](detectors/cfg.md) | `cfg-*` | Per-function control flow | Auth gaps, unguarded sinks, error fallthrough, resource release on all paths | +| [State model](detectors/state.md) | `state-*` | Per-function state lattice | Use-after-close, double-close, leaks, unauthenticated access | +| [AST patterns](detectors/patterns.md) | `..` | Tree-sitter structural match | Banned APIs, weak crypto, dangerous constructs | + +```mermaid +flowchart LR + Taint["Taint analysis
cross-file source-to-sink"] --> Normalize["Normalize findings"] + Cfg["CFG structural
guards, exits, resource paths"] --> Normalize + State["State model
resource and auth lattice"] --> Normalize + Ast["AST patterns
tree-sitter structural match"] --> Normalize + Normalize --> Dedupe["Deduplicate
same site, rule, severity"] + Dedupe --> Rank["Rank
severity, evidence, context"] + Rank --> Output["Console, JSON, SARIF, UI"] +``` + +The taint family is split into cap-specific rule classes when a sink callee carries multiple vulnerability classes: + +| Rule id | Cap | Surface | +|---|---|---| +| `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. + +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). + +## How they combine + +In `--mode full`: + +1. **Taint and AST can both fire on one line.** If `eval(userInput)` triggers both `js.code_exec.eval` (AST) and `taint-unsanitised-flow` (taint), both are kept with distinct rule IDs. The taint finding ranks higher because of the analysis-kind bonus. +2. **State supersedes CFG on resource leaks.** When `state-resource-leak` and `cfg-resource-leak` fire at the same location, the CFG one is dropped. +3. **Exact duplicates are removed.** Same line, column, rule ID, severity → one finding. + +## Modes + +| Mode | Active detectors | +|---|---| +| `full` (default) | All four | +| `ast` | AST patterns only | +| `cfg` | Taint + CFG + State (no AST patterns) | +| `taint` | Taint + State | + +## Attack-surface ranking + +Every finding gets a deterministic score. Findings are sorted by descending score by default. Disable with `--no-rank` or `output.attack_surface_ranking = false`. + +``` +score = severity_base + analysis_kind + evidence_strength + state_bonus - validation_penalty +``` + +| Component | Values | +|---|---| +| Severity base | High=60, Medium=30, Low=10 | +| Analysis kind | taint=+10, taint-data-exfiltration=+7, state=+8, cfg with evidence=+5, cfg without evidence=+3, ast=+0 | +| Evidence strength | +1 per evidence item up to 4; +2 to +6 for source kind | +| State bonus | use-after-close / unauthed=+6, double-close=+3, must-leak=+2, may-leak=+1 | +| Validation penalty | -5 if path-validated | + +DATA_EXFIL is calibrated below other taint classes by design. Severity is High only when the source carries credential / session material (cookies, env vars); other Sensitive sources (request headers, file system, database, caught exception) downgrade to Medium. Confidence is capped at Medium and only fires Medium when the abstract / symbolic domain corroborates a concrete string body reaching the outbound payload; otherwise it falls to Low. A guarded flow (`path_validated`) drops a confidence tier. The intent is to seat data-exfiltration findings below SSRF / SQLi / command-injection but above informational AST patterns. + +Source-kind contributions (taint only): + +| Source | Bonus | +|---|---| +| User input (`req.body`, `argv`, `stdin`, `form`, `query`, `params`) | +6 | +| Environment (`env::var`, `getenv`, `process.env`) | +5 | +| Unknown | +4 | +| File system | +3 | +| Database | +2 | + +Approximate score ranges: + +| Finding type | Score | +|---|---| +| High taint with user input | 76 to 81 | +| High state (use-after-close) | ~74 | +| High CFG structural | 63 to 68 | +| High DATA_EXFIL (cookie / env source, body confirmed) | ~76 | +| Medium taint with env source | 45 to 50 | +| Medium DATA_EXFIL (header / fs / db / caught-exception source) | 40 to 45 | +| Medium state (resource leak) | ~40 | +| Low AST-only pattern | ~10 | + +For the engine's runtime model (passes, summaries, SCC fixed-point), see [how-it-works.md](how-it-works.md). diff --git a/docs/detectors/cfg.md b/docs/detectors/cfg.md new file mode 100644 index 00000000..7cc8e1c4 --- /dev/null +++ b/docs/detectors/cfg.md @@ -0,0 +1,130 @@ +# CFG structural analysis + +Nyx builds an intra-procedural control-flow graph per function and checks structural properties: whether sinks are guarded by sanitizers or validators, whether web handlers check authentication, whether resources are released on all exit paths, and whether error paths terminate before reaching dangerous code. + +These detectors use dominator analysis. A guard dominates a sink when the guard must execute before the sink on every path from entry. + +## Rule IDs + +| Rule ID | Severity | +|---|---| +| `cfg-unguarded-sink` | High/Medium | +| `cfg-auth-gap` | High | +| `cfg-unreachable-sink` | Medium | +| `cfg-unreachable-sanitizer` | Low | +| `cfg-unreachable-source` | Low | +| `cfg-error-fallthrough` | High/Medium | +| `cfg-resource-leak` | Medium | +| `cfg-lock-not-released` | Medium | + +## What it detects + +**`cfg-unguarded-sink`**: A sink call (`system`, `eval`, `Command::new`, `db.execute`, etc.) is reachable from function entry without passing through any guard or sanitizer that matches the sink's capability. + +**`cfg-auth-gap`**: A function identified as a web handler (by parameter naming conventions like `req`, `res`, `ctx`, `request`, language-dependent) reaches a privileged sink (shell execution, file I/O) without a preceding authentication call. + +**`cfg-unreachable-*`**: Sinks, sanitizers, or sources in dead code. Usually signals a refactoring error that silently disabled security-relevant logic. + +**`cfg-error-fallthrough`**: An error-handling branch (null check, error-return check) does not terminate. Execution falls through to a dangerous operation on the error path. + +**`cfg-resource-leak`, `cfg-lock-not-released`**: A resource acquisition (`File::open`, `fopen`, `socket`, `Lock`) is not matched by a release on every exit path from the function. + +## What it can't detect + +- **Inter-procedural guards.** Middleware-level auth, helper functions that internally call auth, and cleanup performed in a caller are invisible. +- **Dynamic dispatch.** Virtual calls, function pointers, closures resolve to no specific callee. +- **Correctness of guards.** The detector checks *a* guard dominates the sink. It cannot check the guard is correct. A no-op `if true {}` would suppress the finding. +- **Custom validation logic.** Only recognised guard names are checked. `if password == expected` is not a recognised guard. +- **Cross-function resource flows.** If a file handle opens in one function and closes in another, the opener gets flagged as a leak. This is the largest source of FPs on factory-pattern code. + +## Common false positives + +| Scenario | Why | Mitigation | +|---|---|---| +| Framework middleware auth | Handler doesn't call auth directly | Expected; suppress with severity filter or exclude handlers | +| RAII / defer cleanup | Implicit release not visible to CFG (partially handled for Rust Drop and Go defer) | Known limitation | +| Custom guard name | Function not in the recognised guard list | Add it as a sanitizer rule in config | +| Test handlers | Intentional lack of auth | Default non-prod downgrade reduces severity; or exclude test dirs | + +## Common false negatives + +| Scenario | Why | +|---|---| +| Auth in a called helper | Cross-function guards not tracked | +| Type-system guards | Rust `AuthenticatedUser` wrappers, typestate patterns not analysed | +| Cleanup in `finally`/`ensure`/`defer` in callers | Cross-function cleanup not tracked | + +## Tuning + +### Recognised guard names + +Nyx accepts these patterns as dominating guards: + +| Pattern | Applies to | +|---|---| +| `validate*`, `sanitize*` | All sinks | +| `check_*`, `verify_*`, `assert_*` | All sinks | +| `shell_escape` | Shell sinks | +| `html_escape` | HTML/XSS sinks | +| `url_encode` | URL sinks | +| `which` | Shell execution (binary lookup) | + +### Recognised auth names + +| Pattern | Language | +|---|---| +| `is_authenticated`, `require_auth`, `check_permission`, `authorize`, `authenticate`, `require_login`, `check_auth`, `verify_token`, `validate_token` | Cross-language | +| `middleware.auth`, `auth.required` | Go | +| `isAuthenticated`, `checkPermission`, `hasAuthority`, `hasRole` | Java | + +For Rust auth checks (`require_*`, ownership equality, row-level checks), see [auth.md](../auth.md). + +### Custom guards + +```toml +[[analysis.languages.python.rules]] +matchers = ["validate_request", "check_csrf"] +kind = "sanitizer" +cap = "all" +``` + +### Custom auth functions + +```toml +[[analysis.languages.javascript.rules]] +matchers = ["ensureLoggedIn", "requirePermission"] +kind = "sanitizer" +cap = "all" +``` + +## Examples + +Unguarded sink: + +```go +func handler(w http.ResponseWriter, r *http.Request) { + cmd := r.URL.Query().Get("cmd") + exec.Command("sh", "-c", cmd).Run() // cfg-unguarded-sink +} +``` + +Auth gap: + +```javascript +app.get('/admin/delete', (req, res) => { + // No auth call + db.execute("DELETE FROM users WHERE id = " + req.params.id); // cfg-auth-gap +}); +``` + +Resource leak: + +```c +void process() { + FILE *f = fopen("data.txt", "r"); + if (error) { + return; // cfg-resource-leak: f not closed on this path + } + fclose(f); +} +``` diff --git a/docs/detectors/patterns.md b/docs/detectors/patterns.md new file mode 100644 index 00000000..38ac501b --- /dev/null +++ b/docs/detectors/patterns.md @@ -0,0 +1,116 @@ +# AST patterns + +AST patterns are tree-sitter queries that match dangerous structural shapes in source. No dataflow, no CFG. A match means the construct is present; it's not proof the construct is exploitable. + +Patterns run in every analysis mode. In `--mode ast` they're the only active detector. + +## Rule IDs + +``` +.. +``` + +Examples: `js.code_exec.eval`, `py.deser.pickle_loads`, `c.memory.gets`, `java.sqli.execute_concat`. + +Full list: [rules.md](../rules.md). + +## Tiers + +| Tier | Meaning | +|---|---| +| **A** | Structural presence alone is high-signal. `gets`, `eval`, `pickle.loads`, `mem::transmute` | +| **B** | Pattern includes a tree-sitter heuristic guard. Example: `java.sqli.execute_concat` only fires when `executeQuery` receives a `binary_expression` (string concatenation), not a literal or a parameterized statement | + +## Categories + +| Category | Examples | +|---|---| +| CommandExec | `system`, `os.system`, `Runtime.exec`, backticks | +| CodeExec | `eval`, `Function`, PHP `assert("string")`, `class_eval`, `instance_eval` | +| Deserialization | `pickle.loads`, `yaml.load`, `Marshal.load`, `readObject`, `unserialize` | +| SqlInjection | `executeQuery`/`Query`/`execute` with concatenated argument (Tier B) | +| PathTraversal | PHP `include $var` | +| Xss | `document.write`, `outerHTML`, `insertAdjacentHTML`, `getWriter().print` | +| Crypto | `md5`, `sha1`, `Math.random`, `java.util.Random` for security use | +| Secrets | hardcoded API keys (Go, JS, TS) | +| InsecureTransport | `InsecureSkipVerify`, `fetch("http://...")` | +| Reflection | `Class.forName`, `Method.invoke`, `send`, `constantize` | +| MemorySafety | `transmute`, `unsafe`, `gets`, `strcpy`, `sprintf` | +| Prototype | `__proto__` assignment, `Object.prototype.*` | +| Config | CORS dynamic origin, `rejectUnauthorized: false`, insecure session settings | +| CodeQuality | `unwrap`, `panic!`, `as any` | + +## What patterns can't tell you + +- **Dataflow.** `eval("1+1")` (safe) and `eval(userInput)` (dangerous) both match `js.code_exec.eval`. The taint detector is the one that distinguishes them. +- **Reachability.** A pattern in dead code matches identically. +- **Semantics.** `strcpy(dst, src)` always matches, regardless of buffer sizes. +- **Indirect calls.** `let e = eval; e(input)` doesn't match `eval`. +- **Aliased imports.** `from os import system as s; s(cmd)` won't match `system`. +- **Macro expansions.** Tree-sitter parses the macro call site, not the expansion. + +## Common false positives + +| Scenario | Why | Mitigation | +|---|---|---| +| `eval("hardcoded literal")` | Pattern matches structure | Run `--mode cfg` to drop AST patterns and rely on taint | +| `unsafe` block with sound justification | Every `unsafe` matches `rs.quality.unsafe_block` | Filter `>=MEDIUM` (it's Medium) or accept the noise | +| `.unwrap()` in tests | Acceptable in test code | Default non-prod severity downgrade reduces it | +| `md5` for non-cryptographic checksums | Pattern can't see intent in most languages | PHP recognises non-crypto consuming context structurally (cache keys, ETag, dedup, `getCacheKey()` returns) and suppresses. Other languages: `--severity ">=MEDIUM"` or per-line `nyx:ignore` | +| SQL concat with trusted data (Tier B) | Heuristic can't verify the source | Taint is more precise; or convert to a parameterized query | +| C++ `reinterpret_cast(...)` for byte-pointer / void* / `sockaddr` | Pattern fires on every cast regardless of target type | Suppressed when the target is well-defined by C++ aliasing rules: `char*`, `unsigned char*`, `signed char*`, `wchar_t*`, `uint8_t*`, `int8_t*`, `std::byte*`, `byte*`, `void*`, `uintptr_t` / `intptr_t` (and `std::` variants), and the BSD socket address family. User-defined struct or class pointer targets keep firing. | +| JS / TS `secrets.fallback_secret` on `process.env.X \|\| ""` | Empty-string fallback satisfies non-undefined string types without committing a secret | Empty-string fallbacks are excluded from the rule. Non-empty literal fallbacks still fire. | + +## Confidence levels + +Every AST pattern carries an explicit confidence: + +| Confidence | Use | +|---|---| +| High | Inherently dangerous construct with no safe usage. `gets`, `pickle.loads`, `eval` with no guard | +| Medium | Likely issue, context may change the call. SQL concatenation (Tier B), `unsafe` blocks, `exec` | +| Low | Heuristic. Often appears in safe code. Weak crypto for checksums, `unwrap` outside tests, `Math.random` | + +`--min-confidence medium` (or `output.min_confidence = "medium"`) drops Low-confidence matches. + +## Tuning + +```bash +nyx scan . --severity ">=MEDIUM" # drop Low-tier patterns +nyx scan . --severity HIGH # banned APIs and code-exec only +nyx scan . --mode cfg # drop AST patterns; keep taint + state + cfg +``` + +```toml +[scanner] +excluded_directories = ["node_modules", "vendor", "generated"] +``` + +## Examples + +Tier A, structural presence: + +```c +char buf[64]; +gets(buf); // c.memory.gets +``` + +```python +import pickle +data = pickle.loads(user_input) // py.deser.pickle_loads +``` + +Tier B, heuristic guard: + +```java +// Fires: concatenated argument +stmt.executeQuery("SELECT * FROM users WHERE id=" + userId); // java.sqli.execute_concat + +// Does not fire: parameterized +stmt.executeQuery(preparedSql); +``` + +```c +printf(user_input); // c.memory.printf_no_fmt: fires (variable as fmt) +printf("%s", user_input); // does not fire (literal fmt) +``` diff --git a/docs/detectors/state.md b/docs/detectors/state.md new file mode 100644 index 00000000..e123a23a --- /dev/null +++ b/docs/detectors/state.md @@ -0,0 +1,136 @@ +# State model analysis + +Tracks resource lifecycle and authentication state through a function. Detects use-after-close, double-close, leaks, and unauthenticated access to privileged operations. + +State analysis is on by default. Disable with `scanner.enable_state_analysis = false`. It runs in `--mode full` and `--mode taint`; AST-only mode skips it. + +## Rule IDs + +| Rule ID | Severity | +|---|---| +| `state-use-after-close` | High | +| `state-double-close` | Medium | +| `state-resource-leak` | Medium | +| `state-resource-leak-possible` | Low | +| `state-unauthed-access` | High | + +## What it detects + +**`state-use-after-close`**: Resource transitions to CLOSED (via `close`, `fclose`, `disconnect`, …), then a use operation happens on it. + +```c +FILE *f = fopen("data.txt", "r"); +fclose(f); +fread(buf, 1, 100, f); // state-use-after-close +``` + +**`state-double-close`**: Resource closed twice. Crashes or undefined behaviour on most runtimes. + +**`state-resource-leak`**: Resource opened but never closed on any path through the function. Definite leak. + +**`state-resource-leak-possible`**: Resource closed on some paths but not others. Lower confidence; often an early-return error path. + +**`state-unauthed-access`**: A function recognised as a web handler reaches a privileged sink without an auth call on the path. + +A function counts as a web handler if its name starts with `handle_`, `route_`, or `api_` (sufficient on its own), or starts with `serve_`/`process_` and the file uses web-shaped parameter names (`request`, `req`, `ctx`, `res`, `response`, `w`, `writer`, language-dependent). `main` is excluded. + +## Managed-resource suppression + +Several language-specific cleanup patterns suppress leak findings: + +| Pattern | Languages | Effect | +|---|---|---| +| RAII / Drop | Rust | All leak findings suppressed except `alloc`/`dealloc` | +| Smart pointers | C++ | `make_unique`/`make_shared` treated as managed; raw `new`/`malloc` still tracked | +| `defer` | Go | `defer f.Close()` suppresses leak at exit | +| `with` context manager | Python | `with open(f) as f:` suppresses leak for the bound name | +| try-with-resources | Java | TWR-bound resources suppressed | + +## What it can't detect + +- **Cross-function resource ownership.** Open in one function, close in another, leak gets reported in the opener. The most common FP source for leak detection. +- **Factory / builder functions** that return a resource for the caller to manage. +- **Variable shadowing across scopes.** Same name in inner and outer scope shares one symbol; an inner close masks an outer leak. +- **Resources stored in collections.** Handles in arrays / maps / channels and cleaned up via iteration are not tracked. +- **Dynamic dispatch.** Close called via trait object or interface may not be recognised. +- **Type-state authentication.** `AuthenticatedRequest` and similar Rust patterns are not recognised as auth. + +## Common false positives + +| Scenario | Why | Mitigation | +|---|---|---| +| Factory returns a resource | Caller owns it | Known limitation | +| Framework-managed handles | Connection pool, request scope | Exclude framework code or downgrade | +| Variable name shadowing | Same name reused | Known limitation | + +## Per-language detection + +| Language | Leak | Double-close | Use-after-close | Notes | +|---|---|---|---|---| +| C | yes | yes | yes | `fopen`/`fclose`, `malloc`/`free`, `pthread_mutex_*` | +| C++ | yes | yes | yes | C pairs plus `new`/`delete`; smart pointers suppressed | +| Python | yes | yes | yes | `with` suppressed; `open`, `socket`, `connect` | +| Go | yes | yes | yes | `defer` suppressed; `os.Open` / `.Close` | +| Rust | unsafe only | n/a | n/a | RAII suppresses everything except `alloc`/`dealloc` | +| JavaScript | yes | yes | partial | `fs.openSync`/`closeSync` | +| TypeScript | yes | yes | partial | Same as JS | +| PHP | yes | yes | partial | `fopen`/`fclose`, `curl_init`/`curl_close`, `mysqli_*` | +| Ruby | partial | partial | partial | `File.open`/`close`, `TCPSocket` | +| Java | limited | limited | limited | Constructor-callee matching is incomplete | + +## Tuning + +```bash +nyx scan . --severity ">=MEDIUM" # Skip "possible" leaks (Low) +``` + +```toml +[scanner] +enable_state_analysis = true # default +excluded_directories = ["tests", "test", "spec"] +``` + +## Recognised pairs + +The state engine ships these acquire/release pairs. Custom pairs are not yet configurable; file an issue if you need one. + +**C / C++** + +| Acquire | Release | +|---|---| +| `fopen` | `fclose` | +| `open` | `close` | +| `socket` | `close` | +| `malloc`, `calloc`, `realloc` | `free` | +| `pthread_mutex_lock` | `pthread_mutex_unlock` | +| `new`, `new[]` *(C++)* | `delete`, `delete[]` | + +**Rust** + +| Acquire | Release | +|---|---| +| `File::open`, `File::create` | `drop`, `close` | +| `TcpStream::connect` | `shutdown` | +| `lock`, `read`, `write` (Mutex/RwLock) | `drop` | + +**Java** + +| Acquire | Release | +|---|---| +| `new FileInputStream` (and friends) | `close` | +| `getConnection` | `close` | +| `new Socket` | `close` | + +Go, Python, JavaScript, Ruby, PHP follow language-idiomatic equivalents. + +## Use-after-close triggers + +These operations on a closed resource fire `state-use-after-close`: + +``` +read, write, send, recv, fread, fwrite, fgets, fputs, fprintf, fscanf, +fflush, fseek, ftell, rewind, feof, ferror, fgetc, fputc, getc, putc, +ungetc, query, execute, fetch, sendto, recvfrom, ioctl, fcntl, +strcpy, strncpy, strcat, strncat, memcpy, memmove, memset, memcmp, +strcmp, strncmp, strlen, sprintf, snprintf +``` diff --git a/docs/detectors/taint.md b/docs/detectors/taint.md new file mode 100644 index 00000000..cb703278 --- /dev/null +++ b/docs/detectors/taint.md @@ -0,0 +1,271 @@ +# Taint analysis + +Nyx tracks untrusted data from **sources** (where it enters the program) through assignments and function calls to **sinks** (where it's used dangerously). If the flow reaches a sink without passing a matching **sanitizer**, a finding fires. + +The engine is a monotone forward dataflow over a finite lattice with guaranteed termination. It's flow-sensitive inside a function, and interprocedural across files via persisted per-function summaries. + +## Rule ID + +``` +taint-unsanitised-flow (source :) +``` + +One rule ID, parameterized by the source location. Suppressions can target either the base ID or the full string. + +## What it detects + +- User input flowing to shell execution: `req.body.cmd` → `child_process.exec` +- User input flowing to code evaluation: `req.query.code` → `eval` +- User input flowing to SQL: `request.args.get('id')` → `cursor.execute(f"... {id}")` +- Environment variables flowing to shell: `env::var("CMD")` → `Command::new("sh").arg("-c")` +- Request parameters flowing to HTML: `req.query.name` → `innerHTML` +- File contents flowing to privileged sinks: `fs::read_to_string` → `db.execute` +- Any other source-to-sink flow where the sink's required capability is not stripped along the way + +## What it can't detect + +- **Library calls without summaries.** If a callee has no summary (no source, binary-only dependency), Nyx treats it as neither propagating nor sanitizing. This is conservative for sanitization but lossy for propagation. +- **Deep pointer aliasing.** `let y = &x; sink(*y)` works through one level, but arbitrary chains of pointer arithmetic and aliased writes (`*p`, `p->field` in C/C++) are not tracked end-to-end. Function pointers and indirect calls resolve to no callee. +- **Implicit flows.** Taint follows explicit data, not branching signal. `if (secret) x = 1 else x = 0` does not taint `x`. +- **Globals and statics across functions.** Not tracked across function boundaries. + +## Common false positives + +| Scenario | Why | Mitigation | +|---|---|---| +| Custom sanitizer not recognised | Only built-in + configured sanitizers match | Add a custom sanitizer rule in config | +| Container holds mixed-typed items the engine cannot tell apart | A `vector` of port numbers and a `vector` of user input share the same store/load model | Sanitize the values on the way in (numeric parse / explicit validator) so the values themselves carry no cap, not just the container | +| Dead branches | Path-insensitive within a function | Constraint solving catches trivially infeasible combos; path-validated findings are scored lower | +| Library wrapper re-introduces taint | Wrapper opaque, or summary marks it as propagating | Summarize the wrapper explicitly or add it as a sanitizer | + +## Common false negatives + +| Scenario | Why | +|---|---| +| Third-party library on the path | No summary available, callee treated opaquely | +| Globals / statics across function boundaries | Not tracked | +| Some closure captures | Closure analysis is limited. JS/TS/Ruby/Go anonymous functions passed as callbacks *are* analyzed as separate scopes | +| Very deep cross-file chains | Summary approximation loses precision at depth | + +## Confidence signals + +Higher confidence: +- Source + Sink both present in evidence with specific call locations. +- `source_kind: user_input` (direct attacker control). +- `path_validated: false`. +- No dominating guard on the path. +- Symex produced a witness string (rendered sink value visible in JSON/SARIF `evidence.symbolic.witness`). + +Lower confidence: +- Path-validated taint (`path_validated: true`). +- Source is a database read or internal file (pre-validated at insertion is common). +- Any non-informational engine note (`SsaLoweringBailed`, `ParseTimeout`, `PredicateStateWidened`, `PathEnvCapped`, `WorklistCapped`, etc.). Use `--require-converged` to drop over-report and bail notes in strict gates. + +## Tuning + +### Custom sanitizer + +```toml +# nyx.local +[[analysis.languages.javascript.rules]] +matchers = ["escapeHtml", "sanitizeInput"] +kind = "sanitizer" +cap = "html_escape" +``` + +Or: `nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape`. + +### Filter by severity or confidence + +```bash +nyx scan . --severity HIGH +nyx scan . --min-confidence medium +``` + +### Skip dataflow entirely + +```bash +nyx scan . --mode ast +``` + +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: + +

Nyx finding detail: HIGH taint-unsanitised-flow with numbered source → call → sink steps and How to fix guidance

+ +## Example + +Rust: + +```rust +use std::env; +use std::process::Command; + +fn main() { + let cmd = env::var("USER_CMD").unwrap(); // source + Command::new("sh").arg("-c").arg(&cmd).output(); // sink +} +``` + +Finding: + +``` +[HIGH] taint-unsanitised-flow (source 5:15) src/main.rs:6:5 + Unsanitised user input flows from env::var → Command::new + Source: env::var (5:15) + Sink: Command::new +``` + +Safe rewrite: drop the shell and pass the value as argv directly (`Command::new(&cmd).output()`), or validate against an allowlist before passing to the shell. + +## Capabilities + +Sources, sanitizers, and sinks are linked by named capabilities. A sanitizer only clears taint for the cap it declares. A sink only fires when the remaining taint still carries its required cap. + +| Capability | Typical source | Typical sanitizer | Typical sink | +|---|---|---|---| +| `env_var` | `env::var`, `getenv`, `process.env` | | | +| `html_escape` | | `html.escape`, `DOMPurify.sanitize` | `innerHTML`, `document.write` | +| `shell_escape` | | `shlex.quote`, `shell_escape::escape` | `system`, `Command::new`, `eval` | +| `url_encode` | | `encodeURIComponent` | `location.href`, HTTP client URL arg | +| `json_parse` | | `JSON.parse` | | +| `file_io` | | `os.path.realpath`, `filepath.Clean`, canonicalise + `starts_with`-rooted guard | `open`, `fs::read_to_string`, `send_file` | +| `fmt_string` | | | `printf(var)` | +| `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 | +| `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. + +## Source sensitivity + +Some detector classes need to know not just *that* a value is attacker-influenced but *what kind* of value it is. Each source carries a `SourceKind` (`UserInput`, `Cookie`, `Header`, `EnvironmentConfig`, `FileSystem`, `Database`, `CaughtException`, `Unknown`) and a derived sensitivity tier: + +| Tier | Source kinds | Meaning | +|---|---|---| +| `Plain` | `UserInput` (request bodies, query strings, form fields, argv, stdin) | Attacker-controlled but already in the attacker's hands. Echoing it back to them is not a disclosure. | +| `Sensitive` | `Cookie`, `Header`, `EnvironmentConfig`, `FileSystem`, `Database`, `CaughtException`, `Unknown` | Operator-bound state that should not leak across boundaries. | +| `Secret` | (reserved for explicit credential sources) | Highest tier; treated identically to `Sensitive` today. | + +`Cap::DATA_EXFIL` only fires when the contributing source is at least `Sensitive`. Plain user input flowing into an outbound `fetch` body is suppressed at finding-emission time. That is the canonical false-positive class for API gateways and telemetry forwarders that proxy `req.body`. SSRF and other classes are unaffected; the gate is scoped to `DATA_EXFIL`. + +If a project legitimately classifies a request body as sensitive (e.g. an internal forwarder where `req.body` carries a pre-authenticated user token), override via custom rules in `nyx.conf`: + +```toml +# Treat the forwarder's outbound payload as already-sanitized so the +# DATA_EXFIL gate stops firing on it. +[[analysis.languages.javascript.rules]] +matchers = ["sanitizeOutbound"] +kind = "sanitizer" +cap = "data_exfil" +``` + +Or re-classify the source itself with a custom Source rule whose name matches one of the Sensitive substrings (`cookie`, `header`). + +## DATA_EXFIL suppression layers + +Three suppression knobs ship by default so projects can match the cap to their architecture without per-call suppressions. + +### 1. Forwarding-wrapper sanitizer convention + +A named function that exists to *forward* a payload across a known boundary is the developer's explicit decision to send the data. The default sanitizer rules treat the following identifiers as `Sanitizer(data_exfil)` in JavaScript and TypeScript: + +``` +serializeForUpstream +forwardPayload +tracker.send +analytics.track +metrics.report +logEvent +``` + +If your codebase follows this convention, the cap stops firing on these calls automatically. Extend the convention with your own forwarding wrappers via the standard custom-rule path: + +```toml +[[analysis.languages.javascript.rules]] +matchers = ["dispatchTelemetry", "sendToBus"] +kind = "sanitizer" +cap = "data_exfil" +``` + +The rule of thumb: a function that *only* exists to ship a payload to a known boundary belongs in this list. A function that *might* leak (a generic HTTP wrapper, a logging helper that writes to an arbitrary destination) does not. + +### 2. Destination allowlist + +Configure a set of trusted outbound prefixes once and the cap is dropped on every site whose destination argument has a static prefix that begins with one of them: + +```toml +[detectors.data_exfil] +trusted_destinations = [ + "https://api.internal/", + "https://telemetry.", +] +``` + +Use full origins or origin-pinned paths so a partial-host match across unrelated origins cannot occur. `https://api.` would also match `https://api.evil.example.com/`, so the entry must include the path separator (`/`) at the end of the host. + +The match consults the abstract string domain: a literal URL is a static prefix; a template literal `\`https://api.internal/${id}\`` exposes the prefix `https://api.internal/`; a fully dynamic URL has no prefix and the cap fires as usual. + +### 3. Detector-class disable + +Some projects forward user-bound payloads as a matter of architecture. Turn the entire detector class off when the noise is permanent: + +```toml +[detectors.data_exfil] +enabled = false +``` + +`enabled = false` strips `Cap::DATA_EXFIL` from sink caps before event emission, so no `taint-data-exfiltration` finding reaches the report. The decision is per-project; other projects loaded by the same `nyx serve` instance keep their own settings. + +## DATA_EXFIL sinks per language + +Sinks Nyx ships with for `Cap::DATA_EXFIL`. The body, headers, or json payload arg fires; the URL arg routes through the SSRF gate and emits `taint-unsanitised-flow` instead. + +| Language | Sinks | Example | +|---|---|---| +| JavaScript, TypeScript | `fetch(url, {body, headers, json})` body-bind, `XMLHttpRequest.prototype.send`, type-qualified `HttpClient.send` | `fetch('/upload', {method: 'POST', body: req.cookies.session})` | +| Python | `requests.post / put / patch` body and json kwargs, `httpx.AsyncClient().post` json kwarg, `aiohttp.ClientSession().post` body, dict round-trip into json | `requests.post('https://api.internal/ingest', json={'k': os.environ.get('SECRET')})` | +| Java | `HttpClient.send` with `BodyPublishers.ofString`, OkHttp `newCall(req).execute` body chain, Apache `HttpClient.execute(HttpPost)`, `RestTemplate.postForEntity / exchange`, `WebClient.post().bodyValue / body` | `client.send(HttpRequest.newBuilder().uri(...).POST(BodyPublishers.ofString(token)).build(), ...)` | +| Go | `http.Post(url, ct, body)` body arg, `http.PostForm` form arg, `(*http.Client).Do(req)` after `http.NewRequest`, `(*http.Request).Body` assignment | `http.Post("https://analytics.internal/track", "text/plain", strings.NewReader(c.Value))` | +| Rust | `reqwest::Client.post().body / json / form / multipart().send()`, `ureq::post().send_string / send_form / send_json`, `surf::post().body_string / body_json`, `hyper::Request::builder().body()` | `reqwest::Client::new().post(url).form(&secret).send()` | +| Ruby | `Net::HTTP.post(uri, body)` body arg, `Net::HTTP::Post.new(uri).body=`, `RestClient.post / put`, `HTTParty.post(url, body: ...)` body | `Net::HTTP.post(URI('https://analytics.internal/track'), "session=#{request.cookies[:auth]}")` | +| C, C++ | `curl_easy_setopt(handle, CURLOPT_POSTFIELDS, body)` and `CURLOPT_COPYPOSTFIELDS` gated sinks (macro-arg activation), `CURLOPT_POSTFIELDSIZE` body-bind | `curl_easy_setopt(curl, CURLOPT_POSTFIELDS, getenv("AUTH_TOKEN"));` | +| PHP | `curl_setopt($ch, CURLOPT_POSTFIELDS, $body)`, `Guzzle\Client.post($url, ['body' => $tainted])`, `Symfony\HttpClient->request('POST', $url, ['body' => $tainted])` | `curl_setopt($ch, CURLOPT_POSTFIELDS, $_COOKIE['session']);` | + +Add project-specific sinks with `nyx config add-rule --kind sink --cap data_exfil --matcher ` or the equivalent TOML rule. + +## DATA_EXFIL calibration ranges + +`taint-data-exfiltration` is calibrated below the other taint classes on purpose. + +| Source kind | Severity | Confidence ceiling | +|---|---|---| +| Cookie, environment variable | High | Medium | +| Header | Medium | Medium | +| File system, database | Medium | Medium | +| Caught exception | Medium | Low | + +Path-validated flows (`path_validated: true`) drop one severity tier. Confidence drops to Low when the abstract or symbolic domain cannot corroborate a concrete string reaching the outbound payload (for example, when the body comes from a callee with no summary). + +Attack-surface score ranges: + +| Finding shape | Score | +|---|---| +| High DATA_EXFIL, cookie or env source, body confirmed | around 76 | +| Medium DATA_EXFIL, header, fs, db, or caught-exception source | 40 to 45 | +| Low DATA_EXFIL, no abstract corroboration, path-validated | 18 to 25 | + +For reference: High SSRF, SQLi, cmdi land at 76 to 81; Medium taint with env source lands at 45 to 50; AST-only patterns sit around 10. Data-exfil sits below the direct-compromise classes but above informational AST patterns. diff --git a/docs/dynamic.md b/docs/dynamic.md new file mode 100644 index 00000000..01006be3 --- /dev/null +++ b/docs/dynamic.md @@ -0,0 +1,396 @@ +# Dynamic verification + +Static analysis tells you a sink is reachable from a source. Dynamic +verification tries to prove it. When verification is on, Nyx builds a small +harness around each finding, runs it in a sandbox against a curated payload +set, and stamps the result onto `evidence.dynamic_verdict`. + +It is a second signal, not a replacement for review. A `Confirmed` verdict +means Nyx triggered the sink in its harness with an attacker-controlled +payload and proved the benign control stayed clean. `NotConfirmed` means the +harness ran but nothing fired. Neither verdict closes a finding on its own. + +Default Nyx builds include the `dynamic` feature. Custom +`--no-default-features` builds run static-only unless rebuilt with +`--features dynamic`. + +## How confirmation works + +Every cap that can be verified ships a curated corpus of payload pairs: at +least one vulnerable payload and one benign control. The verifier runs both +through the same harness and compares. + +- The vulnerable payload must fire the sink. A payload "fires" when an + oracle predicate matches the observed behavior, not when a string appears + in the output. +- The benign control must stay clean. It exercises the same code path with a + value that a correct implementation handles safely. + +A finding is `Confirmed` only when at least one vulnerable payload fires and +every paired benign control stays clean. This differential rule is what keeps +the verifier from confirming a finding just because the harness echoed an +input. + +Oracles are behavioral, scoped to the cap: + +| Cap | Oracle | What it observes | +| --- | --- | --- | +| Command/code injection | stub event | the harness's exec boundary saw the injected command | +| SQL injection | stub event | the SQL boundary saw the injected clause | +| SSRF, data exfil | outbound host | the request left for a host outside the allowlist | +| Path traversal | stub event | the filesystem boundary opened a path outside the root | +| Template injection | template eval | `{{7*7}}` rendered as `49`, not echoed as text | +| Deserialization | gadget marker | a non-allowlisted class was resolved during decode | +| XXE | entity expansion | an external entity was expanded by the parser | +| LDAP / XPath injection | result count | the malicious filter returned more rows than the benign one | +| Header / CRLF | header split | an injected `\r\n` split or added a response header | +| Open redirect | redirect host | the `Location` header pointed off-origin | +| Prototype pollution | canary touch | a property write reached `Object.prototype` | +| Weak crypto | key entropy | the produced key fit inside a 16-bit search space | +| JSON parse abuse | parse depth | the parser accepted a depth past its limit | +| IDOR | ownership cross | the read crossed from the caller's id to another owner's | + +Every canary is derived per-run from `BLAKE3(spec_hash || run_nonce)`, so it is +unique per finding, collision-resistant against ambient harness output, and +never appears on the host. + +## Running it + +```bash +nyx scan # verifies Medium and High confidence findings +nyx scan --no-verify # static analysis only +nyx scan --verify # explicit form of the default behavior +nyx scan --verify-all-confidence # also verify Low-confidence findings +``` + +Use `--no-verify` for fast local checks or editor workflows. Keep +verification on for CI when scan time allows it. `--verify-all-confidence` is +slower and noisier; reach for it when tuning payloads or chasing coverage. + +## Verdicts + +| Status | Meaning | +| --- | --- | +| `Confirmed` | A vulnerable payload fired the sink and every benign control stayed clean. | +| `PartiallyConfirmed` | The sink was reached but no oracle marker was observed. The exploit chain did not complete. Treat as a strong lead, not a proof. | +| `NotConfirmed` | The harness ran but no payload fired. The path is likely infeasible or the corpus does not cover this shape. The original finding stays open until reviewed. | +| `Inconclusive` | Nyx could not finish the check. Carries a typed reason (build failed, spec derivation failed, sandbox error, policy denied, and others). | +| `Unsupported` | Nyx did not attempt the finding. Carries a typed reason (language unsupported, entry kind unsupported, no payloads for cap, confidence below threshold, no sound oracle). | + +When a `Confirmed` sink sits behind a recognized input-validation or +output-sanitization guard (Spring `@PreAuthorize`, Express `helmet`, Nest +`@UseGuards`, Django `@permission_classes`), the verdict demotes to +`ConfirmedWithKnownGuard` and the guard names land on +`differential.known_guards`. Authentication-only filters do not trigger the +demotion, since they do not mitigate injection. + +`PartiallyConfirmed` is deliberate. It marks the cases where engine work can +ratchet without the tool overstating what it proved. + +## Capability coverage + +Caps split into two groups. Data-style injection (SQL, command, path, +SSRF, XSS) uses language-neutral payload bytes (`' OR 1=1--`, `../../etc/passwd`, +a callback URL), so the harness emitter for any language can carry them. The +caps below have language-specific payloads (a Java gadget chain is not a +Python pickle), so each language is curated on its own. + +A checkmark means a tuned per-language payload set ships for that cell. Cells +without a checkmark in the data-style rows still run, falling back to the +language-neutral payload union. + +| Cap | Py | JS | TS | Java | PHP | Ruby | Go | Rust | C | C++ | +| --- | -- | -- | -- | ---- | --- | ---- | -- | ---- | - | --- | +| Command / code injection | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| SQL injection | union | union | union | union | union | union | union | ✓ | union | union | +| Path traversal | union | union | union | union | union | union | union | ✓ | union | union | +| SSRF | union | union | union | union | union | union | union | ✓ | union | union | +| XSS | union | union | union | union | union | union | union | ✓ | union | union | +| Format string | | | | | | | | | ✓ | | +| Deserialization | ✓ | | | ✓ | ✓ | ✓ | | | | | +| Template injection | ✓ | ✓ | | ✓ | ✓ | ✓ | | | | | +| XXE | ✓ | | | ✓ | ✓ | ✓ | ✓ | | | | +| LDAP injection | ✓ | | | ✓ | ✓ | | | | | | +| XPath injection | ✓ | ✓ | | ✓ | ✓ | | | | | | +| Header / CRLF | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| Open redirect | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| Prototype pollution | | ✓ | ✓ | | | | | | | | +| Weak crypto | ✓ | | | ✓ | ✓ | | ✓ | ✓ | | | +| JSON parse abuse | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| IDOR | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | | +| Data exfiltration | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | | | + +`ENV_VAR`, `SHELL_ESCAPE`, and `URL_ENCODE` are source and sanitizer caps with +no externally observable sink behavior. They route to +`Unsupported(SoundOracleUnavailable)` rather than counting as a missing-payload +gap. + +## Framework adapters + +Adapters bind a function to its external entry surface so the harness can +drive the real entry point (an HTTP request through the framework, a published +message, a scheduled fire) instead of calling the function in isolation. +Middleware and request validation participate in the verdict that way. + +| Language | HTTP routers | Other surfaces | +| --- | --- | --- | +| Python | Flask, Django, FastAPI, Starlette | Jinja2, pickle, LDAP, Celery, Kafka, SQS, Pub/Sub, RabbitMQ, Django Channels, Socket.IO, Django middleware, Django + Flask migrations | +| JavaScript | Express, Koa, NestJS, Fastify | Handlebars, Apollo + Relay GraphQL, lodash.merge + JSON deep-assign, Socket.IO, SQS, Express middleware, Knex + Prisma + Sequelize migrations | +| TypeScript | NestJS | Object.assign + lodash.merge + JSON deep-assign | +| Java | Spring, Quarkus, Micronaut, Jakarta Servlet | Thymeleaf, ObjectInputStream, Spring LDAP, Kafka, SQS, RabbitMQ, Quartz, Spring middleware, Flyway + Liquibase migrations | +| PHP | Laravel, Symfony, CodeIgniter | Twig, unserialize, LDAP, Laravel middleware, Laravel migrations | +| Ruby | Rails, Sinatra, Hanami | ERB, Marshal, Sidekiq, ActionCable, Rails middleware, Rails migrations | +| Go | Gin, Echo, Fiber, Chi | gqlgen GraphQL, NATS, Pub/Sub, go-migrate migrations | +| Rust | Axum, Actix, Rocket, Warp | Juniper GraphQL, Refinery + SQLx migrations | +| C / C++ | none | argv / stdin entry only | + +Adapters are sanitizer-aware. An XXE, header-injection, open-redirect, SSTI, +LDAP, XPath, deserialization, crypto, or data-exfil adapter declines the +binding when the surrounding source visibly hardens the call: a parser set to +`disallow-doctype-decl` or `resolve_entities=False`, a value routed through +`LdapEncoder.filterEncode` or `escape_filter_chars`, a weak primitive swapped +for `secrets.token_bytes` or `crypto.randomBytes` or `SecureRandom`, or a +redirect host checked against an allowlist. That cuts adapter false positives +without losing the genuinely dangerous calls. + +## Entry points + +The verifier knows how to stand up these entry shapes: + +`Function`, `HttpRoute`, `CliSubcommand`, `LibraryApi`, `ClassMethod`, +`MessageHandler`, `ScheduledJob`, `GraphQLResolver`, `WebSocket`, +`Middleware`, `Migration`. + +`ClassMethod` walks constructor parameters and builds the receiver, preferring +a default constructor and otherwise stubbing dependencies (`MockHttpClient`, +`MockDatabaseConnection`, `MockLogger`) up to a bounded depth. `MessageHandler` +boots an in-sandbox broker stub on loopback and publishes the payload. +`Migration` runs under a database-in-test-mode profile with no real +connection. An entry kind a language emitter does not yet support produces +`Inconclusive(EntryKindUnsupported)` with a hint, never a silent skip. + +## Sandbox backends + +```bash +nyx scan --backend auto # docker when available, else process (default) +nyx scan --backend docker # require docker +nyx scan --backend process # run on the host with weaker isolation +nyx scan --unsafe-sandbox # alias for --backend process +nyx scan --harden strict # full process-backend lockdown +``` + +Docker is the preferred backend. It mounts only the entry file's directory and +blocks outbound network by default. Nyx binds a loopback OOB listener at scan +start for callback-style payloads (SSRF, blind SSTI). When the bind succeeds, +Docker switches to bridge networking with a host-gateway route so the harness +can reach the listener; OOB payloads are skipped if the bind fails. + +The process backend runs on the host. It is useful for development and +machines without Docker, and it does not provide the same isolation. Hardening +profiles apply to it: + +- `standard` (default): no-new-privs plus a memory rlimit on Linux. No + `sandbox-exec` wrap on macOS. +- `strict`: namespace unshare, chroot to the workdir, and a default-deny + seccomp filter on Linux; `sandbox-exec -f .sb` on macOS. Opt-in, + because interpreted Linux harnesses can SIGSYS until the per-language seccomp + allowlists are widened. + +Every sink under test passes through the policy deny rules in +`src/dynamic/policy.rs` before the harness builds. Network egress, writes +outside the sandbox root, and process spawns can be denied per rule, and the +deny decision lands in the trace. + +## Performance + +Verification adds a harness build and a sandbox run per finding. Two pieces of +infrastructure keep that affordable at corpus scale. + +Per-language build pools reuse a warm toolchain across findings instead of +cold-starting one each time. Java runs a long-lived `javac` daemon; Node, PHP, +Ruby, Go, Rust, C, and C++ reuse shared module, package, and object caches; +Python layers a read-only venv with a warmed bytecode cache. The target is a +P50 harness build at or under 200ms hot and 1.5s cold, with an OWASP-scale run +finishing in 10 minutes on the dev reference machine. + +Copy-on-write workdirs (`clonefile` on macOS, `reflink` or `copy_file_range` +on Linux) replace per-finding file copies, and the worker pool routes findings +into per-cap concurrency lanes so a slow `DESERIALIZE` harness does not block +fast `SSRF` ones. + +The CI ship gate holds the with-verify to static-only wall-clock ratio at or +under 1.5x on `benches/fixtures/`. If a change pushes it past that, the gate +fails. + +## Repro artifacts + +Confirmed findings write a hermetic bundle under Nyx's platform cache +directory: + +```text +/nyx/dynamic/repro// +``` + +On Linux this is usually `~/.cache/nyx/dynamic/repro//`; on macOS +it is usually `~/Library/Caches/nyx/dynamic/repro//`. + +The bundle carries the harness spec, payload, expected output, trace, and a +`reproduce.sh`. When the toolchain is pinned in `tools/image-builder/images.toml` +it also writes a `docker_pull.sh`. + +The easiest replay path starts from the finding id shown in scan output or the +browser UI: + +```bash +nyx repro --finding +nyx repro --finding --docker +``` + +You can also replay an exact bundle by spec hash, or inspect the shell script +directly: + +```bash +nyx repro --spec-hash +cd /nyx/dynamic/repro/ +./reproduce.sh +./reproduce.sh --docker +``` + +Use the Docker form when the bundle records a pinned image or when host +toolchains differ from the original run. + +## Configuration + +```toml +[scanner] +verify = true # run dynamic verification after static analysis +verify_all_confidence = false # include findings below Confidence::Medium +verify_backend = "auto" # auto | docker | process | firecracker +harden_profile = "standard" # standard | strict +``` + +Set `verify = false` to make scans static-only unless the command line +overrides it. See [Configuration](configuration.md) for the full table. + +## Event log + +Nyx writes verdict events to: + +```text +~/.cache/nyx/dynamic/events.jsonl +``` + +Each line is a JSON object with a versioned envelope: + +```json +{ + "schema_version": 1, + "nyx_version": "0.8.0", + "corpus_version": "15", + "kind": "verdict", + "ts": "2026-06-01T18:42:09Z", + "finding_id": "a3b1...", + "spec_hash": "9f4e...", + "lang": "python", + "cap": "SQL_QUERY", + "status": "Confirmed", + "toolchain_id": "python-3.11", + "toolchain_match": "exact", + "duration_ms": 312, + "build_attempts": 1 +} +``` + +The literal `nyx_version` and `corpus_version` values shift between releases; +see `crate::dynamic::telemetry::CORPUS_VERSION` for the active payload-corpus +version your binary writes. + +| Field | Meaning | +| --- | --- | +| `schema_version` | Event schema version. Readers reject mismatches. | +| `nyx_version` | Version of the Nyx binary that wrote the event. | +| `corpus_version` | Payload corpus version used for the verdict. | +| `kind` | `verdict` or `rank_delta`. Feedback rows use an `event: "verify_feedback"` field instead. | +| `ts` | Write time in RFC 3339 format. | +| `finding_id` | Stable finding identifier. | +| `spec_hash` | Hash of the harness spec. | +| `lang` | Language slug, or `unknown` when spec derivation failed. | +| `cap` | Sink capability, such as `SQL_QUERY` or `CODE_EXEC`. | +| `status` | `Confirmed`, `PartiallyConfirmed`, `NotConfirmed`, `Inconclusive`, or `Unsupported`. | +| `inconclusive_reason` | Present when `status` is `Inconclusive`. | + +If the schema changes, move or delete the old `events.jsonl` before reading it +with the new binary. Programmatic readers should use +`crate::dynamic::telemetry::read_events(path)`. + +### Sampling + +`[telemetry]` in `nyx.toml` controls event retention: + +```toml +[telemetry] +keep_all_confirmed = true +keep_all_inconclusive = true +sample_rate_other = 1.0 +``` + +`sample_rate_other` accepts `0.0` to `1.0` and applies to `NotConfirmed` and +`Unsupported` verdicts. The decision is deterministic for a given `spec_hash`. +Confirmed, Inconclusive, and rank-delta events are always kept by default. + +Set `NYX_NO_TELEMETRY=1` to disable event writes. + +## Feedback + +To record a bad verdict: + +```bash +nyx verify-feedback --wrong "reason" +``` + +Feedback is written to the local event log. Nyx does not upload it. + +## Determinism + +Every random source is seeded from the spec hash, so two runs of the same spec +produce identical payloads and identical verdicts. `scripts/check_no_unseeded_rand.sh` +audits the tree for unseeded `rand` usage on every CI run. + +## Limitations + +- The harness drives the finding's enclosing entry function when one is + derivable, routing the payload to the tainted parameter, so a guard in the + code around the sink (a merge target built with `Object.create(null)`, an + `ObjectInputStream` subclass whose `resolveClass` enforces an allowlist, a + const-name check before `Marshal.load`) runs first and participates in the + verdict. The build-time choice is recorded on the verify trace as + `entry_invocation` (`mode=entry_function` or `mode=direct_sink`). When no + enclosing entry can be derived the harness falls back to driving the sink + directly, and that fallback can over-confirm a guard it never executes. Read + a `direct_sink` `Confirmed` as "this sink is reachable and fires on attacker + input," not "this exact code path has no in-line mitigation." Framework-level + guards (auth middleware, helmet) are also recognized and demote to + `ConfirmedWithKnownGuard`. +- Per-language payload curation is uneven. Command and code injection ship for + all ten languages; the classic data-style injection caps (SQL, path + traversal, SSRF, XSS) ship a tuned set for Rust and fall back to a + language-neutral payload union elsewhere; the framework-specific caps are + curated for the languages where they occur. The matrix above is the precise + state. +- A `NotConfirmed` verdict is not a clean bill. It means the harness did not + fire, which can be an infeasible path or a corpus that does not cover the + shape. Keep reviewing `NotConfirmed` findings. +- The process backend is weaker isolation than Docker. Use `--backend docker` + or `--harden strict` for untrusted code, and never `--unsafe-sandbox` in CI. +- Real-corpus acceptance rows (OWASP Benchmark, NodeGoat, Juice Shop, and the + polyglot set) self-skip in CI unless the corresponding `NYX_*_CORPUS` + environment variable points at a checkout. They are not vendored into the + repo. +- C and C++ have no framework adapters. Findings in those languages verify + through `argv` and `stdin` entry points only. + +## Browser UI + +`nyx serve` shows dynamic verdicts on finding detail pages, uses them in +ranking, and can compare verdict changes between saved scans. See +[Output formats](output.md) for the `dynamic_verdict` schema. diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 00000000..e9dff6d0 --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,71 @@ +# How Nyx works + +If you're going to act on a finding, it helps to know how the scanner got there. This page is the short version. Source paths are linked where the answer to "exactly what does it do" lives in the code. + +## The pipeline + +A scan runs in two passes over the file tree, with an optional SQLite index that lets the second scan skip files whose content hash hasn't changed. + +```mermaid +flowchart TD + Walk["Walk file tree"] --> Pass1["Pass 1 per file
tree-sitter parse, CFG, SSA"] + Pass1 --> Summaries["Per-function summaries
sources, sinks, sanitizers, returns, points-to"] + Pass1 --> Hierarchy["Type hierarchy index
extends, implements, impl-for, includes"] + Summaries --> Global["GlobalSummaries map
plus optional SQLite cache"] + Hierarchy --> Global + Global --> Pass2["Pass 2 per file
cross-file context"] + Pass2 --> Taint["Forward SSA taint worklist
finite lattice, guaranteed convergence"] + Pass2 --> Calls["Call precision
k=1 inline, summaries, SCC fixed-point"] + Taint --> Findings["Findings with evidence
source, path, sink, engine notes"] + Calls --> Findings + Findings --> Rank["Rank and dedupe
severity, confidence, score"] + Rank --> Verify["Dynamic verification
sandboxed harnesses, verdicts"] + Verify --> Emit["Emit
console, JSON, SARIF, UI"] +``` + +**Pass 1, per file.** Tree-sitter parses the file. Nyx builds an intra-procedural control-flow graph, lowers it to SSA, and extracts a summary per function describing what that function does at the boundary: which arguments flow to sinks, which sources it reads from, which sinks it calls, what taint it strips, what it returns. Summaries are persisted to SQLite ([`src/summary/`](https://github.com/elicpeter/nyx/tree/master/src/summary/), [`src/database.rs`](https://github.com/elicpeter/nyx/blob/master/src/database.rs)). + +**Summary merge.** All per-file summaries get unioned into a global map keyed by qualified function name. + +**Pass 2, per file.** Each file is reanalysed with the global summaries available. The taint engine runs a forward dataflow worklist over the SSA representation. When it hits a call, it consults summaries to decide whether the call propagates taint, sanitizes it, or terminates the flow. Findings are produced when tainted data reaches a sink whose required capability is still set on the value. + +Two extra layers tune precision around calls. **Context-sensitive inlining** (k=1) re-runs intra-file callees with the actual argument taint at the call site, so a helper called once with tainted input and once with sanitized input produces the right result for each call. **SCC fixed-point**: when a group of mutually-recursive functions forms a strongly-connected component in the call graph, the engine iterates summaries to a joint fixed-point (capped at 64 iterations). SCCs that span files are also handled. + +When a method call has a receiver typed as a super-class, trait, or interface, **hierarchy fan-out** widens the resolved callee set to every concrete implementer the engine has seen. A class diagram extracted in pass 1 (Java extends/implements, Rust impl-for, TS/JS extends, Python bases, Ruby includes, PHP extends/implements, C++ inheritance) feeds an index that the call resolver consults during pass 2. The fan-out is capped at 8 implementers per call site; over-fanning is a precision tax, not a soundness issue. + +A separate **field-sensitive points-to** pass tracks abstract locations down to the field level, so `c.mu.Lock()` is a lock on `Field(c, mu)` rather than on `c` as a whole. That distinction is what lets the resource-lifecycle and taint passes tell `obj.field = tainted; sink(obj.other_field)` apart from the conservative whole-variable approximation. Subscript reads and writes (`arr[i]`, `map[k] = v`) lower to synthetic `__index_get__` / `__index_set__` calls so the same container model handles them. Set `NYX_POINTER_ANALYSIS=0` to fall back to the pre-pointer-pass behaviour for baseline comparison. + +**Dynamic verification.** After ranking and dedupe, default builds verify Medium and High confidence findings unless `--no-verify` or `scanner.verify = false` is set. The verifier derives a small harness from the finding, runs it in a sandbox against curated payloads, and stores the result on `evidence.dynamic_verdict`. `Confirmed` means a vulnerable payload fired and its benign control stayed clean. `NotConfirmed` means the harness ran but did not fire, not that the finding is closed. + +## Optional analyses on top + +These run on top of the forward taint pass. They're independently switchable via `[analysis.engine]` config or matching CLI flags. See [advanced-analysis.md](advanced-analysis.md) for the full description and tradeoffs. + +| Pass | Purpose | Default | +|---|---|---| +| Abstract interpretation | Carries interval and string prefix/suffix bounds alongside taint. Suppresses findings on proven-bounded integers and locked-prefix URLs | on | +| Context sensitivity | k=1 inlining for intra-file callees | on | +| Field-sensitive points-to | Distinguishes `obj.field` from `obj` itself, so a tainted write to one field does not poison reads from another. Also gives the resource-lifecycle pass per-field locks | on | +| Hierarchy fan-out | When a method call's receiver is typed as a super-class, trait, or interface, widens callee resolution to every concrete implementer the engine has seen | on | +| Constraint solving | Drops paths whose accumulated branch predicates are unsatisfiable. Optional Z3 backend with `--features smt` | on | +| Symbolic execution | Builds an expression tree per tainted value. Produces a witness string at the sink. Detects sanitization patterns the taint engine alone would miss | on | +| Backwards analysis | After the forward pass, walks backwards from each sink to confirm or invalidate the flow. Annotates findings as `backwards-confirmed`, `backwards-infeasible`, or `backwards-budget-exhausted` | off | + +`--engine-profile fast | balanced | deep` flips groups of these at once. `balanced` is the default and the configuration the benchmark numbers in [language-maturity.md](language-maturity.md) are measured against. + +## Where bounds live + +Static analysis at scale means choosing where to stop. Nyx exposes its bounds rather than hiding them: + +- **Inline depth** is k=1. Callees larger than the inline body-size cap fall back to summary-based resolution. +- **SCC fixed-point** is capped at 64 iterations. If a recursive cluster doesn't converge, the engine emits the best summary it has and records an `engine_note` on affected findings. +- **Lattice width** is bounded. Taint origin sets cap at 32 entries per SSA value (`--max-origins`); points-to sets cap at 32 heap objects (`--max-pointsto`). Truncation is recorded as `OriginsTruncated` / `PointsToTruncated` so you can see when precision was lost. +- **Symbolic expressions** cap at depth 32. Deeper expressions degrade to `Unknown` rather than growing without bound. + +Findings whose engine notes indicate a bound was hit can be filtered with `--require-converged` for strict CI gates. The flag drops over-reports and bails; under-reports (where the emitted finding is still real but the result set is a lower bound) are kept. + +## What you get out + +Each finding carries the source location, the sink location, the path in between (when symex produced one), the rule ID, severity, attack-surface score, confidence level, dynamic verdict when one was attempted, and a list of engine notes describing any precision loss along the way. Console output is human-readable; JSON and SARIF carry the full evidence object for tooling. + +For the JSON shape and SARIF mapping, see [output.md](output.md). diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..9112ee8d --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,77 @@ +# Installation + +For the happy path (`cargo install nyx-scanner`, release binary on PATH), see the README. This page covers platform-specific notes and upgrade paths. + +## Supported platforms + +Release binaries are published for: + +| Platform | Archive | +|---|---| +| Linux x86_64 | `nyx-x86_64-unknown-linux-gnu.zip` | +| macOS Intel | `nyx-x86_64-apple-darwin.zip` | +| macOS Apple Silicon | `nyx-aarch64-apple-darwin.zip` | +| Windows x86_64 | `nyx-x86_64-pc-windows-msvc.zip` | + +Build from source works on any stable Rust 1.88+ target (edition 2024). + +## Verify the download + +Each release attaches a `SHA256SUMS` file. When the maintainer signs the release, a detached `SHA256SUMS.asc` is published alongside it. + +```bash +# Verify the checksum file's signature (skip if .asc isn't present) +gpg --verify SHA256SUMS.asc SHA256SUMS + +# Then check your archive against it +sha256sum -c SHA256SUMS --ignore-missing +``` + +If `sha256sum` is missing on macOS, `shasum -a 256 -c SHA256SUMS --ignore-missing` is equivalent. + +## Windows + +```powershell +Expand-Archive -Path nyx-x86_64-pc-windows-msvc.zip -DestinationPath . +Move-Item -Path .\nyx.exe -Destination "C:\Program Files\Nyx\" +# Add C:\Program Files\Nyx to PATH in System Properties → Environment Variables +nyx --version +``` + +## Build from source + +```bash +git clone https://github.com/elicpeter/nyx.git +cd nyx +cargo build --release +# Binary at target/release/nyx +``` + +The frontend is built and embedded into the binary during `cargo build`, so there's no separate step for `nyx serve`. Node is only required if you're working on the frontend itself; see `CONTRIBUTING.md`. + +Optional features: + +| Flag | Adds | +|---|---| +| `--features smt` | Bundles Z3 for stronger path-constraint solving. MIT-licensed; distributors should include Z3's license in their attribution | +| `--features smt-system-z3` | Links against a system-installed Z3 instead of bundling | + +## Upgrading + +Nyx stores its scanner version in the project's index database. When the binary's version differs from the stored version, the index is wiped on the next scan and rebuilt against the new engine. You'll see one info-level log line: + +``` +engine version changed (), rebuilding index +``` + +No flag needed. If you see this on *every* scan, the metadata row isn't being persisted; file an issue. + +## Corrupt database recovery + +If the SQLite file itself is damaged (killed scan, full disk), delete it and let the next scan rebuild from scratch: + +```bash +rm "$(nyx config path)"/.sqlite* +``` + +Only the named project's rows are affected. diff --git a/docs/language-maturity.md b/docs/language-maturity.md new file mode 100644 index 00000000..8f7e1746 --- /dev/null +++ b/docs/language-maturity.md @@ -0,0 +1,310 @@ +# Language Maturity Matrix + +Nyx supports ten languages, but support depth is not uniform. This page gives an +honest per-language picture so you can calibrate expectations before depending +on Nyx for a given stack. + +The classifications here are grounded in three concrete signals: + +1. **Rule depth**: how many distinct source / sanitizer / sink matchers exist + for the language in `src/labels/.rs`, and how many vulnerability + classes (Cap bits) those matchers cover. +2. **Benchmark results**: rule-level precision / recall / F1 on the synthetic + corpus in + [`tests/benchmark/RESULTS.md`](https://github.com/elicpeter/nyx/blob/master/tests/benchmark/RESULTS.md). + `RESULTS.md` is the authoritative case counts and per-language scores. +3. **Known weak spots**: FPs and FNs the maintainers have deliberately left + in the benchmark rather than suppressed, plus structural engine + limitations the corpus does not stress, documented in + [`RESULTS.md`](https://github.com/elicpeter/nyx/blob/master/tests/benchmark/RESULTS.md). + +The synthetic corpus has effectively saturated: every +real-CVE fixture fires and rule-level precision and recall are both 100%. +All ten languages report rule-level F1 = 100.0%. Aggregate rule-level +P=1.000, R=1.000, F1=1.000. That means F1 alone no longer differentiates +tiers, so the differentiators are **rule depth**, **gated-sink coverage**, +and **structural idioms the corpus does not fully stress** (deep pointer +aliasing in C/C++, framework-specific context). All parser integrations +use tree-sitter and are stable; parsing is not a differentiator. + +--- + +## Tier Summary + +| Tier | Languages | F1 | What to expect | +|------|-----------|----|----------------| +| **Stable** | Python, JavaScript, TypeScript | 100% | Deep rule sets, gated sinks (argument-role-aware), framework detection, extensive fixtures, and the bulk of advanced-analysis (SSA two-level solve, context-sensitivity, symbolic execution, abstract interpretation) coverage. Safe to depend on in CI gates. | +| **Beta** | Go, Java, PHP, Ruby, Rust | 100% | Solid mid-depth rule sets with narrower cap coverage and **no gated sinks**. Cross-file flows work; some idioms (variable-typed method receivers, framework context, string interpolation, match-arm guards) are partially modeled. Usable in CI; review FP/FN lists before tightening gates. | +| **Preview** | C, C++ | 100% on synthetic corpus | Recent work taught the engine to follow taint through `std::vector` / `std::string` / map containers (including `c_str()`), through fluent builder chains like `Socket::builder().host(h).connect()`, and through inline class member functions. Function pointers and deeper pointer aliasing through `*p` / `p->field` are still not tracked. Rule-level scores against a corpus of obvious unsafe-API uses look perfect, but that is not the same as a clean audit on a real codebase. Pair with clang-tidy, Clang Static Analyzer, or Infer. | + +--- + +## Per-Language Detail + +### Stable tier + +#### Python + +- **Rule depth**: deep source / sanitizer / sink coverage in + [`src/labels/python.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/python.rs) + spanning HTML, URL, Shell, SQL, Code, SSRF, File I/O, and Deserialization. +- **Framework context**: Flask, Django, argparse source matchers; `flask_request` + import-alias support. +- **Advanced analysis**: gated sinks (`Popen`, `subprocess.run/call` with + activation-arg awareness), most SSA-equivalence and symbolic-execution + fixtures target Python. +- **Fixtures**: extensive `.py` coverage under `tests/fixtures/` plus the benchmark cases. +- **Blind spots**: f-string interpolation is not explicitly modeled as a + distinct taint-producing construct; string-formatting flows are caught by + the general concatenation path. + +#### JavaScript + +- **Rule depth**: deep source / sanitizer / sink coverage in + [`src/labels/javascript.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/javascript.rs) + spanning HTML, URL, JSON, Shell, SQL, Code, SSRF, and File I/O. +- **Advanced analysis**: gated sinks (`setAttribute`, `parseFromString`), + two-level SSA solve for top-level + per-function scopes + (`analyse_ssa_js_two_level`), prefix-locked SSRF suppression via + StringFact, abstract-interpretation interval tracking. +- **Framework context**: Express, Koa, Fastify (via in-file import scan when + `package.json` is absent). +- **Fixtures**: the largest `.js` set under `tests/fixtures/` of any + language. +- **Blind spots**: template literals are lowered through concatenation rather + than modeled as a first-class taint operator; dynamic property access + (`obj[user]`) is conservatively treated. + +#### TypeScript + +- **Rule depth**: shares the JS ruleset (see + [`src/labels/typescript.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/typescript.rs)) + plus TS-specific grammar handling. +- **Advanced analysis**: TSX and JSX grammars wired; + discriminated-union narrowing, generic erasure, decorator flow, and + interface dispatch are all validated against adversarial type-system + stressors. +- **Framework context**: Fastify detection via `detect_in_file_frameworks` + (import-driven, no `package.json` required). +- **Fixtures**: dedicated `.ts` / `.tsx` set under `tests/fixtures/` plus the benchmark cases. +- **Blind spots**: `as any` casts and `any`-typed flows are handled + conservatively (treated as tainted). + +### Beta tier + +#### Go + +- **Rule depth**: mid-depth source / sanitizer / sink coverage in + [`src/labels/go.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/go.rs) + covering HTML, URL, Shell, SQL, SSRF, Crypto, and File I/O. +- **Framework context**: Gin, Echo source matchers. +- **Recent fix**: `strings.ReplaceAll` is now recognised as a CMDi sanitiser + in chain-wrapper / call-site-replace shapes, clearing the last open + Go safe-fixture FP (`go-safe-009`, `validate(s string)` wrapping a + `strings.ReplaceAll` over `;`). +- **Known gaps**: no gated sinks, no deserialization class. `fmt.Sprintf` + is deliberately not a sink. Cap coverage is narrower than the Stable + tier and argument-role-aware sink modeling is not yet implemented for Go, + so production CI gates may surface additional FPs the corpus does not + exercise. + +#### Java + +- **Rule depth**: mid-depth source / sanitizer / sink coverage in + [`src/labels/java.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/java.rs) + covering HTML, URL, Shell, SQL, Code, SSRF, and Deserialization. +- **Framework context**: Spring, JPA, Hibernate ORM rules; JNDI injection + sinks. +- **Known gaps**: no gated sinks. Variable-receiver method calls + (`client.send(...)` vs `HttpClient.send(...)`) rely on type-qualified + resolution from receiver-type inference; flows where the receiver type + cannot be inferred are conservatively over-tainted on unusual builder + chains. + +#### PHP + +- **Rule depth**: sources include `$_GET`, `$_POST`, `$_REQUEST` + superglobals plus sanitizer / sink matchers in + [`src/labels/php.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/php.rs) + covering HTML, URL, Shell, SQL, Code, SSRF, File I/O, and Deserialization. +- **Known gaps**: no gated sinks. Limited framework context (Laravel raw + methods only). `echo` language-construct detection is wired but its + inner-argument propagation is narrower than function-call sinks. + +#### Ruby + +- **Rule depth**: source / sanitizer / sink coverage in + [`src/labels/ruby.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/ruby.rs) + covering HTML, Shell, SQL, Code, SSRF, File I/O, and Deserialization. SSRF + coverage includes `URI.open` and the low-level `OpenURI.open_uri` it + delegates to (the canonical CarrierWave CVE-2021-21288 sink). + Statement-level chained-call wrappers + (`YAML.safe_load(File.read(filename))`, `Marshal.load(File.read(p))`, + `String.new(File.read(x))`) classify the inner sink for cross-function + summary extraction so the outer call does not strip the sink classification + on the helper. +- **Framework context**: Rails helpers (`sanitize_sql`, `permit`, `require`). +- **Known gaps**: string interpolation inside shell and SQL strings is + recognized structurally but not modeled as a distinct operator. + `begin/rescue/ensure` exception-edge wiring is not implemented. + +#### Rust + +Rust holds the largest per-language adversarial corpus. PathFact-driven +path-domain narrowing covers the `rs-safe-*` regression set. + +- **Rule depth**: source / sanitizer / sink coverage in + [`src/labels/rust.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/rust.rs) + covering HTML, Shell, SQL, SSRF, Deserialization, and File I/O. + Extensive framework source coverage (Axum, Actix, Rocket); the most of + any language on the source side. The narrow sanitizer rule set (prefix + and type-coercion only) is the primary reason Rust is not in the Stable + tier. Engine-side path/typed sanitizer recognition (PathFact) + compensates, but the ruleset itself is shallow. +- **Coverage**: SQL class (`rusqlite`, `sqlx`, `diesel`, `postgres`), + Deserialization class (`serde_yaml`, `bincode`, `rmp_serde`, `ciborium`, + `ron`, `toml`), file I/O (`fs::remove_file/dir/rename/copy`), and the + `reqwest` SSRF builder chain. +- **PathFact-narrowed shapes** (`src/abstract_interp/path_domain.rs` plus + per-return-path PathFact entries on `SsaFuncSummary`) cover + `.replace("..","")` sanitisers, negative-validation returns, match-arm + guards via condition lifting, static-map lookups, + `.contains("..")` + `.starts_with('/')` rejection, Option-returning + user sanitisers, `Path::new(p).is_absolute()` typed rejection, + cross-function `.contains("..")` rejection, and the + `CVE-2018-20997` / `CVE-2022-36113` / `CVE-2024-24576` patch shapes. +- **Not yet covered**: unsafe FFI / `std::mem::transmute` (no rules), Tokio + `process::Command` async variants (not distinguished from sync), + `hyper` / `surf` / `ureq` SSRF clients (reqwest family only). + +### Preview tier + +C and C++ remain **Preview** despite reporting 100% rule-level F1 on the +synthetic corpus. The engine follows taint through STL containers, builder +chains, inline member functions, and the wider `std::sto*` family, so the +gap between "passes the synthetic corpus" and "would catch the same flow +on a real codebase" is narrower than the synthetic numbers suggest. It is +not zero. The biggest remaining gaps are deep pointer aliasing and function +pointers, both of which are pervasive in real C/C++ code. Treat a clean +report as a starting point, not an audit. Pair Nyx with clang-tidy, the +Clang Static Analyzer, or Infer for production use. + +**What works:** + +- STL container flow. `vec.push_back(tainted)` followed by + `vec.front().c_str()` carries taint into a downstream `system()` sink. + `std::map::insert_or_assign`, `find`, `count`, `at`, and `data` all + participate in the container store/load model. +- Inline class member functions. `class C { void run(...) { ... } };` + bodies are now extracted as their own functions, so an intra-file call + like `inner.run(input)` resolves to the body summary. Same fix covers + `struct_specifier`, `union_specifier`, `enum_specifier`, + `template_declaration`, and `extern "C"` blocks. +- Lambda passthrough. `auto echo = [](const char* s) { return s; };` carries + argument taint into the result via the engine's default call-argument + propagation. +- Builder chains. `Socket::builder().host(user).port(8080).connect()` + resolves the chained returns and fires on `.connect()` when `user` is + tainted; the safe variant with a hardcoded host stays quiet. +- Wider numeric sanitizer family. The full `std::sto*` set (including + `stoll`, `stoull`, `stold`) and the C-stdlib forms (`atoi`, `atof`, + `strtol`, etc.) clear all caps when they're called. +- More header / source extensions. `.cc`, `.cxx`, `.hpp`, `.hxx`, `.hh`, + and `.h++` are recognized as C++ on top of `.cpp` and `.c++`. `.h` is + intentionally still routed to C since it's ambiguous without a build + system. + +**Still not modeled** (common to both C and C++): + +- Deep pointer aliasing. Taint through `*p`, `p->field`, and arbitrary + pointer arithmetic is not tracked through arbitrary aliased writes. + Field-sensitive points-to (see [Advanced analysis](advanced-analysis.md)) + handles the "lock on a sub-field" case but is not a general escape + analysis. +- Function pointers and callback dispatch. An indirect call through + `void (*fn)(char *)` resolves to no callee, so cross-pointer flows are + invisible. +- Array-element taint by index. Writes to `buf[i]` do not always propagate + taint to `buf` as a whole; subscript-handling helps the general case but + doesn't make `buf` an alias for every element. +- Nested classes beyond one level (C++ only). + +#### C + +- **Rule depth**: source / sanitizer / sink coverage in + [`src/labels/c.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/c.rs). + Sanitizers are limited to the `sanitize_*` prefix and numeric-parse + functions; sinks span Shell, File, SSRF, and Format-String. +- **Known gaps**: no framework rules, no gated sinks. The structural + limitations listed above are the dominant concern; rule additions alone + will not lift this language out of the Preview tier. + +#### C++ + +- **Rule depth**: builds on the C ruleset (see + [`src/labels/cpp.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/cpp.rs)) + with `std::cin` / `std::getline` sources and a wider numeric-sanitizer + set covering the full `std::sto*` family. +- **Known gaps**: still no framework rules and no gated sinks. The + structural blind spots are now narrower than they were a release ago + (see "What now works" above), but function pointers and the harder + pointer-aliasing patterns still produce false negatives. + +--- + +## How the tiers were assigned + +Because rule-level F1 has saturated for nine of ten languages, the tier +boundaries are drawn primarily on **rule depth** and **engine coverage of +real-world idioms** rather than on benchmark scores alone. + +A language lands in **Stable** when all three hold: + +- Rule set covers ≥ 8 vulnerability classes with both source and sink + matchers, and at least one class has argument-role-aware **gated-sink** + modeling (e.g. `setAttribute("href", url)` only flags href-like attrs). +- Benchmark F1 ≥ 95% on a corpus of ≥ 25 cases. +- Advanced analysis (SSA lowering, context-sensitivity, symbolic execution, + abstract interpretation) is exercised by fixtures for the language. + +A language lands in **Beta** when benchmark F1 is in the mid-90s or higher +on a meaningful corpus but at least one Stable criterion fails. Typical +gaps: absence of gated sinks, or sanitizer rule depth narrow enough that +the engine compensates structurally rather than via the ruleset. + +A language lands in **Preview** when the engine has documented structural +blind spots for constructs that are pervasive in typical codebases for that +language. For C and C++ that means deep pointer aliasing, function +pointers, and array-element taint; STL container flow and builder chains +have moved out of the blind-spot list. Synthetic-corpus F1 is not a +reliable signal for Preview-tier languages: a clean report can coexist +with structural gaps. + +(No language currently sits in the **Experimental** tier; it is reserved +for future additions whose corpus has not yet stabilised.) + +--- + +## What this means for you + +- **CI gates**: safe to set strict `--fail-on HIGH` gates on Stable-tier + languages. On Beta-tier, expect occasional FP triage on production code + (the synthetic corpus does not cover every framework idiom); the + weak-spot lists above tell you what to skim for. On Preview-tier, treat + Nyx findings as a starting point for manual review rather than + authoritative. STL container flow and builder chains are tracked now, + but deep pointer aliasing and function pointers are not, so a clean + report does not tell you what the engine could not see. +- **Rule contributions**: the shortest path to raising a language's tier is + contributing sink matchers and gated-sink registrations. Label files live + at `src/labels/.rs`; benchmark cases live at + `tests/benchmark/corpus//`. +- **Scope planning**: if your primary stack is C or C++, Nyx will surface + real findings on obvious unsafe-API uses, but budget for review time and + combine Nyx with `clang-tidy` or the Clang Static Analyzer. Rust is now + Beta-tier and suitable as a CI gate; pair with `cargo-audit` for + dependency CVEs. + +The benchmark thresholds in `tests/benchmark_test.rs` are deliberately set +~5 pp below current baselines so any drop in a language's F1 fails CI. Tier +promotions require sustained benchmark performance, not just rule additions. diff --git a/docs/mermaid-init.js b/docs/mermaid-init.js new file mode 100644 index 00000000..45a008fb --- /dev/null +++ b/docs/mermaid-init.js @@ -0,0 +1,69 @@ +(function () { + const MERMAID_URL = + "https://cdn.jsdelivr.net/npm/mermaid@10.9.3/dist/mermaid.esm.min.mjs"; + + async function renderMermaid() { + const blocks = Array.from( + document.querySelectorAll("pre > code.language-mermaid"), + ); + if (blocks.length === 0) { + return; + } + + try { + const mermaidModule = await import(MERMAID_URL); + const mermaid = mermaidModule.default; + + mermaid.initialize({ + startOnLoad: false, + securityLevel: "strict", + theme: "base", + themeVariables: { + background: "transparent", + fontFamily: + "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif", + primaryColor: "#0f172a", + primaryTextColor: "#e5e7eb", + primaryBorderColor: "#22d3ee", + secondaryColor: "#134e4a", + secondaryTextColor: "#e5e7eb", + secondaryBorderColor: "#2dd4bf", + tertiaryColor: "#1e293b", + tertiaryTextColor: "#e5e7eb", + tertiaryBorderColor: "#64748b", + lineColor: "#94a3b8", + edgeLabelBackground: "#0f172a", + clusterBkg: "#111827", + clusterBorder: "#475569", + }, + }); + + for (const block of blocks) { + const pre = block.parentElement; + if (!pre) { + continue; + } + + const wrapper = document.createElement("div"); + wrapper.className = "nyx-mermaid"; + + const diagram = document.createElement("div"); + diagram.className = "mermaid"; + diagram.textContent = block.textContent.trim(); + + wrapper.appendChild(diagram); + pre.replaceWith(wrapper); + } + + await mermaid.run({ querySelector: ".nyx-mermaid .mermaid" }); + } catch (error) { + console.warn("Mermaid rendering failed", error); + } + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", renderMermaid); + } else { + renderMermaid(); + } +})(); diff --git a/docs/mermaid.css b/docs/mermaid.css new file mode 100644 index 00000000..9d160d6b --- /dev/null +++ b/docs/mermaid.css @@ -0,0 +1,15 @@ +.nyx-mermaid { + margin: 1.5rem 0; + padding: 1rem; + overflow-x: auto; + border: 1px solid rgba(148, 163, 184, 0.35); + border-radius: 8px; + background: rgba(15, 23, 42, 0.28); +} + +.nyx-mermaid svg { + display: block; + max-width: 100%; + height: auto; + margin: 0 auto; +} diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 00000000..852e3e9e --- /dev/null +++ b/docs/output.md @@ -0,0 +1,391 @@ +# Output Formats + +Nyx supports three output formats, selected with `--format` or `output.default_format` in config. + +## Console (default) + +Human-readable, color-coded output to stdout. Status messages go to stderr. + +``` +[HIGH] taint-unsanitised-flow (source 5:11) src/handler.rs:12:5 (Score: 76, Confidence: High) + Source: env::var("CMD") → Command::new("sh").arg("-c") + +[MEDIUM] cfg-unguarded-sink src/handler.rs:12:5 (Score: 35, Confidence: Medium) + +[LOW] rs.quality.unwrap src/lib.rs:88:5 (Score: 10, Confidence: High) +``` + +### Severity indicators + +| Tag | Color | Meaning | +|-----|-------|---------| +| `[HIGH]` | Red, bold | Critical, likely exploitable | +| `[MEDIUM]` | Orange, bold | Important, may be exploitable | +| `[LOW]` | Muted blue-gray | Informational: code quality or weak signal | + +### Evidence fields + +Taint and state findings include structured evidence: + +| Label | Meaning | +|-------|---------| +| **Source** | Where tainted data originated (function name + location) | +| **Sink** | Where the dangerous operation happens | +| **Path guard** | Type of validation predicate protecting the path | + +### Score + +When attack-surface ranking is enabled (default), each finding shows a `Score` value. Higher scores indicate greater exploitability. See [Detector Overview](detectors.md) for the scoring formula. + +### Rollup findings + +High-frequency LOW Quality findings (e.g. `rs.quality.unwrap`) are grouped into rollup findings by `(file, rule)`: + +``` + 21:10 ● [LOW] rs.quality.unwrap + rs.quality.unwrap (38 occurrences) + Examples: 21:10, 50:10, 79:10, 105:10, 134:10 + Run: nyx scan --show-instances rs.quality.unwrap +``` + +Rollups count as **one finding** for LOW budget enforcement. Use `--show-instances ` to expand a specific rule or `--all` to disable rollups entirely. + +### Suppression footer + +When findings are suppressed by the prioritization pipeline, a footer is shown: + +``` +Suppressed 195 LOW/Quality findings. +Active filters: + include_quality = false + max_low = 20 + max_low_per_file = 1 + max_low_per_rule = 10 + +Use --include-quality, --max-low, or --all to adjust. +``` + +--- + +## JSON + +Machine-readable JSON object. The main keys are: + +| Key | Type | Description | +|-----|------|-------------| +| `findings` | array | Finding objects | +| `chains` | array | Composed exploit chains, when emitted | +| `dynamic_verification` | object | Count of attached dynamic verdicts | +| `verdict_diff` | object | Baseline comparison, only when `--baseline` is used | + +```json +{ + "findings": [ + { + "path": "src/handler.rs", + "line": 12, + "col": 5, + "severity": "High", + "id": "taint-unsanitised-flow (source 5:11)", + "path_validated": false, + "labels": [ + ["Source", "env::var(\"CMD\") at 5:11"], + ["Sink", "Command::new(\"sh\").arg(\"-c\")"] + ], + "confidence": "High", + "evidence": { + "source": { + "path": "src/handler.rs", + "line": 5, + "col": 11, + "kind": "source", + "snippet": "env::var(\"CMD\")" + }, + "sink": { + "path": "src/handler.rs", + "line": 12, + "col": 5, + "kind": "sink", + "snippet": "Command::new(\"sh\")" + }, + "notes": ["source_kind:EnvironmentConfig"], + "dynamic_verdict": { + "finding_id": "a3b12f0c91e04420", + "status": "Confirmed", + "triggered_payload": "cmdi-echo-marker" + } + }, + "rank_score": 76.0, + "rank_reason": [ + ["severity_base", "60"], + ["analysis_kind", "10"], + ["source_kind", "5"], + ["evidence_count", "1"] + ] + } + ], + "chains": [], + "dynamic_verification": { + "total": 1, + "confirmed": 1, + "partially_confirmed": 0, + "not_confirmed": 0, + "inconclusive": 0, + "unsupported": 0 + } +} +``` + +### Field descriptions + +| Field | Type | Always present | Description | +|-------|------|----------------|-------------| +| `path` | string | yes | File path relative to scan root | +| `line` | int | yes | 1-indexed line number | +| `col` | int | yes | 1-indexed column number | +| `severity` | string | yes | `"High"`, `"Medium"`, or `"Low"` | +| `id` | string | yes | Rule ID | +| `category` | string | yes | Finding category: `"Security"`, `"Reliability"`, or `"Quality"` | +| `path_validated` | bool | no | True if guarded by validation predicate | +| `guard_kind` | string | no | Predicate type (e.g. `"NullCheck"`, `"ValidationCall"`) | +| `message` | string | no | Human-readable context (state analysis findings) | +| `labels` | array | no | Array of `[label, value]` pairs for console display | +| `confidence` | string | no | Confidence level: `"Low"`, `"Medium"`, or `"High"` | +| `evidence` | object | no | Structured evidence (source/sink spans, state, notes) | +| `rank_score` | float | no | Attack-surface score (omitted when ranking disabled) | +| `rank_reason` | array | no | Score breakdown (omitted when ranking disabled) | +| `rollup` | object | no | Rollup data when findings are grouped (see below) | +| `chain_member_of` | int | no | Stable hash of the emitted chain this finding belongs to | + +Fields marked "no" are omitted when empty/null/false to keep output compact. + +### Confidence levels + +| Level | Meaning | +|-------|---------| +| `High` | Strong signal: taint-confirmed flow, definite state violation | +| `Medium` | Moderate signal: resource leak, path-validated taint, CFG structural | +| `Low` | Weak signal: AST pattern match, possible resource leak, degraded analysis | + +### Evidence object + +The `evidence` field provides structured provenance data: + +| Field | Type | Description | +|-------|------|-------------| +| `source` | object | Source span (path, line, col, kind, snippet) | +| `sink` | object | Sink span (path, line, col, kind, snippet) | +| `guards` | array | Validation guard spans | +| `sanitizers` | array | Sanitizer spans | +| `state` | object | State-machine evidence (machine, subject, from_state, to_state) | +| `notes` | array | Free-form notes (e.g. `"source_kind:UserInput"`, `"path_validated"`) | +| `dynamic_verdict` | object | Dynamic verification result, when verification ran or was skipped for a typed reason | + +All fields are omitted when empty/null. + +### Dynamic verdict object + +`evidence.dynamic_verdict` uses this shape: + +| Field | Type | Description | +|-------|------|-------------| +| `finding_id` | string | Stable 16-character hex finding id | +| `status` | string | `Confirmed`, `PartiallyConfirmed`, `NotConfirmed`, `Inconclusive`, or `Unsupported` | +| `triggered_payload` | string | Payload label for `Confirmed` verdicts | +| `reason` | object/string | Typed reason for `Unsupported` | +| `inconclusive_reason` | object/string | Typed reason for `Inconclusive` | +| `detail` | string | Extra build, sandbox, or policy detail | +| `attempts` | array | Per-payload attempt summaries | +| `toolchain_match` | string | `exact` or `drift` | +| `differential` | object | Vulnerable versus benign control result, when both ran | +| `hardening_outcome` | object | Process-backend hardening result, when recorded | + +The top-level `dynamic_verification` object counts verdict statuses across the emitted findings: + +```json +{ + "total": 4, + "confirmed": 2, + "partially_confirmed": 0, + "not_confirmed": 1, + "inconclusive": 0, + "unsupported": 1 +} +``` + +### Rollup object + +When a finding is a rollup (grouped from multiple occurrences), the `rollup` field is present: + +```json +{ + "rollup": { + "count": 38, + "occurrences": [ + { "line": 21, "col": 10 }, + { "line": 50, "col": 10 }, + { "line": 79, "col": 10 } + ] + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `count` | int | Total number of occurrences | +| `occurrences` | array | First N example locations (controlled by `rollup_examples`) | + +--- + +## SARIF (Static Analysis Results Interchange Format) + +SARIF 2.1.0 JSON, suitable for GitHub Code Scanning and other SARIF-compatible tools. + +```bash +nyx scan . --format sarif > results.sarif +``` + +The SARIF output includes: + +- **Tool metadata**: Nyx name and version +- **Rules**: Rule ID, description, severity mapping +- **Results**: One result per finding with location, message, and properties +- **Properties**: Each result includes `category` and optionally `confidence`, `rollup.count`, and `nyx_dynamic_verdict` +- **Fingerprints**: Dynamic verdict status is added as `partialFingerprints.dynamic_verdict_status` when present +- **Related locations**: Rollup findings include example locations in `relatedLocations` +- **Artifacts**: File paths referenced by findings + +### GitHub Code Scanning integration + +```yaml +- name: Run Nyx + run: nyx scan . --format sarif > results.sarif + +- name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif +``` + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Scan completed successfully; no findings matched `--fail-on` threshold | +| `1` | `--fail-on` threshold breached (at least one finding meets or exceeds the specified severity) | +| `2` | `--gate` policy tripped (e.g. `no-new-confirmed` saw a new Confirmed finding, or `resolve-all-confirmed` saw a previously Confirmed finding still open) | +| Other non-zero | Error (I/O, config, database, parse error) | + +Without `--fail-on` or `--gate`, Nyx always exits `0` on a successful scan regardless of findings count. + +--- + +## Repository Triage + +`nyx scan` and `nyx serve` share `.nyx/triage.json` in the scan root. The file +uses portable fingerprints so committed triage decisions survive different +checkout paths in local runs and CI. + +When the file exists, CLI scans apply it automatically: + +- `open` and `investigating` findings remain active. +- `false_positive`, `accepted_risk`, `suppressed`, and `fixed` findings are + excluded from output and `--fail-on` checks by default. +- `--show-suppressed` includes terminal triage findings and emits + `triage_state` plus `triage_note` when present. + +`nyx serve` continues to read and write the same file when triage sync is +enabled, so browser triage and CI gating use the same decisions. + +--- + +## Severity Levels + +| Level | Description | Typical rules | +|-------|-------------|---------------| +| **High** | Critical vulnerabilities, likely exploitable | Command injection, unsafe deserialization, banned C functions, taint-confirmed flows with user input sources | +| **Medium** | Important issues, may be exploitable with additional context | SQL concatenation, XSS sinks, reflection, unguarded sinks, resource leaks | +| **Low** | Informational: code quality or weak signals | Weak crypto algorithms, insecure randomness, `unwrap()`/`panic!()`, type-safety escapes | + +### Non-production severity downgrade + +By default, findings in paths matching common non-production patterns (`tests/`, `test/`, `vendor/`, `build/`, `examples/`, `benchmarks/`) are downgraded by one tier: + +- High → Medium +- Medium → Low +- Low → Low (unchanged) + +Use `--keep-nonprod-severity` to disable this behavior. + +--- + +## Inline Suppressions + +Suppress specific findings directly in source code using `nyx:ignore` comments. Suppressed findings are excluded from output, severity counts, and `--fail-on` checks by default. + +### Comment syntax + +| Language | Comment styles | +|----------|---------------| +| Rust, C, C++, Java, Go, JS, TS | `// nyx:ignore ...` or `/* nyx:ignore ... */` | +| Python, Ruby | `# nyx:ignore ...` | +| PHP | `// nyx:ignore ...`, `# nyx:ignore ...`, or `/* nyx:ignore ... */` | + +### Directive forms + +```python +x = dangerous() # nyx:ignore taint-unsanitised-flow (suppresses this line) +# nyx:ignore-next-line taint-unsanitised-flow +x = dangerous() (suppressed by the comment above) +``` + +- `nyx:ignore `: suppresses findings on the **same line** as the comment. +- `nyx:ignore-next-line `: suppresses findings on the **next line**. +- For taint findings, the primary line is the **sink line** (the `line` field in output). + +### Rule ID matching + +- **Case-sensitive**, exact match after canonicalization. +- Comma-separated: `nyx:ignore rule-a, rule-b` +- Wildcard suffix: `nyx:ignore rs.quality.*` matches any ID starting with `rs.quality.` +- Taint IDs are canonicalized: `nyx:ignore taint-unsanitised-flow` matches `taint-unsanitised-flow (source 5:1)` (parenthetical suffix stripped). + +### Console behavior + +- **Default**: suppressed findings are hidden entirely. +- **`--show-suppressed`**: suppressed findings appear dimmed with `[SUPPRESSED]` tag. Summary shows `"N issues (M suppressed)"`. + +### JSON / SARIF behavior + +- **Default**: suppressed findings are excluded from JSON/SARIF output. +- **`--show-suppressed`**: suppressed findings are included with additional fields: + +```json +{ + "suppressed": true, + "suppression": { + "kind": "SameLine", + "matched_pattern": "taint-unsanitised-flow", + "directive_line": 42 + } +} +``` + +### Exit code + +Suppressed findings do **not** trigger `--fail-on`. A scan with only suppressed findings exits `0`. + +--- + +## Rule ID Format + +| Prefix | Detector | Example | +|--------|----------|---------| +| `taint-*` | Taint analysis | `taint-unsanitised-flow (source 5:11)` | +| `cfg-*` | CFG structural | `cfg-unguarded-sink`, `cfg-auth-gap` | +| `state-*` | State model | `state-use-after-close`, `state-resource-leak` | +| `.*.*` | AST patterns | `rs.memory.transmute`, `js.code_exec.eval` | + +See the [Rule Reference](rules.md) for a complete listing. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..7d6a8754 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,109 @@ +# Quick start + +After `cargo install nyx-scanner` (or dropping a release binary on your PATH), point Nyx at a directory: + +```bash +nyx scan ./my-project +``` + +First run builds a SQLite index under `.nyx/`; later runs skip files whose content hash hasn't changed. Default builds also verify Medium and High confidence findings in a sandbox. Use `--no-verify` when you want a static-only local loop. + +## What a finding looks like + +

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

+ +The same scan in console form: + +``` +/tmp/demo/cmdi_direct.py + 6:5 ✖ [HIGH] taint-unsanitised-flow (source 5:11) (Score: 81, Confidence: High) + Unsanitised user input flows from request.args.get → os.system + + Source: request.args.get (5:11) + Sink: os.system + [DYN: confirmed via cmdi-echo-marker-python] + + 6:5 ✖ [HIGH] py.cmdi.os_system (Score: 64, Confidence: High) + os.system() runs a shell command + +/tmp/demo/xss_document_write.js + 5:5 ✖ [HIGH] taint-unsanitised-flow (source 3:18) (Score: 81, Confidence: High) + Unsanitised user input flows from req.query.content → document.write + + Source: req.query.content (3:18) + Sink: document.write + [DYN: confirmed via xss-script-marker] + + 5:5 ⚠ [MEDIUM] js.xss.document_write (Score: 34, Confidence: High) + document.write() is an XSS sink + +Dynamic verification: 4 verdicts (2 confirmed, 0 partially confirmed, 1 not confirmed, 0 inconclusive, 1 unsupported) + +warning 'demo' generated 10 issues. +Finished in 1.842s. +``` + +Each finding is one line of header plus evidence. Fields that matter: + +| Field | Meaning | +|---|---| +| `[HIGH]` / `[MEDIUM]` / `[LOW]` | Severity after the non-prod downgrade | +| Rule ID | Either a taint rule (`taint-unsanitised-flow`), a structural rule (`cfg-*`, `state-*`), or an AST pattern (`..`) | +| Score | Attack-surface ranking (severity + analysis kind + source kind + evidence). Higher is more exploitable | +| Confidence | `High`, `Medium`, `Low`. Drops for AST-only matches, capped widened flows, and lowered-to-Low backwards-infeasible findings | +| Source / Sink | Where tainted data entered and where the dangerous call happened | +| `[DYN: ...]` | Dynamic verifier result, when Nyx built and ran a harness for the finding | + +Two rules firing on the same line (the taint finding plus the AST pattern) is normal. The pattern matches the structural presence of `document.write`; the taint rule adds the evidence that `req.query.content` actually reached it. Both carry distinct rule IDs so suppressions can target one without the other. + +## Fail a CI job on High findings + +```bash +nyx scan . --fail-on HIGH --quiet +``` + +Exit 1 if any HIGH finding remains. `--quiet` drops the "Using default configuration" banner so CI logs stay tidy. + +## Emit SARIF for GitHub Code Scanning + +```bash +nyx scan . --format sarif > results.sarif +``` + +Full SARIF schema and GitHub Actions wiring: [cli.md](cli.md) and [output.md](output.md). + +## Tighten the gate + +```bash +# Only HIGH findings +nyx scan . --severity HIGH + +# HIGH + MEDIUM +nyx scan . --severity ">=MEDIUM" + +# Drop anything below Medium confidence (useful for CI) +nyx scan . --min-confidence medium + +# Also drop findings the engine could not fully resolve (widened / bailed) +nyx scan . --require-converged +``` + +`--require-converged` keeps `under-report` findings (the emitted flow is still real) but drops over-reports and widenings. Intended for strict gates where a noisy finding is worse than nothing. + +## Skip work for a fast first pass + +```bash +nyx scan . --mode ast +nyx scan . --no-verify +``` + +AST-only mode runs tree-sitter patterns without building a CFG or running taint. It's fast and still catches banned-API uses, weak crypto, and obvious XSS sinks, but it can't tell `eval("1+1")` apart from `eval(userInput)`. Use it as a pre-commit filter, not as a CI gate replacement. + +`--no-verify` keeps the static engine on but skips sandboxed execution. Use it when you are iterating locally and only need the analyzer result. + +## Next + +- [CLI reference](cli.md) for every flag and subcommand. +- [Configuration](configuration.md) for the `nyx.conf` / `nyx.local` schema, profiles, and custom rules. +- [`nyx serve`](serve.md) for the browser UI, triage workflow, and scan history. +- [Language maturity](language-maturity.md) for per-language tier and known FP/FN patterns. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 00000000..01c9ff2a --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1 @@ +{{#include ../ROADMAP.md}} diff --git a/docs/rules.md b/docs/rules.md new file mode 100644 index 00000000..34fea839 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,276 @@ +# Rule reference + +Every finding Nyx emits has a rule ID. This page enumerates the IDs that ship with the scanner, grouped by family. + +> This page is written by hand and drifts against the code. Authoritative sources: [`src/patterns/.rs`](https://github.com/elicpeter/nyx/tree/master/src/patterns) for AST patterns, [`src/labels/.rs`](https://github.com/elicpeter/nyx/tree/master/src/labels) for taint matchers, and [`src/auth_analysis/config.rs`](https://github.com/elicpeter/nyx/blob/master/src/auth_analysis/config.rs) for auth rules. If a rule fires that isn't listed here, the source file is right and this page is wrong. + +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: + +

Nyx Rules page: filterable list of 218 rules with language, kind (SOURCE/SANITIZER/SINK), capability, and finding count columns

+ +## ID format + +| Prefix | Detector | Example | +|---|---|---| +| `taint-*` | Taint analysis | `taint-unsanitised-flow (source 5:11)` | +| `cfg-*` | CFG structural | `cfg-unguarded-sink`, `cfg-auth-gap` | +| `state-*` | State model | `state-use-after-close`, `state-resource-leak` | +| `.auth.*` | Auth analysis | `rs.auth.missing_ownership_check` | +| `..` | AST patterns | `rs.memory.transmute`, `js.code_exec.eval` | + +Language prefixes: `rs`, `c`, `cpp`, `go`, `java`, `js`, `ts`, `py`, `php`, `rb`. + +## Cross-language rules + +### Taint + +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 | 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 | + +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/.rs`. [Language maturity](language-maturity.md) gives per-language counts and what's covered. + +### CFG structural + +| Rule ID | Severity | +|---|---| +| `cfg-unguarded-sink` | High/Medium | +| `cfg-auth-gap` | High | +| `cfg-unreachable-sink` | Medium | +| `cfg-unreachable-sanitizer` | Low | +| `cfg-unreachable-source` | Low | +| `cfg-error-fallthrough` | High/Medium | +| `cfg-resource-leak` | Medium | +| `cfg-lock-not-released` | Medium | + +### State model + +| Rule ID | Severity | +|---|---| +| `state-use-after-close` | High | +| `state-double-close` | Medium | +| `state-resource-leak` | Medium | +| `state-resource-leak-possible` | Low | +| `state-unauthed-access` | High | + +### Auth analysis (Rust only, today) + +| Rule ID | Severity | +|---|---| +| `rs.auth.missing_ownership_check` | High | +| `rs.auth.missing_ownership_check.taint` | High (gated by `scanner.enable_auth_as_taint`) | + +See [auth.md](auth.md) for scope, the five sink-classes, and tuning. + +## AST patterns by language + +Each language ships a tree-sitter pattern registry. Structural match on the pattern, no dataflow. Some patterns also have a Tier B heuristic guard (e.g. SQL execute must receive a concatenation, not a literal) noted in the registry. + +The tables below are generated from `src/patterns/.rs` by [`tools/docgen`](https://github.com/elicpeter/nyx/tree/master/tools/docgen). Run `cargo run --features docgen --bin nyx-docgen` after changing the registry to refresh them. + + + +### C: 8 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `c.cmdi.system` | High | A | High | +| `c.memory.gets` | High | A | High | +| `c.memory.printf_no_fmt` | High | B | Medium | +| `c.memory.scanf_percent_s` | High | A | High | +| `c.memory.sprintf` | High | A | High | +| `c.memory.strcat` | High | A | High | +| `c.memory.strcpy` | High | A | High | +| `c.cmdi.popen` | Medium | A | High | + +### C++: 9 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `cpp.cmdi.popen` | High | A | High | +| `cpp.cmdi.system` | High | A | High | +| `cpp.memory.gets` | High | A | High | +| `cpp.memory.printf_no_fmt` | High | B | Medium | +| `cpp.memory.sprintf` | High | A | High | +| `cpp.memory.strcat` | High | A | High | +| `cpp.memory.strcpy` | High | A | High | +| `cpp.memory.const_cast` | Medium | A | High | +| `cpp.memory.reinterpret_cast` | Medium | A | High | + +### Go: 8 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `go.cmdi.exec_command` | High | A | High | +| `go.transport.insecure_skip_verify` | High | A | High | +| `go.deser.gob_decode` | Medium | A | High | +| `go.memory.unsafe_pointer` | Medium | A | High | +| `go.secrets.hardcoded_key` | Medium | A | High | +| `go.sqli.query_concat` | Medium | B | Medium | +| `go.crypto.md5` | Low | A | Medium | +| `go.crypto.sha1` | Low | A | Medium | + +### Java: 9 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `java.cmdi.runtime_exec` | High | A | High | +| `java.code_exec.text4shell_interpolator` | High | A | High | +| `java.deser.readobject` | High | A | High | +| `java.deser.snakeyaml_unsafe_constructor` | High | A | High | +| `java.crypto.weak_algorithm` | Medium | A | Medium | +| `java.reflection.class_forname` | Medium | A | High | +| `java.reflection.method_invoke` | Medium | A | High | +| `java.sqli.execute_concat` | Medium | B | Medium | +| `java.crypto.insecure_random` | Low | A | Medium | + +### JavaScript: 23 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `js.code_exec.eval` | High | A | High | +| `js.code_exec.new_function` | High | A | High | +| `js.config.cors_dynamic_origin` | High | A | Medium | +| `js.code_exec.settimeout_string` | Medium | A | High | +| `js.config.insecure_session_httponly` | Medium | A | High | +| `js.config.reject_unauthorized` | Medium | A | High | +| `js.config.verbose_error_response` | Medium | A | Medium | +| `js.crypto.weak_hash_import` | Medium | A | Medium | +| `js.prototype.extend_object` | Medium | A | High | +| `js.prototype.proto_assignment` | Medium | A | High | +| `js.secrets.fallback_secret` | Medium | A | Medium | +| `js.xss.cookie_write` | Medium | A | High | +| `js.xss.document_write` | Medium | A | High | +| `js.xss.insert_adjacent_html` | Medium | A | High | +| `js.xss.location_assign` | Medium | A | High | +| `js.xss.outer_html` | Medium | A | High | +| `js.config.insecure_session_samesite` | Low | A | High | +| `js.config.insecure_session_secure` | Low | A | Medium | +| `js.crypto.hardcoded_key` | Low | A | Medium | +| `js.crypto.math_random` | Low | A | Medium | +| `js.crypto.weak_hash` | Low | A | Medium | +| `js.secrets.hardcoded_secret` | Low | A | Medium | +| `js.transport.fetch_http` | Low | A | Medium | + +### PHP: 11 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `php.cmdi.system` | High | A | High | +| `php.code_exec.assert_string` | High | A | High | +| `php.code_exec.create_function` | High | A | High | +| `php.code_exec.eval` | High | A | High | +| `php.code_exec.preg_replace_e` | High | A | High | +| `php.deser.unserialize` | High | A | High | +| `php.path.include_variable` | High | B | Medium | +| `php.sqli.query_concat` | Medium | B | Medium | +| `php.crypto.md5` | Low | A | Medium | +| `php.crypto.rand` | Low | A | Medium | +| `php.crypto.sha1` | Low | A | Medium | + +### Python: 17 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `py.cmdi.os_popen` | High | A | High | +| `py.cmdi.os_system` | High | A | High | +| `py.cmdi.subprocess_shell` | High | B | Medium | +| `py.code_exec.eval` | High | A | High | +| `py.code_exec.exec` | High | A | High | +| `py.deser.pickle_loads` | High | A | High | +| `py.deser.yaml_load` | High | A | High | +| `py.code_exec.compile` | Medium | A | High | +| `py.deser.shelve_open` | Medium | A | High | +| `py.sqli.execute_format` | Medium | B | Medium | +| `py.sqli.text_format` | Medium | B | Medium | +| `py.xss.jinja_from_string` | Medium | A | High | +| `py.xss.make_response_format` | Medium | B | Medium | +| `py.crypto.md5` | Low | A | Medium | +| `py.crypto.md5_bare` | Low | A | Low | +| `py.crypto.sha1` | Low | A | Medium | +| `py.crypto.sha1_bare` | Low | A | Low | + +### Ruby: 11 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `rb.cmdi.backtick` | High | A | High | +| `rb.cmdi.system_interp` | High | A | High | +| `rb.code_exec.class_eval` | High | A | High | +| `rb.code_exec.eval` | High | A | High | +| `rb.code_exec.instance_eval` | High | A | High | +| `rb.deser.marshal_load` | High | A | High | +| `rb.deser.yaml_load` | High | A | High | +| `rb.reflection.constantize` | Medium | A | High | +| `rb.reflection.send_dynamic` | Medium | B | Medium | +| `rb.ssrf.open_uri` | Medium | A | High | +| `rb.crypto.md5` | Low | A | Medium | + +### Rust: 13 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `rs.memory.copy_nonoverlapping` | High | A | High | +| `rs.memory.get_unchecked` | High | A | High | +| `rs.memory.mem_zeroed` | High | A | High | +| `rs.memory.ptr_read` | High | A | High | +| `rs.memory.transmute` | High | A | High | +| `rs.quality.unsafe_block` | Medium | A | High | +| `rs.quality.unsafe_fn` | Medium | A | High | +| `rs.memory.mem_forget` | Low | A | High | +| `rs.memory.narrow_cast` | Low | A | Medium | +| `rs.quality.expect` | Low | A | High | +| `rs.quality.panic_macro` | Low | A | High | +| `rs.quality.todo` | Low | A | High | +| `rs.quality.unwrap` | Low | A | High | + +### TypeScript: 23 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `ts.code_exec.eval` | High | A | High | +| `ts.code_exec.new_function` | High | A | High | +| `ts.config.cors_dynamic_origin` | High | A | Medium | +| `ts.code_exec.settimeout_string` | Medium | A | High | +| `ts.config.insecure_session_httponly` | Medium | A | High | +| `ts.config.reject_unauthorized` | Medium | A | High | +| `ts.config.verbose_error_response` | Medium | A | Medium | +| `ts.crypto.weak_hash_import` | Medium | A | Medium | +| `ts.prototype.proto_assignment` | Medium | A | High | +| `ts.secrets.fallback_secret` | Medium | A | Medium | +| `ts.xss.document_write` | Medium | A | High | +| `ts.xss.insert_adjacent_html` | Medium | A | High | +| `ts.xss.location_assign` | Medium | A | High | +| `ts.xss.outer_html` | Medium | A | High | +| `ts.config.insecure_session_samesite` | Low | A | High | +| `ts.config.insecure_session_secure` | Low | A | Medium | +| `ts.crypto.hardcoded_key` | Low | A | Medium | +| `ts.crypto.math_random` | Low | A | Medium | +| `ts.crypto.weak_hash` | Low | A | Medium | +| `ts.quality.any_annotation` | Low | A | Medium | +| `ts.quality.as_any` | Low | A | Medium | +| `ts.secrets.hardcoded_secret` | Low | A | Medium | +| `ts.xss.cookie_write` | Low | A | Medium | + + + +## Capability list for custom rules + +`nyx config add-rule --cap ` 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`, `data_exfil`, `ldap_injection`, `xpath_injection`, `header_injection`, `open_redirect`, `ssti`, `xxe`, `prototype_pollution`, `all` + +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`). diff --git a/docs/serve.md b/docs/serve.md new file mode 100644 index 00000000..712afc76 --- /dev/null +++ b/docs/serve.md @@ -0,0 +1,176 @@ +# `nyx serve`: the browser UI + +The CLI is fine for CI. For triage, you want context: the source snippet, the dataflow path, the history of how a finding has moved across scans, and a place to record decisions that survive the next run. `nyx serve` boots a local React UI bound to loopback. + +```bash +nyx serve # opens http://localhost:9700 in your default browser +nyx serve ./my-project # serve a specific project root +nyx serve --port 9750 # override port +nyx serve --no-browser # don't auto-open +``` + +Persistent settings live under `[server]` in `nyx.conf` / `nyx.local`. + +```mermaid +flowchart LR + Scan["nyx scan
or UI-started scan"] --> Cache[".nyx findings
plus SQLite project index"] + Cache --> Serve["nyx serve
loopback API and embedded React UI"] + Serve --> Review["Review findings
flow, evidence, history"] + Review --> Triage["Update triage state
investigate, suppress, accept, fix"] + Triage --> Sync[".nyx/triage.json
optional repo-synced state"] + Sync --> Cache +``` + +Starting a scan from the UI runs dynamic verification on `Confidence >= Medium` +findings by default. Check "Skip dynamic verification" in the scan modal to get +a fast static-only result. See [Dynamic verification](dynamic.md) for details. + +

Nyx UI overview: total findings, severity breakdown, language and category distribution, top affected files

+ +## What it serves, and what it doesn't + +The frontend is built and embedded into the `nyx` binary at compile time. There's no separate install step, and the binary serves the entire UI from memory; nothing is fetched from a CDN. The UI talks to the local Nyx process over a small JSON API. + +There is **no** account, no telemetry, no remote logging, no auto-update ping. The data the UI shows is the data on your disk: the SQLite project index plus `.nyx/triage.json`. + +## Security model + +`nyx serve` enforces three things: + +1. **Loopback bind only.** `--host` and `[server].host` are clamped to `127.0.0.1`, `localhost`, or `::1`. Any other value is refused at startup with `Nyx serve only binds to loopback addresses; refused host ''` ([`src/commands/serve.rs`](https://github.com/elicpeter/nyx/blob/master/src/commands/serve.rs)). +2. **Host-header check.** Every request must carry a `Host` header that matches the bound address and port. Missing or mismatched headers get a `400 invalid Host header`. Defends against DNS rebinding ([`src/server/security.rs`](https://github.com/elicpeter/nyx/blob/master/src/server/security.rs)). +3. **CSRF on mutations.** `POST` / `PUT` / `PATCH` / `DELETE` requests must carry a per-process CSRF token in the `x-nyx-csrf` header. The token is generated once when the server starts and exposed at `GET /api/health` so the embedded SPA can read it. Cross-origin mutations are rejected before the CSRF check via the `Origin` header. + +If you forward the port over SSH or expose it through a reverse proxy, the host-header check will reject the request because the `Host` won't match `localhost:9700`. That's the intended behaviour. Don't do this without a deliberate reason; the loopback bind is part of the security model. + +## The pages + +| Path | Page | +|---|---| +| `/` | Overview | +| `/findings` | Findings list | +| `/findings/:id` | Finding detail | +| `/triage` | Triage | +| `/explorer` | Explorer | +| `/scans` | Scans | +| `/scans/:id` | Scan detail and compare | +| `/rules` | Rules | +| `/rules/:id` | Rule detail | +| `/config` | Config | + +The numeric `:id` for finding URLs is the position index in the current scan, not a stable fingerprint. Bookmarks across scans aren't reliable; rely on file path + line. + +### Overview and Health Score + +The overview is the landing page after a scan. Severity counts, top affected files, OWASP coverage, and a 0 to 100 Health Score with a letter grade. + +#### How the Health Score is calculated + +Two things drive the score. The density of risk in the codebase, and hard guardrails that decide what the grade can mean. + +Each finding contributes weight = `severity_base × confidence_factor × verdict_factor × context_factor`: + +- Severity base: HIGH 10, MEDIUM 3, LOW (security) 0.5 +- Confidence: High 1.0, Medium 0.6, Low 0.3 +- Symex verdict: Confirmed 1.2, NotAttempted 1.0, Inconclusive 0.7, Infeasible 0.1 +- Context: cross-file taint flow 1.15, intra-file flow 1.0, AST-only or no flow 0.75, test path 0.3 + +Quality lints (rule IDs containing `.quality.`) skip the per-finding weight and instead apply a saturating drag, capped at 15 points (so 1000 unwrap lints don't grade worse than 300 do). Total weight gets divided by `sqrt(files / 100)`, clamped between 1 and roughly 22, so a 100-file repo and a 50000-file repo see different denominators but a monorepo can't dilute its way out of a real HIGH. + +The result feeds a log curve into a 0 to 100 base, minus the quality drag. Then HIGH guardrails apply, keyed on the *credibility-adjusted* HIGH count rather than the raw count: + +| effective HIGH | ceiling | +|---|---| +| 0 | 100 | +| 1 | 85 | +| 2 | 78 | +| 3 to 5 | 68 | +| 6 to 10 | 58 | +| 11+ | 45 | + +A repo with zero effective HIGHs never grades below C 70. That floor is the structural promise that the score isn't an automated F-machine for projects that have lots of LOW noise but no critical issues. + +Modifiers in the ±5 range nudge the result for trend (only after the second scan), triage coverage (only when total findings ≥ 20), reintroduced findings, and stale HIGHs more than 30 days old. + +#### What the score doesn't measure + +It's a Nyx-finding-pressure metric, not a security audit. Score 100 means Nyx didn't find anything under its current rules and language coverage; it doesn't certify the absence of vulnerabilities. The score doesn't see runtime config, IAM, secret stores, dependency CVEs, or anything outside the source tree being scanned. A repo of mostly Kotlin (where Nyx coverage is thin) will score artificially well because most of the code never gets evaluated. + +Ceilings are calibrated for the current scanner false-positive rates. As symex coverage and rule precision improve, the ceilings may tighten. + +### Findings and Finding detail + +The findings list is filterable by severity, confidence, category, language, rule ID, and triage state. + +

Nyx findings list: 13 findings filtered by severity/confidence/rule, with status badges, file paths, and language tags

+ +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. + +

Nyx finding detail: HIGH taint-unsanitised-flow showing source → call → sink steps, How to fix guidance, and evidence panel

+ +Engine notes call out when precision was bounded for that finding (`OriginsTruncated`, `PointsToTruncated`, `WorklistCapped`, `PredicateStateWidened`, `SsaLoweringBailed`, etc.). Each note carries a direction tag: `under-report` means the emitted flow is real and the result set is a lower bound; `over-report` means widening dropped a guard; `bail` means analysis aborted before producing a trustworthy result. `--require-converged` in the CLI drops over-report and bail notes for strict gates. + +### Triage + +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. + +

Nyx triage page: 13 findings need attention, severity breakdown, Findings/Suppression rules/Audit log tabs, rule chips, Investigate buttons

+ +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: + +```bash +git add .nyx/triage.json +``` + +It carries decisions across machines so a teammate's local scan reflects yours. The format is documented in [`src/server/triage_sync.rs`](https://github.com/elicpeter/nyx/blob/master/src/server/triage_sync.rs); the schema is stable and round-trip-safe with `nyx serve` re-imports. + +### Explorer + +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". + +

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

+ +The path query string preselects a file: `/explorer?file=src/handler.rs`. + +### Scans and compare + +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. + +

Nyx scans list: completed scan run with root, duration, finding count, languages, and started timestamp

+ +Each run drills into a detail page with files scanned, findings count, duration, languages, and a per-pass timing breakdown. + +

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

+ +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. + +### Rules + +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. + +

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

+ +User-added rules can be deleted from this page; built-ins are immutable. Built-ins live in `src/labels/.rs` and `src/patterns/.rs`; user-added entries write to `nyx.local`. + +### Config + +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. + +

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

+ +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. + +## API surface + +For tooling, the JSON endpoints under `/api/` are stable enough to script against. The full route map lives in [`src/server/routes/mod.rs`](https://github.com/elicpeter/nyx/blob/master/src/server/routes/mod.rs). Mutating endpoints require the `x-nyx-csrf` header (read it from `GET /api/health`). + +## Disabling + +If you don't want the UI for a project, set: + +```toml +[server] +enabled = false +``` + +`nyx serve` will refuse to start. The CLI continues to work. diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..99f805ff --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,4 @@ +node_modules +tsconfig.tsbuildinfo +dist +../src/server/assets/dist diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 00000000..a20502b7 --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..93f73d79 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: [ + 'node_modules', + 'tsconfig.tsbuildinfo', + 'dist', + '../src/server/assets/dist', + ], + }, + { + files: ['**/*.{ts,tsx}'], + extends: [js.configs.recommended, ...tseslint.configs.recommended], + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2020, + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'react-refresh/only-export-components': 'off', + }, + }, +); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..e9f900e4 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,21 @@ + + + + + + Nyx + + + + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..692a4d70 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6095 @@ +{ + "name": "nyx-frontend", + "version": "0.7.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nyx-frontend", + "version": "0.7.0", + "license": "GPL-3.0-or-later", + "dependencies": { + "@tanstack/react-query": "^5.101.0", + "elkjs": "^0.11.1", + "graphology": "^0.26.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.17.0", + "sigma": "^3.0.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.2.16", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^10.4.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "license-checker-rseidelsohn": "^5.0.1", + "prettier": "^3.8.3", + "typescript": "~6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vitest": "^4.1.8" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "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": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/arborist": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-9.6.0.tgz", + "integrity": "sha512-Dku9UWbrrX+UCu8rQ1obGKaQAL4kwdt3hHCNXrd0n0R/4B8oq3CzloUAShwFjfsAGM6KY27gPuNftOUEZ4nhOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^5.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/query": "^5.0.0", + "@npmcli/redact": "^4.0.0", + "@npmcli/run-script": "^10.0.0", + "bin-links": "^6.0.0", + "cacache": "^20.0.1", + "common-ancestor-path": "^2.0.0", + "hosted-git-info": "^9.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^9.0.0", + "npm-install-checks": "^8.0.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", + "parse-conflict-json": "^5.0.1", + "proc-log": "^6.0.0", + "proggy": "^4.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^13.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/arborist/node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-5.0.3.tgz", + "integrity": "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "glob": "^13.0.0", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-9.0.3.tgz", + "integrity": "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cacache": "^20.0.0", + "json-parse-even-better-errors": "^5.0.0", + "pacote": "^21.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-4.0.0.tgz", + "integrity": "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-5.0.0.tgz", + "integrity": "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "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, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", + "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "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": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bin-links": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.2.tgz", + "integrity": "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w==", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "proc-log": "^6.0.0", + "read-cmd-shim": "^6.0.0", + "write-file-atomic": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cmd-shim": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz", + "integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "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" + }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "license": "EPL-2.0" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphology": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz", + "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0" + }, + "peerDependencies": { + "graphology-types": ">=0.24.0" + } + }, + "node_modules/graphology-types": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", + "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", + "license": "MIT", + "peer": true + }, + "node_modules/graphology-utils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", + "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", + "license": "MIT", + "peerDependencies": { + "graphology-types": ">=0.23.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "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": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-nice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/just-diff": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/just-diff-apply": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-checker-rseidelsohn": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/license-checker-rseidelsohn/-/license-checker-rseidelsohn-5.0.1.tgz", + "integrity": "sha512-9X+ikKxt9Hy3zOrOZzW1dXL4St5akoYjLt63Am9JZVzU6aTdN+xfDvqySpnJT+gF/h5RmtMk2waW6TDNNCKbqQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@npmcli/arborist": "9.6.0", + "@npmcli/package-json": "7.0.5", + "chalk": "4.1.2", + "debug": "^4.3.4", + "lodash.clonedeep": "^4.5.0", + "mkdirp": "^1.0.4", + "nopt": "^7.2.0", + "semver": "^7.3.5", + "spdx-correct": "^3.2.0", + "spdx-expression-parse": "^4.0.0", + "spdx-satisfies": "^6.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker-rseidelsohn": "bin/license-checker-rseidelsohn.js" + }, + "engines": { + "node": ">=24", + "npm": ">=11" + } + }, + "node_modules/license-checker-rseidelsohn/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "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": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.5.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.0.tgz", + "integrity": "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse-conflict-json": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-5.0.1.tgz", + "integrity": "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^5.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/proggy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz", + "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-all-reject-late": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-call-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", + "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-router": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", + "license": "MIT", + "dependencies": { + "react-router": "7.17.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cmd-shim": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz", + "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sigma": { + "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", + "graphology-utils": "^2.5.2" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-compare/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-6.0.0.tgz", + "integrity": "sha512-oOWQocnRbFVtBnBITfFgzjhnOklHossTvI+6C1hB2slvp3HgTsfru5wuo8HY2rQpwSm5JuIhNzIuqOfR5IuojQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-satisfies/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "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.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "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" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/treeverse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", + "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/write-file-atomic": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "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": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..164626a4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "nyx-frontend", + "private": true, + "version": "0.7.0", + "license": "GPL-3.0-or-later", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "license:check": "node ./scripts/check-licenses.mjs", + "lint": "eslint .", + "typecheck": "tsc -b", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "@tanstack/react-query": "^5.101.0", + "elkjs": "^0.11.1", + "graphology": "^0.26.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.17.0", + "sigma": "^3.0.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.2.16", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^10.4.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "license-checker-rseidelsohn": "^5.0.1", + "prettier": "^3.8.3", + "typescript": "~6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", + "vitest": "^4.1.8" + } +} diff --git a/frontend/public/favicon-180.png b/frontend/public/favicon-180.png new file mode 100644 index 00000000..ab8d9031 Binary files /dev/null and b/frontend/public/favicon-180.png differ diff --git a/frontend/public/favicon-32.png b/frontend/public/favicon-32.png new file mode 100644 index 00000000..b66ce1e8 Binary files /dev/null and b/frontend/public/favicon-32.png differ diff --git a/frontend/public/favicon-64.png b/frontend/public/favicon-64.png new file mode 100644 index 00000000..73fcc196 Binary files /dev/null and b/frontend/public/favicon-64.png differ diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 00000000..fecb6e78 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/scripts/check-licenses.mjs b/frontend/scripts/check-licenses.mjs new file mode 100644 index 00000000..301417ca --- /dev/null +++ b/frontend/scripts/check-licenses.mjs @@ -0,0 +1,81 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const frontendDir = join(scriptDir, '..'); +const repoRoot = join(frontendDir, '..'); +const aboutToml = join(repoRoot, 'about.toml'); +const frontendPackageJson = join(frontendDir, 'package.json'); + +const aboutContents = readFileSync(aboutToml, 'utf8'); +const acceptedBlock = aboutContents.match(/accepted\s*=\s*\[([\s\S]*?)\]/); + +if (!acceptedBlock) { + console.error(`Could not find accepted licenses in ${aboutToml}`); + process.exit(1); +} + +const rawAcceptedLicenses = [...acceptedBlock[1].matchAll(/"([^"]+)"/g)].map( + ([, license]) => license, +); + +if (rawAcceptedLicenses.length === 0) { + console.error(`No accepted licenses found in ${aboutToml}`); + process.exit(1); +} + +// cargo-about rejects modern SPDX `-only` / `-or-later` forms in its allow +// list, so about.toml uses the deprecated bare identifiers (e.g. "GPL-3.0"). +// npm ecosystems standardize on the modern forms, so accept both here. +const deprecatedSpdxFamily = /^(?:L?GPL|AGPL|GFDL)-\d+\.\d+$/; +const acceptedLicenses = [ + ...new Set( + rawAcceptedLicenses.flatMap((license) => + deprecatedSpdxFamily.test(license) + ? [license, `${license}-only`, `${license}-or-later`] + : [license], + ), + ), +]; + +const frontendPackage = JSON.parse(readFileSync(frontendPackageJson, 'utf8')); +const frontendLicense = frontendPackage.license; + +if (!frontendLicense) { + console.error( + `Package "${frontendPackage.name}@${frontendPackage.version}" is missing a license field.`, + ); + process.exit(1); +} + +if (!acceptedLicenses.includes(frontendLicense)) { + console.error( + `Package "${frontendPackage.name}@${frontendPackage.version}" is licensed under "${frontendLicense}" which is not permitted.`, + ); + process.exit(1); +} + +const result = spawnSync( + './node_modules/.bin/license-checker-rseidelsohn', + [ + '--start', + '.', + '--excludePrivatePackages', + '--onlyAllow', + acceptedLicenses.join(';'), + '--summary', + ], + { + cwd: frontendDir, + stdio: 'inherit', + }, +); + +if (result.error) { + console.error(result.error.message); + process.exit(1); +} + +process.exit(result.status ?? 1); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..0611e6de --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,25 @@ +import { QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import { queryClient } from './api/queryClient'; +import { SSEProvider } from './contexts/SSEContext'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { ToastProvider } from './contexts/ToastContext'; +import { Toaster } from './components/ui/Toaster'; +import { AppLayout } from './components/layout/AppLayout'; + +export function App() { + return ( + + + + + + + + + + + + + ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 00000000..57fa48dc --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,174 @@ +const BASE = '/api'; +const CSRF_HEADER = 'X-Nyx-CSRF'; +let csrfTokenPromise: Promise | 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 was not structured, + * `network` for fetch failures, `http_` for plain-text responses. + */ + public code: string; + public detail?: unknown; + + constructor( + status: number, + message: string, + code?: string, + detail?: unknown, + ) { + super(message); + this.name = 'ApiError'; + this.status = status; + this.code = code ?? `http_${status}`; + this.detail = detail; + } + + public status: number; + + /** True when the failure was a network/abort, not an HTTP response. */ + isNetwork(): boolean { + return this.status === 0; + } +} + +/** Build an ApiError from a non-OK Response, parsing a JSON error body if present. */ +async function errorFromResponse(res: Response): Promise { + const text = await res.text().catch(() => ''); + if (text) { + try { + const parsed = JSON.parse(text) as { + error?: unknown; + code?: unknown; + detail?: unknown; + }; + const msg = + typeof parsed.error === 'string' && parsed.error.length > 0 + ? parsed.error + : res.statusText || `HTTP ${res.status}`; + 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. + return new ApiError(res.status, text); + } + } + return new ApiError(res.status, res.statusText || `HTTP ${res.status}`); +} + +async function getCsrfToken(): Promise { + if (!csrfTokenPromise) { + csrfTokenPromise = fetch(`${BASE}/session`) + .then(async (res) => { + if (!res.ok) { + throw await errorFromResponse(res); + } + + const text = await res.text(); + const payload = text + ? (JSON.parse(text) as { csrf_token?: unknown }) + : {}; + if ( + typeof payload.csrf_token !== 'string' || + payload.csrf_token.length === 0 + ) { + throw new ApiError(500, 'Missing CSRF token', 'missing_csrf_token'); + } + + return payload.csrf_token; + }) + .catch((error) => { + csrfTokenPromise = null; + throw error; + }); + } + + return csrfTokenPromise; +} + +function isMutatingMethod(method?: string): boolean { + const upper = (method || 'GET').toUpperCase(); + return ( + upper === 'POST' || + upper === 'PUT' || + upper === 'PATCH' || + upper === 'DELETE' + ); +} + +async function request(path: string, opts: RequestInit = {}): Promise { + const { headers: rawHeaders, ...rest } = opts; + const url = `${BASE}${path}`; + const headers: Record = { + ...(rawHeaders as Record), + }; + if (isMutatingMethod(rest.method)) { + headers[CSRF_HEADER] = await getCsrfToken(); + } + if (opts.body) { + headers['Content-Type'] = 'application/json'; + } + let res: Response; + try { + res = await fetch(url, { + ...rest, + headers, + }); + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + throw err; + } + const message = + err instanceof Error ? err.message : 'Network request failed'; + throw new ApiError(0, message, 'network'); + } + + if (!res.ok) { + throw await errorFromResponse(res); + } + + // Handle empty responses + const text = await res.text(); + if (!text) return undefined as T; + return JSON.parse(text) as T; +} + +export function apiGet(path: string, signal?: AbortSignal): Promise { + return request(path, { signal }); +} + +export function apiPost( + path: string, + body?: unknown, + signal?: AbortSignal, +): Promise { + return request(path, { + method: 'POST', + body: body != null ? JSON.stringify(body) : undefined, + signal, + }); +} + +export function apiPut( + path: string, + body?: unknown, + signal?: AbortSignal, +): Promise { + return request(path, { + method: 'PUT', + body: body != null ? JSON.stringify(body) : undefined, + signal, + }); +} + +export function apiDelete( + path: string, + body?: unknown, + signal?: AbortSignal, +): Promise { + return request(path, { + method: 'DELETE', + body: body != null ? JSON.stringify(body) : undefined, + signal, + }); +} diff --git a/frontend/src/api/mutations/baseline.ts b/frontend/src/api/mutations/baseline.ts new file mode 100644 index 00000000..66b22222 --- /dev/null +++ b/frontend/src/api/mutations/baseline.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiDelete, apiPost } from '../client'; + +export function usePinBaseline() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (scanId: string) => + apiPost('/overview/baseline', { scan_id: scanId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['overview'] }); + }, + }); +} + +export function useUnpinBaseline() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: () => apiDelete('/overview/baseline'), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['overview'] }); + }, + }); +} diff --git a/frontend/src/api/mutations/config.ts b/frontend/src/api/mutations/config.ts new file mode 100644 index 00000000..ef96ff0f --- /dev/null +++ b/frontend/src/api/mutations/config.ts @@ -0,0 +1,182 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiPost, apiPut, apiDelete } from '../client'; +import type { LabelEntryView, TerminatorView, ProfileView } from '../types'; + +// --- Sources --- + +export interface AddLabelBody { + lang: string; + matchers: string[]; + cap: string; + case_sensitive?: boolean; +} + +export function useAddSource() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiPost('/config/sources', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sources'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +export function useDeleteSource() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiDelete('/config/sources', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sources'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +// --- Sinks --- + +export function useAddSink() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiPost('/config/sinks', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sinks'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +export function useDeleteSink() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => apiDelete('/config/sinks', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sinks'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +// --- Sanitizers --- + +export function useAddSanitizer() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiPost('/config/sanitizers', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sanitizers'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +export function useDeleteSanitizer() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiDelete('/config/sanitizers', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sanitizers'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +// --- Terminators --- + +export interface AddTerminatorBody { + lang: string; + name: string; +} + +export function useAddTerminator() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddTerminatorBody) => + apiPost('/config/terminators', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'terminators'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +export function useDeleteTerminator() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddTerminatorBody) => + apiDelete('/config/terminators', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'terminators'] }); + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +// --- Profiles --- + +export function useAddProfile() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { name: string; settings: Record }) => + apiPost('/config/profiles', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'profiles'] }); + }, + }); +} + +export function useDeleteProfile() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => + apiDelete(`/config/profiles/${encodeURIComponent(name)}`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'profiles'] }); + }, + }); +} + +export function useActivateProfile() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => + apiPost(`/config/profiles/${encodeURIComponent(name)}/activate`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config'] }); + qc.invalidateQueries({ queryKey: ['config', 'profiles'] }); + }, + }); +} + +// --- Raw nyx.local TOML --- + +export function useSaveRawConfig() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (content: string) => + apiPut<{ status: string; path: string; bytes: number }>('/config/raw', { + content, + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config'] }); + }, + }); +} + +// --- Triage Sync --- + +export function useToggleTriageSync() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { enabled: boolean }) => + apiPost('/config/triage-sync', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['triage', 'sync-status'] }); + }, + }); +} diff --git a/frontend/src/api/mutations/rules.ts b/frontend/src/api/mutations/rules.ts new file mode 100644 index 00000000..fc71cae3 --- /dev/null +++ b/frontend/src/api/mutations/rules.ts @@ -0,0 +1,24 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiPost } from '../client'; + +export function useToggleRule() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => + apiPost(`/rules/${encodeURIComponent(id)}/toggle`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} + +export function useCloneRule() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { rule_id: string }) => + apiPost('/rules/clone', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['rules'] }); + }, + }); +} diff --git a/frontend/src/api/mutations/scans.ts b/frontend/src/api/mutations/scans.ts new file mode 100644 index 00000000..467f2f83 --- /dev/null +++ b/frontend/src/api/mutations/scans.ts @@ -0,0 +1,49 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiPost, apiDelete } from '../client'; +import type { ScanView } from '../types'; + +export type ScanMode = 'full' | 'ast' | 'cfg' | 'taint'; +export type EngineProfile = 'fast' | 'balanced' | 'deep'; +export type VerifyBackend = 'auto' | 'docker' | 'process' | 'firecracker'; +export type HardenProfile = 'standard' | 'strict'; + +export interface StartScanBody { + scan_root?: string; + mode?: ScanMode; + engine_profile?: EngineProfile; + /** + * Override dynamic verification for this scan. + * true - force on. + * false - force off. + * absent - use server config default. + */ + verify?: boolean; + /** Also verify Confidence < Medium findings. Default false. */ + verify_all_confidence?: boolean; + /** Sandbox backend for dynamic verification. */ + verify_backend?: VerifyBackend; + /** Process-backend hardening profile. */ + harden_profile?: HardenProfile; +} + +export function useStartScan() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body?: StartScanBody) => apiPost('/scans', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['scans'] }); + }, + }); +} + +export function useDeleteScan() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => + apiDelete(`/scans/${encodeURIComponent(id)}`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['scans'] }); + qc.invalidateQueries({ queryKey: ['overview'] }); + }, + }); +} diff --git a/frontend/src/api/mutations/triage.ts b/frontend/src/api/mutations/triage.ts new file mode 100644 index 00000000..e5c84def --- /dev/null +++ b/frontend/src/api/mutations/triage.ts @@ -0,0 +1,86 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiPost, apiDelete } from '../client'; + +export interface BulkTriageBody { + fingerprints: string[]; + state: string; + note?: string; +} + +export interface UpdateFindingTriageBody { + state: string; + note?: string; +} + +export interface AddSuppressionBody { + by: string; + value: string; + note?: string; +} + +export function useBulkTriage() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: BulkTriageBody) => apiPost('/triage', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['findings'] }); + qc.invalidateQueries({ queryKey: ['triage'] }); + qc.invalidateQueries({ queryKey: ['overview'] }); + }, + }); +} + +export function useUpdateFindingTriage() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ + index, + ...body + }: UpdateFindingTriageBody & { index: number | string }) => + apiPost(`/findings/${index}/triage`, body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['findings'] }); + qc.invalidateQueries({ queryKey: ['triage'] }); + }, + }); +} + +export function useAddSuppression() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddSuppressionBody) => + apiPost('/triage/suppress', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['triage'] }); + qc.invalidateQueries({ queryKey: ['findings'] }); + qc.invalidateQueries({ queryKey: ['triage', 'suppress'] }); + }, + }); +} + +export function useDeleteSuppression() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => apiDelete(`/triage/suppress?id=${id}`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['triage', 'suppress'] }); + }, + }); +} + +export function useTriageExport() { + return useMutation({ + mutationFn: () => apiPost('/triage/export'), + }); +} + +export function useTriageImport() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: () => apiPost('/triage/import'), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['triage'] }); + qc.invalidateQueries({ queryKey: ['findings'] }); + }, + }); +} diff --git a/frontend/src/api/queries/config.ts b/frontend/src/api/queries/config.ts new file mode 100644 index 00000000..cabec4ae --- /dev/null +++ b/frontend/src/api/queries/config.ts @@ -0,0 +1,61 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { LabelEntryView, TerminatorView, ProfileView } from '../types'; + +export function useConfig() { + return useQuery({ + queryKey: ['config'], + queryFn: ({ signal }) => apiGet('/config', signal), + }); +} + +export interface RawConfigView { + path: string; + exists: boolean; + content: string; +} + +export function useRawConfig() { + return useQuery({ + queryKey: ['config', 'raw'], + queryFn: ({ signal }) => apiGet('/config/raw', signal), + }); +} + +export function useSources() { + return useQuery({ + queryKey: ['config', 'sources'], + queryFn: ({ signal }) => + apiGet('/config/sources', signal), + }); +} + +export function useSinks() { + return useQuery({ + queryKey: ['config', 'sinks'], + queryFn: ({ signal }) => apiGet('/config/sinks', signal), + }); +} + +export function useSanitizers() { + return useQuery({ + queryKey: ['config', 'sanitizers'], + queryFn: ({ signal }) => + apiGet('/config/sanitizers', signal), + }); +} + +export function useTerminators() { + return useQuery({ + queryKey: ['config', 'terminators'], + queryFn: ({ signal }) => + apiGet('/config/terminators', signal), + }); +} + +export function useProfiles() { + return useQuery({ + queryKey: ['config', 'profiles'], + queryFn: ({ signal }) => apiGet('/config/profiles', signal), + }); +} diff --git a/frontend/src/api/queries/debug.ts b/frontend/src/api/queries/debug.ts new file mode 100644 index 00000000..20f413c3 --- /dev/null +++ b/frontend/src/api/queries/debug.ts @@ -0,0 +1,150 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { + FunctionInfo, + CfgGraphView, + SsaBodyView, + TaintAnalysisView, + AbstractInterpView, + SymexView, + CallGraphView, + FuncSummaryView, + PointerView, + TypeFactsView, + AuthAnalysisView, +} from '../types'; + +export function useDebugFunctions(file: string | null) { + return useQuery({ + queryKey: ['debug', 'functions', file], + queryFn: ({ signal }) => + apiGet( + `/debug/functions?file=${encodeURIComponent(file!)}`, + signal, + ), + enabled: !!file, + }); +} + +export function useDebugCfg(file: string | null, fn_name: string | null) { + return useQuery({ + queryKey: ['debug', 'cfg', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/cfg?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugSsa(file: string | null, fn_name: string | null) { + return useQuery({ + queryKey: ['debug', 'ssa', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/ssa?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugTaint(file: string | null, fn_name: string | null) { + return useQuery({ + queryKey: ['debug', 'taint', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/taint?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugAbstractInterp( + file: string | null, + fn_name: string | null, +) { + return useQuery({ + queryKey: ['debug', 'abstract-interp', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/abstract-interp?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugSymex(file: string | null, fn_name: string | null) { + return useQuery({ + queryKey: ['debug', 'symex', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/symex?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugCallGraph(scope: string, file?: string | null) { + const params = new URLSearchParams({ scope }); + if (file) params.set('file', file); + return useQuery({ + queryKey: ['debug', 'call-graph', scope, file], + queryFn: ({ signal }) => + apiGet(`/debug/call-graph?${params}`, signal), + }); +} + +export function useDebugSummaries( + file?: string | null, + fn_name?: string | null, +) { + const params = new URLSearchParams(); + if (file) params.set('file', file); + if (fn_name) params.set('function', fn_name); + return useQuery({ + queryKey: ['debug', 'summaries', file, fn_name], + queryFn: ({ signal }) => + apiGet(`/debug/summaries?${params}`, signal), + }); +} + +export function useDebugPointer(file: string | null, fn_name: string | null) { + return useQuery({ + queryKey: ['debug', 'pointer', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/pointer?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugTypeFacts(file: string | null, fn_name: string | null) { + return useQuery({ + queryKey: ['debug', 'type-facts', file, fn_name], + queryFn: ({ signal }) => + apiGet( + `/debug/type-facts?file=${encodeURIComponent(file!)}&function=${encodeURIComponent(fn_name!)}`, + signal, + ), + enabled: !!file && !!fn_name, + }); +} + +export function useDebugAuth(file: string | null) { + return useQuery({ + queryKey: ['debug', 'auth', file], + queryFn: ({ signal }) => + apiGet( + `/debug/auth?file=${encodeURIComponent(file!)}`, + signal, + ), + enabled: !!file, + }); +} diff --git a/frontend/src/api/queries/explorer.ts b/frontend/src/api/queries/explorer.ts new file mode 100644 index 00000000..2174ed5e --- /dev/null +++ b/frontend/src/api/queries/explorer.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { TreeEntry, SymbolEntry, ExplorerFinding } from '../types'; + +export function useExplorerTree(path?: string) { + return useQuery({ + queryKey: ['explorer', 'tree', path ?? ''], + queryFn: ({ signal }) => { + const qs = path ? `?path=${encodeURIComponent(path)}` : ''; + return apiGet(`/explorer/tree${qs}`, signal); + }, + }); +} + +export function useExplorerSymbols(path: string | null) { + return useQuery({ + queryKey: ['explorer', 'symbols', path], + queryFn: ({ signal }) => + apiGet( + `/explorer/symbols?path=${encodeURIComponent(path!)}`, + signal, + ), + enabled: !!path, + }); +} + +export function useExplorerFindings(path: string | null) { + return useQuery({ + queryKey: ['explorer', 'findings', path], + queryFn: ({ signal }) => + apiGet( + `/explorer/findings?path=${encodeURIComponent(path!)}`, + signal, + ), + enabled: !!path, + }); +} diff --git a/frontend/src/api/queries/findings.ts b/frontend/src/api/queries/findings.ts new file mode 100644 index 00000000..405a881f --- /dev/null +++ b/frontend/src/api/queries/findings.ts @@ -0,0 +1,64 @@ +import { useQuery, type QueryClient } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { PaginatedFindings, FindingView, FilterValues } from '../types'; + +export interface FindingsParams { + page?: number; + per_page?: number; + severity?: string; + category?: string; + confidence?: string; + language?: string; + rule_id?: string; + status?: string; + verification?: string; + search?: string; + sort_by?: string; + sort_dir?: string; +} + +function buildQuery(params: FindingsParams): string { + const entries = Object.entries(params).filter( + ([, v]) => v !== undefined && v !== null && v !== '', + ); + if (entries.length === 0) return ''; + const qs = new URLSearchParams( + entries.map(([k, v]) => [k, String(v)]), + ).toString(); + return `?${qs}`; +} + +export function useFindings(params: FindingsParams = {}) { + return useQuery({ + queryKey: ['findings', params], + queryFn: ({ signal }) => + apiGet(`/findings${buildQuery(params)}`, signal), + }); +} + +export function useFinding(id: number | string) { + return useQuery({ + queryKey: ['findings', id], + queryFn: ({ signal }) => apiGet(`/findings/${id}`, signal), + enabled: id !== undefined && id !== null && id !== '', + }); +} + +export function fetchFindingDetail( + qc: QueryClient, + index: number, + signal?: AbortSignal, +): Promise { + return qc.fetchQuery({ + queryKey: ['findings', String(index)], + queryFn: ({ signal: s }) => + apiGet(`/findings/${index}`, s ?? signal), + }); +} + +export function useFindingFilters() { + return useQuery({ + queryKey: ['findings', 'filters'], + queryFn: ({ signal }) => apiGet('/findings/filters', signal), + }); +} diff --git a/frontend/src/api/queries/health.ts b/frontend/src/api/queries/health.ts new file mode 100644 index 00000000..0615333d --- /dev/null +++ b/frontend/src/api/queries/health.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { HealthResponse } from '../types'; + +export function useHealth() { + return useQuery({ + queryKey: ['health'], + queryFn: ({ signal }) => apiGet('/health', signal), + staleTime: 60_000, + }); +} diff --git a/frontend/src/api/queries/overview.ts b/frontend/src/api/queries/overview.ts new file mode 100644 index 00000000..49f12d30 --- /dev/null +++ b/frontend/src/api/queries/overview.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { OverviewResponse, TrendPoint } from '../types'; + +export function useOverview() { + return useQuery({ + queryKey: ['overview'], + queryFn: ({ signal }) => apiGet('/overview', signal), + }); +} + +export function useOverviewTrends() { + return useQuery({ + queryKey: ['overview', 'trends'], + queryFn: ({ signal }) => apiGet('/overview/trends', signal), + }); +} diff --git a/frontend/src/api/queries/rules.ts b/frontend/src/api/queries/rules.ts new file mode 100644 index 00000000..7e506512 --- /dev/null +++ b/frontend/src/api/queries/rules.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { RuleListItem, RuleDetailView } from '../types'; + +export function useRules() { + return useQuery({ + queryKey: ['rules'], + queryFn: ({ signal }) => apiGet('/rules', signal), + }); +} + +export function useRuleDetail(id: string) { + return useQuery({ + queryKey: ['rules', id], + queryFn: ({ signal }) => apiGet(`/rules/${id}`, signal), + enabled: !!id, + }); +} diff --git a/frontend/src/api/queries/scans.ts b/frontend/src/api/queries/scans.ts new file mode 100644 index 00000000..9a21ed57 --- /dev/null +++ b/frontend/src/api/queries/scans.ts @@ -0,0 +1,89 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { + ScanView, + PaginatedFindings, + ScanLogEntry, + ScanMetricsSnapshot, + CompareResponse, +} from '../types'; + +export function useScans() { + return useQuery({ + queryKey: ['scans'], + queryFn: ({ signal }) => apiGet('/scans', signal), + }); +} + +export function useScan(id: string) { + return useQuery({ + queryKey: ['scans', id], + queryFn: ({ signal }) => apiGet(`/scans/${id}`, signal), + enabled: !!id, + }); +} + +export interface ScanFindingsParams { + page?: number; + per_page?: number; + severity?: string; + category?: string; + search?: string; +} + +function buildQuery( + params: Record, +): string { + const entries = Object.entries(params).filter( + ([, v]) => v !== undefined && v !== null && v !== '', + ); + if (entries.length === 0) return ''; + const qs = new URLSearchParams( + entries.map(([k, v]) => [k, String(v)]), + ).toString(); + return `?${qs}`; +} + +export function useScanFindings(id: string, params: ScanFindingsParams = {}) { + return useQuery({ + queryKey: ['scans', id, 'findings', params], + queryFn: ({ signal }) => + apiGet( + `/scans/${id}/findings${buildQuery({ ...params })}`, + signal, + ), + enabled: !!id, + }); +} + +export function useScanLogs(id: string, level?: string) { + return useQuery({ + queryKey: ['scans', id, 'logs', level], + queryFn: ({ signal }) => { + const qs = level ? `?level=${encodeURIComponent(level)}` : ''; + return apiGet(`/scans/${id}/logs${qs}`, signal); + }, + enabled: !!id, + }); +} + +export function useScanMetrics(id: string) { + return useQuery({ + queryKey: ['scans', id, 'metrics'], + queryFn: ({ signal }) => + apiGet(`/scans/${id}/metrics`, signal), + enabled: !!id, + }); +} + +export function useScanCompare(left: string, right: string) { + return useQuery({ + queryKey: ['scans', 'compare', left, right], + queryFn: ({ signal }) => + apiGet( + `/scans/compare?left=${encodeURIComponent(left)}&right=${encodeURIComponent(right)}`, + signal, + ), + enabled: !!left && !!right, + }); +} diff --git a/frontend/src/api/queries/surface.ts b/frontend/src/api/queries/surface.ts new file mode 100644 index 00000000..32a19adb --- /dev/null +++ b/frontend/src/api/queries/surface.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { SurfaceMap } from '../types'; + +export function useSurfaceMap() { + return useQuery({ + queryKey: ['surface'], + queryFn: ({ signal }) => apiGet('/surface', signal), + staleTime: 30_000, + }); +} diff --git a/frontend/src/api/queries/targets.ts b/frontend/src/api/queries/targets.ts new file mode 100644 index 00000000..4593c33e --- /dev/null +++ b/frontend/src/api/queries/targets.ts @@ -0,0 +1,43 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiDelete, apiGet, apiPost } from '../client'; +import type { TargetView } from '../types'; + +export function useTargets() { + return useQuery({ + queryKey: ['targets'], + queryFn: ({ signal }) => apiGet('/targets', signal), + }); +} + +export function useAddTarget() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { path: string }) => + apiPost('/targets', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['targets'] }); + }, + }); +} + +export function useSelectTarget() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { id?: string; path?: string }) => + apiPost('/targets/select', body), + onSuccess: () => { + qc.invalidateQueries(); + }, + }); +} + +export function useDeleteTarget() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => + apiDelete(`/targets/${encodeURIComponent(id)}`), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['targets'] }); + }, + }); +} diff --git a/frontend/src/api/queries/triage.ts b/frontend/src/api/queries/triage.ts new file mode 100644 index 00000000..f15552fd --- /dev/null +++ b/frontend/src/api/queries/triage.ts @@ -0,0 +1,67 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { + PaginatedTriage, + PaginatedAudit, + SuppressionRule, + SyncStatus, +} from '../types'; + +export interface TriageParams { + state?: string; + page?: number; + per_page?: number; +} + +export interface TriageAuditParams { + fingerprint?: string; + page?: number; + per_page?: number; +} + +function buildQuery( + params: Record, +): string { + const entries = Object.entries(params).filter( + ([, v]) => v !== undefined && v !== null && v !== '', + ); + if (entries.length === 0) return ''; + const qs = new URLSearchParams( + entries.map(([k, v]) => [k, String(v)]), + ).toString(); + return `?${qs}`; +} + +export function useTriage(params: TriageParams = {}) { + return useQuery({ + queryKey: ['triage', params], + queryFn: ({ signal }) => + apiGet(`/triage${buildQuery({ ...params })}`, signal), + }); +} + +export function useTriageAudit(params: TriageAuditParams = {}) { + return useQuery({ + queryKey: ['triage', 'audit', params], + queryFn: ({ signal }) => + apiGet( + `/triage/audit${buildQuery({ ...params })}`, + signal, + ), + }); +} + +export function useSuppressions() { + return useQuery({ + queryKey: ['triage', 'suppress'], + queryFn: ({ signal }) => + apiGet<{ rules: SuppressionRule[] }>('/triage/suppress', signal), + }); +} + +export function useSyncStatus() { + return useQuery({ + queryKey: ['triage', 'sync-status'], + queryFn: ({ signal }) => apiGet('/triage/sync-status', signal), + }); +} diff --git a/frontend/src/api/queryClient.ts b/frontend/src/api/queryClient.ts new file mode 100644 index 00000000..c9942790 --- /dev/null +++ b/frontend/src/api/queryClient.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: true, + retry: 1, + }, + }, +}); diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 00000000..e846dfc8 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,1026 @@ +// Evidence types (from src/evidence.rs) +export type Confidence = 'Low' | 'Medium' | 'High'; +export type FlowStepKind = 'source' | 'assignment' | 'call' | 'phi' | 'sink'; + +// Dynamic verification types (from src/evidence.rs VerifyStatus / VerifyResult) +export type VerifyStatus = + | 'Confirmed' + | 'PartiallyConfirmed' + | 'NotConfirmed' + | 'Inconclusive' + | 'Unsupported'; + +export interface AttemptSummary { + payload_label: string; + exit_code?: number; + timed_out: boolean; + triggered: boolean; + sink_hit?: boolean; +} + +export interface VerifyResult { + finding_id: string; + status: VerifyStatus; + triggered_payload?: string; + /** Typed UnsupportedReason (PascalCase string) */ + reason?: string; + /** Typed InconclusiveReason (PascalCase string) */ + inconclusive_reason?: string; + detail?: string; + attempts?: AttemptSummary[]; + toolchain_match?: string; +} + +export interface DynamicVerificationSummary { + total: number; + confirmed: number; + partially_confirmed: number; + not_confirmed: number; + inconclusive: number; + unsupported: number; +} + +export interface FlowStep { + step: number; + kind: FlowStepKind; + file: string; + line: number; + col: number; + snippet?: string; + variable?: string; + callee?: string; + function?: string; + is_cross_file?: boolean; +} + +export interface SpanEvidence { + path: string; + line: number; + col: number; + kind: string; + snippet?: string; +} + +export interface StateEvidence { + machine: string; + subject?: string; + from_state: string; + to_state: string; +} + +export interface Evidence { + source?: SpanEvidence; + sink?: SpanEvidence; + guards: SpanEvidence[]; + sanitizers: SpanEvidence[]; + state?: StateEvidence; + notes: string[]; + flow_steps: FlowStep[]; + explanation?: string; + confidence_limiters: string[]; + /** Dynamic verification result; present only when --verify was active. */ + dynamic_verdict?: VerifyResult; +} + +// Finding types +export interface CodeContextView { + start_line: number; + lines: string[]; + highlight_line: number; +} + +export interface RelatedFindingView { + index: number; + rule_id: string; + path: string; + line: number; + severity: string; +} + +// Baseline / patch-validation types (M6.5) +export type VerdictTransition = + | 'New' + | 'Unchanged' + | 'Resolved' + | 'Regressed' + | 'FlippedConfirmed' + | 'FlippedNotConfirmed'; + +export interface VerdictDiffEntry { + stable_hash: number; + path: string; + line: number; + rule_id: string; + baseline_status?: VerifyStatus; + current_status?: VerifyStatus; + transition: VerdictTransition; +} + +export interface FindingView { + index: number; + fingerprint: string; + portable_fingerprint?: string; + /** Blake3-derived stable cross-commit identity (M6.5). */ + stable_hash?: number; + path: string; + line: number; + col: number; + severity: string; + rule_id: string; + category: string; + confidence?: Confidence; + rank_score?: number; + message?: string; + labels: [string, string][]; + path_validated: boolean; + suppressed: boolean; + language?: string; + status: string; + triage_state: string; + triage_note?: string; + code_context?: CodeContextView; + evidence?: Evidence; + dynamic_verdict?: VerifyResult; + guard_kind?: string; + rank_reason?: [string, string][]; + sanitizer_status?: string; + related_findings: RelatedFindingView[]; +} + +export interface FindingSummary { + total: number; + by_severity: Record; + by_category: Record; + by_rule: Record; + by_file: Record; +} + +export interface FilterValues { + severities: string[]; + categories: string[]; + confidences: string[]; + languages: string[]; + rules: string[]; + statuses: string[]; + verification_statuses: string[]; +} + +// Scan types +export interface TimingBreakdown { + walk_ms: number; + pass1_ms: number; + call_graph_ms: number; + pass2_ms: number; + post_process_ms: number; + dynamic_verify_ms?: number; +} + +export interface ScanMetricsSnapshot { + cfg_nodes: number; + call_edges: number; + functions_analyzed: number; + summaries_reused: number; + unresolved_calls: number; +} + +export interface ScanView { + id: string; + status: string; + scan_root: string; + started_at?: string; + finished_at?: string; + duration_secs?: number; + finding_count?: number; + error?: string; + engine_version?: string; + languages?: string[]; + files_scanned?: number; + timing?: TimingBreakdown; + metrics?: ScanMetricsSnapshot; +} + +export interface TargetView { + id: string; + name: string; + path: string; + db_path: string; + last_seen_at: string; + last_scan_at?: string; + active: boolean; + exists: boolean; +} + +// Scan Comparison types +export interface CompareScanInfo { + id: string; + started_at?: string; + finding_count: number; +} + +export interface CompareSummary { + new_count: number; + fixed_count: number; + changed_count: number; + unchanged_count: number; + severity_delta: Record; +} + +export interface ComparedFinding extends FindingView { + fingerprint: string; +} + +export interface FieldChange { + field: string; + old_value: string; + new_value: string; +} + +export interface ChangedFinding extends FindingView { + fingerprint: string; + changes: FieldChange[]; +} + +export interface CompareResponse { + left_scan: CompareScanInfo; + right_scan: CompareScanInfo; + summary: CompareSummary; + new_findings: ComparedFinding[]; + fixed_findings: ComparedFinding[]; + changed_findings: ChangedFinding[]; + unchanged_findings: ComparedFinding[]; + /** Verdict-level diff (M6.5). Present when findings carry stable_hash values. */ + verdict_diff?: VerdictDiffEntry[]; +} + +// Overview types +export interface OverviewCount { + name: string; + count: number; +} + +export interface NoisyRule { + rule_id: string; + finding_count: number; + suppression_rate: number; +} + +export interface ScanSummary { + id: string; + status: string; + started_at?: string; + duration_secs?: number; + finding_count?: number; +} + +export interface Insight { + kind: string; + message: string; + severity: string; + action_url?: string; +} + +export interface TrendPoint { + scan_id: string; + timestamp: string; + total: number; + by_severity: Record; +} + +export interface OverviewResponse { + state: string; + total_findings: number; + new_since_last: number; + fixed_since_last: number; + high_confidence_rate: number; + triage_coverage: number; + latest_scan_duration_secs?: number; + latest_scan_id?: string; + latest_scan_at?: string; + by_severity: Record; + by_category: Record; + by_language: Record; + top_files: OverviewCount[]; + top_directories: OverviewCount[]; + top_rules: OverviewCount[]; + noisy_rules: NoisyRule[]; + recent_scans: ScanSummary[]; + insights: Insight[]; + + // Tier 1 + health?: HealthScore; + posture?: PostureSummary; + backlog?: BacklogStats; + weighted_top_files?: WeightedFile[]; + confidence_distribution?: ConfidenceDistribution; + + // Tier 2 + scanner_quality?: ScannerQuality; + issue_categories?: IssueCategoryBucket[]; + hot_sinks?: HotSink[]; + owasp_buckets?: OwaspBucket[]; + cross_file_ratio?: number; + + // Tier 3 + baseline?: BaselineInfo; + language_health?: LanguageHealth[]; + suppression_hygiene?: SuppressionHygiene; +} + +export interface HealthComponent { + label: string; + score: number; + weight: number; + detail: string; +} + +export interface HealthScore { + score: number; + grade: string; + components: HealthComponent[]; +} + +export interface PostureSummary { + trend: 'improving' | 'regressing' | 'stable' | 'unknown' | string; + severity: 'success' | 'warning' | 'danger' | 'info' | string; + message: string; + reintroduced_count: number; +} + +export interface BacklogStats { + oldest_open_days?: number; + median_age_days?: number; + stale_count: number; + age_buckets: OverviewCount[]; +} + +export interface WeightedFile { + name: string; + score: number; + high: number; + medium: number; + low: number; + total: number; +} + +export interface ConfidenceDistribution { + high: number; + medium: number; + low: number; + none: number; +} + +export interface ScannerQuality { + files_scanned: number; + files_skipped: number; + parse_success_rate: number; + functions_analyzed: number; + call_edges: number; + unresolved_calls: number; + call_resolution_rate: number; + symex_verified_rate: number; + symex_breakdown: Record; + dynamic_verification: DynamicVerificationSummary; +} + +export interface IssueCategoryBucket { + label: string; + count: number; +} + +export interface HotSink { + callee: string; + count: number; +} + +export interface OwaspBucket { + code: string; + label: string; + count: number; +} + +export interface LanguageHealth { + language: string; + findings: number; + high: number; + medium: number; + low: number; +} + +export interface SuppressionHygiene { + fingerprint_level: number; + rule_level: number; + file_level: number; + rule_in_file_level: number; + blanket_rate: number; +} + +export interface BaselineInfo { + scan_id: string; + started_at?: string; + baseline_total: number; + drift_new: number; + drift_fixed: number; +} + +// Rules types +export interface RuleListItem { + id: string; + title: string; + language: string; + kind: string; + cap: string; + matchers: string[]; + enabled: boolean; + is_custom: boolean; + is_gated: boolean; + is_class: boolean; + case_sensitive: boolean; + finding_count: number; + suppression_rate: number; +} + +export interface RuleDetailView extends RuleListItem { + example_findings: RelatedFindingView[]; +} + +// Config types +export interface RuleView { + lang: string; + matchers: string[]; + kind: string; + cap: string; +} + +export interface TerminatorView { + lang: string; + name: string; +} + +export interface LabelEntryView { + lang: string; + matchers: string[]; + cap: string; + case_sensitive: boolean; + is_builtin: boolean; +} + +export interface ProfileView { + name: string; + is_builtin: boolean; + settings: Record; +} + +// Health +export interface HealthResponse { + status: string; + version: string; + scan_root: string; +} + +// Paginated response wrappers +export interface PaginatedFindings { + findings: FindingView[]; + total: number; + page: number; + per_page: number; +} + +// Triage types +export interface TriageEntry { + fingerprint: string; + state: string; + note: string; + updated_at: string; + finding?: FindingView; +} + +export interface PaginatedTriage { + entries: TriageEntry[]; + total: number; + page: number; + per_page: number; +} + +export interface AuditEntry { + id: number; + fingerprint: string; + action: string; + previous_state: string; + new_state: string; + note: string; + timestamp: string; +} + +export interface PaginatedAudit { + entries: AuditEntry[]; + total: number; + page: number; + per_page: number; +} + +export interface SuppressionRule { + id: number; + suppress_by: string; + match_value: string; + state: string; + note: string; + created_at: string; +} + +export interface SyncStatus { + file_path: string; + file_exists: boolean; + sync_enabled: boolean; + decisions: number; + suppression_rules: number; +} + +// File viewer +export interface FileResponse { + path: string; + lines: { number: number; content: string }[]; + total_lines: number; +} + +// Explorer types +export interface TreeEntry { + name: string; + entry_type: 'file' | 'dir'; + path: string; + language?: string; + finding_count: number; + severity_max?: string; +} + +export interface SymbolEntry { + name: string; + /// Legacy display kind (`"function"` | `"method"`) used by existing + /// CSS classes. Prefer `func_kind` for new logic. + kind: string; + /// Structural FuncKind slug: `"fn"` | `"method"` | `"closure"` | + /// `"ctor"` | `"getter"` | `"setter"` | `"toplevel"`. + func_kind: string; + /// Enclosing container (class / impl / module / outer function). + /// Empty for free top-level functions. + container: string; + line?: number; + finding_count: number; + namespace?: string; + arity?: number; +} + +export interface ExplorerFinding { + index: number; + line: number; + col: number; + severity: string; + rule_id: string; + category: string; + message?: string; + confidence?: string; +} + +// Scan log entry +export interface ScanLogEntry { + timestamp: string; + level: string; + message: string; + file_path?: string; + detail?: string; +} + +// ── Debug view types ───────────────────────────────────────────────────────── + +export interface FunctionInfo { + name: string; + namespace: string; + /// Enclosing container (class / impl / module / outer function). + container: string; + /// Structural FuncKind slug: `"fn"` | `"method"` | `"closure"` | etc. + func_kind: string; + param_count: number; + line: number; + source_caps: string[]; + sanitizer_caps: string[]; + sink_caps: string[]; +} + +// CFG +export interface CfgNodeView { + id: number; + kind: string; + span: [number, number]; + line: number; + defines?: string; + uses: string[]; + callee?: string; + labels: string[]; + condition_text?: string; + enclosing_func?: string; +} + +export interface CfgEdgeView { + source: number; + target: number; + kind: string; +} + +export interface CfgGraphView { + nodes: CfgNodeView[]; + edges: CfgEdgeView[]; + entry: number; +} + +// SSA +export interface SsaInstView { + value: number; + op: string; + operands: string[]; + var_name?: string; + span: [number, number]; + line: number; +} + +export interface SsaBlockView { + id: number; + phis: SsaInstView[]; + body: SsaInstView[]; + terminator: string; + preds: number[]; + succs: number[]; +} + +export interface SsaBodyView { + blocks: SsaBlockView[]; + entry: number; + num_values: number; +} + +// Taint +export interface TaintValueView { + ssa_value: number; + var_name?: string; + caps: string[]; + uses_summary: boolean; +} + +export interface TaintBlockStateView { + block_id: number; + values: TaintValueView[]; + validated_must: number; + validated_may: number; +} + +export interface TaintEventView { + sink_node: number; + sink_caps: string[]; + tainted_values: TaintValueView[]; + all_validated: boolean; + uses_summary: boolean; +} + +export interface TaintAnalysisView { + block_states: TaintBlockStateView[]; + events: TaintEventView[]; +} + +// Abstract Interpretation +export interface AbstractValueView { + ssa_value: number; + var_name?: string; + interval_lo?: number; + interval_hi?: number; + string_prefix?: string; + string_suffix?: string; + known_zero: number; + known_one: number; +} + +export interface AbstractBlockView { + block_id: number; + values: AbstractValueView[]; +} + +export interface TypeFactView { + ssa_value: number; + var_name?: string; + type_kind: string; + nullable: boolean; +} + +export interface ConstValueViewEntry { + ssa_value: number; + var_name?: string; + value: string; +} + +export interface AbstractInterpView { + blocks: AbstractBlockView[]; + type_facts: TypeFactView[]; + const_values: ConstValueViewEntry[]; +} + +// Symbolic Execution +export interface SymexValueView { + ssa_value: number; + var_name?: string; + expression: string; +} + +export interface PathConstraintView { + block: number; + condition: string; + polarity: boolean; +} + +export interface SymexView { + values: SymexValueView[]; + path_constraints: PathConstraintView[]; + tainted_roots: number[]; +} + +// Call Graph +export interface CallGraphNodeView { + id: number; + name: string; + file: string; + lang: string; + namespace: string; + arity?: number; +} + +export interface CallGraphEdgeView { + source: number; + target: number; + call_site: string; +} + +export interface CallGraphView { + nodes: CallGraphNodeView[]; + edges: CallGraphEdgeView[]; + sccs: number[][]; + unresolved_count: number; + ambiguous_count: number; +} + +// Summaries +export interface ParamReturnView { + param_index: number; + transform: string; +} + +export interface ParamSinkView { + param_index: number; + sink_caps: string[]; +} + +export interface SsaSummaryView { + param_to_return: ParamReturnView[]; + param_to_sink: ParamSinkView[]; + source_caps: string[]; +} + +export interface FuncSummaryView { + name: string; + file_path: string; + lang: string; + namespace: string; + /// Enclosing container (class / impl / module / outer function). + container: string; + /// Structural FuncKind slug: `"fn"` | `"method"` | `"closure"` | etc. + func_kind: string; + arity?: number; + param_count: number; + source_caps: string[]; + sanitizer_caps: string[]; + sink_caps: string[]; + propagates_taint: boolean; + propagating_params: number[]; + tainted_sink_params: number[]; + callees: string[]; + ssa_summary?: SsaSummaryView; +} + +// ── Pointer (field-sensitive Steensgaard) ───────────────────────────────── +export interface PointerLocationView { + id: number; + kind: 'Top' | 'Alloc' | 'Param' | 'SelfParam' | 'Field'; + display: string; + parent?: number; + field?: string; +} + +export interface PointerValueView { + ssa_value: number; + var_name?: string; + points_to: number[]; + is_top: boolean; +} + +export interface PointerFieldEntryView { + /// `null` means the implicit receiver. + param_index: number | null; + field: string; +} + +export interface PointerView { + locations: PointerLocationView[]; + values: PointerValueView[]; + field_reads: PointerFieldEntryView[]; + field_writes: PointerFieldEntryView[]; + location_count: number; +} + +// ── Type Facts (standalone) ──────────────────────────────────────────────── +export interface DtoFieldView { + name: string; + kind: string; +} + +export interface DtoFactView { + class_name: string; + fields: DtoFieldView[]; +} + +export interface TypeFactDetailView { + ssa_value: number; + var_name?: string; + line: number; + kind: string; + nullable: boolean; + container?: string; + dto?: DtoFactView; +} + +export interface TypeFactsView { + facts: TypeFactDetailView[]; + total_values: number; + unknown_count: number; +} + +// ── Auth Analysis ────────────────────────────────────────────────────────── +export interface AuthValueRefView { + source_kind: string; + name: string; + base?: string; + field?: string; + index?: string; + line: number; +} + +export interface AuthCheckView { + kind: string; + callee: string; + line: number; + subjects: AuthValueRefView[]; + args: string[]; + condition_text?: string; +} + +export interface AuthOperationView { + kind: string; + sink_class?: string; + callee: string; + line: number; + text: string; + subjects: AuthValueRefView[]; +} + +export interface AuthCallSiteView { + name: string; + line: number; + args: string[]; +} + +export interface AuthUnitView { + kind: string; + name?: string; + line: number; + params: string[]; + auth_checks: AuthCheckView[]; + operations: AuthOperationView[]; + call_sites: AuthCallSiteView[]; + self_actor_vars: string[]; + typed_bounded_vars: string[]; + authorized_sql_vars: string[]; + const_bound_vars: string[]; +} + +export interface AuthRouteView { + framework: string; + method: string; + path: string; + middleware: string[]; + handler_params: string[]; + line: number; + unit_idx: number; +} + +export interface AuthAnalysisView { + routes: AuthRouteView[]; + units: AuthUnitView[]; + enabled: boolean; +} + +// ── Surface map (Phase 21–23) ─────────────────────────────────────── + +export interface SurfaceSourceLocation { + file: string; + line: number; + col: number; +} + +export type SurfaceFramework = + | 'flask' + | 'fast_api' + | 'django' + | 'express' + | 'koa' + | 'spring' + | 'jax_rs' + | 'quarkus' + | 'rails' + | 'sinatra' + | 'laravel' + | 'slim' + | 'axum' + | 'actix' + | 'rocket' + | 'net_http' + | 'gin' + | 'next_app_router' + | 'next_server_action'; + +export type SurfaceHttpMethod = + | 'GET' + | 'HEAD' + | 'POST' + | 'PUT' + | 'PATCH' + | 'DELETE' + | 'OPTIONS'; + +export type SurfaceDataStoreKind = + | 'sql' + | 'key_value' + | 'document' + | 'blob_store' + | 'filesystem' + | 'unknown'; + +export type SurfaceExternalKind = + | 'http_api' + | 'message_broker' + | 'search_index' + | 'auth_provider' + | 'unknown'; + +export type SurfaceEdgeKind = + | 'calls' + | 'reads_from' + | 'writes_to' + | 'talks_to' + | 'reaches' + | 'triggers' + | 'auth_required_on'; + +export type SurfaceNode = + | { + node: 'entry_point'; + location: SurfaceSourceLocation; + framework: SurfaceFramework; + method: SurfaceHttpMethod; + route: string; + handler_name: string; + handler_location: SurfaceSourceLocation; + auth_required: boolean; + } + | { + node: 'data_store'; + location: SurfaceSourceLocation; + kind: SurfaceDataStoreKind; + label: string; + } + | { + node: 'external_service'; + location: SurfaceSourceLocation; + kind: SurfaceExternalKind; + label: string; + } + | { + node: 'dangerous_local'; + location: SurfaceSourceLocation; + function_name: string; + cap_bits: number; + }; + +export interface SurfaceEdge { + from: number; + to: number; + kind: SurfaceEdgeKind; +} + +export interface SurfaceMap { + nodes: SurfaceNode[]; + edges: SurfaceEdge[]; +} diff --git a/frontend/src/components/CopyMarkdownButton.tsx b/frontend/src/components/CopyMarkdownButton.tsx new file mode 100644 index 00000000..670820ac --- /dev/null +++ b/frontend/src/components/CopyMarkdownButton.tsx @@ -0,0 +1,171 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +type Status = 'idle' | 'working' | 'copied' | 'failed'; + +interface CopyMarkdownButtonProps { + getMarkdown: () => string | Promise; + 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 ( + + ); +} + +function CheckIcon() { + return ( + + ); +} + +function FailIcon() { + return ( + + ); +} + +export function CopyMarkdownButton({ + getMarkdown, + label = 'Copy', + className, + title, + stopPropagation, + iconOnly, +}: CopyMarkdownButtonProps) { + const [status, setStatus] = useState('idle'); + const timeoutRef = useRef(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) => { + 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' ? ( + + ) : status === 'failed' ? ( + + ) : ( + + ); + + return ( + + ); +} diff --git a/frontend/src/components/VerdictBadge.tsx b/frontend/src/components/VerdictBadge.tsx new file mode 100644 index 00000000..f61a72cf --- /dev/null +++ b/frontend/src/components/VerdictBadge.tsx @@ -0,0 +1,62 @@ +import type { VerifyResult, VerifyStatus } from '../api/types'; + +const STATUS_LABELS: Record = { + Confirmed: 'Confirmed', + PartiallyConfirmed: 'Partially confirmed', + NotConfirmed: 'Not confirmed', + Inconclusive: 'Inconclusive', + Unsupported: 'Unsupported', +}; + +function verdictTooltip(verdict: VerifyResult): string { + const { status, triggered_payload, reason, inconclusive_reason, detail } = + verdict; + switch (status) { + case 'Confirmed': + return triggered_payload + ? `Confirmed via payload: ${triggered_payload}` + : 'Dynamically confirmed exploitable'; + case 'PartiallyConfirmed': + return detail + ? `Partially confirmed (sink reached): ${detail}` + : 'Partially confirmed: sink reached but exploit chain did not complete'; + case 'NotConfirmed': + return (verdict.attempts?.length ?? 0) > 0 + ? `Not confirmed after ${verdict.attempts?.length ?? 0} payload attempt(s)` + : 'Not confirmed'; + case 'Unsupported': + return reason + ? `Unsupported: ${reason}` + : 'Dynamic verification not supported'; + case 'Inconclusive': + return inconclusive_reason + ? `Inconclusive: ${inconclusive_reason}${detail ? `: ${detail}` : ''}` + : detail || 'Inconclusive'; + } +} + +interface VerdictBadgeProps { + verdict: VerifyResult | undefined; + /** Show full label (default) or compact icon-only mode */ + compact?: boolean; +} + +export function VerdictBadge({ verdict, compact = false }: VerdictBadgeProps) { + if (!verdict) { + return -; + } + + const { status } = verdict; + const label = STATUS_LABELS[status] ?? status; + const tooltip = verdictTooltip(verdict); + + return ( + + {compact ? status.charAt(0) : label} + + ); +} diff --git a/frontend/src/components/charts/HorizontalBarChart.tsx b/frontend/src/components/charts/HorizontalBarChart.tsx new file mode 100644 index 00000000..b0976ef1 --- /dev/null +++ b/frontend/src/components/charts/HorizontalBarChart.tsx @@ -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 ( +
+

No data

+
+ ); + } + + 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); + + return ( +
+ + {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 ( + + + {item.label} + + + + {item.value} + + + ); + })} + +
+ ); +} diff --git a/frontend/src/components/charts/LineChart.tsx b/frontend/src/components/charts/LineChart.tsx new file mode 100644 index 00000000..ef19c6a6 --- /dev/null +++ b/frontend/src/components/charts/LineChart.tsx @@ -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 = 240, +}: LineChartProps) { + if (!points || points.length < 2) { + return ( +
+

Need multiple scans for trends

+
+ ); + } + + 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 ( +
+ + {/* Grid lines */} + {gridLines.map((g, i) => ( + + + + {g.val} + + + ))} + + {/* Area fill */} + + + {/* Line */} + + + {/* Dots */} + {coords.map((c, i) => ( + + ))} + + {/* X-axis labels */} + {coords.map((c, i) => { + if (i % step !== 0 && i !== coords.length - 1) return null; + return ( + + {formatShortDate(c.label)} + + ); + })} + +
+ ); +} diff --git a/frontend/src/components/data-display/CodeViewer.tsx b/frontend/src/components/data-display/CodeViewer.tsx new file mode 100644 index 00000000..98fd8f83 --- /dev/null +++ b/frontend/src/components/data-display/CodeViewer.tsx @@ -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; + 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(null); + + const { + data: fileData, + isLoading, + error, + } = useQuery({ + queryKey: ['files', filePath], + queryFn: ({ signal }) => + apiGet( + `/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(); + 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 ( +
+ Loading file... +
+ ); + } + + if (error) { + return ( +
+
+

+ Could not load file:{' '} + {error instanceof Error ? error.message : 'Unknown error'} +

+
+
+ ); + } + + if (!fileData) return null; + + return ( +
+ 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 ( +
+ + {gutterFinding ? ( + + ) : ( + + )} + + {l.number} + +
+ ); + })} +
+ ); +} + +function severityRank(s: string): number { + switch (s.toUpperCase()) { + case 'HIGH': + return 3; + case 'MEDIUM': + return 2; + case 'LOW': + return 1; + default: + return 0; + } +} diff --git a/frontend/src/components/data-display/FileTree.tsx b/frontend/src/components/data-display/FileTree.tsx new file mode 100644 index 00000000..96290da1 --- /dev/null +++ b/frontend/src/components/data-display/FileTree.tsx @@ -0,0 +1,155 @@ +import { FolderIcon } from '../icons/Icons'; +import type { TreeEntry } from '../../api/types'; + +interface FileTreeProps { + entries: TreeEntry[]; + expandedPaths: Set; + selectedPath: string | null; + onToggleExpand: (path: string) => void; + onSelectFile: (path: string) => void; + loadedChildren: Map; +} + +export function FileTree({ + entries, + expandedPaths, + selectedPath, + onToggleExpand, + onSelectFile, + loadedChildren, +}: FileTreeProps) { + return ( +
+ {entries.map((entry) => ( + + ))} +
+ ); +} + +interface FileTreeNodeProps { + entry: TreeEntry; + depth: number; + expandedPaths: Set; + selectedPath: string | null; + onToggleExpand: (path: string) => void; + onSelectFile: (path: string) => void; + loadedChildren: Map; +} + +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 ( + <> +
+ + {isDir ? (isExpanded ? '▾' : '▸') : ''} + + + {isDir ? ( + + ) : ( + + )} + + + {entry.name} + + {entry.finding_count > 0 && ( + {entry.finding_count} + )} +
+ {isDir && isExpanded && children && ( +
+ {children.map((child) => ( + + ))} +
+ )} + + ); +} + +function FileIcon({ language }: { language?: string }) { + const label = (language || '').charAt(0).toUpperCase() || '·'; + const color = langColor(language); + return ( + + {label} + + ); +} + +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)'; + } +} diff --git a/frontend/src/components/explorer/AnalysisWorkspace.tsx b/frontend/src/components/explorer/AnalysisWorkspace.tsx new file mode 100644 index 00000000..2cc96008 --- /dev/null +++ b/frontend/src/components/explorer/AnalysisWorkspace.tsx @@ -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 ? ( + + ) : null; + + return ( +
+ {inspectorSide === 'left' && inspectorPanel} +
{canvas}
+ {inspectorSide === 'right' && inspectorPanel} +
+ ); +} diff --git a/frontend/src/components/icons/Icons.tsx b/frontend/src/components/icons/Icons.tsx new file mode 100644 index 00000000..309419bd --- /dev/null +++ b/frontend/src/components/icons/Icons.tsx @@ -0,0 +1,211 @@ +import type { FC, SVGProps } from 'react'; + +export interface IconProps { + className?: string; + size?: number; +} + +type SvgBaseProps = SVGProps & 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 ( + + + + + + + ); +} + +export function FindingsIcon({ className, size = 18 }: IconProps) { + return ( + + + + + + ); +} + +export function ScansIcon({ className, size = 18 }: IconProps) { + return ( + + + + + ); +} + +export function RulesIcon({ className, size = 18 }: IconProps) { + return ( + + + + + + + + + ); +} + +export function TriageIcon({ className, size = 18 }: IconProps) { + return ( + + + + + + ); +} + +export function ConfigIcon({ className, size = 18 }: IconProps) { + return ( + + + + + + + + + ); +} + +export function ExplorerIcon({ className, size = 18 }: IconProps) { + return ( + + + + + + + ); +} + +export function DebugIcon({ className, size = 18 }: IconProps) { + return ( + + + + + + + ); +} + +export function FolderIcon({ className, size = 14 }: IconProps) { + return ( + + + + ); +} + +export function TagIcon({ className, size = 14 }: IconProps) { + return ( + + + + + ); +} + +export function CloseIcon({ className, size = 14 }: IconProps) { + return ( + + + + ); +} + +export function CheckIcon({ className, size = 14 }: IconProps) { + return ( + + + + ); +} + +export function SunIcon({ className, size = 16 }: IconProps) { + return ( + + + + + ); +} + +export function MoonIcon({ className, size = 16 }: IconProps) { + return ( + + + + ); +} + +export function RefreshIcon({ className, size = 16 }: IconProps) { + return ( + + + + + ); +} + +export function CommandIcon({ className, size = 16 }: IconProps) { + return ( + + + + ); +} + +/** Map of icon name to component, for dynamic lookup */ +export const ICONS: Record> = { + overview: OverviewIcon, + findings: FindingsIcon, + scans: ScansIcon, + rules: RulesIcon, + triage: TriageIcon, + config: ConfigIcon, + explorer: ExplorerIcon, + debug: DebugIcon, + folder: FolderIcon, + tag: TagIcon, + check: CheckIcon, +}; diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx new file mode 100644 index 00000000..6bfd6700 --- /dev/null +++ b/frontend/src/components/layout/AppLayout.tsx @@ -0,0 +1,179 @@ +import { useCallback, useMemo, useState } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import { Sidebar } from './Sidebar'; +import { HeaderBar } from './HeaderBar'; +import { NewScanModal } from '../../modals/NewScanModal'; +import { CommandPalette, type PaletteCommand } from '../ui/CommandPalette'; +import { ShortcutsHelp } from '../ui/ShortcutsHelp'; +import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'; +import { useChordNavigation } from '../../hooks/useChordNavigation'; +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 { ExplorerPage } from '../../pages/ExplorerPage'; +import { SurfacePage } from '../../pages/SurfacePage'; +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 [paletteOpen, setPaletteOpen] = useState(false); + const [helpOpen, setHelpOpen] = useState(false); + + const handleStartScan = useCallback(() => { + setScanModalOpen(true); + }, []); + + const commands = useMemo( + () => [ + // Navigation + { id: 'go-overview', group: 'Navigate', label: 'Overview', to: '/' }, + { + id: 'go-findings', + group: 'Navigate', + label: 'Findings', + to: '/findings', + }, + { id: 'go-scans', group: 'Navigate', label: 'Scans', to: '/scans' }, + { id: 'go-rules', group: 'Navigate', label: 'Rules', to: '/rules' }, + { id: 'go-triage', group: 'Navigate', label: 'Triage', to: '/triage' }, + { id: 'go-config', group: 'Navigate', label: 'Config', to: '/config' }, + { + id: 'go-explorer', + group: 'Navigate', + label: 'Explorer', + to: '/explorer', + }, + { + id: 'go-surface', + group: 'Navigate', + label: 'Attack surface', + to: '/surface', + }, + { + id: 'go-debug-cg', + group: 'Navigate', + label: 'Call Graph', + hint: 'Debug', + to: '/debug/call-graph', + }, + { + id: 'go-debug-summaries', + group: 'Navigate', + label: 'Summary Explorer', + hint: 'Debug', + to: '/debug/summaries', + }, + // Actions + { + id: 'start-scan', + group: 'Actions', + label: 'Start new scan', + keywords: ['scan', 'run'], + action: () => setScanModalOpen(true), + }, + { + id: 'show-shortcuts', + group: 'Actions', + label: 'Show keyboard shortcuts', + keywords: ['help', 'keys'], + shortcut: '?', + action: () => setHelpOpen(true), + }, + ], + [], + ); + + useChordNavigation(); + + const shortcuts = useMemo( + () => [ + { + key: 'k', + meta: true, + description: 'Open command palette', + handler: () => setPaletteOpen(true), + allowInInput: true, + }, + { + key: '?', + shift: true, + description: 'Show keyboard shortcuts', + handler: () => setHelpOpen(true), + }, + { + key: 'Escape', + description: 'Close modal / palette', + handler: () => { + if (paletteOpen) setPaletteOpen(false); + else if (helpOpen) setHelpOpen(false); + else if (scanModalOpen) setScanModalOpen(false); + }, + allowInInput: true, + }, + ], + [paletteOpen, helpOpen, scanModalOpen], + ); + + useKeyboardShortcuts(shortcuts); + + return ( +
+ +
+ setPaletteOpen(true)} + /> +
+ + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } + /> + } /> + } /> + } + /> + + +
+
+ setScanModalOpen(false)} + /> + setPaletteOpen(false)} + commands={commands} + /> + setHelpOpen(false)} /> +
+ ); +} diff --git a/frontend/src/components/layout/HeaderBar.tsx b/frontend/src/components/layout/HeaderBar.tsx new file mode 100644 index 00000000..653566df --- /dev/null +++ b/frontend/src/components/layout/HeaderBar.tsx @@ -0,0 +1,120 @@ +import { Link, useLocation } from 'react-router-dom'; +import { CommandIcon } from '../icons/Icons'; + +const SECTION_TITLES: Record = { + overview: 'Overview', + findings: 'Findings', + scans: 'Scans', + rules: 'Rules', + triage: 'Triage', + config: 'Config', + explorer: 'Explorer', + debug: 'Debug', +}; + +const ROUTE_TITLES: Record = { + '/debug/cfg': 'CFG Viewer', + '/debug/ssa': 'SSA Viewer', + '/debug/call-graph': 'Call Graph', + '/debug/taint': 'Taint Debugger', + '/debug/summaries': 'Summaries', +}; + +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 }> = []; + + const sectionPath = section === 'overview' ? '/' : `/${section}`; + crumbs.push({ label: sectionTitle, path: sectionPath }); + + if (ROUTE_TITLES[pathname]) { + crumbs.push({ label: ROUTE_TITLES[pathname] }); + } else { + const parts = pathname.split('/').filter(Boolean); + if (parts.length > 1) { + const sub = parts.slice(1).join('/'); + crumbs.push({ label: sub }); + } + } + + return crumbs; +} + +interface HeaderBarProps { + onStartScan?: () => void; + onOpenPalette?: () => void; +} + +const PALETTE_HINT = + typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform) + ? '⌘K' + : 'Ctrl K'; + +export function HeaderBar({ onStartScan, onOpenPalette }: HeaderBarProps) { + const { pathname } = useLocation(); + const crumbs = buildBreadcrumbs(pathname); + + return ( +
+
+ +
+
+ {onOpenPalette && ( + + )} + {onStartScan && ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 00000000..0731c8e7 --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,344 @@ +import { NavLink } from 'react-router-dom'; +import { + OverviewIcon, + FindingsIcon, + ScansIcon, + RulesIcon, + TriageIcon, + ConfigIcon, + ExplorerIcon, + DebugIcon, + TagIcon, +} from '../icons/Icons'; +import { useEffect, useRef, useState, type FC, type FormEvent } from 'react'; +import type { IconProps } from '../icons/Icons'; +import { useHealth } from '../../api/queries/health'; +import { useOverview } from '../../api/queries/overview'; +import { + useAddTarget, + useSelectTarget, + useTargets, +} from '../../api/queries/targets'; +import { useSSE } from '../../contexts/SSEContext'; + +interface NavItem { + id: string; + label: string; + path: string; + Icon: FC; + 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: 'explorer', + label: 'Explorer', + path: '/explorer', + Icon: ExplorerIcon, + group: 'secondary', + }, + { + id: 'surface', + label: 'Surface', + path: '/surface', + Icon: ExplorerIcon, + group: 'secondary', + }, + { + id: 'debug', + label: 'Debug', + path: '/debug', + Icon: DebugIcon, + group: 'secondary', + }, + { + id: 'config', + label: 'Config', + path: '/config', + Icon: ConfigIcon, + group: 'footer', + }, +]; + +function navLinkClass({ isActive }: { isActive: boolean }) { + return `nav-link${isActive ? ' active' : ''}`; +} + +function targetNameFromPath(path: string) { + const parts = path.split(/[\\/]/).filter(Boolean); + return parts[parts.length - 1] || path || 'Project'; +} + +function targetInitial(name: string) { + return name.trim().charAt(0).toUpperCase() || '?'; +} + +function compactPath(path: string) { + return path.replace(/^\/Users\/[^/]+/, '~'); +} + +function TargetSwitcher({ scanRoot }: { scanRoot?: string }) { + const { data: targets = [] } = useTargets(); + const addTarget = useAddTarget(); + const selectTarget = useSelectTarget(); + const [open, setOpen] = useState(false); + const [newPath, setNewPath] = useState(''); + const menuRef = useRef(null); + + const activeTarget = + targets.find((target) => target.active) ?? + (scanRoot + ? { + id: '__active__', + name: targetNameFromPath(scanRoot), + path: scanRoot, + active: true, + exists: true, + } + : undefined); + + useEffect(() => { + if (!open) return; + function handlePointerDown(event: MouseEvent) { + if ( + menuRef.current && + event.target instanceof Node && + !menuRef.current.contains(event.target) + ) { + setOpen(false); + } + } + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') setOpen(false); + } + document.addEventListener('mousedown', handlePointerDown); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handlePointerDown); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [open]); + + function handleSelect(id: string) { + selectTarget.mutate( + { id }, + { + onSuccess: () => setOpen(false), + }, + ); + } + + function handleAddSubmit(event: FormEvent) { + event.preventDefault(); + const path = newPath.trim(); + if (!path || addTarget.isPending) return; + addTarget.mutate( + { path }, + { + onSuccess: (target) => { + setNewPath(''); + selectTarget.mutate( + { id: target.id }, + { + onSuccess: () => setOpen(false), + }, + ); + }, + }, + ); + } + + const isBusy = addTarget.isPending || selectTarget.isPending; + const errorMessage = + addTarget.error instanceof Error ? addTarget.error.message : null; + + return ( +
+ + + {open && ( +
+
+ {targets.map((target) => ( + + ))} +
+ +
+ setNewPath(event.target.value)} + placeholder="/path/to/project" + aria-label="Project path" + /> + +
+ {errorMessage &&
{errorMessage}
} +
+ )} +
+ ); +} + +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 ( + + ); +} diff --git a/frontend/src/components/overview/OverviewWidgets.tsx b/frontend/src/components/overview/OverviewWidgets.tsx new file mode 100644 index 00000000..d962195a --- /dev/null +++ b/frontend/src/components/overview/OverviewWidgets.tsx @@ -0,0 +1,666 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { + HealthScore, + PostureSummary, + BacklogStats, + ConfidenceDistribution, + ScannerQuality, + HotSink, + OwaspBucket, + LanguageHealth, + SuppressionHygiene, + BaselineInfo, + WeightedFile, + OverviewCount, +} from '../../api/types'; +import { truncPath } from '../../utils/truncPath'; + +// ── HealthScoreCard ───────────────────────────────────────────────────────── + +export function HealthScoreCard({ + health, + posture, +}: { + health: HealthScore; + 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 ( +
+
Health Score
+
+
+ {health.grade} +
+
+
+ {health.score} + / 100 +
+ {posture && ( +
+ {posture.message} +
+ )} +
+
+ {health.components.map((c) => { + const barColor = + c.score >= 70 + ? 'var(--green)' + : c.score >= 40 + ? 'var(--amber)' + : 'var(--red)'; + return ( +
+
{c.label}
+
+
+
+
{c.score}
+
+ ); + })} +
+
+
+ ); +} + +// ── PostureBanner ────────────────────────────────────────────────────────── + +export function PostureBanner({ posture }: { posture: PostureSummary }) { + return ( +
+ + {posture.message} +
+ ); +} + +// ── BacklogCard ──────────────────────────────────────────────────────────── + +export function BacklogCard({ backlog }: { backlog: BacklogStats }) { + const total = backlog.age_buckets.reduce((s, b) => s + b.count, 0); + const noHistory = + backlog.oldest_open_days == null && backlog.age_buckets.length === 0; + if (noHistory) { + return null; + } + return ( +
+
Backlog Age
+
+
+
+ {backlog.oldest_open_days != null + ? `${backlog.oldest_open_days}d` + : '–'} +
+
Oldest open
+
+
+
+ {backlog.median_age_days != null + ? `${backlog.median_age_days}d` + : '–'} +
+
Median age
+
+
+
{backlog.stale_count}
+
Older than 30 days
+
+ {total > 0 && ( +
+ +
+ )} +
+
+ ); +} + +function BucketBar({ buckets }: { buckets: OverviewCount[] }) { + const total = buckets.reduce((s, b) => s + b.count, 0); + if (total === 0) return null; + const colors = [ + 'var(--accent)', + 'var(--green)', + 'var(--amber)', + 'var(--red)', + 'var(--muted)', + ]; + return ( +
`${b.name}: ${b.count}`).join(' · ')} + > + {buckets.map((b, i) => ( +
+ ))} +
+ ); +} + +// ── ConfidenceDistributionChart ──────────────────────────────────────────── + +export function ConfidenceDistributionChart({ + dist, +}: { + dist: ConfidenceDistribution; +}) { + const total = dist.high + dist.medium + dist.low + dist.none; + if (total === 0) { + return ( +
+

No data

+
+ ); + } + const segments = [ + { 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 ( +
+
+ {segments.map((s) => + s.value > 0 ? ( +
+ ) : null, + )} +
+
+ {segments.map((s) => ( +
+ + {s.label} + {s.value} +
+ ))} +
+
+ ); +} + +// ── ScannerQualityPanel ──────────────────────────────────────────────────── + +export function ScannerQualityPanel({ + quality, + crossFileRatio, +}: { + quality: ScannerQuality; + crossFileRatio?: number; +}) { + const symexAttempted = Object.entries(quality.symex_breakdown || {}) + .filter(([k]) => k !== 'not_attempted') + .reduce((s, [, v]) => s + v, 0); + const symexTotal = Object.values(quality.symex_breakdown || {}).reduce( + (s, v) => s + v, + 0, + ); + const totalFiles = quality.files_scanned + quality.files_skipped; + const filesValue = totalFiles.toLocaleString(); + const filesDetail = + quality.files_skipped > 0 + ? `${quality.files_scanned.toLocaleString()} fresh · ${quality.files_skipped.toLocaleString()} from cache` + : quality.files_scanned > 0 + ? `${quality.files_scanned.toLocaleString()} freshly indexed` + : undefined; + const dynamic = quality.dynamic_verification ?? { + total: 0, + confirmed: 0, + partially_confirmed: 0, + not_confirmed: 0, + inconclusive: 0, + unsupported: 0, + }; + const dynamicDetail = + dynamic.total > 0 + ? `${dynamic.total.toLocaleString()} verdicts · ${dynamic.partially_confirmed.toLocaleString()} partially confirmed · ${dynamic.not_confirmed.toLocaleString()} not confirmed · ${dynamic.inconclusive.toLocaleString()} inconclusive · ${dynamic.unsupported.toLocaleString()} unsupported` + : 'no dynamic verdicts in latest scan'; + + const rows: Array<{ + label: string; + hint: string; + value: string; + detail?: string; + }> = [ + { + label: 'Files', + hint: 'Files the scanner saw on this run.', + value: filesValue, + detail: filesDetail, + }, + { + label: 'Functions analyzed', + hint: 'Function bodies the call graph saw.', + value: quality.functions_analyzed.toLocaleString(), + }, + { + label: 'Call edges resolved', + hint: 'Share of call sites that the scanner resolved to a known callee. The remainder are typically external/library calls.', + value: `${(quality.call_resolution_rate * 100).toFixed(1)}%`, + detail: + quality.unresolved_calls > 0 + ? `${quality.unresolved_calls.toLocaleString()} unresolved` + : undefined, + }, + { + label: 'Cross-file flows', + hint: 'Findings whose taint path crosses a file boundary.', + value: + crossFileRatio != null ? `${(crossFileRatio * 100).toFixed(1)}%` : '0%', + detail: 'of findings', + }, + { + label: 'Symbolic verification', + hint: 'Taint findings the symbolic engine attempted to verify (confirmed, infeasible, or inconclusive).', + value: + symexTotal > 0 + ? `${(quality.symex_verified_rate * 100).toFixed(1)}%` + : 'n/a', + detail: + symexTotal > 0 + ? `${symexAttempted} of ${symexTotal} taint findings` + : 'no taint findings', + }, + { + label: 'Dynamic verification', + hint: 'Findings re-run in generated harnesses against the dynamic payload corpus.', + value: + dynamic.total > 0 + ? `${dynamic.confirmed.toLocaleString()} confirmed` + : 'not run', + detail: dynamicDetail, + }, + ]; + + return ( +
+ {rows.map((r) => ( +
+
+ {r.label} +
+
+
{r.value}
+ {r.detail &&
{r.detail}
} +
+
+ ))} +
+ ); +} + +// ── HotSinksList ─────────────────────────────────────────────────────────── + +export function HotSinksList({ sinks }: { sinks: HotSink[] }) { + if (!sinks.length) { + return ( +
+

No data

+
+ ); + } + return ( + + + + + + + + + {sinks.map((s) => ( + + + + + ))} + +
SinkFindings
{s.callee}{s.count}
+ ); +} + +// ── OwaspChart ───────────────────────────────────────────────────────────── + +export function OwaspChart({ buckets }: { buckets: OwaspBucket[] }) { + if (!buckets.length) { + return ( +
+

No data

+
+ ); + } + const max = Math.max(...buckets.map((b) => b.count), 1); + return ( +
    + {buckets.map((b) => ( +
  • + {b.code} + {b.label} +
    +
    +
    + {b.count} +
  • + ))} +
+ ); +} + +// ── WeightedTopFiles ─────────────────────────────────────────────────────── + +export function WeightedTopFiles({ + files, + onRowClick, +}: { + files: WeightedFile[]; + onRowClick?: (name: string) => void; +}) { + if (!files.length) { + return ( +
+

No data

+
+ ); + } + return ( + + + + + + + + + + {files.map((f) => ( + onRowClick(f.name) : undefined} + > + + + + + ))} + +
FileSeverityScore
{truncPath(f.name, 45)} + + {f.score}
+ ); +} + +function SeverityStack({ + high, + medium, + low, +}: { + high: number; + medium: number; + low: number; +}) { + const total = high + medium + low; + if (total === 0) return null; + return ( +
+ {high > 0 && ( +
+ {high} +
+ )} + {medium > 0 && ( +
+ {medium} +
+ )} + {low > 0 && ( +
+ {low} +
+ )} +
+ ); +} + +// ── LanguageHealthTable ──────────────────────────────────────────────────── + +export function LanguageHealthTable({ rows }: { rows: LanguageHealth[] }) { + if (!rows.length) { + return ( +
+

No data

+
+ ); + } + return ( + + + + + + + + + + {rows.map((r) => ( + + + + + + ))} + +
LanguageFindingsSeverity
{r.language}{r.findings} + +
+ ); +} + +// ── SuppressionHygieneCard ───────────────────────────────────────────────── + +export function SuppressionHygieneCard({ + hygiene, +}: { + hygiene: SuppressionHygiene; +}) { + const total = + hygiene.fingerprint_level + + hygiene.rule_level + + hygiene.file_level + + hygiene.rule_in_file_level; + const blanket = + hygiene.rule_level + hygiene.file_level + hygiene.rule_in_file_level; + const blanketDisplay = + total > 0 ? `${(hygiene.blanket_rate * 100).toFixed(0)}%` : 'n/a'; + const blanketDetail = + total > 0 + ? `${blanket} of ${total} suppressions are not pinned to a specific finding` + : 'No suppressions yet'; + return ( +
+
+
+ Blanket rate + Lower is better +
+
+
{blanketDisplay}
+
{blanketDetail}
+
+
+
+
+ By fingerprint + Most specific +
+
+
{hygiene.fingerprint_level}
+
+
+
+
+ By rule in a file +
+
+
{hygiene.rule_in_file_level}
+
+
+
+
+ By rule +
+
+
{hygiene.rule_level}
+
+
+
+
+ By file + Least specific +
+
+
{hygiene.file_level}
+
+
+
+ ); +} + +// ── BaselinePinControl ───────────────────────────────────────────────────── + +interface BaselinePinControlProps { + baseline?: BaselineInfo; + latestScanId?: string; + onPin: (scanId: string) => void; + onUnpin: () => void; + isPending: boolean; +} + +export function BaselinePinControl({ + baseline, + latestScanId, + onPin, + onUnpin, + isPending, +}: BaselinePinControlProps) { + const navigate = useNavigate(); + if (baseline) { + const net = baseline.drift_new - baseline.drift_fixed; + const driftClass = + net > 0 + ? 'baseline-drift-bad' + : net < 0 + ? 'baseline-drift-good' + : 'baseline-drift-flat'; + return ( +
+ Baseline: + + + drift: +{baseline.drift_new} new / -{baseline.drift_fixed} fixed ( + {net >= 0 ? '+' : ''} + {net}) + + +
+ ); + } + if (!latestScanId) return null; + return ( +
+ No baseline pinned. + +
+ ); +} diff --git a/frontend/src/components/ui/CommandPalette.tsx b/frontend/src/components/ui/CommandPalette.tsx new file mode 100644 index 00000000..2c764b76 --- /dev/null +++ b/frontend/src/components/ui/CommandPalette.tsx @@ -0,0 +1,183 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from 'react'; +import { useNavigate } from 'react-router-dom'; + +export interface PaletteCommand { + id: string; + /** Visible label. */ + label: string; + /** Optional secondary line such as section, hint, or shortcut. */ + hint?: string; + /** Group label for visual separation. */ + group?: string; + /** Search aliases beyond the label. */ + keywords?: string[]; + /** Optional leading icon. */ + icon?: ReactNode; + /** Optional trailing keyboard hint. */ + shortcut?: string; + /** Either a route to navigate to, or an action callback. One must be set. */ + to?: string; + action?: () => void; +} + +interface CommandPaletteProps { + open: boolean; + onClose: () => void; + commands: PaletteCommand[]; + placeholder?: string; +} + +function rank(query: string, cmd: PaletteCommand): number { + if (!query) return 0; + const q = query.toLowerCase(); + const haystacks = [cmd.label, cmd.hint ?? '', ...(cmd.keywords ?? [])].map( + (s) => s.toLowerCase(), + ); + let best = -1; + for (const h of haystacks) { + if (h.startsWith(q)) return 100; + const idx = h.indexOf(q); + if (idx >= 0 && (best < 0 || idx < best)) best = idx; + } + if (best < 0) return -1; + return 50 - best; +} + +export function CommandPalette({ + open, + onClose, + commands, + placeholder = 'Type a command or page...', +}: CommandPaletteProps) { + const [query, setQuery] = useState(''); + const [highlight, setHighlight] = useState(0); + const inputRef = useRef(null); + const navigate = useNavigate(); + + // Reset state on each open so the palette feels fresh and the highlight + // doesn't stick to a now-filtered-out item. + useEffect(() => { + if (open) { + setQuery(''); + setHighlight(0); + requestAnimationFrame(() => inputRef.current?.focus()); + } + }, [open]); + + const filtered = useMemo(() => { + if (!query) return commands; + return commands + .map((cmd) => [cmd, rank(query, cmd)] as const) + .filter(([, r]) => r >= 0) + .sort((a, b) => b[1] - a[1]) + .map(([cmd]) => cmd); + }, [commands, query]); + + // Keep highlight inside the filtered range. + useEffect(() => { + if (highlight >= filtered.length) setHighlight(0); + }, [filtered.length, highlight]); + + const run = useCallback( + (cmd: PaletteCommand) => { + onClose(); + if (cmd.action) cmd.action(); + else if (cmd.to) navigate(cmd.to); + }, + [navigate, onClose], + ); + + const onKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + onClose(); + } else if (event.key === 'ArrowDown') { + event.preventDefault(); + setHighlight((h) => Math.min(h + 1, filtered.length - 1)); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + setHighlight((h) => Math.max(h - 1, 0)); + } else if (event.key === 'Enter') { + event.preventDefault(); + const cmd = filtered[highlight]; + if (cmd) run(cmd); + } + }, + [filtered, highlight, onClose, run], + ); + + if (!open) return null; + + // Group while preserving filtered order. + const groups = new Map(); + for (const cmd of filtered) { + const g = cmd.group ?? ''; + const arr = groups.get(g) ?? []; + arr.push(cmd); + groups.set(g, arr); + } + + let runningIndex = 0; + return ( +
+
+
+ setQuery(e.target.value)} + onKeyDown={onKeyDown} + aria-label="Command search" + aria-autocomplete="list" + /> +
    + {filtered.length === 0 && ( +
  • No matches
  • + )} + {Array.from(groups.entries()).map(([group, items]) => ( +
  • + {group &&
    {group}
    } +
      + {items.map((cmd) => { + const idx = runningIndex++; + const active = idx === highlight; + return ( +
    • setHighlight(idx)} + onClick={() => run(cmd)} + > + {cmd.icon && ( + {cmd.icon} + )} + {cmd.label} + {cmd.hint && ( + {cmd.hint} + )} + {cmd.shortcut && ( + {cmd.shortcut} + )} +
    • + ); + })} +
    +
  • + ))} +
+
+
+ ); +} diff --git a/frontend/src/components/ui/Dropdown.tsx b/frontend/src/components/ui/Dropdown.tsx new file mode 100644 index 00000000..fc9c2b2b --- /dev/null +++ b/frontend/src/components/ui/Dropdown.tsx @@ -0,0 +1,104 @@ +import { + useCallback, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; +import { CheckIcon } from '../icons/Icons'; + +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(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 ( +
+
setOpen((v) => !v)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setOpen((v) => !v); + } + }} + > + {trigger({ open })} +
+ {open && ( +
+ {children({ close })} +
+ )} +
+ ); +} + +interface DropdownItemProps { + onClick: () => void; + children: ReactNode; + checked?: boolean; + hint?: string; + tone?: 'default' | 'warning'; +} + +export function DropdownItem({ + onClick, + children, + checked, + hint, + tone = 'default', +}: DropdownItemProps) { + return ( + + ); +} diff --git a/frontend/src/components/ui/EmptyState.tsx b/frontend/src/components/ui/EmptyState.tsx new file mode 100644 index 00000000..587d261e --- /dev/null +++ b/frontend/src/components/ui/EmptyState.tsx @@ -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 ( +
+ {icon &&
{icon}
} + {message &&

{message}

} + {children} +
+ ); +} diff --git a/frontend/src/components/ui/ErrorState.tsx b/frontend/src/components/ui/ErrorState.tsx new file mode 100644 index 00000000..3aed2038 --- /dev/null +++ b/frontend/src/components/ui/ErrorState.tsx @@ -0,0 +1,73 @@ +import { ApiError } from '../../api/client'; +import { RefreshIcon } from '../icons/Icons'; + +interface ErrorStateProps { + title?: string; + /** Either a plain message string or any thrown value (Error, ApiError, unknown). */ + message?: string; + error?: unknown; + onRetry?: () => void; + retryLabel?: string; +} + +interface FriendlyError { + title: string; + message: string; + hint?: string; +} + +/** Translate a thrown value into a title + message + hint we can render. */ +function friendly(error: unknown, fallbackTitle: string): FriendlyError { + if (error instanceof ApiError) { + if (error.isNetwork()) { + return { + title: 'Network error', + message: error.message || 'Could not reach the Nyx server.', + }; + } + if (error.status === 404) { + return { title: 'Not found', message: error.message }; + } + if (error.status === 403) { + return { title: 'Forbidden', message: error.message }; + } + if (error.status === 409) { + return { title: 'Conflict', message: error.message }; + } + if (error.status >= 500) { + return { + title: 'Server error', + message: error.message || 'The Nyx server returned an error.', + hint: 'Server logs may have more detail.', + }; + } + return { title: fallbackTitle, message: error.message }; + } + if (error instanceof Error) { + return { title: fallbackTitle, message: error.message }; + } + if (typeof error === 'string') { + return { title: fallbackTitle, message: error }; + } + return { title: fallbackTitle, message: 'An unknown error occurred.' }; +} + +export function ErrorState({ + title, + message, + error, + onRetry, + retryLabel = 'Try again', +}: ErrorStateProps) { + const fallbackTitle = title ?? 'Error'; + const resolved = error + ? friendly(error, fallbackTitle) + : { title: fallbackTitle, message: message ?? 'An error occurred.' }; + + return ( +
+

{resolved.title}

+

{resolved.message}

+
+ ); +} diff --git a/frontend/src/components/ui/LoadingState.tsx b/frontend/src/components/ui/LoadingState.tsx new file mode 100644 index 00000000..4eb4bc62 --- /dev/null +++ b/frontend/src/components/ui/LoadingState.tsx @@ -0,0 +1,19 @@ +interface LoadingStateProps { + message?: string; + /** + * 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 + * actually slow. + */ + delaySpinnerMs?: number; +} + +export function LoadingState({ message = 'Loading...' }: LoadingStateProps) { + return ( +
+
+ ); +} diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx new file mode 100644 index 00000000..b56711a6 --- /dev/null +++ b/frontend/src/components/ui/Modal.tsx @@ -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( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > + {children} +
, + document.body, + ); +} diff --git a/frontend/src/components/ui/Pagination.tsx b/frontend/src/components/ui/Pagination.tsx new file mode 100644 index 00000000..39f57e59 --- /dev/null +++ b/frontend/src/components/ui/Pagination.tsx @@ -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 ( +
+
+ Per page: + +
+ +
+ + + + Page {page} of {totalPages} + + + +
+ +
+ {total} total +
+
+ ); +} diff --git a/frontend/src/components/ui/ShortcutsHelp.tsx b/frontend/src/components/ui/ShortcutsHelp.tsx new file mode 100644 index 00000000..ed9f2062 --- /dev/null +++ b/frontend/src/components/ui/ShortcutsHelp.tsx @@ -0,0 +1,87 @@ +interface ShortcutsHelpProps { + open: boolean; + onClose: () => void; +} + +interface Row { + keys: string[]; + description: string; +} + +const ROWS: { section: string; rows: Row[] }[] = [ + { + section: 'Global', + rows: [ + { keys: ['⌘', 'K'], description: 'Open command palette' }, + { keys: ['/'], description: 'Focus search (on findings page)' }, + { keys: ['?'], description: 'Show this help' }, + { keys: ['Esc'], description: 'Close modal / palette' }, + ], + }, + { + section: 'Findings list', + rows: [ + { keys: ['j'], description: 'Next finding' }, + { keys: ['k'], description: 'Previous finding' }, + { keys: ['Enter'], description: 'Open highlighted finding' }, + ], + }, + { + section: 'Navigation', + rows: [ + { keys: ['g', 'o'], description: 'Go to Overview' }, + { keys: ['g', 'f'], description: 'Go to Findings' }, + { keys: ['g', 's'], description: 'Go to Scans' }, + { keys: ['g', 'r'], description: 'Go to Rules' }, + { keys: ['g', 't'], description: 'Go to Triage' }, + ], + }, +]; + +export function ShortcutsHelp({ open, onClose }: ShortcutsHelpProps) { + if (!open) return null; + return ( +
+
+
+
+

Keyboard shortcuts

+ +
+
+ {ROWS.map((section) => ( +
+

{section.section}

+
+ {section.rows.map((row) => ( +
+
+ {row.keys.map((k, i) => ( + + {i > 0 && then} + {k} + + ))} +
+
{row.description}
+
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/ui/StatCard.tsx b/frontend/src/components/ui/StatCard.tsx new file mode 100644 index 00000000..3224cfa9 --- /dev/null +++ b/frontend/src/components/ui/StatCard.tsx @@ -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 ( +
+
{label}
+
+ {value} + {delta != null && delta !== 0 && ( + 0 ? 'up' : 'down'}`}> + {delta > 0 ? '\u25B2' : '\u25BC'} {Math.abs(delta)} + + )} +
+ {subtitle &&
{subtitle}
} +
+ ); +} diff --git a/frontend/src/components/ui/Toaster.tsx b/frontend/src/components/ui/Toaster.tsx new file mode 100644 index 00000000..61cc0e96 --- /dev/null +++ b/frontend/src/components/ui/Toaster.tsx @@ -0,0 +1,38 @@ +import { useToast } from '../../contexts/ToastContext'; +import { CloseIcon } from '../icons/Icons'; + +export function Toaster() { + const { toasts, dismiss } = useToast(); + + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map((t) => ( +
+
+ {t.title &&
{t.title}
} +
{t.message}
+
+ +
+ ))} +
+ ); +} diff --git a/frontend/src/contexts/SSEContext.tsx b/frontend/src/contexts/SSEContext.tsx new file mode 100644 index 00000000..6ec0f994 --- /dev/null +++ b/frontend/src/contexts/SSEContext.tsx @@ -0,0 +1,117 @@ +import { + createContext, + useContext, + useEffect, + useState, + useRef, + useCallback, + type ReactNode, +} from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import type { TimingBreakdown } from '../api/types'; + +export interface ScanProgress { + job_id: string; + stage: string; + files_discovered: number; + files_parsed: number; + files_analyzed: number; + files_skipped: number; + batches_total: number; + batches_completed: number; + dynamic_enabled?: boolean; + dynamic_total: number; + dynamic_completed: number; + current_file: string; + elapsed_ms: number; + timing: TimingBreakdown; +} + +interface SSEState { + scanProgress: ScanProgress | null; + isScanRunning: boolean; +} + +const SSEContext = createContext({ + scanProgress: null, + isScanRunning: false, +}); + +export function useSSE() { + return useContext(SSEContext); +} + +export function SSEProvider({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + const [scanProgress, setScanProgress] = useState(null); + const [isScanRunning, setIsScanRunning] = useState(false); + const esRef = useRef(null); + const reconnectTimer = useRef | undefined>( + undefined, + ); + + const connect = useCallback(() => { + if (esRef.current) { + esRef.current.close(); + } + + const es = new EventSource('/api/events'); + esRef.current = es; + + es.addEventListener('scan_started', () => { + setIsScanRunning(true); + queryClient.invalidateQueries({ queryKey: ['scans'] }); + queryClient.invalidateQueries({ queryKey: ['targets'] }); + }); + + es.addEventListener('scan_progress', (e) => { + try { + const data = JSON.parse(e.data); + setScanProgress(data.data ?? data); + } catch { + /* ignore parse errors */ + } + }); + + es.addEventListener('scan_completed', () => { + setScanProgress(null); + setIsScanRunning(false); + queryClient.invalidateQueries({ queryKey: ['scans'] }); + queryClient.invalidateQueries({ queryKey: ['overview'] }); + queryClient.invalidateQueries({ queryKey: ['findings'] }); + queryClient.invalidateQueries({ queryKey: ['targets'] }); + }); + + es.addEventListener('scan_failed', () => { + setScanProgress(null); + setIsScanRunning(false); + queryClient.invalidateQueries({ queryKey: ['scans'] }); + queryClient.invalidateQueries({ queryKey: ['targets'] }); + }); + + es.addEventListener('config_changed', () => { + queryClient.invalidateQueries({ queryKey: ['config'] }); + queryClient.invalidateQueries({ queryKey: ['rules'] }); + }); + + es.onerror = () => { + es.close(); + esRef.current = null; + reconnectTimer.current = setTimeout(connect, 3000); + }; + }, [queryClient]); + + useEffect(() => { + connect(); + return () => { + if (esRef.current) esRef.current.close(); + if (reconnectTimer.current) clearTimeout(reconnectTimer.current); + }; + }, [connect]); + + return ( + + {children} + + ); +} diff --git a/frontend/src/contexts/ThemeContext.tsx b/frontend/src/contexts/ThemeContext.tsx new file mode 100644 index 00000000..7556a54e --- /dev/null +++ b/frontend/src/contexts/ThemeContext.tsx @@ -0,0 +1,91 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + type ReactNode, +} from 'react'; +import { usePersistedState } from '../hooks/usePersistedState'; + +export type ThemePreference = + | 'light' + | 'dark' + | 'system' + | 'hc-light' + | 'hc-dark'; +export type ResolvedTheme = 'light' | 'dark' | 'hc-light' | 'hc-dark'; + +interface ThemeContextValue { + preference: ThemePreference; + resolved: ResolvedTheme; + setPreference: (next: ThemePreference) => void; + /** Cycle light → dark → system → light. Used by the toolbar toggle. */ + cycle: () => void; +} + +const ThemeContext = createContext(null); + +function systemPrefersDark(): boolean { + return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false; +} + +function resolve(pref: ThemePreference): ResolvedTheme { + if (pref === 'system') return systemPrefersDark() ? 'dark' : 'light'; + return pref; +} + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [preference, setPreference] = usePersistedState( + 'theme', + 'light', + ); + + const resolved = useMemo(() => resolve(preference), [preference]); + + // Reflect the resolved theme onto so CSS rules under + // [data-theme="dark"] take effect. + useEffect(() => { + document.documentElement.setAttribute('data-theme', resolved); + }, [resolved]); + + // When the user picks "system", react to OS-level changes live. + useEffect(() => { + if (preference !== 'system') return; + const mq = window.matchMedia?.('(prefers-color-scheme: dark)'); + if (!mq) return; + const handler = () => { + document.documentElement.setAttribute( + 'data-theme', + systemPrefersDark() ? 'dark' : 'light', + ); + }; + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, [preference]); + + const cycle = useCallback(() => { + setPreference((prev) => { + if (prev === 'hc-light') return 'hc-dark'; + if (prev === 'hc-dark') return 'hc-light'; + if (prev === 'light') return 'dark'; + if (prev === 'dark') return 'system'; + return 'light'; + }); + }, [setPreference]); + + const value = useMemo( + () => ({ preference, resolved, setPreference, cycle }), + [preference, resolved, setPreference, cycle], + ); + + return ( + {children} + ); +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error('useTheme must be used inside '); + return ctx; +} diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx new file mode 100644 index 00000000..5ec44fc6 --- /dev/null +++ b/frontend/src/contexts/ToastContext.tsx @@ -0,0 +1,97 @@ +import { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, + type ReactNode, +} from 'react'; + +export type ToastTone = 'info' | 'success' | 'warning' | 'error'; + +export interface Toast { + id: number; + tone: ToastTone; + title?: string; + message: string; + durationMs: number; +} + +interface ToastContextValue { + toasts: Toast[]; + push: ( + t: Omit & { durationMs?: number }, + ) => number; + dismiss: (id: number) => void; + /** 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; + error: (message: string, title?: string) => number; +} + +const ToastContext = createContext(null); + +const DEFAULT_DURATION: Record = { + info: 4000, + success: 4000, + warning: 6000, + // Error toasts stick longer because failures usually need a deliberate read. + error: 8000, +}; + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + const nextId = useRef(1); + const timers = useRef(new Map()); + + const dismiss = useCallback((id: number) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + const handle = timers.current.get(id); + if (handle !== undefined) { + window.clearTimeout(handle); + timers.current.delete(id); + } + }, []); + + const push = useCallback( + ({ tone, title, message, durationMs }) => { + const id = nextId.current++; + const duration = durationMs ?? DEFAULT_DURATION[tone]; + setToasts((prev) => [ + ...prev, + { id, tone, title, message, durationMs: duration }, + ]); + if (duration > 0) { + const handle = window.setTimeout(() => dismiss(id), duration); + timers.current.set(id, handle); + } + return id; + }, + [dismiss], + ); + + const value = useMemo( + () => ({ + toasts, + push, + dismiss, + info: (message, title) => push({ tone: 'info', message, title }), + success: (message, title) => push({ tone: 'success', message, title }), + warning: (message, title) => push({ tone: 'warning', message, title }), + error: (message, title) => push({ tone: 'error', message, title }), + }), + [toasts, push, dismiss], + ); + + return ( + {children} + ); +} + +export function useToast(): ToastContextValue { + const ctx = useContext(ToastContext); + if (!ctx) throw new Error('useToast must be used inside '); + return ctx; +} diff --git a/frontend/src/graph/adapters/callgraph.ts b/frontend/src/graph/adapters/callgraph.ts new file mode 100644 index 00000000..358d1726 --- /dev/null +++ b/frontend/src/graph/adapters/callgraph.ts @@ -0,0 +1,57 @@ +import type { CallGraphNodeView, CallGraphView } from '@/api/types'; +import type { GraphModel } from '../types'; + +const MAX_LABEL = 44; +const MAX_DETAIL = 48; + +function truncate(value: string, max: number): string { + return value.length > max ? `${value.slice(0, max - 1)}…` : value; +} + +function summarizeNode(node: CallGraphNodeView): string { + if (node.namespace) return truncate(node.namespace, MAX_DETAIL); + + const segments = node.file.split(/[\\/]/); + return truncate(segments[segments.length - 1] ?? node.file, MAX_DETAIL); +} + +export function adaptCallGraph(data: CallGraphView): GraphModel { + const recursiveNodes = new Set(); + for (const scc of data.sccs) { + for (const id of scc) recursiveNodes.add(id); + } + + return { + kind: 'callgraph', + nodes: data.nodes.map((node) => ({ + key: String(node.id), + rawId: node.id, + label: truncate(node.name, MAX_LABEL), + kind: 'Call', + detail: summarizeNode(node), + metadata: { + ...node, + isRecursive: recursiveNodes.has(node.id), + searchText: [ + node.name, + node.namespace, + node.file, + node.lang, + node.arity == null ? '' : String(node.arity), + ] + .filter(Boolean) + .join(' ') + .toLowerCase(), + }, + })), + edges: data.edges.map((edge, index) => ({ + key: `call:${edge.source}:${edge.target}:${index}`, + source: String(edge.source), + target: String(edge.target), + kind: 'Call', + metadata: { + ...edge, + }, + })), + }; +} diff --git a/frontend/src/graph/adapters/cfg.ts b/frontend/src/graph/adapters/cfg.ts new file mode 100644 index 00000000..fe1770af --- /dev/null +++ b/frontend/src/graph/adapters/cfg.ts @@ -0,0 +1,85 @@ +import type { CfgEdgeView, CfgGraphView, CfgNodeView } from '@/api/types'; +import type { GraphModel } from '../types'; + +function truncate(value: string, max: number): string { + return value.length > max ? `${value.slice(0, max - 1)}…` : value; +} + +function normalizeText(value: string): string { + return value.replace(/\s+/g, ' ').trim(); +} + +const CFG_EDGE_PRIORITY: Record = { + True: 4, + False: 4, + Exception: 3, + Back: 2, + Seq: 1, +}; + +function getCfgEdgePriority(kind: string): number { + return CFG_EDGE_PRIORITY[kind] ?? 2; +} + +export function formatCfgNodeLabel(node: CfgNodeView): string { + const summary = + node.kind === 'Call' + ? (node.callee ?? node.defines) + : (node.defines ?? node.callee); + + if (summary) return `${node.kind}: ${truncate(normalizeText(summary), 56)}`; + return node.kind; +} + +export function normalizeCfgEdges(edges: CfgEdgeView[]): CfgEdgeView[] { + const deduped = new Map(); + + for (const edge of edges) { + const key = `${edge.source}:${edge.target}`; + const current = deduped.get(key); + + if ( + !current || + getCfgEdgePriority(edge.kind) > getCfgEdgePriority(current.kind) + ) { + deduped.set(key, edge); + } + } + + return [...deduped.values()]; +} + +export function adaptCfgGraph(data: CfgGraphView): GraphModel { + const edges = normalizeCfgEdges(data.edges); + + return { + kind: 'cfg', + nodes: data.nodes.map((node) => ({ + key: String(node.id), + rawId: node.id, + label: formatCfgNodeLabel(node), + kind: node.kind, + detail: `Line ${node.line}`, + sublabel: node.condition_text + ? truncate(node.condition_text, 40) + : undefined, + badges: node.labels.length > 0 ? node.labels.slice(0, 4) : undefined, + line: node.line, + metadata: { + ...node, + isEntry: node.id === data.entry, + isExit: node.kind === 'Exit' || node.kind === 'Return', + }, + })), + edges: edges.map((edge, index) => ({ + key: `cfg:${edge.source}:${edge.target}:${edge.kind}:${index}`, + source: String(edge.source), + target: String(edge.target), + kind: edge.kind, + label: edge.kind !== 'Seq' ? edge.kind : undefined, + metadata: { + ...edge, + }, + })), + }; +} diff --git a/frontend/src/graph/adapters/surface.ts b/frontend/src/graph/adapters/surface.ts new file mode 100644 index 00000000..8d2ad947 --- /dev/null +++ b/frontend/src/graph/adapters/surface.ts @@ -0,0 +1,84 @@ +import type { SurfaceEdge, SurfaceMap, SurfaceNode } from '@/api/types'; +import type { GraphModel } from '../types'; + +const MAX_LABEL = 44; +const MAX_DETAIL = 48; + +function truncate(value: string, max: number): string { + return value.length > max ? `${value.slice(0, max - 1)}…` : value; +} + +export const SURFACE_NODE_KIND: Record = { + entry_point: 'EntryPoint', + data_store: 'DataStore', + external_service: 'ExternalService', + dangerous_local: 'DangerousLocal', +}; + +function nodeTitle(node: SurfaceNode): string { + switch (node.node) { + case 'entry_point': + return `${node.method} ${node.route}`; + case 'data_store': + return `${node.kind}: ${node.label}`; + case 'external_service': + return `${node.kind}: ${node.label}`; + case 'dangerous_local': + return node.function_name; + } +} + +function nodeDetail(node: SurfaceNode): string { + switch (node.node) { + case 'entry_point': + return `${node.framework} · ${node.handler_name}`; + case 'data_store': + return 'data store'; + case 'external_service': + return 'external service'; + case 'dangerous_local': + return `cap=0x${node.cap_bits.toString(16)}`; + } +} + +function nodeLocation(node: SurfaceNode): { file: string; line: number } { + if (node.node === 'entry_point') return node.handler_location; + return node.location; +} + +export function adaptSurfaceMap(data: SurfaceMap): GraphModel { + return { + kind: 'surface', + nodes: data.nodes.map((node, index) => { + const loc = nodeLocation(node); + const title = nodeTitle(node); + const detail = nodeDetail(node); + const searchText = [title, detail, loc.file].join(' ').toLowerCase(); + const authBadge = + node.node === 'entry_point' && node.auth_required + ? ['auth'] + : undefined; + return { + key: String(index), + rawId: index, + label: truncate(title, MAX_LABEL), + kind: SURFACE_NODE_KIND[node.node], + detail: truncate(detail, MAX_DETAIL), + line: loc.line, + badges: authBadge, + metadata: { + surfaceKind: node.node, + node, + searchText, + }, + }; + }), + edges: data.edges.map((edge: SurfaceEdge, index) => ({ + key: `surface:${edge.from}:${edge.to}:${edge.kind}:${index}`, + source: String(edge.from), + target: String(edge.to), + kind: edge.kind, + metadata: { ...edge }, + })), + }; +} diff --git a/frontend/src/graph/components/CallGraphCanvas.tsx b/frontend/src/graph/components/CallGraphCanvas.tsx new file mode 100644 index 00000000..93afbbf6 --- /dev/null +++ b/frontend/src/graph/components/CallGraphCanvas.tsx @@ -0,0 +1,125 @@ +import { useMemo, useState } from 'react'; +import type { CallGraphView } from '@/api/types'; +import { adaptCallGraph } from '../adapters/callgraph'; +import { useElkLayout } from '../hooks/useElkLayout'; +import { + collectSearchMatches, + extractNeighborhoodSubgraph, +} from '../reduction/neighborhood'; +import { SigmaGraph } from '../rendering/sigma/SigmaGraph'; + +interface CallGraphCanvasProps { + data: CallGraphView; + selectedNodeId: number | null; + onSelectNode: (id: number) => void; +} + +export function CallGraphCanvas({ + data, + selectedNodeId, + onSelectNode, +}: CallGraphCanvasProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [neighborhoodOnly, setNeighborhoodOnly] = useState(false); + const [radius, setRadius] = useState(1); + + const fullGraph = useMemo(() => adaptCallGraph(data), [data]); + const selectedNodeKey = + selectedNodeId == null ? null : String(selectedNodeId); + + const matches = useMemo( + () => collectSearchMatches(fullGraph, searchQuery, 60), + [fullGraph, searchQuery], + ); + const matchKeys = useMemo( + () => new Set(matches.map((node) => node.key)), + [matches], + ); + + const visibleGraph = useMemo(() => { + if (!neighborhoodOnly || !selectedNodeKey) return fullGraph; + return extractNeighborhoodSubgraph(fullGraph, selectedNodeKey, radius); + }, [fullGraph, neighborhoodOnly, radius, selectedNodeKey]); + + const { graph, isLoading, error } = useElkLayout(visibleGraph); + + if (error) { + return ( +
+ Failed to compute the call graph layout. +
+ ); + } + + if (!graph) { + return
Preparing call graph…
; + } + + const extras = ( + <> + + + + + + ); + + return ( + onSelectNode(Number(key))} + searchMatchKeys={matchKeys} + toolbarExtras={extras} + loading={isLoading} + /> + ); +} diff --git a/frontend/src/graph/components/CfgGraphCanvas.tsx b/frontend/src/graph/components/CfgGraphCanvas.tsx new file mode 100644 index 00000000..5e705785 --- /dev/null +++ b/frontend/src/graph/components/CfgGraphCanvas.tsx @@ -0,0 +1,204 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { CfgGraphView, CfgNodeView } from '@/api/types'; +import { AnalysisWorkspace } from '@/components/explorer/AnalysisWorkspace'; +import { + adaptCfgGraph, + formatCfgNodeLabel, + normalizeCfgEdges, +} from '../adapters/cfg'; +import { useElkLayout } from '../hooks/useElkLayout'; +import { SigmaGraph } from '../rendering/sigma/SigmaGraph'; + +interface CfgGraphCanvasProps { + data: CfgGraphView; +} + +function formatNodeList( + ids: number[], + nodeMap: Map, +): string { + if (ids.length === 0) return 'None'; + + return ids + .map((id) => { + const node = nodeMap.get(id); + return node ? `${id} (${node.kind})` : `${id}`; + }) + .join(', '); +} + +function NodeDetail({ + node, + label, + predecessorIds, + successorIds, + nodeMap, +}: { + node: CfgNodeView; + label: string; + predecessorIds: number[]; + successorIds: number[]; + nodeMap: Map; +}) { + return ( +
+
+ Kind + {node.kind} +
+
+ Label + {label} +
+
+ Source + + L{node.line} • span {node.span[0]}-{node.span[1]} + +
+ {node.defines && ( +
+ Defines + {node.defines} +
+ )} + {node.uses.length > 0 && ( +
+ Uses + + {node.uses.join(', ')} + +
+ )} + {node.callee && ( +
+ Callee + {node.callee} +
+ )} + {node.labels.length > 0 && ( +
+ Labels +
+ {node.labels.map((labelValue, index) => ( + + {labelValue} + + ))} +
+
+ )} + {node.condition_text && ( +
+ Condition + {node.condition_text} +
+ )} + {node.enclosing_func && ( +
+ Function + {node.enclosing_func} +
+ )} +
+ Predecessors + + {formatNodeList(predecessorIds, nodeMap)} + +
+
+ Successors + + {formatNodeList(successorIds, nodeMap)} + +
+
+ ); +} + +export function CfgGraphCanvas({ data }: CfgGraphCanvasProps) { + const [selectedNodeKey, setSelectedNodeKey] = useState(null); + + const normalizedEdges = useMemo( + () => normalizeCfgEdges(data.edges), + [data.edges], + ); + const fullGraph = useMemo(() => adaptCfgGraph(data), [data]); + const nodeMap = useMemo( + () => new Map(data.nodes.map((node) => [node.id, node])), + [data.nodes], + ); + const { graph, isLoading, error } = useElkLayout(fullGraph); + + useEffect(() => { + if (!selectedNodeKey) return; + if (fullGraph.nodes.some((node) => node.key === selectedNodeKey)) return; + setSelectedNodeKey(null); + }, [fullGraph.nodes, selectedNodeKey]); + + if (error) { + return
Failed to compute the CFG layout.
; + } + + if (!graph) { + return
Preparing CFG…
; + } + + const selectedVisibleNode = + selectedNodeKey == null + ? undefined + : fullGraph.nodes.find((node) => node.key === selectedNodeKey); + + const selectedRawNode = + selectedVisibleNode && selectedVisibleNode.rawId >= 0 + ? nodeMap.get(selectedVisibleNode.rawId) + : undefined; + + const predecessorIds = + selectedRawNode == null + ? [] + : normalizedEdges + .filter((edge) => edge.target === selectedRawNode.id) + .map((edge) => edge.source); + const successorIds = + selectedRawNode == null + ? [] + : normalizedEdges + .filter((edge) => edge.source === selectedRawNode.id) + .map((edge) => edge.target); + + const inspector = + selectedRawNode != null ? ( + + ) : undefined; + + const inspectorTitle = selectedRawNode + ? `Node ${selectedRawNode.id}` + : undefined; + + return ( + + + setSelectedNodeKey((current) => (current === key ? null : key)) + } + loading={isLoading} + /> +
+ } + /> + ); +} diff --git a/frontend/src/graph/components/GraphToolbar.tsx b/frontend/src/graph/components/GraphToolbar.tsx new file mode 100644 index 00000000..9dcafb4d --- /dev/null +++ b/frontend/src/graph/components/GraphToolbar.tsx @@ -0,0 +1,88 @@ +import type { ReactNode } from 'react'; + +interface GraphToolbarProps { + zoomPercentage: number; + onZoomIn: () => void; + onZoomOut: () => void; + onFitGraph: () => void; + onFocusSelection?: () => void; + focusDisabled?: boolean; + extras?: ReactNode; + status?: ReactNode; +} + +export function GraphToolbar({ + zoomPercentage, + onZoomIn, + onZoomOut, + onFitGraph, + onFocusSelection, + focusDisabled, + extras, + status, +}: GraphToolbarProps) { + return ( +
+
+ + {zoomPercentage}% + +
+ + {onFocusSelection && ( + + )} +
+ {extras ?
{extras}
: null} + {status ?
{status}
: null} +
+ ); +} diff --git a/frontend/src/graph/components/SurfaceGraphCanvas.tsx b/frontend/src/graph/components/SurfaceGraphCanvas.tsx new file mode 100644 index 00000000..ea21e48c --- /dev/null +++ b/frontend/src/graph/components/SurfaceGraphCanvas.tsx @@ -0,0 +1,123 @@ +import { useMemo, useState } from 'react'; +import type { SurfaceMap } from '@/api/types'; +import { adaptSurfaceMap } from '../adapters/surface'; +import { useElkLayout } from '../hooks/useElkLayout'; +import { + collectSearchMatches, + extractNeighborhoodSubgraph, +} from '../reduction/neighborhood'; +import { SigmaGraph } from '../rendering/sigma/SigmaGraph'; + +interface SurfaceGraphCanvasProps { + data: SurfaceMap; + selectedNodeId: number | null; + onSelectNode: (id: number) => void; +} + +export function SurfaceGraphCanvas({ + data, + selectedNodeId, + onSelectNode, +}: SurfaceGraphCanvasProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [neighborhoodOnly, setNeighborhoodOnly] = useState(false); + const [radius, setRadius] = useState(2); + + const fullGraph = useMemo(() => adaptSurfaceMap(data), [data]); + const selectedNodeKey = + selectedNodeId == null ? null : String(selectedNodeId); + + const matches = useMemo( + () => collectSearchMatches(fullGraph, searchQuery, 60), + [fullGraph, searchQuery], + ); + const matchKeys = useMemo( + () => new Set(matches.map((node) => node.key)), + [matches], + ); + + const visibleGraph = useMemo(() => { + if (!neighborhoodOnly || !selectedNodeKey) return fullGraph; + return extractNeighborhoodSubgraph(fullGraph, selectedNodeKey, radius); + }, [fullGraph, neighborhoodOnly, radius, selectedNodeKey]); + + const { graph, isLoading, error } = useElkLayout(visibleGraph); + + if (error) { + return ( +
Failed to compute the surface layout.
+ ); + } + + if (!graph) { + return
Preparing surface graph…
; + } + + const extras = ( + <> + + + + + + ); + + return ( + onSelectNode(Number(key))} + searchMatchKeys={matchKeys} + toolbarExtras={extras} + loading={isLoading} + /> + ); +} diff --git a/frontend/src/graph/hooks/useElkLayout.ts b/frontend/src/graph/hooks/useElkLayout.ts new file mode 100644 index 00000000..42779f8e --- /dev/null +++ b/frontend/src/graph/hooks/useElkLayout.ts @@ -0,0 +1,99 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { layoutGraphWithElk } from '../layout/elk'; +import type { ElkLayoutPreset, GraphModel, LayoutGraphModel } from '../types'; + +interface LayoutState { + graph: LayoutGraphModel | null; + isLoading: boolean; + error: Error | null; +} + +function createLayoutKey( + graph: GraphModel, + overrides?: Partial, +): string { + const nodeKey = graph.nodes + .map( + (node) => `${node.key}:${node.label}:${node.kind}:${node.detail ?? ''}`, + ) + .join('|'); + const edgeKey = graph.edges + .map((edge) => `${edge.key}:${edge.source}:${edge.target}:${edge.kind}`) + .join('|'); + return JSON.stringify({ + kind: graph.kind, + nodeKey, + edgeKey, + overrides, + }); +} + +const layoutCache = new Map(); + +// The hook stays async even on the main thread so moving ELK into a worker later +// does not require rewriting the React call sites. +export function useElkLayout( + graph: GraphModel, + overrides?: Partial, +): LayoutState { + const layoutKey = useMemo( + () => createLayoutKey(graph, overrides), + [graph, overrides], + ); + const [state, setState] = useState(() => { + const cached = layoutCache.get(layoutKey) ?? null; + return { + graph: cached, + isLoading: cached == null, + error: null, + }; + }); + const requestRef = useRef(0); + + useEffect(() => { + const cached = layoutCache.get(layoutKey); + if (cached) { + setState({ + graph: cached, + isLoading: false, + error: null, + }); + return; + } + + const requestId = requestRef.current + 1; + requestRef.current = requestId; + let cancelled = false; + + setState((current) => ({ + graph: current.graph, + isLoading: true, + error: null, + })); + + void layoutGraphWithElk(graph, overrides) + .then((layout) => { + if (cancelled || requestRef.current !== requestId) return; + layoutCache.set(layoutKey, layout); + setState({ + graph: layout, + isLoading: false, + error: null, + }); + }) + .catch((error: unknown) => { + if (cancelled || requestRef.current !== requestId) return; + setState({ + graph: null, + isLoading: false, + error: error instanceof Error ? error : new Error('Layout failed'), + }); + }); + + return () => { + cancelled = true; + }; + }, [graph, layoutKey, overrides]); + + return state; +} diff --git a/frontend/src/graph/layout/elk.ts b/frontend/src/graph/layout/elk.ts new file mode 100644 index 00000000..299d5a83 --- /dev/null +++ b/frontend/src/graph/layout/elk.ts @@ -0,0 +1,296 @@ +import ELK from 'elkjs/lib/elk.bundled.js'; +import type { ElkEdgeSection, ElkNode } from 'elkjs/lib/elk-api'; +import { getNodeTextLayout } from './text'; +import type { + ElkLayoutPreset, + GraphModel, + GraphNodeModel, + GraphPoint, + GraphViewKind, + LayoutGraphEdge, + LayoutGraphModel, + LayoutGraphNode, +} from '../types'; + +const elk = new ELK(); + +const CHAR_WIDTH = 7.1; +const LINE_HEIGHT = 16; +const HORIZONTAL_PADDING = 30; +const VERTICAL_PADDING = 18; +const MIN_WIDTH = 112; +const BADGE_HEIGHT = 16; +const MAX_WIDTH = 360; + +const PRESETS: Record = { + callgraph: { + direction: 'DOWN', + nodeSpacing: 42, + layerSpacing: 148, + edgeNodeSpacing: 24, + padding: 36, + edgeRouting: 'POLYLINE', + }, + cfg: { + direction: 'DOWN', + nodeSpacing: 36, + layerSpacing: 128, + edgeNodeSpacing: 24, + padding: 32, + edgeRouting: 'ORTHOGONAL', + }, + surface: { + direction: 'RIGHT', + nodeSpacing: 44, + layerSpacing: 156, + edgeNodeSpacing: 28, + padding: 36, + edgeRouting: 'POLYLINE', + }, +}; + +function measureNode( + node: GraphNodeModel, + viewKind: GraphViewKind, +): { + width: number; + height: number; + text: ReturnType; +} { + const text = getNodeTextLayout(node, viewKind); + const width = Math.max( + MIN_WIDTH, + Math.min(MAX_WIDTH, text.maxChars * CHAR_WIDTH + HORIZONTAL_PADDING), + ); + const height = + Math.max(1, text.lineCount) * LINE_HEIGHT + + VERTICAL_PADDING + + (node.badges?.length ? BADGE_HEIGHT : 0); + + return { width, height, text }; +} + +function estimateSigmaNodeSize( + node: GraphNodeModel, + width: number, + height: number, +): number { + const base = Math.max(6, Math.min(18, Math.sqrt(width * height) / 8)); + if (node.kind === 'Entry' || node.kind === 'Exit') return base + 1.5; + if (node.kind === 'If' || node.kind === 'Loop') return base + 0.75; + return base; +} + +function buildLayoutOptions( + graph: GraphModel, + overrides?: Partial, +): ElkNode['layoutOptions'] { + const preset = { ...PRESETS[graph.kind], ...overrides }; + + return { + 'elk.algorithm': 'layered', + 'elk.direction': preset.direction, + 'elk.spacing.nodeNode': String(preset.nodeSpacing), + 'elk.layered.spacing.nodeNodeBetweenLayers': String(preset.layerSpacing), + 'elk.spacing.edgeNode': String(preset.edgeNodeSpacing), + 'elk.edgeRouting': preset.edgeRouting, + 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', + 'elk.layered.unnecessaryBendpoints': 'true', + 'elk.layered.thoroughness': graph.kind === 'callgraph' ? '6' : '8', + }; +} + +function sortSections( + sections: ElkEdgeSection[] | undefined, +): ElkEdgeSection[] { + if (!sections || sections.length <= 1) return sections ?? []; + + const sectionById = new Map(sections.map((section) => [section.id, section])); + const head = + sections.find( + (section) => + !section.incomingSections || section.incomingSections.length === 0, + ) ?? sections[0]; + + const ordered: ElkEdgeSection[] = []; + const seen = new Set(); + let cursor: ElkEdgeSection | undefined = head; + + while (cursor && !seen.has(cursor.id)) { + ordered.push(cursor); + seen.add(cursor.id); + + const nextId: string | undefined = cursor.outgoingSections?.[0]; + cursor = nextId ? sectionById.get(nextId) : undefined; + } + + if (ordered.length === sections.length) return ordered; + return sections; +} + +function dedupePoints(points: GraphPoint[]): GraphPoint[] { + const deduped: GraphPoint[] = []; + for (const point of points) { + const previous = deduped[deduped.length - 1]; + if (previous && previous.x === point.x && previous.y === point.y) continue; + deduped.push(point); + } + return deduped; +} + +function extractRoute(sections: ElkEdgeSection[] | undefined): GraphPoint[] { + const points: GraphPoint[] = []; + + for (const section of sortSections(sections)) { + points.push(section.startPoint); + if (section.bendPoints) points.push(...section.bendPoints); + points.push(section.endPoint); + } + + return dedupePoints(points); +} + +function collectBounds( + nodes: LayoutGraphNode[], + edges: LayoutGraphEdge[], + padding: number, +) { + let minX = Number.POSITIVE_INFINITY; + let maxX = Number.NEGATIVE_INFINITY; + let minY = Number.POSITIVE_INFINITY; + let maxY = Number.NEGATIVE_INFINITY; + + const includePoint = (x: number, y: number) => { + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + }; + + for (const node of nodes) { + includePoint(node.x - node.width / 2, node.y - node.height / 2); + includePoint(node.x + node.width / 2, node.y + node.height / 2); + } + + for (const edge of edges) { + for (const point of edge.route) { + includePoint(point.x, point.y); + } + } + + if (minX === Number.POSITIVE_INFINITY) minX = 0; + if (maxX === Number.NEGATIVE_INFINITY) maxX = 0; + if (minY === Number.POSITIVE_INFINITY) minY = 0; + if (maxY === Number.NEGATIVE_INFINITY) maxY = 0; + + const offsetX = padding - minX; + const offsetY = padding - minY; + + return { + offsetX, + offsetY, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; +} + +export async function layoutGraphWithElk( + graph: GraphModel, + overrides?: Partial, +): Promise { + if (graph.nodes.length === 0) { + return { + kind: graph.kind, + nodes: [], + edges: [], + bounds: { width: 0, height: 0 }, + }; + } + + const preset = { ...PRESETS[graph.kind], ...overrides }; + const dimensions = new Map< + string, + { + width: number; + height: number; + text: ReturnType; + } + >(); + + const elkGraph: ElkNode = { + id: 'root', + layoutOptions: buildLayoutOptions(graph, overrides), + children: graph.nodes.map((node) => { + const size = measureNode(node, graph.kind); + dimensions.set(node.key, size); + return { + id: node.key, + width: size.width, + height: size.height, + }; + }), + edges: graph.edges.map((edge) => ({ + id: edge.key, + sources: [edge.source], + targets: [edge.target], + })), + }; + + const layout = await elk.layout(elkGraph); + const edgeById = new Map( + layout.edges?.map((edge) => [edge.id ?? '', edge]) ?? [], + ); + const layoutNodesById = new Map( + layout.children?.map((node) => [node.id, node]) ?? [], + ); + + const nodes: LayoutGraphNode[] = graph.nodes.map((node) => { + const layoutNode = layoutNodesById.get(node.key); + const size = dimensions.get(node.key) ?? measureNode(node, graph.kind); + const x = (layoutNode?.x ?? 0) + size.width / 2; + const y = (layoutNode?.y ?? 0) + size.height / 2; + + return { + ...node, + x, + y, + width: size.width, + height: size.height, + sigmaSize: estimateSigmaNodeSize(node, size.width, size.height), + labelLines: size.text.labelLines, + detailLines: size.text.detailLines, + sublabelLines: size.text.sublabelLines, + }; + }); + + const edges: LayoutGraphEdge[] = graph.edges.map((edge) => { + const layoutEdge = edgeById.get(edge.key); + const route = extractRoute(layoutEdge?.sections); + return { + ...edge, + route, + }; + }); + + const bounds = collectBounds(nodes, edges, preset.padding); + + return { + kind: graph.kind, + nodes: nodes.map((node) => ({ + ...node, + x: node.x + bounds.offsetX, + y: node.y + bounds.offsetY, + })), + edges: edges.map((edge) => ({ + ...edge, + route: edge.route.map((point) => ({ + x: point.x + bounds.offsetX, + y: point.y + bounds.offsetY, + })), + })), + bounds: { + width: bounds.width, + height: bounds.height, + }, + }; +} diff --git a/frontend/src/graph/layout/text.ts b/frontend/src/graph/layout/text.ts new file mode 100644 index 00000000..0c94c610 --- /dev/null +++ b/frontend/src/graph/layout/text.ts @@ -0,0 +1,126 @@ +import type { GraphNodeModel, GraphViewKind } from '../types'; + +interface TextLayoutConfig { + primaryChars: number; + secondaryChars: number; + maxPrimaryLines: number; + maxSecondaryLines: number; + maxSublabelLines: number; +} + +export interface NodeTextLayout { + labelLines: string[]; + detailLines: string[]; + sublabelLines: string[]; + lineCount: number; + maxChars: number; +} + +const CONFIG: Record = { + callgraph: { + primaryChars: 28, + secondaryChars: 30, + maxPrimaryLines: 2, + maxSecondaryLines: 1, + maxSublabelLines: 1, + }, + cfg: { + primaryChars: 30, + secondaryChars: 34, + maxPrimaryLines: 3, + maxSecondaryLines: 2, + maxSublabelLines: 1, + }, + surface: { + primaryChars: 32, + secondaryChars: 32, + maxPrimaryLines: 2, + maxSecondaryLines: 2, + maxSublabelLines: 1, + }, +}; + +function normalizeWhitespace(value: string): string { + return value.replace(/\s+/g, ' ').trim(); +} + +function chooseBreakIndex(value: string, maxChars: number): number { + const probe = value.slice(0, maxChars + 1); + const preferred = Math.max( + probe.lastIndexOf(' '), + probe.lastIndexOf('.'), + probe.lastIndexOf(':'), + probe.lastIndexOf('/'), + probe.lastIndexOf('_'), + probe.lastIndexOf('('), + probe.lastIndexOf(')'), + probe.lastIndexOf(','), + ); + + if (preferred >= Math.floor(maxChars * 0.55)) { + return preferred + 1; + } + + return maxChars; +} + +export function wrapGraphText( + value: string | undefined, + maxChars: number, +): string[] { + if (!value) return []; + + const normalized = normalizeWhitespace(value); + if (!normalized) return []; + + const lines: string[] = []; + let remaining = normalized; + + while (remaining.length > maxChars) { + const breakIndex = chooseBreakIndex(remaining, maxChars); + lines.push(remaining.slice(0, breakIndex).trim()); + remaining = remaining.slice(breakIndex).trim(); + } + + if (remaining) lines.push(remaining); + return lines; +} + +function clampLines(lines: string[], maxLines: number): string[] { + if (lines.length <= maxLines) return lines; + + const visible = lines.slice(0, maxLines); + const last = visible[maxLines - 1]; + if (!last) return visible; + + visible[maxLines - 1] = last.endsWith('…') ? last : `${last.slice(0, -1)}…`; + return visible; +} + +export function getNodeTextLayout( + node: GraphNodeModel, + viewKind: GraphViewKind, +): NodeTextLayout { + const config = CONFIG[viewKind]; + const labelLines = clampLines( + wrapGraphText(node.label, config.primaryChars), + config.maxPrimaryLines, + ); + const detailLines = clampLines( + wrapGraphText(node.detail, config.secondaryChars), + config.maxSecondaryLines, + ); + const sublabelLines = clampLines( + wrapGraphText(node.sublabel, config.secondaryChars), + config.maxSublabelLines, + ); + const allLines = labelLines.concat(detailLines, sublabelLines); + + return { + labelLines, + detailLines, + sublabelLines, + lineCount: allLines.length, + maxChars: Math.max(...allLines.map((line) => line.length), 8), + }; +} diff --git a/frontend/src/graph/reduction/cfgCompaction.ts b/frontend/src/graph/reduction/cfgCompaction.ts new file mode 100644 index 00000000..5700e64d --- /dev/null +++ b/frontend/src/graph/reduction/cfgCompaction.ts @@ -0,0 +1,165 @@ +import type { + GraphCompactionResult, + GraphEdgeModel, + GraphModel, + GraphNodeModel, +} from '../types'; + +const CONTROL_KINDS = new Set([ + 'Entry', + 'Exit', + 'If', + 'Loop', + 'Return', + 'Break', + 'Continue', +]); + +function buildLineRange(nodes: GraphNodeModel[]): string | undefined { + const lines = nodes + .map((node) => node.line) + .filter((line): line is number => typeof line === 'number' && line > 0); + + if (lines.length === 0) return undefined; + const minLine = Math.min(...lines); + const maxLine = Math.max(...lines); + return minLine === maxLine ? `L${minLine}` : `L${minLine}-L${maxLine}`; +} + +export function compactGraph(graph: GraphModel): GraphCompactionResult { + if (graph.kind !== 'cfg' || graph.nodes.length <= 3) { + return { graph, compounds: new Map() }; + } + + const seqOut = new Map(); + const seqIn = new Map(); + const seqOutCount = new Map(); + const seqInCount = new Map(); + const totalOutCount = new Map(); + const totalInCount = new Map(); + + for (const node of graph.nodes) { + seqOutCount.set(node.key, 0); + seqInCount.set(node.key, 0); + totalOutCount.set(node.key, 0); + totalInCount.set(node.key, 0); + } + + for (const edge of graph.edges) { + totalOutCount.set(edge.source, (totalOutCount.get(edge.source) ?? 0) + 1); + totalInCount.set(edge.target, (totalInCount.get(edge.target) ?? 0) + 1); + + if (edge.kind !== 'Seq') continue; + seqOutCount.set(edge.source, (seqOutCount.get(edge.source) ?? 0) + 1); + seqInCount.set(edge.target, (seqInCount.get(edge.target) ?? 0) + 1); + seqOut.set(edge.source, edge.target); + seqIn.set(edge.target, edge.source); + } + + const nodeMap = new Map(graph.nodes.map((node) => [node.key, node])); + const chainable = new Set(); + + for (const node of graph.nodes) { + if (CONTROL_KINDS.has(node.kind)) continue; + + if ( + totalInCount.get(node.key) === 1 && + totalOutCount.get(node.key) === 1 && + seqInCount.get(node.key) === 1 && + seqOutCount.get(node.key) === 1 + ) { + chainable.add(node.key); + } + } + + const consumed = new Set(); + const chains: string[][] = []; + + for (const node of graph.nodes) { + if (consumed.has(node.key) || chainable.has(node.key)) continue; + if (seqOutCount.get(node.key) !== 1) continue; + + const next = seqOut.get(node.key); + if (!next || !chainable.has(next)) continue; + + const chain: string[] = []; + let cursor: string | undefined = next; + while (cursor && chainable.has(cursor) && !consumed.has(cursor)) { + chain.push(cursor); + consumed.add(cursor); + cursor = seqOut.get(cursor); + } + + if (chain.length >= 2) chains.push(chain); + } + + if (chains.length === 0) return { graph, compounds: new Map() }; + + const removedKeys = new Set(); + const compounds = new Map(); + const compoundNodes: GraphNodeModel[] = []; + const replacement = new Map(); + + let nextCompoundIndex = 0; + for (const chain of chains) { + const members = chain + .map((key) => nodeMap.get(key)) + .filter((member): member is GraphNodeModel => member != null); + if (members.length !== chain.length) continue; + + for (const key of chain) removedKeys.add(key); + + const compoundKey = `compound:${nextCompoundIndex}`; + nextCompoundIndex += 1; + compounds.set(compoundKey, chain); + for (const key of chain) replacement.set(key, compoundKey); + + compoundNodes.push({ + key: compoundKey, + rawId: -1, + label: `${chain.length} statements`, + kind: 'Compound', + detail: buildLineRange(members), + line: members[0].line, + metadata: { + isCompound: true, + memberKeys: chain, + memberRawIds: members.map((member) => member.rawId), + }, + }); + } + + const nodes = graph.nodes + .filter((node) => !removedKeys.has(node.key)) + .concat(compoundNodes); + + const dedupe = new Set(); + const edges: GraphEdgeModel[] = []; + + for (const edge of graph.edges) { + const source = replacement.get(edge.source) ?? edge.source; + const target = replacement.get(edge.target) ?? edge.target; + + if (source === target) continue; + + const dedupeKey = `${source}:${target}:${edge.kind}`; + if (dedupe.has(dedupeKey)) continue; + dedupe.add(dedupeKey); + + edges.push({ + ...edge, + key: `${edge.key}:compact:${source}:${target}`, + source, + target, + }); + } + + return { + graph: { + kind: graph.kind, + nodes, + edges, + }, + compounds, + }; +} diff --git a/frontend/src/graph/reduction/neighborhood.ts b/frontend/src/graph/reduction/neighborhood.ts new file mode 100644 index 00000000..1bf2ffd2 --- /dev/null +++ b/frontend/src/graph/reduction/neighborhood.ts @@ -0,0 +1,66 @@ +import type { GraphModel, GraphNodeModel } from '../types'; + +export function collectSearchMatches( + graph: GraphModel, + query: string, + limit = 200, +): GraphNodeModel[] { + const normalized = query.trim().toLowerCase(); + if (!normalized) return []; + + const matches: GraphNodeModel[] = []; + for (const node of graph.nodes) { + const haystack = String( + node.metadata?.searchText ?? node.label, + ).toLowerCase(); + if (!haystack.includes(normalized)) continue; + matches.push(node); + if (matches.length >= limit) break; + } + + return matches; +} + +export function extractNeighborhoodSubgraph( + graph: GraphModel, + centerKey: string | null, + radius: number, +): GraphModel { + if (!centerKey || radius < 1) return graph; + + const nodeKeys = new Set(graph.nodes.map((node) => node.key)); + if (!nodeKeys.has(centerKey)) return graph; + + const adjacency = new Map>(); + for (const node of graph.nodes) adjacency.set(node.key, new Set()); + for (const edge of graph.edges) { + adjacency.get(edge.source)?.add(edge.target); + adjacency.get(edge.target)?.add(edge.source); + } + + const visible = new Set([centerKey]); + let frontier = new Set([centerKey]); + + for (let depth = 0; depth < radius; depth += 1) { + const next = new Set(); + for (const key of frontier) { + const neighbors = adjacency.get(key); + if (!neighbors) continue; + for (const neighbor of neighbors) { + if (visible.has(neighbor)) continue; + visible.add(neighbor); + next.add(neighbor); + } + } + if (next.size === 0) break; + frontier = next; + } + + return { + kind: graph.kind, + nodes: graph.nodes.filter((node) => visible.has(node.key)), + edges: graph.edges.filter( + (edge) => visible.has(edge.source) && visible.has(edge.target), + ), + }; +} diff --git a/frontend/src/graph/rendering/sigma/SigmaGraph.tsx b/frontend/src/graph/rendering/sigma/SigmaGraph.tsx new file mode 100644 index 00000000..86b35a5e --- /dev/null +++ b/frontend/src/graph/rendering/sigma/SigmaGraph.tsx @@ -0,0 +1,332 @@ +import type { MutableRefObject, ReactNode } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import Sigma from 'sigma'; +import { GraphToolbar } from '../../components/GraphToolbar'; +import { readGraphPalette } from '../../styles'; +import type { + GraphThemePalette, + GraphViewKind, + SigmaEdgeAttributes, + SigmaNodeAttributes, +} from '../../types'; +import { buildSigmaGraph } from './buildGraph'; +import { buildInteractionState, drawGraphOverlay } from './edgeOverlay'; +import type { LayoutGraphModel } from '../../types'; + +interface SigmaGraphProps { + graph: LayoutGraphModel; + viewKind: GraphViewKind; + selectedNodeKey: string | null; + onNodeClick?: (key: string) => void; + searchMatchKeys?: Set; + toolbarExtras?: ReactNode; + loading?: boolean; +} + +const EMPTY_MATCHES = new Set(); +const MIN_CAMERA_RATIO = 0.001; +const NOOP_NODE_HOVER = () => {}; + +function zoomPercentage( + renderer: Sigma | null, +): number { + if (!renderer) return 100; + const ratio = renderer.getCamera().getState().ratio; + return Math.max(10, Math.round(100 / ratio)); +} + +function clampCameraRatio( + renderer: Sigma, + ratio: number, +): number { + const minCameraRatio = renderer.getSetting('minCameraRatio') ?? 0; + const maxCameraRatio = + renderer.getSetting('maxCameraRatio') ?? Number.POSITIVE_INFINITY; + + return Math.min(maxCameraRatio, Math.max(minCameraRatio, ratio)); +} + +function getReadableFocusRatio( + renderer: Sigma, + graph: LayoutGraphModel, + nodeKey: string, +): number { + const currentRatio = renderer.getCamera().getState().ratio; + const node = graph.nodes.find((entry) => entry.key === nodeKey); + if (!node) return currentRatio; + + const center = renderer.graphToViewport({ x: node.x, y: node.y }); + const rightEdge = renderer.graphToViewport({ + x: node.x + node.width / 2, + y: node.y, + }); + const bottomEdge = renderer.graphToViewport({ + x: node.x, + y: node.y + node.height / 2, + }); + const renderedWidth = Math.max(1, Math.abs(rightEdge.x - center.x) * 2); + const renderedHeight = Math.max(1, Math.abs(bottomEdge.y - center.y) * 2); + const totalLines = + node.labelLines.length + + node.detailLines.length + + node.sublabelLines.length; + const maxLineChars = Math.max( + 1, + ...node.labelLines.map((line) => line.length), + ...node.detailLines.map((line) => line.length), + ...node.sublabelLines.map((line) => line.length), + ); + const { width, height } = renderer.getDimensions(); + const desiredWidth = Math.min( + width * 0.4, + Math.max(170, maxLineChars * 9.5 + 40), + ); + const desiredHeight = Math.min( + height * 0.28, + Math.max(72, totalLines * 16 + (node.badges?.length ? 18 : 12)), + ); + const widthRatio = currentRatio * (renderedWidth / desiredWidth); + const heightRatio = currentRatio * (renderedHeight / desiredHeight); + const targetRatio = Math.min(widthRatio, heightRatio, currentRatio); + + return clampCameraRatio(renderer, Math.max(MIN_CAMERA_RATIO, targetRatio)); +} + +function createNodeReducer( + interactionRef: MutableRefObject>, +) { + return (nodeKey: string, data: SigmaNodeAttributes) => { + const interaction = interactionRef.current; + const isFocused = + interaction.selectedNodeKey === nodeKey || + interaction.hoveredNodeKey === nodeKey || + interaction.highlightedNodeKeys.has(nodeKey) || + interaction.searchMatchKeys.has(nodeKey); + + return { + ...data, + color: 'rgba(0, 0, 0, 0)', + size: data.size, + highlighted: false, + forceLabel: false, + zIndex: isFocused ? 2 : 1, + }; + }; +} + +export function SigmaGraph({ + graph, + viewKind, + selectedNodeKey, + onNodeClick, + searchMatchKeys = EMPTY_MATCHES, + toolbarExtras, + loading = false, +}: SigmaGraphProps) { + const containerRef = useRef(null); + const rendererRef = useRef | null>(null); + const overlayCanvasRef = useRef(null); + const [hoveredNodeKey, setHoveredNodeKey] = useState(null); + const [zoom, setZoom] = useState(100); + const palette = useMemo(() => readGraphPalette(), []); + const renderGraph = useMemo( + () => buildSigmaGraph(graph, palette, false), + [graph, palette], + ); + const overlayGraph = useMemo( + () => buildSigmaGraph(graph, palette, true), + [graph, palette], + ); + const interactionRef = useRef( + buildInteractionState( + overlayGraph, + selectedNodeKey, + hoveredNodeKey, + searchMatchKeys, + ), + ); + + interactionRef.current = buildInteractionState( + overlayGraph, + selectedNodeKey, + hoveredNodeKey, + searchMatchKeys, + ); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const renderer = new Sigma( + renderGraph, + container, + { + allowInvalidContainer: true, + autoCenter: true, + autoRescale: true, + defaultEdgeType: 'arrow', + defaultDrawNodeHover: NOOP_NODE_HOVER, + enableEdgeEvents: false, + renderEdgeLabels: false, + renderLabels: false, + hideLabelsOnMove: true, + labelDensity: viewKind === 'callgraph' ? 0.85 : 0.95, + labelRenderedSizeThreshold: viewKind === 'callgraph' ? 10 : 8, + minCameraRatio: MIN_CAMERA_RATIO, + maxCameraRatio: 4, + nodeReducer: createNodeReducer(interactionRef), + edgeReducer: () => ({ + hidden: true, + }), + stagePadding: 24, + zIndex: true, + }, + ); + + rendererRef.current = renderer; + setZoom(zoomPercentage(renderer)); + + const overlayCanvas = renderer.createCanvas('graphOverlay', { + afterLayer: 'edges', + style: { + pointerEvents: 'none', + }, + }); + overlayCanvasRef.current = overlayCanvas; + + const redraw = () => { + if (!overlayCanvasRef.current || !rendererRef.current) return; + drawGraphOverlay( + overlayCanvasRef.current, + rendererRef.current, + overlayGraph, + viewKind, + palette, + interactionRef.current, + ); + }; + + const handleClickNode = ({ node }: { node: string }) => { + onNodeClick?.(node); + const nodeDisplay = renderer.getNodeDisplayData(node); + if (!nodeDisplay) return; + + const camera = renderer.getCamera(); + const targetRatio = getReadableFocusRatio(renderer, graph, node); + void camera.animate( + { + x: nodeDisplay.x, + y: nodeDisplay.y, + ratio: targetRatio, + }, + { duration: 240 }, + ); + }; + + const handleEnterNode = ({ node }: { node: string }) => { + setHoveredNodeKey(node); + }; + + const handleLeaveNode = () => { + setHoveredNodeKey(null); + }; + + const handleAfterRender = () => { + setZoom(zoomPercentage(renderer)); + redraw(); + }; + + renderer.on('clickNode', handleClickNode); + renderer.on('enterNode', handleEnterNode); + renderer.on('leaveNode', handleLeaveNode); + renderer.on('afterRender', handleAfterRender); + + const resizeObserver = + typeof ResizeObserver === 'undefined' + ? null + : new ResizeObserver(() => { + renderer.resize(); + renderer.refresh({ schedule: true }); + }); + resizeObserver?.observe(container); + + redraw(); + + return () => { + resizeObserver?.disconnect(); + renderer.off('clickNode', handleClickNode); + renderer.off('enterNode', handleEnterNode); + renderer.off('leaveNode', handleLeaveNode); + renderer.off('afterRender', handleAfterRender); + if (overlayCanvasRef.current) { + renderer.killLayer('graphOverlay'); + overlayCanvasRef.current = null; + } + renderer.kill(); + rendererRef.current = null; + }; + }, [graph, onNodeClick, overlayGraph, palette, renderGraph, viewKind]); + + useEffect(() => { + const renderer = rendererRef.current; + if (!renderer) return; + renderer.refresh({ schedule: true, skipIndexation: true }); + }, [hoveredNodeKey, overlayGraph, searchMatchKeys, selectedNodeKey]); + + const handleZoomIn = () => { + void rendererRef.current?.getCamera().animatedZoom(); + }; + + const handleZoomOut = () => { + void rendererRef.current?.getCamera().animatedUnzoom(); + }; + + const handleFitGraph = () => { + void rendererRef.current?.getCamera().animatedReset(); + }; + + const handleFocusSelection = () => { + if (!selectedNodeKey) return; + const renderer = rendererRef.current; + if (!renderer) return; + const nodeDisplay = renderer.getNodeDisplayData(selectedNodeKey); + if (!nodeDisplay) return; + const camera = renderer.getCamera(); + const targetRatio = getReadableFocusRatio(renderer, graph, selectedNodeKey); + void camera.animate( + { x: nodeDisplay.x, y: nodeDisplay.y, ratio: targetRatio }, + { duration: 240 }, + ); + }; + + return ( +
+ Layouting… + ) : ( + + {graph.nodes.length} nodes + + ) + } + /> +
+ {loading ? ( +
Computing ELK layout…
+ ) : null} +
+
+ ); +} diff --git a/frontend/src/graph/rendering/sigma/buildGraph.ts b/frontend/src/graph/rendering/sigma/buildGraph.ts new file mode 100644 index 00000000..41dee28a --- /dev/null +++ b/frontend/src/graph/rendering/sigma/buildGraph.ts @@ -0,0 +1,53 @@ +import { MultiDirectedGraph } from 'graphology'; +import { getEdgeStyle, getNodeStyle } from '../../styles'; +import type { + GraphThemePalette, + LayoutGraphModel, + SigmaEdgeAttributes, + SigmaNodeAttributes, +} from '../../types'; + +function addNodes( + sigmaGraph: MultiDirectedGraph, + graph: LayoutGraphModel, + palette: GraphThemePalette, +) { + for (const node of graph.nodes) { + const style = getNodeStyle(node.kind, graph.kind, node.metadata, palette); + sigmaGraph.addNode(node.key, { + ...node, + x: node.x, + y: node.y, + size: node.sigmaSize, + color: style.fill, + hidden: false, + }); + } +} + +export function buildSigmaGraph( + graph: LayoutGraphModel, + palette: GraphThemePalette, + includeEdges = true, +): MultiDirectedGraph { + const sigmaGraph = new MultiDirectedGraph< + SigmaNodeAttributes, + SigmaEdgeAttributes + >(); + + addNodes(sigmaGraph, graph, palette); + + if (includeEdges) { + for (const edge of graph.edges) { + const style = getEdgeStyle(edge.kind, graph.kind, palette); + sigmaGraph.addDirectedEdgeWithKey(edge.key, edge.source, edge.target, { + ...edge, + color: style.color, + size: style.width, + hidden: false, + }); + } + } + + return sigmaGraph; +} diff --git a/frontend/src/graph/rendering/sigma/edgeOverlay.ts b/frontend/src/graph/rendering/sigma/edgeOverlay.ts new file mode 100644 index 00000000..d1639bf4 --- /dev/null +++ b/frontend/src/graph/rendering/sigma/edgeOverlay.ts @@ -0,0 +1,656 @@ +import type Sigma from 'sigma'; +import type { MultiDirectedGraph } from 'graphology'; +import { getEdgeStyle, getNodeStyle, withAlpha } from '../../styles'; +import type { + GraphThemePalette, + GraphViewKind, + SigmaEdgeAttributes, + SigmaNodeAttributes, +} from '../../types'; + +export interface GraphInteractionState { + activeNodeKey: string | null; + hoveredNodeKey: string | null; + selectedNodeKey: string | null; + highlightedNodeKeys: Set; + highlightedEdgeKeys: Set; + searchMatchKeys: Set; +} + +const MIN_NODE_TEXT_WIDTH = 58; +const MIN_NODE_TEXT_HEIGHT = 18; +const DETAIL_EDGE_LABEL_KINDS = new Set(['True', 'False', 'Back', 'Exception']); + +export function buildInteractionState( + graph: MultiDirectedGraph, + selectedNodeKey: string | null, + hoveredNodeKey: string | null, + searchMatchKeys: Set, +): GraphInteractionState { + const activeNodeKey = hoveredNodeKey ?? selectedNodeKey; + const highlightedNodeKeys = new Set(searchMatchKeys); + const highlightedEdgeKeys = new Set(); + + if (selectedNodeKey) highlightedNodeKeys.add(selectedNodeKey); + if (hoveredNodeKey) highlightedNodeKeys.add(hoveredNodeKey); + + if (activeNodeKey && graph.hasNode(activeNodeKey)) { + highlightedNodeKeys.add(activeNodeKey); + for (const neighbor of graph.neighbors(activeNodeKey)) { + highlightedNodeKeys.add(neighbor); + } + for (const edge of graph.edges(activeNodeKey)) { + highlightedEdgeKeys.add(edge); + } + } + + return { + activeNodeKey, + hoveredNodeKey, + selectedNodeKey, + highlightedNodeKeys, + highlightedEdgeKeys, + searchMatchKeys, + }; +} + +function setCanvasSize( + canvas: HTMLCanvasElement, + renderer: Sigma, +) { + const { width, height } = renderer.getDimensions(); + const pixelRatio = window.devicePixelRatio || 1; + const nextWidth = Math.max(1, Math.floor(width * pixelRatio)); + const nextHeight = Math.max(1, Math.floor(height * pixelRatio)); + + if (canvas.width !== nextWidth) canvas.width = nextWidth; + if (canvas.height !== nextHeight) canvas.height = nextHeight; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + + const context = canvas.getContext('2d'); + if (!context) return null; + context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + return context; +} + +function parseColor(color: string): [number, number, number] | null { + if (color.startsWith('#')) { + const normalized = color.slice(1); + const expanded = + normalized.length === 3 + ? normalized + .split('') + .map((segment) => segment + segment) + .join('') + : normalized; + const value = Number.parseInt(expanded, 16); + if (Number.isNaN(value)) return null; + return [(value >> 16) & 255, (value >> 8) & 255, value & 255]; + } + + const rgbaMatch = color.match(/rgba?\(([^)]+)\)/); + if (!rgbaMatch) return null; + const parts = rgbaMatch[1] + .split(',') + .slice(0, 3) + .map((part) => part.trim()); + if (parts.length !== 3) return null; + const rgb = parts.map((part) => Number.parseFloat(part)); + if (rgb.some((part) => Number.isNaN(part))) return null; + return [rgb[0], rgb[1], rgb[2]]; +} + +function isLightColor(color: string): boolean { + const rgb = parseColor(color); + if (!rgb) return false; + const [red, green, blue] = rgb.map((channel) => channel / 255); + const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + return luminance > 0.68; +} + +function drawRoundedRect( + context: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, +) { + drawLabelBackdrop(context, x, y, width, height, radius); +} + +function drawDoubleRect( + context: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, +) { + drawRoundedRect(context, x, y, width, height, radius); + drawRoundedRect(context, x + 4, y + 4, width - 8, height - 8, radius - 2); +} + +function drawTerminalRect( + context: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, +) { + drawRoundedRect(context, x, y, width, height, height / 2); +} + +function getViewportRect( + renderer: Sigma, + node: SigmaNodeAttributes, +) { + const center = renderer.graphToViewport({ x: node.x, y: node.y }); + const xExtent = renderer.graphToViewport({ + x: node.x + node.width / 2, + y: node.y, + }); + const yExtent = renderer.graphToViewport({ + x: node.x, + y: node.y + node.height / 2, + }); + + const width = Math.max(8, Math.abs(xExtent.x - center.x) * 2); + const height = Math.max(8, Math.abs(yExtent.y - center.y) * 2); + + return { + x: center.x - width / 2, + y: center.y - height / 2, + width, + height, + centerX: center.x, + centerY: center.y, + }; +} + +function drawNodeBadges( + context: CanvasRenderingContext2D, + node: SigmaNodeAttributes, + rect: { x: number; y: number; width: number; height: number }, + palette: GraphThemePalette, + fill: string, +) { + if (!node.badges?.length || rect.width < 90 || rect.height < 34) return; + + const badges = node.badges.slice(0, 3); + const badgeHeight = 12; + const gap = 4; + const totalWidth = badges.reduce((sum, badge) => { + const badgeWidth = Math.min(52, Math.max(22, badge.length * 5.2 + 10)); + return sum + badgeWidth; + }, 0); + const fullWidth = totalWidth + gap * (badges.length - 1); + let cursor = rect.x + (rect.width - fullWidth) / 2; + const y = rect.y + rect.height - badgeHeight - 4; + const textColor = isLightColor(fill) ? palette.text : '#ffffff'; + + context.save(); + context.font = '600 8px var(--font-mono, "SF Mono", monospace)'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + + for (const badge of badges) { + const badgeWidth = Math.min(52, Math.max(22, badge.length * 5.2 + 10)); + context.fillStyle = withAlpha(palette.background, 0.24); + context.strokeStyle = withAlpha(textColor, 0.18); + context.lineWidth = 0.8; + drawRoundedRect(context, cursor, y, badgeWidth, badgeHeight, 4); + context.fill(); + context.stroke(); + + context.fillStyle = textColor; + context.fillText(badge, cursor + badgeWidth / 2, y + badgeHeight / 2 + 0.5); + cursor += badgeWidth + gap; + } + + context.restore(); +} + +function drawNodeText( + context: CanvasRenderingContext2D, + node: SigmaNodeAttributes, + rect: { x: number; y: number; width: number; height: number }, + palette: GraphThemePalette, + fill: string, +) { + const textLines = node.labelLines + .map((text) => ({ text, secondary: false })) + .concat(node.detailLines.map((text) => ({ text, secondary: true }))) + .concat(node.sublabelLines.map((text) => ({ text, secondary: true }))); + + if (textLines.length === 0) return; + + const availableHeight = rect.height - (node.badges?.length ? 18 : 10); + const lineBudget = Math.max(1, Math.floor(availableHeight / 11)); + const visibleLines = textLines.slice(0, lineBudget); + if ( + rect.width < MIN_NODE_TEXT_WIDTH || + rect.height < MIN_NODE_TEXT_HEIGHT || + visibleLines.length === 0 + ) { + return; + } + + const primaryFont = Math.max( + 8, + Math.min(12.5, rect.height / (visibleLines.length + 1.6)), + ); + const secondaryFont = Math.max(7, primaryFont - 1.5); + const lineHeight = primaryFont + 2; + const blockHeight = visibleLines.reduce( + (sum, line) => sum + (line.secondary ? secondaryFont + 2 : lineHeight), + 0, + ); + const textColor = isLightColor(fill) ? palette.text : '#ffffff'; + const secondaryColor = isLightColor(fill) + ? palette.textSecondary + : withAlpha(textColor, 0.76); + let cursorY = rect.y + (availableHeight - blockHeight) / 2 + primaryFont; + + context.save(); + context.beginPath(); + drawRoundedRect(context, rect.x, rect.y, rect.width, rect.height, 8); + context.clip(); + context.textAlign = 'center'; + context.textBaseline = 'alphabetic'; + + for (const line of visibleLines) { + const fontSize = line.secondary ? secondaryFont : primaryFont; + context.font = `${line.secondary ? '500' : '600'} ${fontSize}px var(--font-mono, "SF Mono", monospace)`; + context.fillStyle = line.secondary ? secondaryColor : textColor; + context.fillText(line.text, rect.x + rect.width / 2, cursorY); + cursorY += line.secondary ? secondaryFont + 2 : lineHeight; + } + + context.restore(); +} + +function drawNodes( + context: CanvasRenderingContext2D, + renderer: Sigma, + graph: MultiDirectedGraph, + viewKind: GraphViewKind, + palette: GraphThemePalette, + interaction: GraphInteractionState, +) { + const nodes = graph + .mapNodes((key, attributes) => ({ + key, + attributes, + })) + .sort((left, right) => { + const leftPriority = + interaction.selectedNodeKey === left.key + ? 3 + : interaction.hoveredNodeKey === left.key + ? 2 + : interaction.highlightedNodeKeys.has(left.key) + ? 1 + : 0; + const rightPriority = + interaction.selectedNodeKey === right.key + ? 3 + : interaction.hoveredNodeKey === right.key + ? 2 + : interaction.highlightedNodeKeys.has(right.key) + ? 1 + : 0; + return leftPriority - rightPriority; + }); + + for (const { key, attributes } of nodes) { + const style = getNodeStyle( + attributes.kind, + viewKind, + attributes.metadata, + palette, + ); + const rect = getViewportRect(renderer, attributes); + const isSelected = interaction.selectedNodeKey === key; + const isHovered = interaction.hoveredNodeKey === key; + const isHighlighted = interaction.highlightedNodeKeys.has(key); + const isSearchMatch = interaction.searchMatchKeys.has(key); + const shouldDim = + Boolean(interaction.activeNodeKey) && + !isSelected && + !isHighlighted && + !isSearchMatch; + + let fill = style.fill; + let stroke = style.stroke; + const opacity = shouldDim ? 0.14 : 1; + + if (isSelected) { + fill = style.accentFill; + stroke = withAlpha(palette.accent, 0.96); + } else if (isHovered || isHighlighted || isSearchMatch) { + fill = style.neighborFill; + stroke = withAlpha(style.accentFill, 0.85); + } + + context.save(); + context.globalAlpha = opacity; + + if (isSelected) { + context.strokeStyle = withAlpha(palette.accent, 0.32); + context.lineWidth = 6; + drawRoundedRect( + context, + rect.x - 4, + rect.y - 4, + rect.width + 8, + rect.height + 8, + 12, + ); + context.stroke(); + } + + context.fillStyle = fill; + context.strokeStyle = stroke; + context.lineWidth = isSelected + ? style.strokeWidth + 0.8 + : style.strokeWidth; + + if (style.shape === 'double') { + drawDoubleRect(context, rect.x, rect.y, rect.width, rect.height, 8); + } else if (style.shape === 'terminal') { + drawTerminalRect(context, rect.x, rect.y, rect.width, rect.height); + } else { + drawRoundedRect(context, rect.x, rect.y, rect.width, rect.height, 8); + } + context.fill(); + context.stroke(); + + drawNodeText(context, attributes, rect, palette, fill); + drawNodeBadges(context, attributes, rect, palette, fill); + context.restore(); + } +} + +function drawArrow( + context: CanvasRenderingContext2D, + from: { x: number; y: number }, + to: { x: number; y: number }, + color: string, + size: number, +) { + const angle = Math.atan2(to.y - from.y, to.x - from.x); + const length = Math.max(5, size * 2.6); + + context.save(); + context.translate(to.x, to.y); + context.rotate(angle); + context.fillStyle = color; + context.beginPath(); + context.moveTo(0, 0); + context.lineTo(-length, length * 0.45); + context.lineTo(-length, -length * 0.45); + context.closePath(); + context.fill(); + context.restore(); +} + +function drawLabelBackdrop( + context: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, +) { + const clampedRadius = Math.min(radius, width / 2, height / 2); + context.beginPath(); + context.moveTo(x + clampedRadius, y); + context.lineTo(x + width - clampedRadius, y); + context.quadraticCurveTo(x + width, y, x + width, y + clampedRadius); + context.lineTo(x + width, y + height - clampedRadius); + context.quadraticCurveTo( + x + width, + y + height, + x + width - clampedRadius, + y + height, + ); + context.lineTo(x + clampedRadius, y + height); + context.quadraticCurveTo(x, y + height, x, y + height - clampedRadius); + context.lineTo(x, y + clampedRadius); + context.quadraticCurveTo(x, y, x + clampedRadius, y); + context.closePath(); +} + +function resolveOpacity( + interaction: GraphInteractionState, + edgeKey: string, + source: string, + target: string, +): number { + if (!interaction.activeNodeKey) return 0.8; + if (interaction.highlightedEdgeKeys.has(edgeKey)) return 0.96; + if ( + interaction.highlightedNodeKeys.has(source) && + interaction.highlightedNodeKeys.has(target) + ) { + return 0.7; + } + return 0.14; +} + +function resolveLineWidth( + baseWidth: number, + interaction: GraphInteractionState, + edgeKey: string, +): number { + if (interaction.highlightedEdgeKeys.has(edgeKey)) return baseWidth + 0.8; + return baseWidth; +} + +function shouldDrawLabel( + renderer: Sigma, + graph: MultiDirectedGraph, + edge: SigmaEdgeAttributes, + interaction: GraphInteractionState, + graphOrder: number, + source: string, + target: string, +): boolean { + if (!edge.label) return false; + if (interaction.highlightedEdgeKeys.has(edge.key)) return true; + + if (DETAIL_EDGE_LABEL_KINDS.has(edge.kind)) { + const sourceNode = graph.getNodeAttributes(source); + const targetNode = graph.getNodeAttributes(target); + const sourceRect = sourceNode + ? getViewportRect(renderer, sourceNode) + : undefined; + const targetRect = targetNode + ? getViewportRect(renderer, targetNode) + : undefined; + const nearReadableNode = [sourceRect, targetRect].some( + (rect) => + rect != null && + rect.width >= MIN_NODE_TEXT_WIDTH && + rect.height >= MIN_NODE_TEXT_HEIGHT, + ); + + return nearReadableNode; + } + + if (graphOrder <= 80) return true; + return renderer.getCamera().getState().ratio < 0.42; +} + +function measureSegmentLength( + start: { x: number; y: number }, + end: { x: number; y: number }, +): number { + return Math.hypot(end.x - start.x, end.y - start.y); +} + +function getLabelPlacement( + points: Array<{ x: number; y: number }>, + edgeKind: string, +) { + if (points.length < 2) return null; + + const totalLength = points.reduce((sum, point, index) => { + if (index === 0) return sum; + return sum + measureSegmentLength(points[index - 1]!, point); + }, 0); + if (totalLength <= 0) return points[0] ?? null; + + const alongPathRatio = + edgeKind === 'True' || edgeKind === 'False' ? 0.24 : 0.5; + const targetDistance = totalLength * alongPathRatio; + let traversed = 0; + + for (let index = 1; index < points.length; index += 1) { + const start = points[index - 1]!; + const end = points[index]!; + const segmentLength = measureSegmentLength(start, end); + if (segmentLength <= 0) continue; + + if ( + traversed + segmentLength >= targetDistance || + index === points.length - 1 + ) { + const distanceOnSegment = Math.max(0, targetDistance - traversed); + const t = Math.min(1, distanceOnSegment / segmentLength); + const directionX = (end.x - start.x) / segmentLength; + const directionY = (end.y - start.y) / segmentLength; + const normalX = -directionY; + const normalY = directionX; + const offset = edgeKind === 'False' ? -10 : edgeKind === 'True' ? 10 : 8; + + return { + x: start.x + (end.x - start.x) * t + normalX * offset, + y: start.y + (end.y - start.y) * t + normalY * offset, + }; + } + + traversed += segmentLength; + } + + return points[Math.floor(points.length / 2)] ?? null; +} + +interface EdgeLabelInstruction { + color: string; + strokeColor: string; + text: string; + x: number; + y: number; +} + +function drawEdgeLabels( + context: CanvasRenderingContext2D, + palette: GraphThemePalette, + labels: EdgeLabelInstruction[], +) { + for (const label of labels) { + const textWidth = Math.max(18, label.text.length * 6.4); + const rectX = label.x - textWidth / 2 - 5; + const rectY = label.y - 10; + + context.fillStyle = withAlpha(palette.background, 0.92); + context.strokeStyle = label.strokeColor; + context.lineWidth = 1; + drawLabelBackdrop(context, rectX, rectY, textWidth + 10, 18, 4); + context.fill(); + context.stroke(); + + context.fillStyle = label.color; + context.font = `600 10px var(--font-mono, "SF Mono", monospace)`; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillText(label.text, label.x, label.y - 0.5); + } +} + +export function drawGraphOverlay( + canvas: HTMLCanvasElement, + renderer: Sigma, + graph: MultiDirectedGraph, + viewKind: GraphViewKind, + palette: GraphThemePalette, + interaction: GraphInteractionState, +) { + const context = setCanvasSize(canvas, renderer); + if (!context) return; + + const { width, height } = renderer.getDimensions(); + context.clearRect(0, 0, width, height); + context.lineCap = 'round'; + context.lineJoin = 'round'; + const edgeLabels: EdgeLabelInstruction[] = []; + + graph.forEachEdge((edgeKey, edge, source, target) => { + const style = getEdgeStyle(edge.kind, viewKind, palette); + const points = + edge.route.length > 1 + ? edge.route.map((point) => renderer.graphToViewport(point)) + : [ + renderer.graphToViewport(graph.getNodeAttributes(source)), + renderer.graphToViewport(graph.getNodeAttributes(target)), + ]; + + if (points.length < 2) return; + + const opacity = resolveOpacity(interaction, edgeKey, source, target); + const lineWidth = resolveLineWidth(style.width, interaction, edgeKey); + const color = withAlpha(style.color, opacity); + + context.save(); + context.strokeStyle = color; + context.lineWidth = lineWidth; + context.setLineDash(style.dash); + context.beginPath(); + context.moveTo(points[0].x, points[0].y); + for (let index = 1; index < points.length; index += 1) { + context.lineTo(points[index].x, points[index].y); + } + context.stroke(); + + const from = points[points.length - 2]; + const to = points[points.length - 1]; + drawArrow(context, from, to, color, lineWidth + 0.5); + + if ( + shouldDrawLabel( + renderer, + graph, + edge, + interaction, + graph.order, + source, + target, + ) + ) { + const labelPoint = getLabelPlacement(points, edge.kind); + if (labelPoint) { + const labelColor = withAlpha( + interaction.highlightedEdgeKeys.has(edgeKey) + ? palette.text + : style.color, + interaction.highlightedEdgeKeys.has(edgeKey) ? 0.96 : 0.8, + ); + edgeLabels.push({ + color: labelColor, + strokeColor: withAlpha(labelColor, 0.25), + text: edge.label!, + x: labelPoint.x, + y: labelPoint.y, + }); + } + } + + context.restore(); + }); + + drawNodes(context, renderer, graph, viewKind, palette, interaction); + drawEdgeLabels(context, palette, edgeLabels); +} diff --git a/frontend/src/graph/styles.ts b/frontend/src/graph/styles.ts new file mode 100644 index 00000000..eeb28acb --- /dev/null +++ b/frontend/src/graph/styles.ts @@ -0,0 +1,357 @@ +import type { GraphMetadata, GraphThemePalette, GraphViewKind } from './types'; + +export interface NodeStyle { + fill: string; + stroke: string; + textFill: string; + secondaryFill: string; + shape: 'rect' | 'terminal' | 'double'; + strokeWidth: number; + accentFill: string; + neighborFill: string; +} + +export interface EdgeStyle { + color: string; + width: number; + dash: number[]; +} + +const FALLBACK_PALETTE: GraphThemePalette = { + 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 { + if (typeof window === 'undefined') return fallback; + const value = getComputedStyle(document.documentElement) + .getPropertyValue(name) + .trim(); + return value || fallback; +} + +function hexToRgb(value: string): [number, number, number] | null { + const normalized = value.replace('#', '').trim(); + if (normalized.length !== 3 && normalized.length !== 6) return null; + + const expanded = + normalized.length === 3 + ? normalized + .split('') + .map((part) => part + part) + .join('') + : normalized; + + const intValue = Number.parseInt(expanded, 16); + if (Number.isNaN(intValue)) return null; + + return [(intValue >> 16) & 255, (intValue >> 8) & 255, intValue & 255]; +} + +export function withAlpha(color: string, alpha: number): string { + if (color.startsWith('rgba(')) { + return color.replace(/rgba\(([^)]+),[^)]+\)/, `rgba($1, ${alpha})`); + } + if (color.startsWith('rgb(')) { + const inner = color.slice(4, -1); + return `rgba(${inner}, ${alpha})`; + } + + const rgb = hexToRgb(color); + if (!rgb) return color; + return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`; +} + +export function readGraphPalette(): GraphThemePalette { + return { + background: readVar('--bg', FALLBACK_PALETTE.background), + backgroundSecondary: readVar( + '--bg-secondary', + FALLBACK_PALETTE.backgroundSecondary, + ), + text: readVar('--text', FALLBACK_PALETTE.text), + textSecondary: readVar('--text-secondary', FALLBACK_PALETTE.textSecondary), + textTertiary: readVar('--text-tertiary', FALLBACK_PALETTE.textTertiary), + border: readVar('--border', FALLBACK_PALETTE.border), + borderLight: readVar('--border-light', FALLBACK_PALETTE.borderLight), + accent: readVar('--accent', FALLBACK_PALETTE.accent), + accentSoft: readVar('--accent-light', FALLBACK_PALETTE.accentSoft), + success: readVar('--success', FALLBACK_PALETTE.success), + warning: readVar('--sev-medium', FALLBACK_PALETTE.warning), + danger: readVar('--sev-high', FALLBACK_PALETTE.danger), + neutral: FALLBACK_PALETTE.neutral, + neutralSoft: FALLBACK_PALETTE.neutralSoft, + }; +} + +function cfgNodeStyle( + type: string, + palette: GraphThemePalette, + metadata?: GraphMetadata, +): NodeStyle { + if (metadata?.isCompound) { + return { + fill: withAlpha(palette.borderLight, 0.9), + stroke: palette.border, + textFill: palette.text, + secondaryFill: palette.textSecondary, + shape: 'rect', + strokeWidth: 1.25, + accentFill: palette.accent, + neighborFill: palette.accentSoft, + }; + } + + switch (type) { + case 'Entry': + return { + fill: palette.success, + stroke: withAlpha(palette.success, 0.85), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.78), + shape: 'double', + strokeWidth: 1.8, + accentFill: palette.accent, + neighborFill: withAlpha(palette.success, 0.75), + }; + case 'Exit': + return { + fill: palette.textSecondary, + stroke: withAlpha(palette.textSecondary, 0.85), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.78), + shape: 'double', + strokeWidth: 1.6, + accentFill: palette.accent, + neighborFill: withAlpha(palette.textSecondary, 0.76), + }; + case 'If': + return { + fill: palette.accent, + stroke: withAlpha(palette.accent, 0.82), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'rect', + strokeWidth: 2, + accentFill: palette.accent, + neighborFill: palette.accentSoft, + }; + case 'Loop': + return { + fill: '#6c6660', + stroke: '#3c3830', + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'rect', + strokeWidth: 2.1, + accentFill: palette.accent, + neighborFill: withAlpha('#6c6660', 0.74), + }; + case 'Call': + return { + fill: palette.warning, + stroke: withAlpha(palette.warning, 0.85), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'rect', + strokeWidth: 1.5, + accentFill: palette.accent, + neighborFill: withAlpha(palette.warning, 0.76), + }; + case 'Return': + return { + fill: palette.danger, + stroke: withAlpha(palette.danger, 0.86), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'terminal', + strokeWidth: 1.7, + accentFill: palette.accent, + neighborFill: withAlpha(palette.danger, 0.75), + }; + default: + return { + fill: withAlpha(palette.neutral, 0.92), + stroke: withAlpha(palette.neutral, 0.8), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.78), + shape: 'rect', + strokeWidth: 1.2, + accentFill: palette.accent, + neighborFill: withAlpha(palette.neutralSoft, 0.88), + }; + } +} + +function surfaceNodeStyle(type: string, palette: GraphThemePalette): NodeStyle { + switch (type) { + case 'EntryPoint': + return { + fill: palette.success, + stroke: withAlpha(palette.success, 0.85), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.78), + shape: 'double', + strokeWidth: 1.8, + accentFill: palette.accent, + neighborFill: withAlpha(palette.success, 0.75), + }; + case 'DataStore': + return { + fill: palette.warning, + stroke: withAlpha(palette.warning, 0.85), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'rect', + strokeWidth: 1.5, + accentFill: palette.accent, + neighborFill: withAlpha(palette.warning, 0.76), + }; + case 'ExternalService': + return { + fill: palette.accent, + stroke: withAlpha(palette.accent, 0.82), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'rect', + strokeWidth: 1.5, + accentFill: palette.accent, + neighborFill: palette.accentSoft, + }; + case 'DangerousLocal': + return { + fill: palette.danger, + stroke: withAlpha(palette.danger, 0.86), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'terminal', + strokeWidth: 1.7, + accentFill: palette.accent, + neighborFill: withAlpha(palette.danger, 0.75), + }; + default: + return { + fill: withAlpha(palette.neutral, 0.92), + stroke: withAlpha(palette.neutral, 0.8), + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.78), + shape: 'rect', + strokeWidth: 1.2, + accentFill: palette.accent, + neighborFill: withAlpha(palette.neutralSoft, 0.88), + }; + } +} + +function surfaceEdgeStyle(type: string, palette: GraphThemePalette): EdgeStyle { + switch (type) { + case 'calls': + return { + color: withAlpha(palette.textSecondary, 0.78), + width: 1.4, + dash: [], + }; + case 'reads_from': + return { color: palette.success, width: 1.5, dash: [] }; + case 'writes_to': + return { color: palette.warning, width: 1.6, dash: [] }; + case 'talks_to': + return { color: palette.accent, width: 1.4, dash: [] }; + case 'reaches': + return { color: palette.danger, width: 1.7, dash: [] }; + case 'triggers': + return { color: palette.success, width: 1.5, dash: [4, 3] }; + case 'auth_required_on': + return { color: palette.textTertiary, width: 1.3, dash: [2, 4] }; + default: + return { + color: withAlpha(palette.textTertiary, 0.78), + width: 1.3, + dash: [], + }; + } +} + +function callGraphNodeStyle( + palette: GraphThemePalette, + metadata?: GraphMetadata, +): NodeStyle { + const isRecursive = metadata?.isRecursive === true; + const fill = isRecursive ? '#5a5042' : palette.neutral; + const stroke = isRecursive ? '#3c3830' : withAlpha(palette.neutral, 0.84); + + return { + fill, + stroke, + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.74), + shape: 'rect', + strokeWidth: isRecursive ? 1.8 : 1.3, + accentFill: palette.accent, + neighborFill: isRecursive ? withAlpha(fill, 0.76) : palette.accentSoft, + }; +} + +export function getNodeStyle( + type: string, + graphKind: GraphViewKind = 'cfg', + metadata?: GraphMetadata, + palette = FALLBACK_PALETTE, +): NodeStyle { + switch (graphKind) { + case 'callgraph': + return callGraphNodeStyle(palette, metadata); + case 'surface': + return surfaceNodeStyle(type, palette); + case 'cfg': + default: + return cfgNodeStyle(type, palette, metadata); + } +} + +export function getEdgeStyle( + type: string, + graphKind: GraphViewKind = 'cfg', + palette = FALLBACK_PALETTE, +): EdgeStyle { + if (graphKind === 'callgraph') { + return { + color: withAlpha(palette.neutralSoft, 0.72), + width: 1.2, + dash: [], + }; + } + + if (graphKind === 'surface') { + return surfaceEdgeStyle(type, palette); + } + + switch (type) { + case 'True': + return { color: palette.success, width: 1.8, dash: [] }; + case 'False': + return { color: palette.danger, width: 1.8, dash: [] }; + case 'Back': + return { color: palette.textTertiary, width: 1.6, dash: [7, 4] }; + case 'Exception': + return { color: palette.warning, width: 1.6, dash: [3, 3] }; + default: + return { + color: withAlpha(palette.textTertiary, 0.78), + width: 1.3, + dash: [], + }; + } +} diff --git a/frontend/src/graph/types.ts b/frontend/src/graph/types.ts new file mode 100644 index 00000000..ecf1e049 --- /dev/null +++ b/frontend/src/graph/types.ts @@ -0,0 +1,111 @@ +export type GraphViewKind = 'callgraph' | 'cfg' | 'surface'; + +export interface GraphPoint { + x: number; + y: number; +} + +export interface GraphMetadata { + [key: string]: unknown; +} + +export interface GraphNodeModel { + key: string; + rawId: number; + label: string; + kind: string; + detail?: string; + sublabel?: string; + badges?: string[]; + line?: number; + metadata?: GraphMetadata; +} + +export type GraphNode = GraphNodeModel; + +export interface GraphEdgeModel { + key: string; + source: string; + target: string; + kind: string; + label?: string; + metadata?: GraphMetadata; +} + +export type GraphEdge = GraphEdgeModel; + +export interface GraphModel { + kind: GraphViewKind; + nodes: GraphNodeModel[]; + edges: GraphEdgeModel[]; +} + +export interface GraphCompactionResult { + graph: GraphModel; + compounds: Map; +} + +export interface LayoutBounds { + width: number; + height: number; +} + +export interface LayoutGraphNode extends GraphNodeModel { + x: number; + y: number; + width: number; + height: number; + sigmaSize: number; + labelLines: string[]; + detailLines: string[]; + sublabelLines: string[]; +} + +export interface LayoutGraphEdge extends GraphEdgeModel { + route: GraphPoint[]; +} + +export interface LayoutGraphModel { + kind: GraphViewKind; + nodes: LayoutGraphNode[]; + edges: LayoutGraphEdge[]; + bounds: LayoutBounds; +} + +export interface ElkLayoutPreset { + direction: 'DOWN' | 'RIGHT'; + nodeSpacing: number; + layerSpacing: number; + edgeNodeSpacing: number; + padding: number; + edgeRouting: 'POLYLINE' | 'ORTHOGONAL'; +} + +export interface GraphThemePalette { + background: string; + backgroundSecondary: string; + text: string; + textSecondary: string; + textTertiary: string; + border: string; + borderLight: string; + accent: string; + accentSoft: string; + success: string; + warning: string; + danger: string; + neutral: string; + neutralSoft: string; +} + +export interface SigmaNodeAttributes extends LayoutGraphNode { + size: number; + color: string; + hidden: boolean; +} + +export interface SigmaEdgeAttributes extends LayoutGraphEdge { + color: string; + size: number; + hidden: boolean; +} diff --git a/frontend/src/hooks/useChordNavigation.ts b/frontend/src/hooks/useChordNavigation.ts new file mode 100644 index 00000000..b15b9270 --- /dev/null +++ b/frontend/src/hooks/useChordNavigation.ts @@ -0,0 +1,62 @@ +import { useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const CHORD_TIMEOUT_MS = 800; + +const ROUTES: Record = { + o: '/', + f: '/findings', + s: '/scans', + r: '/rules', + t: '/triage', + c: '/config', + e: '/explorer', + d: '/debug', +}; + +function isTypingTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false; + const tag = target.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; + if (target.isContentEditable) return true; + return false; +} + +/** + * Vim-style "g then X" navigation: press `g`, then within 800ms press a + * letter to jump to that section. Cancels if the user types in an input. + */ +export function useChordNavigation() { + const navigate = useNavigate(); + const armed = useRef(null); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (isTypingTarget(event.target)) return; + if (event.metaKey || event.ctrlKey || event.altKey) return; + + if (armed.current !== null) { + const route = ROUTES[event.key.toLowerCase()]; + window.clearTimeout(armed.current); + armed.current = null; + if (route) { + event.preventDefault(); + navigate(route); + } + return; + } + + if (event.key === 'g') { + event.preventDefault(); + armed.current = window.setTimeout(() => { + armed.current = null; + }, CHORD_TIMEOUT_MS); + } + }; + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + if (armed.current !== null) window.clearTimeout(armed.current); + }; + }, [navigate]); +} diff --git a/frontend/src/hooks/useDebounce.ts b/frontend/src/hooks/useDebounce.ts new file mode 100644 index 00000000..6c6f0f04 --- /dev/null +++ b/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,16 @@ +import { useState, useEffect } from 'react'; + +/** + * Returns a debounced version of the given value. + * The returned value only updates after `delay` ms of inactivity. + */ +export function useDebounce(value: T, delay: number): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +} diff --git a/frontend/src/hooks/useFileTree.ts b/frontend/src/hooks/useFileTree.ts new file mode 100644 index 00000000..54535899 --- /dev/null +++ b/frontend/src/hooks/useFileTree.ts @@ -0,0 +1,129 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useExplorerTree } from '../api/queries/explorer'; +import type { TreeEntry } from '../api/types'; + +export interface UseFileTreeReturn { + rootEntries: TreeEntry[] | undefined; + isLoading: boolean; + expandedPaths: Set; + loadedChildren: Map; + selectedPath: string | null; + handleToggleExpand: (path: string) => void; + handleSelectFile: (path: string) => void; + setSelectedPath: (path: string | null) => void; +} + +export function useFileTree( + initialPath?: string | null, + onSelectFile?: (path: string) => void, +): UseFileTreeReturn { + const [selectedPath, setSelectedPath] = useState( + initialPath ?? null, + ); + const [expandedPaths, setExpandedPaths] = useState>(new Set()); + const [loadedChildren, setLoadedChildren] = useState< + Map + >(new Map()); + const [expandQueue, setExpandQueue] = useState(null); + + const { data: rootEntries, isLoading } = useExplorerTree(); + const { data: childEntries } = useExplorerTree(expandQueue || undefined); + + // Sync external path changes (e.g. back/forward navigation). + useEffect(() => { + const normalized = initialPath ?? null; + setSelectedPath((prev) => (prev !== normalized ? normalized : prev)); + }, [initialPath]); + + // Auto-expand ancestor directories for deep-linked files so the selected + // file is visible in the tree once its parent directories load. + useEffect(() => { + if (!initialPath) { + return; + } + + const ancestors = getAncestorPaths(initialPath); + if (ancestors.length === 0) { + return; + } + + setExpandedPaths((prev) => { + const next = new Set(prev); + let changed = false; + for (const ancestor of ancestors) { + if (!next.has(ancestor)) { + next.add(ancestor); + changed = true; + } + } + return changed ? next : prev; + }); + + const nextToLoad = ancestors.find( + (ancestor) => !loadedChildren.has(ancestor), + ); + if (nextToLoad && expandQueue !== nextToLoad) { + setExpandQueue(nextToLoad); + } + }, [expandQueue, initialPath, loadedChildren]); + + // Store child entries when they arrive for an expanded directory. + useEffect(() => { + if (expandQueue && childEntries) { + setLoadedChildren((prev) => { + const next = new Map(prev); + next.set(expandQueue, childEntries); + return next; + }); + setExpandQueue(null); + } + }, [expandQueue, childEntries]); + + const handleToggleExpand = useCallback( + (path: string) => { + setExpandedPaths((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + if (!loadedChildren.has(path)) { + setExpandQueue(path); + } + } + return next; + }); + }, + [loadedChildren], + ); + + const handleSelectFile = useCallback( + (path: string) => { + setSelectedPath(path); + onSelectFile?.(path); + }, + [onSelectFile], + ); + + return { + rootEntries, + isLoading, + expandedPaths, + loadedChildren, + selectedPath, + handleToggleExpand, + handleSelectFile, + setSelectedPath, + }; +} + +function getAncestorPaths(path: string): string[] { + const parts = path.split('/').filter(Boolean); + const ancestors: string[] = []; + + for (let i = 1; i < parts.length; i += 1) { + ancestors.push(parts.slice(0, i).join('/')); + } + + return ancestors; +} diff --git a/frontend/src/hooks/useFindingsURLState.ts b/frontend/src/hooks/useFindingsURLState.ts new file mode 100644 index 00000000..23e3b4e4 --- /dev/null +++ b/frontend/src/hooks/useFindingsURLState.ts @@ -0,0 +1,162 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { usePersistedState } from './usePersistedState'; + +export interface FindingsURLState { + page: string; + per_page: string; + sort_by: string; + sort_dir: string; + severity: string; + category: string; + confidence: string; + language: string; + rule_id: string; + status: string; + verification: string; + search: string; +} + +const FINDINGS_DEFAULTS: FindingsURLState = { + page: '1', + per_page: '50', + sort_by: '', + sort_dir: 'asc', + severity: '', + category: '', + confidence: '', + language: '', + rule_id: '', + status: '', + verification: '', + search: '', +}; + +/** Subset of state we remember across sessions. Filters intentionally are + * 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; + sort_by: string; + sort_dir: string; +} + +const DEFAULT_PREFS: PersistedFindingsPrefs = { + per_page: '50', + sort_by: '', + sort_dir: 'asc', +}; + +const FILTER_KEYS: ReadonlySet = new Set([ + 'severity', + 'category', + 'confidence', + 'language', + 'rule_id', + 'status', + 'verification', + 'search', +]); + +/** Keys that do NOT trigger a page reset when changed. */ +const NON_RESET_KEYS: ReadonlySet = new Set([ + 'page', + 'sort_by', + 'sort_dir', + 'per_page', +]); + +export function useFindingsURLState() { + const [searchParams, setSearchParams] = useSearchParams(); + const [prefs, setPrefs] = usePersistedState( + 'findings:prefs', + DEFAULT_PREFS, + ); + + const state: FindingsURLState = useMemo(() => { + const s = {} as FindingsURLState; + for (const key of Object.keys( + FINDINGS_DEFAULTS, + ) as (keyof FindingsURLState)[]) { + // URL wins; fall back to remembered prefs for keys we persist; + // last resort is the global default. + const fromUrl = searchParams.get(key); + if (fromUrl) { + s[key] = fromUrl; + } else if ( + key === 'per_page' || + key === 'sort_by' || + key === 'sort_dir' + ) { + s[key] = prefs[key] || FINDINGS_DEFAULTS[key]; + } else { + s[key] = FINDINGS_DEFAULTS[key]; + } + } + return s; + }, [searchParams, prefs]); + + // Persist user-driven changes to per_page / sort_*. + useEffect(() => { + setPrefs({ + per_page: state.per_page, + sort_by: state.sort_by, + sort_dir: state.sort_dir, + }); + }, [state.per_page, state.sort_by, state.sort_dir, setPrefs]); + + const updateState = useCallback( + (updates: Partial) => { + setSearchParams((prev) => { + const current = {} as FindingsURLState; + for (const key of Object.keys( + FINDINGS_DEFAULTS, + ) as (keyof FindingsURLState)[]) { + current[key] = prev.get(key) || FINDINGS_DEFAULTS[key]; + } + + const merged = { ...current, ...updates }; + + // Reset page to 1 when any filter/non-pagination field changes + const hasFilterChange = Object.keys(updates).some( + (k) => !NON_RESET_KEYS.has(k), + ); + if (hasFilterChange) { + merged.page = '1'; + } + + // Build new search params, omitting defaults + const next = new URLSearchParams(); + for (const [k, v] of Object.entries(merged)) { + if (v && v !== FINDINGS_DEFAULTS[k as keyof FindingsURLState]) { + next.set(k, v); + } + } + return next; + }); + }, + [setSearchParams], + ); + + const resetFilters = useCallback(() => { + setSearchParams((prev) => { + const next = new URLSearchParams(); + // Preserve per_page but reset everything else + const perPage = prev.get('per_page'); + if (perPage && perPage !== FINDINGS_DEFAULTS.per_page) { + next.set('per_page', perPage); + } + return next; + }); + }, [setSearchParams]); + + const hasActiveFilters = useMemo( + () => + Array.from(FILTER_KEYS).some( + (k) => state[k as keyof FindingsURLState] !== '', + ), + [state], + ); + + return { state, updateState, resetFilters, hasActiveFilters }; +} diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 00000000..353c71d7 --- /dev/null +++ b/frontend/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,63 @@ +import { useEffect } from 'react'; + +export interface Shortcut { + /** Key string per `KeyboardEvent.key` (e.g. "k", "/", "Escape"). */ + key: string; + /** Require Cmd/Ctrl (matches the same on each OS). */ + meta?: boolean; + /** Require Shift. */ + shift?: boolean; + /** Require Alt. */ + alt?: boolean; + description: string; + handler: (event: KeyboardEvent) => void; + /** + * If true, the shortcut still fires when focus is in an input/textarea/ + * contenteditable. Default is false, so shortcuts should not hijack typing. + */ + allowInInput?: boolean; +} + +function isTypingTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false; + const tag = target.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; + if (target.isContentEditable) return true; + return false; +} + +function matches(event: KeyboardEvent, shortcut: Shortcut): boolean { + if (event.key !== shortcut.key) return false; + const wantMeta = !!shortcut.meta; + const hasMeta = event.metaKey || event.ctrlKey; + if (wantMeta !== hasMeta) return false; + if (!!shortcut.shift !== event.shiftKey) return false; + if (!!shortcut.alt !== event.altKey) return false; + return true; +} + +/** + * Register a list of keyboard shortcuts at the document level. + * + * Pass a stable array (memoize or hoist outside the component) to avoid + * unnecessary re-binding. Shortcuts with `meta: true` match either Cmd or + * Ctrl so the same binding works on macOS and Linux/Windows. + */ +export function useKeyboardShortcuts(shortcuts: Shortcut[]) { + useEffect(() => { + if (shortcuts.length === 0) return; + const onKeyDown = (event: KeyboardEvent) => { + const typing = isTypingTarget(event.target); + for (const sc of shortcuts) { + if (typing && !sc.allowInInput) continue; + if (matches(event, sc)) { + event.preventDefault(); + sc.handler(event); + return; + } + } + }; + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, [shortcuts]); +} diff --git a/frontend/src/hooks/usePageTitle.ts b/frontend/src/hooks/usePageTitle.ts new file mode 100644 index 00000000..4df4d8d4 --- /dev/null +++ b/frontend/src/hooks/usePageTitle.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; + +const APP_NAME = 'Nyx'; + +/** + * Sets `document.title` to ` · Nyx`. Restores the previous title on + * unmount so transient pages (e.g. modals that re-render the page) don't + * leave the title stuck. + */ +export function usePageTitle(title: string | null | undefined) { + useEffect(() => { + if (!title) return; + const prev = document.title; + document.title = `${title} · ${APP_NAME}`; + return () => { + document.title = prev; + }; + }, [title]); +} diff --git a/frontend/src/hooks/usePersistedState.ts b/frontend/src/hooks/usePersistedState.ts new file mode 100644 index 00000000..995b3a02 --- /dev/null +++ b/frontend/src/hooks/usePersistedState.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +const STORAGE_PREFIX = 'nyx:'; + +function storageKey(key: string) { + return `${STORAGE_PREFIX}${key}`; +} + +function read(key: string, fallback: T): T { + try { + const raw = window.localStorage.getItem(storageKey(key)); + if (raw === null) return fallback; + return JSON.parse(raw) as T; + } catch { + return fallback; + } +} + +function write(key: string, value: T): void { + try { + window.localStorage.setItem(storageKey(key), JSON.stringify(value)); + } catch { + // Quota exceeded or storage disabled, so silently degrade. + } +} + +/** + * `useState` that persists to `localStorage` under `nyx:`. + * + * Suitable for view preferences (theme, sidebar collapse, default page size). + * 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. + */ +export function usePersistedState( + key: string, + initial: T, +): [T, (next: T | ((prev: T) => T)) => void] { + const [state, setState] = useState(() => read(key, initial)); + + // Avoid writing back the initial value on first mount when nothing changed. + const hydrated = useRef(false); + useEffect(() => { + if (!hydrated.current) { + hydrated.current = true; + return; + } + write(key, state); + }, [key, state]); + + const set = useCallback((next: T | ((prev: T) => T)) => { + setState((prev) => + typeof next === 'function' ? (next as (p: T) => T)(prev) : next, + ); + }, []); + + return [state, set]; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 00000000..99befa59 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import './styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/frontend/src/modals/CodeViewerModal.tsx b/frontend/src/modals/CodeViewerModal.tsx new file mode 100644 index 00000000..cd99be45 --- /dev/null +++ b/frontend/src/modals/CodeViewerModal.tsx @@ -0,0 +1,42 @@ +import { Modal } from '../components/ui/Modal'; +import { CodeViewer } from '../components/data-display/CodeViewer'; +import type { FindingView } from '../api/types'; + +interface CodeViewerModalProps { + open: boolean; + onClose: () => void; + finding: FindingView | null; +} + +export function CodeViewerModal({ + open, + onClose, + finding, +}: CodeViewerModalProps) { + if (!open || !finding) return null; + + return ( + +
+
+ {finding.path} + +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/modals/NewScanModal.tsx b/frontend/src/modals/NewScanModal.tsx new file mode 100644 index 00000000..53138693 --- /dev/null +++ b/frontend/src/modals/NewScanModal.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Modal } from '../components/ui/Modal'; +import { useHealth } from '../api/queries/health'; +import { useToast } from '../contexts/ToastContext'; +import { ApiError } from '../api/client'; +import { + useStartScan, + type ScanMode, + type EngineProfile, + type VerifyBackend, + type HardenProfile, + type StartScanBody, +} from '../api/mutations/scans'; + +interface NewScanModalProps { + open: boolean; + onClose: () => void; +} + +const MODE_HINTS: Record = { + full: 'AST + CFG + taint (default)', + ast: 'AST patterns only. Fastest.', + cfg: 'CFG structural + taint', + taint: 'Taint flows only', +}; + +const PROFILE_HINTS: Record = { + 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. About 2 to 3x slower.', +}; + +const BACKEND_HINTS: Record = { + auto: 'Use Docker when it fits, otherwise fall back to process.', + docker: 'Require Docker-backed harness execution.', + process: 'Unsafe local process backend for quick test runs.', + firecracker: 'Use the Firecracker backend when available.', +}; + +const HARDEN_HINTS: Record = { + standard: 'Baseline process limits.', + strict: 'Stricter process confinement when supported.', +}; + +export function NewScanModal({ open, onClose }: NewScanModalProps) { + const { data: health } = useHealth(); + const startScan = useStartScan(); + const navigate = useNavigate(); + const toast = useToast(); + const defaultRoot = health?.scan_root || ''; + const [scanRoot, setScanRoot] = useState(''); + const [mode, setMode] = useState('full'); + const [engineProfile, setEngineProfile] = useState('balanced'); + const [noVerify, setNoVerify] = useState(false); + const [verifyBackend, setVerifyBackend] = useState('auto'); + const [hardenProfile, setHardenProfile] = useState('standard'); + const showProcessHardening = !noVerify && verifyBackend === 'process'; + + const handleStart = async () => { + const root = scanRoot.trim(); + const body: StartScanBody = {}; + if (root && root !== defaultRoot) body.scan_root = root; + if (mode !== 'full') body.mode = mode; + body.engine_profile = engineProfile; + if (noVerify) { + body.verify = false; + } else { + body.verify_backend = verifyBackend; + if (verifyBackend === 'process') { + body.harden_profile = hardenProfile; + } + } + const payload = Object.keys(body).length ? body : undefined; + try { + await startScan.mutateAsync(payload); + toast.success('Scan started', 'Started'); + onClose(); + navigate('/scans'); + } catch (e) { + const msg = + e instanceof ApiError && e.status === 409 + ? 'A scan is already running' + : e instanceof Error + ? e.message + : 'Failed to start scan'; + toast.error(msg, 'Could not start scan'); + } + }; + + if (!open) return null; + + return ( + +
+

Start new scan

+
+
+ + setScanRoot(e.target.value)} + placeholder="/path/to/project" + /> +
+
+ + + {MODE_HINTS[mode]} +
+
+ + + {PROFILE_HINTS[engineProfile]} +
+
+ +
+ setNoVerify(e.target.checked)} + /> + +
+ + Verification runs by default on Medium and High confidence + findings. Check to skip and get a fast static-only result. + +
+
+ + + {BACKEND_HINTS[verifyBackend]} +
+ {showProcessHardening && ( +
+ + + {HARDEN_HINTS[hardenProfile]} +
+ )} +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx new file mode 100644 index 00000000..3a0e3d95 --- /dev/null +++ b/frontend/src/pages/ConfigPage.tsx @@ -0,0 +1,863 @@ +import { useState, useCallback, useEffect, useMemo } from 'react'; +import { + useConfig, + useRawConfig, + useSources, + useSinks, + useSanitizers, + useTerminators, + useProfiles, +} from '../api/queries/config'; +import { + useAddSource, + useDeleteSource, + useAddSink, + useDeleteSink, + useAddSanitizer, + useDeleteSanitizer, + useAddTerminator, + useDeleteTerminator, + useAddProfile, + useDeleteProfile, + useActivateProfile, + useToggleTriageSync, + useSaveRawConfig, +} from '../api/mutations/config'; +import { LoadingState } from '../components/ui/LoadingState'; +import { ErrorState } from '../components/ui/ErrorState'; +import { usePageTitle } from '../hooks/usePageTitle'; +import { useToast } from '../contexts/ToastContext'; +import { useTheme, type ThemePreference } from '../contexts/ThemeContext'; +import type { LabelEntryView, TerminatorView, ProfileView } from '../api/types'; + +const THEME_OPTIONS: Array<{ value: ThemePreference; label: string }> = [ + { value: 'light', label: 'Light' }, + { value: 'dark', label: 'Dark' }, + { value: 'system', label: 'System' }, + { value: 'hc-light', label: 'High-contrast light' }, + { value: 'hc-dark', label: 'High-contrast dark' }, +]; + +const LANG_OPTIONS = [ + 'javascript', + 'typescript', + 'python', + 'go', + 'java', + 'c', + 'cpp', + 'php', + 'ruby', + 'rust', +]; + +const CAP_OPTIONS = [ + 'all', + 'env_var', + 'html_escape', + 'shell_escape', + 'url_encode', + 'json_parse', + 'file_io', + 'sql_query', + 'deserialize', + 'ssrf', + 'code_exec', + 'crypto', +]; + +type Tab = 'overview' | 'rules' | 'profiles' | 'raw'; + +// ── Collapsible Config Section ─────────────────────────────────────────────── + +function ConfigSection({ + title, + id, + defaultCollapsed = false, + children, +}: { + title: string; + id?: string; + defaultCollapsed?: boolean; + children: React.ReactNode; +}) { + const [collapsed, setCollapsed] = useState(defaultCollapsed); + + return ( +
+
setCollapsed(!collapsed)} + > + + ▼ + {' '} + {title} +
+
+ {children} +
+
+ ); +} + +// ── Top-of-page settings panel (theme + triage sync) ──────────────────────── + +function SettingsSection({ + triageSyncOn, + onToggleTriageSync, +}: { + triageSyncOn: boolean; + onToggleTriageSync: (enabled: boolean) => void; +}) { + const { preference, setPreference } = useTheme(); + + return ( +
+
+ Settings +
+
+
+ + +
+
+ onToggleTriageSync(e.target.checked)} + /> + +
+
+
+ ); +} + +// ── Read-only key/value grid for effective config display ─────────────────── + +function KvGrid({ entries }: { entries: Array<[string, React.ReactNode]> }) { + return ( +
+ {entries.map(([k, v]) => ( +
+
{k}
+
{v}
+
+ ))} +
+ ); +} + +function fmt(v: unknown): React.ReactNode { + if (v === null || v === undefined) return -; + if (typeof v === 'boolean') + return ( + + {v ? 'on' : 'off'} + + ); + if (Array.isArray(v)) { + if (v.length === 0) return []; + return ( + + {v.map(String).map((s, i) => ( + + {s} + + ))} + + ); + } + if (typeof v === 'object') + return {JSON.stringify(v)}; + return {String(v)}; +} + +// ── Custom rules table (no built-ins; built-ins live on /rules) ───────────── + +function CustomLabelSection({ + title, + id, + kind, + entries, + onAdd, + onDelete, +}: { + title: string; + id: string; + kind: 'source' | 'sink' | 'sanitizer'; + entries: LabelEntryView[]; + onAdd: (body: { lang: string; matchers: string[]; cap: string }) => void; + onDelete: (entry: LabelEntryView) => void; +}) { + const [lang, setLang] = useState(''); + const [matcher, setMatcher] = useState(''); + const [cap, setCap] = useState('all'); + + const handleAdd = useCallback(() => { + if (!lang || !matcher) return; + onAdd({ lang, matchers: [matcher], cap }); + setMatcher(''); + }, [lang, matcher, cap, onAdd]); + + return ( + +

+ Custom {kind} rules from your nyx.local. Built-in rules are + listed on the Rules page. +

+
+
+ + +
+
+ + setMatcher(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleAdd(); + }} + /> +
+
+ + +
+ +
+
+ {entries.length === 0 ? ( +
+

No custom {kind} rules yet

+
+ ) : ( + + + + + + + + + + + {entries.map((e, i) => ( + + + + + + + ))} + +
LanguageMatchersCap
{e.lang}{e.matchers.join(', ')}{e.cap} + +
+ )} +
+
+ ); +} + +// ── Raw TOML editor ───────────────────────────────────────────────────────── + +function RawEditor() { + const { data, isLoading, error, refetch } = useRawConfig(); + const save = useSaveRawConfig(); + const [draft, setDraft] = useState(null); + const [saveError, setSaveError] = useState(null); + const [savedAt, setSavedAt] = useState(null); + + // Seed the editor whenever we load fresh data and have no in-flight edit. + useEffect(() => { + if (data && draft === null) { + setDraft(data.content); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + if (isLoading) return ; + if (error) return ; + if (!data) return null; + + const dirty = draft !== null && draft !== data.content; + + const handleSave = async () => { + if (draft === null) return; + setSaveError(null); + try { + await save.mutateAsync(draft); + setSavedAt(Date.now()); + // refresh disk view so {data.content} reflects what's on disk + await refetch(); + } catch (e) { + setSaveError(e instanceof Error ? e.message : String(e)); + } + }; + + const handleDiscard = () => { + setDraft(data.content); + setSaveError(null); + }; + + return ( +
+
+
+ nyx.local +
+ {data.exists ? data.path : `${data.path} (will be created on save)`} +
+
+
+ {dirty && Unsaved changes} + {savedAt && !dirty && Saved} + + +
+
+ {saveError && ( +
+ Save failed: {saveError} +
+ )} +