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/workflows/ci.yml b/.github/workflows/ci.yml index a2d2bda1..607f5d87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ name: CI + permissions: contents: read @@ -8,33 +9,232 @@ on: pull_request: branches: ["master"] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: - test: + frontend: + name: frontend runs-on: ubuntu-latest - strategy: - matrix: - rust: [stable, beta] steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 with: - toolchain: ${{ matrix.rust }} - components: clippy, rustfmt - - uses: Swatinem/rust-cache@v2 + 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 + + 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 - - name: Build & Test - run: cargo test --all-features --verbose + cargo-deny: + name: cargo-deny + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 - - name: Security audit - uses: actions-rs/audit-check@v1 + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} + toolchain: stable + cache: true + + - uses: taiki-e/install-action@cargo-deny - name: License & advisory checks - uses: EmbarkStudios/cargo-deny-action@v2 + run: cargo deny check advisories licenses bans sources + + 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 + + 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 + + rust-stable-test: + name: rust-stable-test + 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) + run: cargo nextest run --all-features + + 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 --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 --all-features + + benchmark-gate: + name: benchmark-gate + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + cache-key: benchmark-gate-release + + - name: Accuracy regression gate (P/R/F1) + run: cargo test --release --all-features --test benchmark_test -- --ignored --nocapture benchmark_evaluation + + - name: Performance regression gate + env: + NYX_CI_BENCH: "1" + run: cargo test --release --all-features --test perf_tests -- --nocapture + + - 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 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..4810eac3 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,45 @@ +name: "CodeQL Advanced" + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + schedule: + - cron: "28 20 * * 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/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..60cbcbd9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,50 @@ +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 + + - name: Cache mdbook + id: cache-mdbook + uses: actions/cache@v4 + 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 + + # mdbook follows the committed docs/assets symlink (→ ../assets) so + # image references in docs resolve both in `mdbook serve` and in CI. + - name: Build + run: mdbook build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: book + + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 3d76a73b..f6ecc322 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -11,12 +11,14 @@ env: BIN_NAME: nyx jobs: - build-and-upload: + build: 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 @@ -27,7 +29,7 @@ jobs: steps: - name: Check out sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 @@ -36,17 +38,25 @@ jobs: 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: Install cargo-about - run: cargo install cargo-about --locked - - - name: Generate license bundle - run: cargo about generate about.hbs -o THIRDPARTY-LICENSES.html + # THIRDPARTY-LICENSES.html is committed at the repo root and kept in + # sync with the dependency graph by the `third-party-licenses` CI + # job. Release builds ship the committed copy directly — no + # regeneration (and no per-runner cargo-about install) on the + # release hot path. - name: Package (Linux & macOS) if: runner.os != 'Windows' @@ -81,9 +91,157 @@ jobs: Add-Content -Path $env:GITHUB_ENV -Value "ASSET=$Archive" - - name: Upload to the release - uses: softprops/action-gh-release@v2 + - 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: + # Supply-chain smoke test: build the release binary twice with pinned + # SOURCE_DATE_EPOCH and path remapping, then diff the SHA256 hashes. + # Gates `publish` so non-reproducible builds cannot ship. Scoped to + # x86_64-linux — the most tractable target for byte-for-byte + # determinism; failures on other targets would be investigated + # separately. + name: reproducibility-check + runs-on: ubuntu-latest + steps: + - name: Check out sources + uses: actions/checkout@v6 + + - 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: + # Collect all matrix build outputs, generate a single SHA256SUMS file, + # then push everything to the GitHub release in one shot. Doing this + # centrally (rather than per-matrix job) is the only way to produce a + # checksum file that covers every published artifact. + name: publish-release + runs-on: ubuntu-latest + needs: [build, reproducibility] + permissions: + contents: write + id-token: write + attestations: write + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + steps: + - name: Check out sources + uses: actions/checkout@v6 + + # Generate the SBOM from the source tree BEFORE downloading + # artifacts. Syft scans `path: .` recursively; if release-artifacts/ + # exists at scan time, it would walk into the zipped binaries and + # produce a polluted manifest. + - name: Generate CycloneDX SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: cyclonedx-json + output-file: nyx-${{ github.event.release.tag_name }}.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 + + - name: Import GPG signing key + if: env.GPG_PRIVATE_KEY != '' + run: | + set -euo pipefail + printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import + gpg --list-secret-keys --keyid-format=long + + - name: Sign SHA256SUMS + if: env.GPG_PRIVATE_KEY != '' + run: | + set -euo pipefail + cd release-artifacts + if [ -n "${GPG_PASSPHRASE:-}" ]; then + printf '%s' "$GPG_PASSPHRASE" \ + | gpg --batch --yes --pinentry-mode loopback \ + --passphrase-fd 0 --armor --detach-sign SHA256SUMS + else + gpg --batch --yes --armor --detach-sign SHA256SUMS + fi + ls -l SHA256SUMS.asc + + - name: Warn if GPG signing was skipped + if: env.GPG_PRIVATE_KEY == '' + run: | + echo "::warning::GPG_PRIVATE_KEY secret not configured; SHA256SUMS will ship unsigned. Add GPG_PRIVATE_KEY (ASCII-armored) and optional GPG_PASSPHRASE to repository secrets to enable signed checksums." + + # SLSA v1 build provenance: signed attestation that these exact + # bytes were produced by this workflow run from this commit. + # Attestations are stored in the GitHub attestations API and can + # be verified 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-${{ github.event.release.tag_name }}.cdx.json + + - name: Upload to the release + uses: softprops/action-gh-release@v3 + with: + files: | + release-artifacts/*.zip + release-artifacts/SHA256SUMS + release-artifacts/SHA256SUMS.asc + nyx-${{ github.event.release.tag_name }}.cdx.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d81f12ed..c7f2d98e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ /target /.idea +/frontend/node_modules +/src/server/assets/dist +/.nyx +/book +.DS_Store diff --git a/.nyx/triage.json b/.nyx/triage.json new file mode 100644 index 00000000..e63bca2a --- /dev/null +++ b/.nyx/triage.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "decisions": [], + "suppression_rules": [] +} \ No newline at end of file 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 index d042bc9d..34ace941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,320 +1,217 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +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). ## [Unreleased] -## [0.4.0] - 2025-02-25 +_No changes yet._ -### Added -- **Low-noise prioritization system** — post-analysis pipeline that reduces noise from high-frequency LOW/Quality findings without hiding security signal. Three-stage process: category filtering, rollup grouping, and LOW budgets. - - **`FindingCategory` enum** (`Security`, `Reliability`, `Quality`) — every `Diag` now carries a `category` field. AST pattern findings derive their category from `PatternCategory` metadata (`CodeQuality` → `Quality`, all others → `Security`). Taint, CFG, and state findings are always `Security`. - - **Category filtering** — Quality-category findings (e.g. `rs.quality.unwrap`, `rs.quality.expect`) are excluded by default. Use `--include-quality` to include them. - - **Rollup grouping** — eligible HIGH-frequency rules (`rs.quality.unwrap`, `rs.quality.expect`, `rs.quality.panic_macro`) are grouped by `(file, rule)` into a single rollup finding with occurrence count and example locations. Canonical location is the first sorted occurrence. Example count controlled by `--rollup-examples` (default 5). - - **LOW budgets** — three configurable limits enforce noise caps: `--max-low` (default 20, total), `--max-low-per-file` (default 1), `--max-low-per-rule` (default 10). Rollups count as one finding for all budgets. High/Medium findings are never dropped. - - **`--all` CLI flag** — disables all prioritization (no category filtering, no rollups, no budgets). - - **`--show-instances `** — bypasses rollup for a specific rule, expanding all individual occurrences. - - **Console suppression footer** — when findings are suppressed, a footer displays the count and active filter values with adjustment hints. - - **`rollup` field on `Diag`** — optional `RollupData` with `count` and `occurrences` (example `Location`s). Serializes to JSON automatically; omitted when not a rollup. - - **SARIF rollup support** — `category` in result properties, rollup count in `properties.rollup.count`, example locations in `relatedLocations`. - - **`max_results` severity stability** — when `max_results` truncation is needed, High findings are kept first, then Medium, then Low. Low findings never displace higher-severity ones. - - New config fields in `[output]`: `include_quality`, `show_all`, `max_low`, `max_low_per_file`, `max_low_per_rule`, `rollup_examples`. - - 14 new unit tests covering category filtering, rollup grouping/examples/canonical, LOW budgets (per-file/per-rule/total), High/Medium immunity, rollup-counts-as-one, show_instances bypass, JSON serialization, and determinism. -- **Pattern-level confidence for AST rules** — each AST pattern in `src/patterns/` now carries an explicit `confidence: Confidence` field (High, Medium, or Low). Confidence is set at the pattern definition site and flows directly into emitted `Diag`s, replacing the old heuristic that inferred AST confidence from severity alone. `compute_confidence()` is retained as a fallback for detectors that don't set confidence (taint, state, legacy). - - Tier A patterns with High/Medium severity → `Confidence::High` (deterministic structural match). - - Tier A patterns with Low severity → `Confidence::Medium` (quality/crypto signals). - - Tier B patterns (heuristic-guarded) → `Confidence::Medium`. - - Example: `rs.quality.expect` now produces `Confidence: High` regardless of its Low severity. -- **Inline per-finding suppressions** — suppress specific findings directly in source code using `nyx:ignore` comments. Two directive forms: `nyx:ignore ` (same line) and `nyx:ignore-next-line ` (next line). Supports comma-separated IDs, wildcard suffixes (`rs.quality.*`), and automatic canonicalization of taint rule IDs (parenthetical suffixes stripped). Comment detection covers all 10 languages with string/raw-string/template-literal guards to avoid false positives. - - **`--show-suppressed` CLI flag** — reveal suppressed findings in output, dimmed with `[SUPPRESSED]` tag. Summary shows `"N issues (M suppressed)"`. In JSON/SARIF mode, suppressed findings include `"suppressed": true` and `"suppression": {...}` metadata fields. - - **`suppressed` and `suppression` fields on `Diag`** — conditionally serialized; JSON output is unchanged when no suppressions are active. - - Suppressed findings are excluded from `--fail-on` exit-code checks and severity counts. - - New module `src/suppress/mod.rs` with 22 unit tests covering all comment styles, string guards, wildcard matching, canonicalization, CRLF, and edge cases. -- **`--min-score ` CLI flag and `output.min_score` config option** — filter out findings whose attack-surface rank score falls below the given threshold. Applied after ranking and severity filtering, before `max_results` truncation. Has no effect when `--no-rank` is used. CLI value overrides config. -- **Attack surface ranking** — deterministic post-analysis scoring layer that prioritizes findings by exploitability. Each `Diag` receives an `f64` score computed from five components: severity base (High=60, Medium=30, Low=10), analysis kind bonus (taint +10 > state +8 > cfg +3/5 > ast 0), evidence strength (+1 per item, +2–6 for source-kind priority), state rule type bonus (+1–6), and a path-validation penalty (−5 for guarded paths). Findings are sorted by descending score before truncation so `max_results` keeps the most important results. Tie-breaking is deterministic by severity, rule ID, file path, line, column, and message hash. - - **`rank_score` and `rank_reason` fields on `Diag`** — optional fields with `#[serde(skip_serializing_if = "Option::is_none")]`; JSON output is unchanged when ranking is disabled. - - **`--no-rank` CLI flag** — disables attack-surface ranking (enabled by default). - - **`output.attack_surface_ranking` config key** — boolean (default `true`) to control ranking via config file. - - **Console score display** — dim `Score: N` appended to each finding's header line when ranking is enabled. - - **New module `src/rank.rs`** — `compute_attack_rank()`, `rank_diags()`, and `sort_key()` functions. Scoring uses only in-memory data; no extra file I/O or graph recomputation. - - 10 new unit tests: ordering correctness (high taint > medium file-io, must-leak > may-leak, taint > cfg-only, state rules, AST lowest at same severity), determinism (input-order-independent), path-validation penalty, and JSON serialization (rank fields omitted when None, present when set). -- **State-model dataflow analysis** — new `src/state/` module implementing a forward worklist dataflow engine over the existing CFG. Tracks per-variable resource lifecycle (`UNINIT`, `OPEN`, `CLOSED`, `MOVED`) via bitset lattice and per-path authentication level (`Unauthed`, `Authed`, `Admin`) as a composable product domain. Detects: - - **Use-after-close** (`state-use-after-close`, High) — variable read/written after its resource handle was closed. - - **Double-close** (`state-double-close`, Medium) — resource handle closed more than once. - - **Must-leak** (`state-resource-leak`, High) — resource acquired but never closed on any exit path. - - **May-leak** (`state-resource-leak-possible`, Medium) — resource open on some but not all exit paths (branch-aware via lattice join). - - **Unauthenticated access** (`state-unauthed-access`, High) — sensitive sink reached without a preceding auth/admin check. -- **State analysis architecture** — six-module design: - - `lattice.rs` — `Lattice` trait (`bot`, `join`, `leq`) for generic fixed-point computation. - - `domain.rs` — `ResourceLifecycle` (bitflag), `ResourceDomainState`, `AuthLevel`, `AuthDomainState`, `ProductState` with lattice impls. - - `symbol.rs` — `SymbolInterner` that builds a string-interning table from CFG node defines/uses; `SymbolId` newtype. - - `transfer.rs` — `DefaultTransfer` function: maps CFG node kinds (Call, Assignment, If, Return) to state transitions using the existing `ResourcePair` definitions from `cfg_analysis::rules`. Emits `TransferEvent` for illegal transitions. - - `engine.rs` — two-phase forward worklist solver: Phase 1 iterates to a fixed point (no events collected to avoid spurious reports from intermediate states); Phase 2 re-applies transfer once over converged states to collect events. Bounded by `MAX_TRACKED_VARS` (64) with guarded degradation. - - `facts.rs` — post-analysis pass: extracts `StateFinding`s from transfer events (use-after-close, double-close) and exit-node state inspection (must-leak, may-leak, unauthed access). -- **`scanner.enable_state_analysis` config option** — opt-in boolean (default `false`) in `ScannerConfig` and `default-nyx.conf`. Requires CFG mode (`full` or `taint`). -- **`Diag.message` field** — optional human-readable message on diagnostic output. State findings carry variable-specific context (e.g. "variable `f` used after close"). Surfaced in console output (dimmed line below the finding), JSON, and SARIF (`message.text` prefers per-finding message over generic rule description). -- **State finding dedup** — when state analysis produces findings on a line, overlapping `cfg-resource-leak` and `cfg-auth-gap` findings on the same line are suppressed (state analysis is more precise). -- **SARIF rule descriptions** for all five state rule IDs. -- 21 integration tests (`tests/state_tests.rs`) with 19 C fixture files covering: use-after-close, double-close, resource leak, clean usage, opt-in gating, may-leak vs must-leak branch semantics, early return, nested branches, both-branches-close, loop convergence, loop use-after-close, handle overwrite, reopen-after-close, multiple handles, conservative join masking, chain operations, malloc/free pairs, straight-line double-close, and message field population. -- 30+ unit tests across state modules: lattice properties, lifecycle join/leq, domain merging, auth-level join, product state composition, may/must leak semantics, symbol interning, and transfer event generation. -- **`--severity ` filter** — replaces `--high-only` with a flexible severity expression supporting single levels (`HIGH`), comma lists (`HIGH,MEDIUM`), and thresholds (`>=MEDIUM`). Parsing is case-insensitive with whitespace tolerance. `SeverityFilter` type with `parse()` and `matches()` in `patterns/mod.rs`. -- **`--mode `** — replaces `--ast-only` and `--cfg-only` with a single canonical analysis mode flag. Enforces mutual exclusivity via clap `ValueEnum`. -- **`--index `** — replaces `--no-index` and `--rebuild-index` with a single flag (default `auto`). -- **`--fail-on `** — CI ergonomics: exit code 1 if any emitted finding meets or exceeds the threshold severity. Example: `--fail-on HIGH`. -- **`--quiet`** — CLI flag to suppress all human-readable status output (equivalent to `output.quiet = true` in config). -- **`--keep-nonprod-severity`** — renamed from `--include-nonprod` for clarity; old name kept as hidden alias. -- **`OutputFormat` enum** — `--format` now uses clap `ValueEnum` with typed `Console`, `Json`, `Sarif` variants (default `Console`). No more empty-string default. -- 10 new unit tests: `SeverityFilter` parsing (single, comma list, threshold, case-insensitive, whitespace, empty rejection, invalid level rejection), `Severity::from_str` rejection of unknown values, and `severity_filter_applied_at_output_stage` integration test verifying that downgraded findings are correctly filtered. -- **AST pattern overhaul** -- all 10 language pattern files (`src/patterns/*.rs`) rewritten with consistent conventions, structured metadata, and validated tree-sitter queries. - - **Pattern schema extensions** -- `PatternTier` (A = structural, B = heuristic-guarded), `PatternCategory` (13 vulnerability classes), and `Hash` on `Severity`. Module-level docs explain conventions and how to add new patterns. - - **Namespaced IDs** -- all pattern IDs follow `..` format (e.g. `java.deser.readobject`, `py.cmdi.os_system`, `js.xss.document_write`). - - **New vulnerability coverage** -- 30+ new patterns across languages: Python deserialization (`pickle.loads`, `yaml.load`, `shelve.open`), Python command injection (`os.system`, `os.popen`), Python weak crypto (`hashlib.md5/sha1`), Java reflection (`Method.invoke`), Java weak digest (`MessageDigest.getInstance("MD5")`), Java XSS (`getWriter().println`), Go TLS misconfiguration (`InsecureSkipVerify: true`), Go SQL concat, Go hardcoded secrets, Go gob deserialization, PHP `assert()` code exec, PHP `include $var` path traversal, PHP weak crypto (`md5`/`sha1`/`rand`), C/C++ `popen()`, C/C++ format-string with variable first arg, C++ `const_cast`, Ruby `Digest::MD5`. - - **Query fixes** -- fixed 11 broken tree-sitter queries: Java `object_creation_expression` used wrong type node (`identifier` → `type_identifier`), C++ `reinterpret_cast`/`const_cast` used non-existent node types (→ `template_function` match), Ruby backtick used `shell_command` (→ `subshell`), Python SQL used `binary_expression` (→ `binary_operator`), TypeScript `as any` used inaccessible field (→ positional child), PHP patterns missing `argument` wrapper nodes, Rust `unsafe fn` regex used unsupported `\b`. - - **No-duplicate rule** -- patterns that overlap with taint sinks use distinct ID namespaces and are documented; dedup in `ast.rs` prevents duplicate findings at the same location. - - **Severity recalibration** -- `unwrap`/`expect`/`panic!`/`todo!` moved to Low (filtered by default `min_severity`). Security patterns remain High/Medium. -- **Pattern test suite** (`tests/pattern_tests.rs`, 26 tests) -- sanity checks (unique IDs, query compilation, non-empty descriptions, naming convention, severity distribution), positive fixture tests (10 languages), and negative fixture tests (10 languages verifying no false positives on safe code). -- **Pattern test fixtures** -- positive and negative fixture files for all 10 languages under `tests/fixtures/patterns//`. -- **Real world test suite** — comprehensive fixture-based test suite (`tests/real_world_tests.rs`) with ~180 test fixtures across all 10 supported languages (C, C++, Go, Java, JavaScript, PHP, Python, Ruby, Rust, TypeScript). Each fixture has an `.expect.json` file declaring expected findings (with `must_match` for hard requirements and soft expectations for aspirational coverage). Fixtures are organized by analysis type (`taint/`, `state/`, `cfg/`, `mixed/`) under `tests/fixtures/real_world//`. A single parameterized test runner validates all fixtures in both `full` and `ast` modes, with verbose output via `NYX_TEST_VERBOSE=1`. +## [0.5.0] — 2026-04-24 +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. -### Changed -- **Console header line now includes confidence** — the finding header shows score and confidence together as a parenthesized suffix: `(Score: 36, Confidence: Medium)`. The previous standalone `Confidence: ...` body line is removed. All four combinations are handled (both, score-only, confidence-only, neither). -- **Confidence display uses Title Case** — `Confidence::Display` now renders as `Low`, `Medium`, `High` (previously lowercase). -- **Breaking**: Config and data directory changed from `dev.ecpeter23.nyx` to `nyx` (e.g. `~/Library/Application Support/nyx/` on macOS). Existing config files (`nyx.conf`, `nyx.local`) and SQLite indexes at the old path will not be picked up automatically — copy them to the new location or re-run `nyx scan` to regenerate. -- **Improved diagnostic output formatting** — overhauled console renderer for a professional, security-tool-grade look: - - Severity is now the strongest visual anchor: HIGH (bold red with ✖), MEDIUM (bold orange ⚠), LOW (muted blue-gray ●). Fewer colors, clearer hierarchy. - - File paths rendered dim blue (never brighter than severity). - - Taint flow messages now use `→` arrow between shortened source/sink instead of backtick-wrapped text. - - Evidence values (Source, Sink) no longer wrapped in backticks — cleaner rendering with no risk of broken backtick spans across wrapped lines. -- **Fixed taint expression rendering** — multi-line sink/source call chains are now normalised before display: - - Whitespace collapsed (`foo() .bar()` → `foo().bar()`). - - Newlines joined into single-line canonical form. - - Spacing artefacts between `)` and `.` in method chains cleaned up. - - Long chains truncated with `…` ellipsis. -- Added `terminal_size` dependency for terminal-width-aware line wrapping. -- **Monotone forward dataflow taint analysis** — replaced the BFS taint engine in `taint/mod.rs` with a proper worklist-based forward dataflow analysis where termination is guaranteed by lattice finiteness. The generic `Transfer` trait in `state/engine.rs` now powers both the resource lifecycle/auth analysis and taint analysis. - - **`TaintState` lattice** (`taint/domain.rs`) — bounded abstract state with per-variable `VarTaint` (Cap bitflags + multi-origin tracking via `SmallVec<[TaintOrigin; 2]>`), dual validation bitsets (`validated_must` for intersection/all-paths, `validated_may` for union/any-path), and monotone `PredicateSummary` for contradiction pruning. Variables stored in sorted `SmallVec` keyed by `SymbolId` for O(n) merge-join. Lattice height bounded at ~8700 (7-bit Cap × 64 vars + validation bits + predicate bits). - - **`TaintTransfer`** (`taint/transfer.rs`) — implements `Transfer` with identical taint logic to the old BFS (source → propagation → sanitization → sink check). Callee resolution unchanged (local → global same-lang → interop edges). Emits `TaintEvent::SinkReached` events during Phase 2 of the engine. - - **JS/TS two-level solve** — prevents cross-function taint leakage (the main source of state explosion in the old BFS) while preserving global-to-function flows. Level 1 solves top-level code; Level 2 solves each function seeded with read-only top-level taint via `global_seed`. - - **Monotone predicate tracking** — path-sensitivity predicates moved from per-BFS-item `PathState` (which duplicated state exponentially) to monotone `PredicateSummary` in the lattice. Contradiction pruning uses `known_true & known_false` bit intersection (NullCheck/EmptyCheck/ErrorCheck only), which is both more precise and guaranteed monotone. - - **Multi-origin tracking** — each tainted variable tracks up to 4 `TaintOrigin` (node + `SourceKind`), enabling multiple findings when distinct sources flow to the same sink. - - **Guaranteed termination** — no more `MAX_BFS_ITERATIONS`/`MAX_SEEN_STATES` safety nets needed (though a 100K worklist iteration budget remains as defense-in-depth). Convergence follows from finite lattice height × finite CFG edges. - - **`analyse_file()` signature unchanged** — `Finding` struct, `Diag` conversion, and all callers are unaffected. -- **Generic dataflow engine** (`state/engine.rs`) — `run_forward()` and `DataflowResult` are now generic over any `S: Lattice` + `T: Transfer`. `DefaultTransfer` (resource lifecycle) implements `Transfer`; `TaintTransfer` implements `Transfer`. Per-domain iteration budget and `on_budget_exceeded` hooks added. -- **`path_state.rs` simplified** — removed `PathState`, `Predicate`, `MAX_PATH_PREDICATES`, `state_hash()`, `priority()` structs/methods. Kept `PredicateKind` enum and `classify_condition()` function (used by the new transfer for predicate classification). -- **Removed BFS infrastructure** — `taint_hash()`, BFS `Item` struct, `pred` predecessor map, two-tier seen-state map, and all bail-out constants (`MAX_BFS_ITERATIONS=200K`, `MAX_SEEN_STATES=100K`, `PATH_SENSITIVITY_NODE_LIMIT=500`, `PATH_SENSITIVITY_QUEUE_LIMIT=10K`, `MAX_PATH_VARIANTS_PER_KEY=4`) are no longer needed and have been removed. -- **Severity filtering applied at output stage** — `--severity` (and legacy `--high-only`) filtering is now applied ONCE in `scan::handle()` after all severity normalization (nonprod downgrades, dedup, truncation). Previously `--high-only` only filtered AST patterns during analysis; taint and CFG findings bypassed the filter entirely. -- **`--format` default is `console`** — previously defaulted to empty string, requiring fallback logic. -- **All status/progress output goes to stderr** — "Checking...", "Finished in...", config notes, and progress bars now use `eprintln!`/stderr exclusively. JSON and SARIF output is stdout-only. -- **`Severity::from_str` returns `Err` for unknown values** — previously returned `Ok(Severity::Low)` for any unrecognized input. -- **Deprecated CLI flags preserved as hidden aliases** — `--high-only`, `--no-index`, `--rebuild-index`, `--ast-only`, `--cfg-only`, and `--include-nonprod` are hidden from help but still functional, mapping to their canonical replacements. -- **Path-sensitive taint analysis** -- the BFS taint engine now carries a `PathState` (bounded set of branch predicates) alongside the taint map. When the BFS traverses a True or False edge from an `If` node, it records a `Predicate` with the condition's variables, kind, and polarity. This enables two new capabilities: - - **Infeasible path pruning** -- paths with contradictory predicates (e.g. `if x.is_none() { return; } if x.is_none() { sink }`) are detected and pruned, eliminating false positives on code guarded by redundant null/empty/error checks. Contradiction detection is conservative: only whitelisted kinds (`NullCheck`, `EmptyCheck`, `ErrorCheck`) with single-variable predicates are pruned. - - **Validation guard annotation** -- when all tainted variables reaching a sink are guarded by a `ValidationCall` predicate (e.g. `if validate(&x) { sink }` or `if !validate(&x) { return; } sink`), the finding is annotated with `path_validated: true` and `guard_kind: ValidationCall`. This metadata is surfaced in JSON and console output without changing severity. -- **Condition metadata on CFG nodes** -- `NodeInfo` now carries `condition_text`, `condition_vars`, and `condition_negated` for `If` nodes, extracted during CFG construction. Negation detection handles `!expr`, `not expr`, and Ruby `unless`. Classification of condition text into `PredicateKind` (NullCheck, EmptyCheck, ErrorCheck, ValidationCall, SanitizerCall, Comparison, Unknown) is conservative: call-based kinds require `(` in the text and a matching callee token. -- **`path_validated` and `guard_kind` fields on `Diag`** -- taint findings carry path-sensitivity metadata in JSON output (fields omitted when not set) and console output (suffix line `Path guard: ValidationCall` when present). Finding IDs are unchanged for dedup stability. -- **`smallvec` dependency** -- used for inline-allocated predicate storage in `PathState` (avoids heap allocation for the common case of ≤4 predicates per path). -- **Interprocedural call graph** -- a whole-program `CallGraph` (`petgraph::DiGraph`) is now built between Pass 1 and Pass 2 of every taint-enabled scan. Each function definition is a node; resolved callee relationships are edges. The graph is constructed from the merged `GlobalSummaries` and is available in both the filesystem and indexed scan paths. -- **Three-valued callee resolution** -- `CalleeResolution` enum distinguishes `Resolved(FuncKey)`, `NotFound`, and `Ambiguous(Vec)`. Ambiguous callees (same name in multiple namespaces, caller in a third namespace) are tracked separately from missing callees for diagnostics. -- **Shared resolution helper** -- `GlobalSummaries::resolve_callee_key()` centralizes same-language callee resolution with arity-aware filtering and namespace disambiguation. Both the call graph builder and the taint engine now use the same resolution logic. -- **Callee-name normalization** -- `normalize_callee_name()` extracts the last segment from qualified callee text (`"env::var"` → `"var"`, `"obj.method"` → `"method"`) before resolution. The raw call-site text is preserved on graph edges for diagnostics. -- **SCC / topological analysis** -- `CallGraphAnalysis` computes strongly connected components via Tarjan's algorithm and exposes a callee-first (leaves-first) topological ordering of SCC indices, ready for future bottom-up taint propagation. -- **Call graph tracing** -- `tracing::info!` log with node count, edge count, unresolved-not-found count, unresolved-ambiguous count, and SCC count is emitted after every call graph build. -- 8 new path-sensitivity integration tests: early-return validation guard, failed-validation branch, contradictory null-check pruning, if/else validation annotation, sanitize-one-branch regression, path-state budget graceful degradation, unknown-predicate non-pruning, multi-var non-pruning. -- 35 new unit tests in `taint::path_state`: classify_condition variants, PathState push/truncation, contradiction detection (whitelisted kinds, single-var only), has_validation_for semantics, state_hash determinism, priority ordering. -- 11 new unit tests: callee normalization, same-name-different-namespaces resolution, cross-language isolation, arity separation, recursive SCC detection, not-found vs ambiguous diagnostics, diamond topo ordering, interop edge resolution, namespace normalization consistency, and raw call-site preservation. -- **Edge-aware taint traversal** -- `analyse_file()` now uses `cfg.edges(node)` instead of `cfg.neighbors(node)`, inspecting `EdgeKind` on each edge. This is required for predicate recording but also makes the taint engine aware of the CFG's branch structure for the first time. -- **Two-tier seen-state deduplication** -- the BFS seen-state map changed from `HashSet<(NodeIndex, u64)>` to a `HashMap` keyed by `(NodeIndex, taint_hash)` mapping to a bounded list of `(path_hash, priority)` pairs. At most `MAX_PATH_VARIANTS_PER_KEY` (4) path variants are tracked per taint state, with deterministic eviction preferring non-truncated states with fewer predicates. -- **Finding deduplication** -- taint findings are now deduplicated by `(sink, source)` pair after analysis, preferring findings with `path_validated = true` (most informative metadata). -- **`taint::Finding` struct** -- added `path_validated: bool` and `guard_kind: Option` fields. Code that constructs `Finding` directly must include these fields. -- **`Diag` struct** -- added `path_validated: bool` and `guard_kind: Option` fields. Both use `#[serde(skip_serializing_if)]` to omit from JSON when not set. -- **`taint::resolve_callee()` refactored** -- the global resolution step now delegates to `GlobalSummaries::resolve_callee_key()` and applies `normalize_callee_name()` before lookup, unifying resolution logic with the call graph builder. -- **Label rules expanded across 8 languages:** - - **Go** — added `r.URL.Query`, `r.URL.Query.Get`, `Request.FormValue`, `Request.URL` sources; `filepath.Clean`/`filepath.Base` sanitizers; `fmt.Fprintf`/`fmt.Sprintf`/`fmt.Printf` format-string sinks; `os.Open`/`os.OpenFile`/`os.Create`/`ioutil.ReadFile`/`os.ReadFile` FILE_IO sinks; `template.HTML` HTML sink; `db.QueryRow`/`db.Prepare` SQL sinks. - - **PHP** — sources now match both `$_GET` and `_GET` (without `$` prefix, matching collect_idents stripping); added `$_FILES`/`_FILES`, `$_SERVER`/`_SERVER`, `$_ENV`/`_ENV` sources; `eval`/`assert` shell sinks; `include`/`include_once`/`require`/`require_once` FILE_IO sinks; `unserialize` sink; `move_uploaded_file`/`copy`/`file_put_contents`/`fwrite` FILE_IO sinks; `basename` FILE_IO sanitizer; `query` SQL sink. - - **Java** — added `readObject`/`readLine` sources; `ProcessBuilder` shell sink; `Class.forName` reflection sink; `println`/`print`/`write` HTML sinks. - - **Python** — added `send_file`/`send_from_directory` FILE_IO sinks; `os.path.realpath` FILE_IO sanitizer; `open` changed from source to FILE_IO sink (fixes source/sink conflict for path traversal detection). - - **Ruby** — `params` source detection now works via subscript handling. - - **Rust** — added `fs::read_to_string`/`fs::write`/`fs::read`/`File::open`/`File::create` as FILE_IO sinks; `fs::read_to_string` removed from sources (was source/sink conflict). - - **C/C++** — added `fopen`/`open` as FILE_IO sinks. -- **Ruby `rb.cmdi.system_interp` pattern broadened** — no longer requires string interpolation in arguments; now matches any `system`/`exec` call, promoted from Tier B to Tier A. -- **C++ `cpp.cmdi.popen` pattern added** — `popen()` command execution detection for C++, using the language-namespaced ID (the C pattern retains `c.cmdi.popen`). -- **Test config enables state analysis** — `test_config()` now sets `enable_state_analysis = true`. +> 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 -### Fixed -- **Taint source kind misclassified as "unknown" for non-call sources** — source-bearing nodes with `CallWrapper` or `Assignment` kind (e.g. `userInput = req.query.data`) had their `callee` field set to `None` because the CFG builder only populated `callee` for `StmtKind::Call` nodes. This caused `infer_source_kind()` to receive an empty string, failing to match any keyword pattern and defaulting to `SourceKind::Unknown`. Fixed by also setting `callee` when a label (Source/Sink/Sanitizer) is detected, so the extracted member text (e.g. "req.query") flows through to source kind inference. Affects severity classification and diagnostic output for property-access sources across all languages. -- **Full KINDS map audit across all 10 languages** — 89 missing tree-sitter node types added to KINDS maps so the CFG builder no longer silently drops code inside switch/case, try/catch/finally, class bodies, closures/lambdas, and other container nodes. Previously, any node not in a language's KINDS map hit the `build_sub` fallback which created a terminal Seq node without recursing into children, effectively making all wrapped code invisible to analysis. - - **C** (+3): `switch_statement`, `case_statement`, `labeled_statement` - - **C++** (+7, 1 fix): `switch_statement`, `case_statement`, `labeled_statement`, `throw_statement` (Return), `try_statement`, `catch_clause`, `lambda_expression`; **critical fix**: `namespace_definition` changed from `Trivia` to `Block` (all function definitions inside namespaces were silently dropped) - - **Java** (+11): `do_statement` (While), `throw_statement` (Return), `switch_expression`, `switch_block`, `switch_block_statement_group`, `try_statement`, `catch_clause`, `finally_clause`, `lambda_expression`, `constructor_body`, `static_initializer` - - **JavaScript** (+11): `switch_statement`, `switch_body`, `switch_case`, `switch_default`, `try_statement`, `catch_clause`, `finally_clause`, `class_declaration`, `class` (expression), `class_body`, `export_statement` - - **TypeScript** (+13): all JS switch/try/class entries plus `abstract_class_declaration`, `export_statement`, `enum_declaration` (Trivia) - - **PHP** (+11): `do_statement` (While), `throw_expression` (Return), `switch_statement`, `switch_block`, `case_statement`, `default_statement`, `try_statement`, `catch_clause`, `finally_clause`, `colon_block`, `class_declaration` - - **Python** (+7): `try_statement`, `except_clause`, `finally_clause`, `class_definition`, `decorated_definition`, `match_statement`, `case_clause` - - **Ruby** (+11): `until` (While), `begin`, `rescue`, `ensure`, `case`, `when`, `class`, `module`, `singleton_method` (Function), `do`, `block` - - **Go** (+10): `expression_switch_statement`, `type_switch_statement`, `expression_case`, `type_case`, `default_case`, `select_statement`, `communication_case`, `go_statement`, `defer_statement`, `func_literal` (Function) - - **Rust** (+5, 1 removal): `closure_expression`, `async_block`, `impl_item`, `trait_item`, `declaration_list`; removed dead `loop_statement` entry (node doesn't exist in tree-sitter-rust 0.24.0) -- Removed unused `Kind::LoopBody` enum variant from `labels/mod.rs` (no arm in `build_sub`, last reference was the dead Rust `loop_statement` entry) -- **CFG: `else_clause` not recursed into for C/C++** — tree-sitter's C and C++ grammars wrap else bodies in an `else_clause` node. This node was missing from both languages' `KINDS` maps, so the CFG builder's fallback arm treated it as a terminal `Seq` node without descending into children. All statements inside else blocks (e.g. `fclose(f)`) were silently dropped from the CFG, causing false-positive resource leak and incorrect branch analysis. Fixed by mapping `"else_clause" => Kind::Block` in `src/labels/c.rs` and `src/labels/cpp.rs`. -- **CFG: `else_clause` missing from Rust, JavaScript, TypeScript, Python, PHP KINDS maps** — same bug class as C/C++: tree-sitter wraps else bodies in an `else_clause` node that was not in KINDS, silently dropping all code inside else blocks from the CFG. Fixed by mapping `"else_clause" => Kind::Block` in all five languages. Also added `"elif_clause" => Kind::Block` (Python), `"else_if_clause" => Kind::Block` (PHP), and `"elsif" => Kind::If` (Ruby) to handle chained elif/elsif nodes. -- **Rust KINDS using wrong tree-sitter node names** — tree-sitter-rust uses `_expression` suffixes (not `_statement`) for `while`, `for`, and `return` nodes. The existing `while_statement`, `for_statement`, and `return_statement` entries were dead code (0 grammar matches). Added `while_expression`, `for_expression`, and `return_expression` mappings. -- **Rust `match_expression`, `match_block`, `match_arm`, `unsafe_block` missing from KINDS** — these wrapper nodes were not mapped, causing all code inside match arms and unsafe blocks to be silently dropped from the CFG. Mapped to `Kind::Block` for sequential traversal. -- **TypeScript missing `throw_statement` and `do_statement`** — `throw` was mapped in JavaScript but not TypeScript; `do_statement` (do-while loops) was missing from both JS and TS. Added `"throw_statement" => Kind::Return` and `"do_statement" => Kind::While` to both languages. -- **Python `raise_statement` and `with_statement` missing from KINDS** — `raise` terminates the current path (mapped to `Kind::Return`); `with` wraps code in a context manager (mapped to `Kind::Block`). Both were silently dropping enclosed code. -- **Dead KINDS entries removed** — `"for_of_statement"` in TypeScript (0 grammar matches; TS inherits `for_in_statement` from JS) and `"method_call"` in Ruby (0 grammar matches; Ruby only has `call`). -- **`--high-only` emitting Low/Medium taint and CFG findings** — severity filter was only applied to AST pattern queries during analysis. Taint findings (whose severity derives from `SourceKind`) and CFG structural findings passed through unfiltered. The filter is now applied at the final output stage after all severity normalization, ensuring `--severity HIGH` never emits downgraded Medium/Low findings. -- **JSON/SARIF output contaminated with status messages on stdout** — status messages ("Checking...", "Finished in...") used `println!` and appeared in stdout alongside machine output. Now all status goes to stderr. -- **CFG: False edge to then-block exits in no-else if statements** -- previously, `if (cond) { body }` without an else block created a `False` edge from the condition node directly to the then-block's exit nodes. This made the false path appear to traverse the then-block, causing incorrect predicate polarity in path-sensitive analysis and duplicate taint findings with contradictory metadata. The CFG now creates a synthetic pass-through `Seq` node for the false path with an explicit `False` edge from the condition, correctly modeling "skip the then-block." This also fixes the frontier: previously, the no-else non-terminating case duplicated `then_exits` in the frontier (`then_exits ++ then_exits.clone()`); it now correctly produces `then_exits ∪ [pass_through]`. -- **Taint BFS non-termination on large JS files** — the BFS taint engine in `taint/mod.rs` had no global iteration bound. The seen-state deduplication keyed on `(node, taint_hash)`, so every distinct taint map at a CFG node was treated as a novel state. In files with loops and many tainted variables (e.g. a 2,200-line JS file with 18+ top-level variables tainted via `window.location.search`), each loop iteration produced a slightly different taint map, causing the BFS to revisit loop bodies indefinitely. Both `--no-index` and `--rebuild-index` scans hung near completion (progress showed e.g. 87/88 files). Fixed by adding two hard bounds: `MAX_BFS_ITERATIONS` (200,000 queue pops) and `MAX_SEEN_STATES` (100,000 unique `(node, taint_hash)` entries in the seen-state map). When either limit is reached the analysis bails out gracefully and returns all findings collected so far. A `tracing::warn!` is emitted on iteration-limit bail-out. Normal files are unaffected (typical BFS uses <1,000 iterations). -- **Rust `if let` / `while let` taint propagation** — the CFG builder now extracts pattern bindings from `let_condition` nodes as variable definitions in `def_use()`, and classifies the value expression (e.g. `env::var("CMD")`) for source/sink labels in `push_node()`. Previously, `if let Ok(cmd) = env::var("CMD") { Command::new("sh").arg(&cmd) }` produced no taint finding because `cmd` was never recognized as a tainted definition. Now correctly detects taint flow through `if let` and `while let` bindings. -- **C++ `popen` pattern ID collision** — renamed `c.cmdi.popen` to `cpp.cmdi.popen` in C++ patterns to fix a cross-language duplicate ID that caused `all_pattern_ids_are_globally_unique` test failure. -- **State analysis early-return leak duplication** — `extract_findings` in `state/facts.rs` now skips early-return nodes when checking for resource leaks, only inspecting the synthesized function exit node. Previously, early-return nodes with path-specific state (OPEN only) emitted `state-resource-leak` alongside the correct `state-resource-leak-possible` from the merged exit state. -- **Severity filter bug** — `min_severity` comparison in `ast.rs` was inverted (`<=` instead of `>`), causing all AST patterns at the minimum severity level to be silently dropped. With the default `min_severity = Low`, all Low-severity patterns (`.unwrap()`, `.expect()`, `panic!`, `todo!`, `mem::forget`, Go crypto patterns, narrow casts) were never reported. Fixed 29 test cases. -- **Nested function analysis** — CFG builder now recurses into function expressions passed as call arguments (e.g., Express `app.get('/path', function(req, res) { ... })`, Sinatra `get '/path' do...end`). Added `collect_nested_function_nodes()` to discover `Kind::Function` nodes inside `CallWrapper`/`CallFn` AST subtrees. Also added `function_expression` to JS/TS KINDS maps, and `do_block`/`block` as `Kind::Function` in Ruby for Sinatra/Rails blocks. Anonymous functions now get unique names (``) to prevent scope collisions in JS two-level taint solve. -- **Chained method call classification** — `classify()` now normalizes chained calls like `r.URL.Query().Get` by stripping internal `()` between `.` segments, producing `r.URL.Query.Get`. Suffix matching is attempted against both the original head and the normalized form, fixing Go HTTP handler source detection and similar patterns. -- **Subscript access source detection** — `first_member_label` and `first_member_text` now handle `subscript_expression`, `subscript`, and `element_reference` nodes, enabling source classification for PHP `$_GET['cmd']`, Ruby `params[:cmd]`, and Python `os.environ['KEY']`. -- **Return-statement call extraction** — `Kind::Return` added to the node types that extract inner call identifiers via `first_call_ident`, fixing cases like `return send_file(path)` where the sink was not classified. -- **Nested call classification** — new `find_classifiable_inner_call()` tries all nested calls when the outermost one doesn't classify, fixing `str(eval(expr))` where `eval` is a sink wrapped in a non-sink call. -- **Java `new` expression text extraction** — added `type` field fallback in `push_node` and `first_call_ident` for `CallFn` nodes, fixing `new ProcessBuilder(...)` not matching as a sink. -- **Function body lookup for anonymous functions** — `Kind::Function` handler now falls back to finding a `Kind::Block` child when `child_by_field_name("body")` returns None, supporting JS/TS anonymous function expressions and Ruby blocks. -- **Function-level resource leak detection** — `extract_findings` in `state/facts.rs` now inspects per-function Return nodes for leaked resources, not just the file-level Exit node. Previously, variables from one function could be overwritten by same-named variables in subsequent functions, masking leaks. -- **Use-after-free for memory functions** — added `strcpy`, `strncpy`, `memcpy`, `memmove`, `memset`, `memcmp`, `strcmp`, `strncmp`, `strlen`, `sprintf`, `snprintf` to `RESOURCE_USE_PATTERNS` in state analysis, enabling use-after-free detection for common C/C++ string and memory functions. +- **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. -## [0.3.0] - 2026-02-25 +### Engine -### Added -- **Configurable analysis rules** -- users can define custom sources, sanitizers, and sinks per language via TOML config (`nyx.local`) or the new `nyx config` CLI. Config rules take priority over built-in rules, so project-specific sanitizers like `escapeHtml()` are recognized without code changes. -- **`nyx config` CLI subcommand** with four actions: - - `show` -- print effective merged configuration as TOML - - `path` -- print config directory path - - `add-rule --lang --matcher --kind --cap ` -- append a label rule to `nyx.local` - - `add-terminator --lang --name ` -- append a terminator function to `nyx.local` -- **`--include-nonprod` CLI flag** -- by default, findings in non-production paths (tests, vendor, benchmarks, examples, fixtures, build scripts, `*.min.js`) are now downgraded by one severity tier (High→Medium, Medium→Low). Pass `--include-nonprod` to restore original severity. Controlled by `scanner.include_nonprod` config key. -- **`SourceKind` enum** in the taint engine -- taint findings now carry a `source_kind` field (`UserInput`, `EnvironmentConfig`, `FileSystem`, `Database`, `Unknown`) inferred from the source callee name and capabilities. Severity is based on source kind rather than hardcoded to High: filesystem and database sources produce Medium, user input and environment sources produce High. -- **Configurable terminators** -- functions like `process.exit()` can be declared as terminators per language; the CFG treats them as dead ends, preventing false positives on code after termination calls. -- **Event handler callback suppression** -- functions passed as arguments to configured event handler calls (e.g. `addEventListener`) are no longer flagged as unreachable code. -- **Exec-path guard rules** -- calls to `which`, `resolve_binary`, `find_program`, `lookup_path`, and `shutil.which` are recognized as guards for `SHELL_ESCAPE` sinks. If such a guard dominates a shell-exec sink, the `cfg-unguarded-sink` finding is suppressed. -- **One-hop constant binding trace** -- the constant-arg sink suppression now traces one hop through the CFG. If a sink's variable was defined by a node with no uses and no Source label, it is treated as constant. Fixes false positives on patterns like `cmd = "git"; subprocess.run([cmd, "status"])`. -- **Evidence-based severity in cfg-only mode** -- when taint analysis is not active (no global summaries and no taint findings), structural `cfg-unguarded-sink` findings without source-derived evidence are downgraded from Medium to Low. -- **FileResponse ownership transfer** -- file handles passed to consuming sinks (`FileResponse`, `StreamingHttpResponse`, `send_file`, `make_response`) are no longer flagged as resource leaks. -- **Lock-not-released refinement** -- mutex findings now require an explicit `.acquire()` or `.lock()` call on the acquired variable. Constructor-only patterns like `lock = threading.Lock()` without acquire no longer produce `cfg-lock-not-released`. -- **Python `connect`/`cursor` exclusions** -- `signal.connect`, `event.connect`, and `.register` are excluded from the Python db-connection acquire pattern, preventing false `cfg-resource-leak` findings on Django signal handlers and event registrations. -- **`location.href` sink rules** for JavaScript -- `location.href`, `window.location.href`, and `document.location.href` assignments are classified as `Sink(URL_ENCODE)`. -- **`throw_statement` as terminator** in JavaScript -- `throw` now terminates the current block in the CFG (mapped to `Kind::Return`), preventing false `cfg-error-fallthrough` findings after throw statements. -- **`Cap::FMT_STRING` capability bit** -- new bitflag (`0b0100_0000`) for format-string vulnerabilities, distinct from HTML injection. Sources using `Cap::all()` automatically match. -- **Python taint sources** -- `open`, `argparse.parse_args`, `urllib.request.urlopen`, `requests.get`, `requests.post` added as `Cap::all()` sources for broader attack-surface coverage. -- **SARIF 2.1.0 output format** (`-f sarif`) -- produces spec-compliant Static Analysis Results Interchange Format JSON on stdout. Includes tool metadata, deduplicated rule definitions with descriptions, severity-to-level mapping (`High→error`, `Medium→warning`, `Low→note`), and physical locations with relative paths. Suitable for GitHub Code Scanning, Azure DevOps, and other SARIF-consuming CI tools. -- **Progress bars** via `indicatif` -- file discovery, Pass 1, and Pass 2 each display a progress bar on stderr with file counts and ETA. Bars are automatically hidden when output format is `json`/`sarif` or quiet mode is enabled. Index building also shows progress. -- **Quiet mode** (`output.quiet = true`) -- suppresses all status messages (config notes, "Checking...", "Finished in...") on stderr. Useful for CI pipelines and scripted invocations. -- **Resource leak detection for Python, Ruby, PHP, JavaScript, and TypeScript** -- new acquire/release pairs: Python (`open`/`.close`, `socket`/`.close`, `connect`/`.close`, `threading.Lock`/`.release`), Ruby (`File.open`/`.close`, `TCPSocket.new`/`.close`, `.lock`/`.unlock`), PHP (`fopen`/`fclose`, `mysqli_connect`/`mysqli_close`, `curl_init`/`curl_close`), JS/TS (`fs.open`/`fs.close`, `createReadStream`/`.close`). -- **Walker config wired up** -- `performance.max_depth`, `scanner.one_file_system`, `scanner.require_git_to_read_vcsignore`, and `scanner.excluded_files` are now enforced during directory walking (previously parsed but ignored). -- **`database.vacuum_on_startup`** -- when enabled, runs SQLite VACUUM before indexed scans to reclaim space. -- 31 new unit tests covering config round-trip, rule merging, classify extension, href classification, throw termination, terminator detection, config sanitizer suppression, Python/C++ precision, unreachable+unguarded dedup, resource leak detection, one-hop constant binding, exec-path guards, cfg-only severity downgrade, FileResponse ownership, lock constructor suppression, signal.connect exclusion, nonprod path detection, and severity downgrade. +- 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. -### Changed -- **`taint::Finding` struct** -- added `source_kind: SourceKind` field. Code that constructs `Finding` directly must include this field. -- **`AnalysisContext` struct** -- added `taint_active: bool` and `analysis_rules` fields. Code that constructs `AnalysisContext` directly must include these fields. -- **`ScannerConfig` struct** -- added `include_nonprod: bool` field (default `false`). Deserialization is unaffected due to `#[serde(default)]`. -- **`proto_pollution` AST pattern severity** -- downgraded from High to Low. The AST-only pattern is a structural indicator; the taint engine separately produces High findings when attacker-controlled data flows to `__proto__`. -- **`location_href_assignment` AST pattern** -- constrained to require a known browser global object (`window`, `location`, `document`, `self`, `top`, `parent`, `frames`). Prevents `el.href = val` from matching; only `window.location.href = val` and similar patterns trigger the finding. -- **Taint finding severity** -- no longer hardcoded to High. Severity is now derived from `SourceKind`: UserInput/EnvironmentConfig/Unknown → High, FileSystem/Database → Medium. -- **C/C++ sink reclassification** -- `printf`/`fprintf` moved from `Sink(HTML_ESCAPE)` to `Sink(FMT_STRING)`. `std::cout`, `std::cerr`, `std::clog` removed from sinks entirely (output/logging, not injection vectors). `sprintf`/`strcpy`/`strcat` remain `Sink(HTML_ESCAPE)`. -- `classify()` now accepts an optional `extra: Option<&[RuntimeLabelRule]>` parameter; config-defined rules are checked first (higher priority) before built-in static rules. -- `build_cfg()`, `build_sub()`, and `push_node()` accept optional `LangAnalysisRules` for config-driven label classification, terminator detection, and event handler awareness. -- `find_guard_nodes()` and `is_guard_call()` now recognize config-defined sanitizers as guards with matching capability bits. -- `merge_configs()` union-merges analysis rules, terminators, and event handlers per language key with dedup. -- Assignment LHS classification now tries the full member expression text (e.g. `location.href`) before falling back to property-only (e.g. `innerHTML`), fixing false positives on `a.href` assignments. -- `handle_command()` now receives `config_dir` to support the `config` subcommand. -- **Fused single-pass analysis** -- AST-only mode now runs a single fused pass (`analyse_file_fused`) that parses each file and builds the CFG once, producing both function summaries and diagnostics. Previously every file was parsed twice (once for summary extraction, once for analysis). Taint mode uses the fused pass for Pass 1, eliminating redundant CFG construction during summary extraction. -- **O(N²) → O(N) function-level dataflow sweep in CFG builder** -- the light-weight dataflow sweep and return-node wiring in `build_sub` for `Kind::Function` now iterate only over nodes created within the current function scope (tracked via a snapshot of the node count) instead of scanning the entire graph. Eliminates quadratic scaling in files with many functions. -- **Parallel summary merging** -- `scan_filesystem` now uses rayon `fold`/`reduce` to build per-thread `GlobalSummaries` maps in parallel, then merges them in a binary reduce tree. Eliminates the serial `merge_summaries` bottleneck. Added `GlobalSummaries::merge()`. -- **Redundant file I/O eliminated in indexed path** -- files are now read once and hashed once per scan. Added `Indexer::should_scan_with_hash()` and `Indexer::upsert_file_with_hash()` to accept pre-computed hashes. Pass 2 uses `run_rules_on_bytes` with already-read bytes instead of re-reading from disk. Previously files could be read up to 4 times and hashed up to 3 times per indexed scan. -- **SQLite mutex mode relaxed** -- switched from `SQLITE_OPEN_FULL_MUTEX` (global serialization) to `SQLITE_OPEN_NO_MUTEX`. The r2d2 connection pool guarantees one-connection-per-thread safety; combined with WAL mode this allows concurrent readers without a global lock. -- **Parallel JSON deserialization in `load_all_summaries`** -- for large result sets (>256 summaries), JSON deserialization is now parallelized with rayon. -- **Zero-allocation taint hashing** -- `taint_hash()` replaced sorted-`Vec` + blake3 with an order-independent XOR-of-FNV scheme. Eliminates a heap allocation and sort per BFS edge in the taint engine. -- **In-place taint transfer** -- `apply_taint()` now mutates the taint map in place instead of cloning and returning a new `HashMap` per node visit. The BFS loop caches hash values and uses `std::mem::take` for the last successor to avoid unnecessary clones. +### Symbolic Execution -### Fixed -- **False positives on one-hop constant bindings** -- `cmd = "git"; Command::new(cmd)` no longer triggers `cfg-unguarded-sink` because the variable is traced back to a constant definition. -- **False positives from exec-path guards** -- `resolve_binary(&bin); Command::new(bin)` is now recognized as guarded. -- **False `cfg-resource-leak` on Django signal handlers** -- `signal.connect(handler)` no longer matches the Python db-connection acquire pattern. -- **False `cfg-lock-not-released` on Lock constructors** -- `threading.Lock()` without `.acquire()` no longer produces a finding. -- **False `cfg-resource-leak` on FileResponse** -- `f = open(...); return FileResponse(f)` is recognized as ownership transfer. -- **Inflated severity in cfg-only mode** -- structural findings without taint evidence now correctly produce Low severity instead of Medium. -- **`el.href = val` false positive in AST patterns** -- the `location_href_assignment` pattern now requires a known browser global, eliminating matches on DOM element `.href` assignments. -- **Structured output modes (`-f json`, `-f sarif`) now produce zero stderr noise** -- config notes, "Checking …", and "Finished in …" messages are fully suppressed (not just redirected to stderr) so that `nyx scan -f json | jq` and CI SARIF upload work without extraneous output. Human-readable console format continues to show status messages. -- **Console output column alignment** -- severity tags are now bracketed and padded to a fixed display width (`[HIGH]`, `[MEDIUM]`, `[LOW]`) so that rule IDs align consistently regardless of severity. ANSI color codes are applied after width calculation, not before. -- **`.href` false positives** -- `el.href = "/about"` no longer triggers `location_href_assignment` or sink classification; only `location.href` (and `window.location.href`, `document.location.href`) match. -- **Constant-arg sink false positives** -- sinks whose arguments are all constants (no variable uses beyond the callee name) with no taint confirmation are now suppressed. Fixes false positives on patterns like `subprocess.run(["make","clean"])` and `printf("hello\n")`. -- **Unreachable + unguarded dedup** -- when both `cfg-unreachable-sink` and `cfg-unguarded-sink` fire on the same span, the unguarded finding is suppressed (unreachable is more specific). -- **`std::cout` false positives** -- `std::cout` no longer classified as a sink, eliminating spurious findings on every C++ iostream print. -- **Break/continue scope correctness** -- `break` and `continue` inside loops now correctly wire to their enclosing loop header/exit. Previously, `break` in a `while`/`for` body created a dead-end node that left post-loop code unreachable, producing false `cfg-unreachable-*` findings. The If handler's no-else case also now correctly flows the false branch to subsequent code when the then-branch terminates (return/break/continue). True/False edge labels are applied to branch entry nodes rather than exit nodes, fixing `cfg-error-fallthrough` false positives on `if (err) { return; }` patterns. -- **Preprocessor dangling-else CFG recovery** -- `#ifdef`/`#endif` blocks that split an `if/else` across preprocessor boundaries no longer orphan subsequent code. The CFG block handler now recovers the frontier after preprocessor nodes, preventing false unreachable-code findings on code following `#ifdef ... #endif` blocks. -- **Wrapper resource function recognition** -- `curlx_fopen`, `curlx_fdopen`, `fdopen`, and `curlx_fclose` are now recognized as acquire/release functions for C file handles, eliminating false `cfg-resource-leak` findings on codebases (e.g. curl) that use wrapper functions around standard I/O. -- **`freopen` false positive** -- `freopen()` (and `curlx_freopen`) no longer triggers `cfg-resource-leak` findings. Previously `freopen` matched the `fopen` acquire pattern via `ends_with`; a new `exclude_acquire` field on `ResourcePair` filters out these false matches for both the file handle and file descriptor resource pairs. -- **Struct field ownership transfer** -- resource leak detection now recognizes ownership transfer via struct field assignment (`s->stream = fp`, `obj.field = ptr`). When an acquired resource is stored into a struct field downstream, the finding is suppressed since the receiving struct assumes lifetime responsibility. -- **Linked-list/global insertion** -- resource leak detection now recognizes linked-list insertion patterns (`p->next = list; list = p`) and global variable assignment as ownership transfers, eliminating false `cfg-resource-leak` findings on common C allocation-and-insert idioms. -- Removed incorrect `value_enum` attribute from CLI `--format` argument. -- Benchmark compilation error: `classify()` calls in `benches/scan_bench.rs` were missing the third `extra` parameter. +- 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. -## [0.2.0] - 2026-02-24 +### Security & Coverage -### Added -- **Cross-file taint analysis** -- two-pass architecture: Pass 1 extracts `FuncSummary` per function (source/sanitizer/sink capabilities, taint propagation, callees), Pass 2 runs BFS taint propagation with cross-file callee resolution. -- **CFG analysis engine** with five detectors: unguarded sinks (`cfg-unguarded-sink`), auth gaps in web handlers (`cfg-auth-gap`), unreachable security code (`cfg-unreachable-*`), error fallthrough (`cfg-error-fallthrough`), and resource leaks (`cfg-resource-leak`). -- **Cross-language interop** -- taint flows across language boundaries via explicit `InteropEdge` structs without false-positive name collisions. -- **Function summaries** persisted to SQLite (`function_summaries` table) with arity, parameter names, capability bitflags, and callee lists. -- **Multi-language CFG + taint support** -- all 10 languages (Rust, C, C++, Java, Go, PHP, Python, Ruby, TypeScript, JavaScript) now have `KINDS` maps, `RULES`, and `PARAM_CONFIG` for full CFG construction and taint analysis. -- **Resource leak detection** for C/C++ (malloc/free, fopen/fclose), Go (os.Open/Close, Lock/Unlock), Rust (alloc/dealloc), and Java (streams, connections). -- **Finding scoring system** -- numeric scores based on severity, proximity to entry point, path complexity, taint confirmation, and confidence multiplier. -- **Analysis modes** -- `Full` (default), `Ast` (`--ast-only`), and `Taint` (`--cfg-only`) selectable via CLI flags or `scanner.mode` config. -- **`GlobalSummaries`** with conservative merge: union caps, OR booleans, union param/callee lists on name collisions across files. -- **Performance optimizations** -- `_from_bytes` variants to read-once/hash-once, lock-free rayon parallelism, SQLite WAL + 8 MB cache + 256 MB mmap. -- **Tracing instrumentation** -- `tracing` spans on all pipeline phases (walk, pass1, merge, pass2, per-file ops, db_init). -- **Benchmark suite** -- criterion benchmarks in `benches/scan_bench.rs` with fixtures. -- 107 unit tests covering taint propagation, cross-file resolution, cross-language interop, CFG analysis, and summaries. +- 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. -### Changed -- Bumped all dependencies to latest compatible versions. -- `Cap` bitflags expanded: `ENV_VAR`, `HTML_ESCAPE`, `SHELL_ESCAPE`, `URL_ENCODE`, `JSON_PARSE`, `FILE_IO`. -- `classify()` in labels uses zero-allocation byte-level case-insensitive comparisons. -- Indexed scans now always re-analyze all files in Pass 2 when taint is enabled (conservative: global summaries may have changed even if a file didn't). +### CLI & Output -### Fixed -- Clippy `ptr_arg` lint in perf tests (`&PathBuf` -> `&Path`). +- `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. -## [0.2.0-alpha] - 2025-06-28 +### Hardening -### Added -- Experimental intra‑procedural CFG + taint analysis for Rust. Nyx now builds a control‑flow graph, applies data‑flow rules, and flags unsanitised Source → Sink paths (e.g. env::var → Command::new). -- O(1) node‑kind lookup via per‑language PHF tables for zero‑cost dispatch. -- Six unit tests covering conditionals, loops, sanitizers, and multiple sources. -- Debug channel target=cfg (use RUST_LOG=nyx::cfg=debug) to inspect generated graphs. +- 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. -### Fixed -- Fixed a bug in the release pipeline where Windows was trying to call the zip, PowerShell doesn't have a zip command +### Persistence -## [0.1.1-alpha] - 2025-06-25 +- 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. -### Fixed -- Fixed a bug where the `scan --no-index` command would not respect the `max_results` config setting (#1) +### Frontend -### Added -- Integration tests covering indexing and scanning pipelines (#3, #4, #5, #8) +- 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, deferred, 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. -## [0.1.0-alpha] - 2025-06-25 +### Removed -### Added -- Initial alpha release of **Nyx** CLI tool -- 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 -- Unit tests for core modules (config, ext, project utils) +- Legacy BFS taint engine, `TaintTransfer`, `TaintState`, and the `NYX_LEGACY` fallback. +- Legacy vanilla-JS frontend (`app.js`). + +## [0.4.0] — 2025-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..eb0f9f73 --- /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/CONTRIBUTING.md b/CONTRIBUTING.md index 0c6b2cf0..81645a9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. ### Prerequisites -- **Rust 1.85+** (edition 2024) +- **Rust 1.88+** (edition 2024) - Git ### Building @@ -284,6 +284,8 @@ cargo clippy --all -- -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. @@ -306,6 +308,8 @@ cargo clippy --all -- -D warnings 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 @@ -348,4 +352,20 @@ Please do **not** open public issues for security-sensitive bugs. See [SECURITY. ## License -By contributing to Nyx, you agree that your contributions will be licensed under the [GPL-3.0](./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 0ddb894b..c3d1dba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -37,9 +43,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -52,15 +58,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +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", ] @@ -71,7 +77,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -82,7 +88,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -105,9 +111,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" dependencies = [ "anstyle", "bstr", @@ -118,6 +124,24 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +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.0" @@ -125,16 +149,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "bitflags" -version = "2.11.0" +name = "axum" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", @@ -161,6 +237,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "bytesize" version = "2.3.1" @@ -175,9 +257,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -189,6 +271,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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.44" @@ -197,6 +290,7 @@ checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", + "serde", "windows-link", ] @@ -229,9 +323,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -239,9 +333,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -251,9 +345,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -263,27 +357,52 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +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.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -300,13 +419,22 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +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" @@ -384,15 +512,15 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "dashmap" -version = "7.0.0-rc2" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a1e35a65fe0538a60167f0ada6e195ad5d477f6ddae273943596d4a1a5730b" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.14.5", "lock_api", + "once_cell", "parking_lot_core", ] @@ -429,7 +557,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -457,7 +585,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -474,9 +602,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -490,6 +618,16 @@ 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" @@ -511,6 +649,54 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "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.2.17" @@ -524,25 +710,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core", "wasip2", "wasip3", ] @@ -577,6 +752,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -595,6 +776,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.11.0" @@ -616,6 +803,86 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +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" @@ -664,12 +931,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -704,15 +971,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -732,25 +999,24 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", ] [[package]] name = "libsqlite3-sys" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -787,12 +1053,45 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -805,14 +1104,14 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -835,9 +1134,10 @@ dependencies = [ [[package]] name = "nyx-scanner" -version = "0.4.0" +version = "0.5.0" dependencies = [ "assert_cmd", + "axum", "bitflags", "blake3", "bytesize", @@ -853,6 +1153,7 @@ dependencies = [ "indicatif", "num_cpus", "once_cell", + "parking_lot", "petgraph", "phf", "predicates", @@ -866,7 +1167,11 @@ dependencies = [ "tempfile", "terminal_size", "thiserror", + "tokio", + "tokio-stream", "toml", + "tower", + "tower-http", "tracing", "tracing-subscriber", "tree-sitter", @@ -880,13 +1185,15 @@ 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" @@ -939,6 +1246,12 @@ dependencies = [ "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" @@ -949,6 +1262,7 @@ dependencies = [ "hashbrown 0.15.5", "indexmap", "serde", + "serde_derive", ] [[package]] @@ -996,15 +1310,15 @@ dependencies = [ [[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" @@ -1046,15 +1360,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "predicates" version = "3.1.4" @@ -1106,18 +1411,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +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" @@ -1132,9 +1437,9 @@ dependencies = [ [[package]] name = "r2d2_sqlite" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6" +checksum = "5576df16239e4e422c4835c8ed00be806d4491855c7847dba60b7aa8408b469b" dependencies = [ "r2d2", "rusqlite", @@ -1143,38 +1448,26 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +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.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1251,9 +1544,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ "bitflags", "fallible-iterator", @@ -1274,7 +1567,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1283,6 +1576,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -1309,9 +1608,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1358,14 +1657,37 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "1.0.4" +name = "serde_path_to_error" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1381,23 +1703,58 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] [[package]] name = "sqlite-wasm-rs" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" dependencies = [ "cc", "js-sys", @@ -1429,26 +1786,32 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.26.0" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +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.1", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -1528,10 +1891,61 @@ dependencies = [ ] [[package]] -name = "toml" -version = "1.0.3+spec-1.1.0" +name = "tokio" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "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 = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -1544,27 +1958,76 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +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" @@ -1572,6 +2035,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1621,9 +2085,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -1643,9 +2107,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.6" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f456d2108c3fef07342ba4689a8503ec1fb5beed245e2b9be93096ef394848" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" dependencies = [ "cc", "regex", @@ -1657,9 +2121,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", @@ -1743,9 +2207,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", @@ -1793,11 +2257,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "rand", "wasm-bindgen", @@ -1842,11 +2306,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -1855,14 +2319,14 @@ 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", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -1873,9 +2337,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1883,9 +2347,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -1896,9 +2360,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -1939,9 +2403,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.90" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -1979,7 +2443,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2047,15 +2511,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2065,76 +2520,11 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "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.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" @@ -2145,6 +2535,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -2225,19 +2621,48 @@ dependencies = [ ] [[package]] -name = "zerocopy" -version = "0.8.39" +name = "z3" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +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.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d0636c99..0faa126d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "nyx-scanner" -version = "0.4.0" +version = "0.5.0" edition = "2024" -description = "A CLI security scanner for automating vulnerability checks" -license = "GPL-3.0" -authors = ["Eli Peter "] +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://github.com/elicpeter/nyx" repository = "https://github.com/elicpeter/nyx" documentation = "https://github.com/elicpeter/nyx/tree/master/docs" @@ -14,18 +15,34 @@ readme = "README.md" default-run = "nyx" exclude = [ "assets/", + "frontend/node_modules/", ".github/", "CLAUDE.md", ".claude/", ".idea/", "tests/", "benches/", - "examples/", "docs/", + ".DS_Store", + ".nyx/", + ".z3-trace", + "target/", + "book/", ] autoexamples = false +[features] +default = ["serve"] +serve = ["dep:axum", "dep:tokio", "dep:tokio-stream", "dep:tower-http"] +smt = ["dep:z3", "z3/bundled"] +smt-system-z3 = ["dep:z3"] +# Build switch for the internal `nyx-docgen` tool. Empty on purpose: it +# only gates the [[bin]] target so consumers of `cargo install nyx-scanner` +# don't pick up the docgen binary. Maintainers run it via +# `cargo run --features docgen --bin nyx-docgen`. +docgen = [] + [lib] name = "nyx_scanner" path = "src/lib.rs" @@ -34,6 +51,11 @@ path = "src/lib.rs" name = "nyx" path = "src/main.rs" +[[bin]] +name = "nyx-docgen" +path = "tools/docgen/main.rs" +required-features = ["docgen"] + [[bench]] name = "scan_bench" harness = false @@ -44,6 +66,7 @@ criterion = { version = "0.8", features = ["html_reports"] } assert_cmd = "2" predicates = "3" glob = "0.3" +tower = { version = "0.5", features = ["util"] } [dependencies] directories = "6.0.0" @@ -54,8 +77,8 @@ toml = "1.0.3" tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json", "ansi","time"] } tracing = "0.1.44" num_cpus = "1.17.0" -rusqlite = { version = "0.38.0", features = ["bundled"] } -r2d2_sqlite = { version = "0.32.0", features = ["bundled"] } +rusqlite = { version = "0.39.0", features = ["bundled"] } +r2d2_sqlite = { version = "0.33.0", features = ["bundled"] } ignore = "0.4.25" tree-sitter = "0.26.6" tree-sitter-rust = "0.24.0" @@ -76,11 +99,24 @@ terminal_size = "0.4" rayon = "1.11.0" r2d2 = "0.8.10" bytesize = "2.3.1" -chrono = { version = "0.4.44", default-features = false, features = ["std", "clock"] } +chrono = { version = "0.4.44", default-features = false, features = ["std", "clock", "serde"] } thiserror = "2.0.18" -dashmap = "7.0.0-rc2" -petgraph = "0.8.3" +dashmap = "6.1.0" +parking_lot = "0.12" +petgraph = { version = "0.8.3", features = ["serde-1"] } bitflags = "2.11.0" phf = { version = "0.13.1", features = ["macros"] } indicatif = "0.18.4" -smallvec = "1.15" +smallvec = { version = "1.15", features = ["serde"] } +uuid = { version = "1", features = ["v4"] } +axum = { version = "0.8", optional = true } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync"], optional = true } +tokio-stream = { version = "0.1", features = ["sync"], optional = true } +tower-http = { version = "0.6", features = ["cors", "compression-gzip", "trace", "set-header", "limit"], optional = true } +z3 = { version = "0.20.0", optional = true} + +[profile.release] +lto = true +codegen-units = 1 +debug = 1 +strip = "none" diff --git a/README.md b/README.md index 062f42d9..1a05a787 100644 --- a/README.md +++ b/README.md @@ -1,405 +1,238 @@
- nyx logo + nyx -**Fast, cross-language cli vulnerability scanner.** +**A local-first security scanner with a browser UI. Scan your repo and triage in your browser, with no cloud and no account.** [![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.85+](https://img.shields.io/badge/rust-1.85%2B-orange)](https://www.rust-lang.org) +[![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)
---- - -## What is Nyx? - -**Nyx** is a lightweight, lightning-fast Rust-native command-line tool that detects security vulnerabilities across 10 programming languages. It combines [`tree-sitter`](https://tree-sitter.github.io/) parsing, intra-procedural control-flow graphs, and cross-file taint analysis with an optional SQLite-backed index to deliver deep, repeatable scans on projects of any size. +

Nyx UI walkthrough: scan, browse findings, inspect flow path, triage

--- -## Key Capabilities +## Scan locally, browse locally -| Capability | Description | +Nyx runs a cross-language taint analysis on your repository, then serves the results to a React UI bound to `127.0.0.1`. You get a finding list with severity, evidence, 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 +cargo install nyx-scanner +nyx scan # runs the analyzer, caches findings in .nyx/ +nyx serve # opens http://localhost:9700 in your browser +``` + +Everything stays on your machine: loopback-only bind, host-header enforcement, CSRF on every mutation, no telemetry, no login. + +

Overview dashboard after two scans: 2 findings remaining (down from 5), 3 fixed, a findings-over-time line trending down, plus severity/language/category breakdowns and top affected files

+ +--- + +## What's in the UI + +| Page | What it shows | |---|---| -| Multi-language support | Rust, C, C++, Java, Go, PHP, Python, Ruby, TypeScript, JavaScript | -| AST-level pattern matching | Language-specific queries written against precise parse trees | -| Control-flow graph analysis | Auth gaps, unguarded sinks, unreachable security code, resource leaks, error fallthrough | -| Cross-file taint tracking | Monotone forward dataflow taint analysis from sources through sanitizers to sinks with function summaries | -| Cross-language interop | Taint flows across language boundaries via explicit interop edges | -| Two-pass architecture | Pass 1 extracts function summaries; Pass 2 runs taint with full cross-file context | -| Incremental indexing | SQLite database stores file hashes, summaries, and findings to skip unchanged files | -| Parallel execution | File walking and analysis run concurrently via Rayon; scales with available CPU cores | -| Configurable analysis rules | Define custom sources, sanitizers, sinks, terminators, and event handlers per language via TOML config or CLI | -| Configurable scan parameters | Exclude directories, set maximum file size, tune worker threads, limit output, and more | -| Multiple output formats | Console (default), JSON, and SARIF 2.1.0 for CI integration | -| Progress reporting | Real-time progress bars for file discovery and analysis passes | +| **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), 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 [`docs/serve.md`](docs/serve.md) for the page-by-page UI tour and security model. --- -## Why choose Nyx? +## CLI for CI -| Advantage | What it means for you | -|---|---| -| **Pure-Rust, single binary** | No JVM, Python, or server to install; drop the `nyx` executable into your `$PATH` and go. | -| **Massively parallel** | Uses Rayon and a thread-pool walker; scales to all CPU cores. Scanning the entire **rust-lang/rust** codebase (~53,000 files) on an M2 MacBook Pro takes **~1 s**. | -| **Deep analysis** | Real CFG construction and monotone dataflow taint analysis with guaranteed termination, not just regex matching. Cross-file function summaries, capability-based sanitizer tracking, and scored findings. | -| **Index-aware** | An optional SQLite index stores file hashes and findings; subsequent scans touch *only* changed files, slashing CI times. | -| **Offline & privacy-friendly** | Requires no login, cloud account, or telemetry. Perfect for air-gapped environments and strict compliance policies. | -| **Tree-sitter precision** | Parses real language grammars, not regexes, giving far fewer false positives than line-based scanners. | -| **Extensible** | Add new patterns with concise `tree-sitter` queries; no SaaS lock-in. | +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 +# Fail the job on medium or higher, emit SARIF +nyx scan --format sarif --fail-on MEDIUM > results.sarif + +# Ad-hoc JSON, no index +nyx scan ./server --format json --index off + +# AST patterns only (fastest; skips CFG + taint) +nyx scan --mode ast + +# 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 +``` + +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 [`docs/cli.md`](docs/cli.md#engine-depth-profile) for the full toggle matrix. + +### GitHub Action + +```yaml +- uses: elicpeter/nyx@v0.5.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). --- -## Installation +## Install -### Install crate +**Cargo (recommended):** ```bash -$ cargo install nyx-scanner +cargo install nyx-scanner ``` -### Install Github release -1. Navigate to the [Releases](https://github.com/elicpeter/nyx/releases) page of the repository. -2. Download the appropriate binary for your system: - - ```nyx-x86_64-unknown-linux-gnu.zip``` for Linux - - ```nyx-x86_64-pc-windows-msvc.zip``` for Windows - - ```nyx-x86_64-apple-darwin.zip``` or ```nyx-aarch64-apple-darwin.zip``` for macOS (Intel or Apple Silicon) - -3. Unzip the file and move the executable to a directory in your system PATH: - ```bash - # Example for Unix systems - unzip nyx-x86_64-unknown-linux-gnu.zip - chmod +x nyx - sudo mv nyx /usr/local/bin/ - ``` - ```bash - # Example for Windows in PowerShell - Expand-Archive -Path nyx-x86_64-pc-windows-msvc.zip -DestinationPath . - Move-Item -Path .\nyx.exe -Destination "C:\Program Files\Nyx\" # Add to PATH manually if needed - ``` - -4. Verify the installation: - ```bash - nyx --version - ``` -### Build from source +**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 -$ git clone https://github.com/elicpeter/nyx.git -$ cd nyx -$ cargo build --release -# optional – copy the binary into PATH -$ cargo install --path . +# 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/ ``` -Nyx targets **stable Rust 1.85 or later**. +**From source:** +```bash +git clone https://github.com/elicpeter/nyx.git +cd nyx && cargo build --release +``` + +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`. --- -## Quick Start +## Languages -```bash -# Scan the current directory (creates/uses an index automatically) -$ nyx scan +All 10 languages parse via tree-sitter and run through the full pipeline, but rule depth is uneven. Tiers reflect benchmark F1 on the 305-case corpus at [`tests/benchmark/ground_truth.json`](tests/benchmark/ground_truth.json): -# Scan a specific path and emit JSON -$ nyx scan ./server --format json - -# Emit SARIF 2.1.0 for CI integration (GitHub Code Scanning, etc.) -$ nyx scan --format sarif > results.sarif - -# Perform an ad-hoc scan without touching the index -$ nyx scan --index off - -# Restrict results to high-severity findings -$ nyx scan --severity HIGH - -# Filter by severity expression (high and medium) -$ nyx scan --severity ">=MEDIUM" - -# AST pattern matching only (fastest, no CFG/taint) -$ nyx scan --mode ast - -# CFG + taint analysis only (skip AST pattern rules) -$ nyx scan --mode cfg - -# CI gate: fail on medium+, SARIF output -$ nyx scan --format sarif --fail-on MEDIUM > results.sarif - -# Suppress status messages (for CI/scripting) -$ nyx scan --quiet --format json - -# Include test/vendor/benchmark paths at original severity -# (by default these are downgraded one tier) -$ nyx scan --keep-nonprod-severity -``` - -### Index Management - -```bash -# Create or rebuild an index -$ nyx index build [PATH] [--force] - -# Display index metadata (size, modified date, etc.) -$ nyx index status [PATH] - -# List all indexed projects (add -v for detailed view) -$ nyx list [-v] - -# Remove a single project or purge all indexes -$ nyx clean -$ nyx clean --all -``` - -### Configuration Management - -```bash -# Print the effective merged configuration -$ nyx config show - -# Print the config directory path -$ nyx config path - -# Add a custom sanitizer rule (written to nyx.local) -$ nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape - -# Add a terminator function -$ nyx config add-terminator --lang javascript --name process.exit -``` - ---- - -## Analysis Modes - -Nyx supports four analysis modes, selectable via `--mode` or the `scanner.mode` config option: - -| Mode | CLI flag | What runs | -|---|---|---| -| **Full** (default) | `--mode full` | AST pattern matching + CFG construction + taint analysis | -| **AST-only** | `--mode ast` | AST pattern matching only; skips CFG and taint entirely | -| **CFG** | `--mode cfg` | CFG + taint analysis only; filters out AST pattern findings | -| **Taint** | `--mode taint` | Alias for `cfg` (CFG + taint analysis) | - -### What the CFG + taint engine detects - -| Finding | Rule ID | Description | -|---|---|---| -| Tainted data flow | `taint-*` | Untrusted data (env vars, user input, file reads) flowing to dangerous sinks (shell exec, SQL, file write) without matching sanitization | -| Unguarded sink | `cfg-unguarded-sink` | Sink calls not dominated by a guard or sanitizer on the control-flow path | -| Auth gap | `cfg-auth-gap` | Web handler functions that reach privileged sinks without an auth check | -| Unreachable security code | `cfg-unreachable-*` | Sanitizers, guards, or sinks in dead code branches | -| Error fallthrough | `cfg-error-fallthrough` | Error-handling branches that don't terminate, allowing execution to fall through to dangerous operations | -| Resource leak | `cfg-resource-leak` | Resources acquired but not released on all exit paths (malloc/free, fopen/fclose, Lock/Unlock) | -| Use-after-close | `state-use-after-close` | Variable read/written after its resource handle was closed | -| Double-close | `state-double-close` | Resource handle closed more than once | -| Must-leak | `state-resource-leak` | Resource acquired but never closed on any exit path | -| May-leak | `state-resource-leak-possible` | Resource open on some but not all exit paths | -| Unauthenticated access | `state-unauthed-access` | Sensitive sink reached without a preceding auth/admin check | - -### Attack Surface Ranking - -Every finding is assigned a deterministic **attack-surface score** that estimates exploitability using only information already in memory — no extra source passes are needed. Findings are sorted by descending score before truncation, so `max_results` always keeps the most important results. - -The score is the sum of five components: - -| Component | Weight | Description | -|---|---|---| -| **Severity base** | High = 60, Medium = 30, Low = 10 | Primary ordering signal. Severity reflects source-kind exploitability and rule confidence. | -| **Analysis kind** | taint = +10, state = +8, cfg = +3/+5, ast = 0 | Taint-confirmed flows are the strongest signal; AST-only pattern matches rank lowest at equal severity. CFG findings with evidence get +5, without get +3. | -| **Evidence strength** | +1 per evidence item (max 4), +2–6 for source kind | More evidence increases confidence. Source-kind priority: user input (+6) > env/config (+5) > unknown (+4) > file system (+3) > database (+2). | -| **State rule type** | +1 to +6 | Use-after-close and unauthenticated access (+6) rank above double-close (+3), must-leak (+2), and may-leak (+1). | -| **Path validation** | −5 | Findings on paths guarded by a validation predicate receive a small exploitability penalty — the guard may prevent triggering. | - -**Score ranges** (approximate): - -| Finding type | Score | -|---|---| -| High taint + user input | ~78 | -| High state (use-after-close) | ~74 | -| High CFG structural | ~63 | -| Medium taint + env source | ~47 | -| Medium state (resource leak) | ~40 | -| Low AST-only pattern | ~10 | - -Tie-breaking is deterministic: severity → rule ID → file path → line → column → message hash. The same set of findings always produces the same ordering regardless of parallelism or input order. - -Ranking is enabled by default. Disable it with `--no-rank` or `output.attack_surface_ranking = false` in config. When disabled, `rank_score` is omitted from JSON/SARIF output. - ---- - -## Supported Languages - -All 10 languages have full AST pattern matching and CFG/taint analysis. Resource leak detection is available where language-specific acquire/release pairs are defined. - -| Language | AST Patterns | CFG + Taint | Resource Leaks | +| Tier | Languages | F1 | Use as a CI gate? | |---|---|---|---| -| Rust | Yes | Yes | Yes | -| C | Yes | Yes | Yes | -| C++ | Yes | Yes | Yes | -| Java | Yes | Yes | Yes | -| Go | Yes | Yes | Yes | -| PHP | Yes | Yes | Yes | -| Python | Yes | Yes | Yes | -| Ruby | Yes | Yes | Yes | -| TypeScript | Yes | Yes | Yes | -| JavaScript | Yes | Yes | Yes | +| **Stable** | Python, JavaScript, TypeScript | 96.8% to 100% | Yes | +| **Beta** | Go, Java, Ruby, PHP | 92.9% to 97.0% | Yes, with light FP triage | +| **Preview** | C, C++ | 88.9% to 92.3% | No. Pair with clang-tidy or Clang Static Analyzer | +| **Experimental** | Rust | 86.4% | Review findings, don't block merges | + +Per-dimension detail and known blind spots live in [`docs/language-maturity.md`](docs/language-maturity.md). + +### Validated against real CVEs + +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. + +| 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-26159](https://nvd.nist.gov/vuln/detail/CVE-2023-26159) | follow-redirects | TypeScript | SSRF | +| [CVE-2022-30323](https://nvd.nist.gov/vuln/detail/CVE-2022-30323) | hashicorp/go-getter | Go | Command injection | +| [CVE-2015-7501](https://nvd.nist.gov/vuln/detail/CVE-2015-7501) | Apache Commons Collections | Java | Deserialization | +| [CVE-2013-0156](https://nvd.nist.gov/vuln/detail/CVE-2013-0156) | Ruby on Rails | Ruby | Deserialization | +| [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-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-2017-12629](https://nvd.nist.gov/vuln/detail/CVE-2017-12629) | Apache Solr | Java | Command injection | + +Fixtures live under [`tests/benchmark/cve_corpus/`](tests/benchmark/cve_corpus/) with upstream attribution headers. --- -## Configuration Overview +## How it works -Nyx merges a default configuration file (`nyx.conf`) with user overrides (`nyx.local`). Both live in the platform-specific configuration directory shown below. +Two passes over the filesystem, with an optional SQLite index to skip unchanged files: -| Platform | Directory | -|---|---| -| Linux | `~/.config/nyx/` | -| macOS | `~/Library/Application Support/nyx/` | -| Windows | `%APPDATA%\elicpeter\nyx\config\` | +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, emit**: findings are scored by severity × evidence strength × source-kind exploitability, then emitted to console, JSON, or SARIF. -Minimal example (`nyx.local`): +Detector families: taint (cross-file source→sink), CFG structural (auth gaps, unguarded sinks, resource leaks), state model (use-after-close, double-close, must-leak, unauthed-access), AST patterns (tree-sitter structural match). Full detector docs: [`docs/detectors.md`](docs/detectors.md). + +--- + +## 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 | taint -min_severity = "Medium" -follow_symlinks = true -excluded_extensions = ["mp3", "mp4"] +mode = "full" # full | ast | cfg | taint +min_severity = "Medium" -[output] -default_format = "json" -max_results = 200 -quiet = true # suppress status messages - -[performance] -worker_threads = 8 # 0 = auto-detect -batch_size = 200 -channel_multiplier = 2 -``` - -### Custom Analysis Rules - -You can define custom sources, sanitizers, sinks, terminators, and event handlers per language. These take priority over built-in rules, letting you teach Nyx about project-specific functions. - -```toml -[analysis.languages.javascript] -terminators = ["process.exit"] -event_handlers = ["addEventListener"] +[server] +host = "127.0.0.1" +port = 9700 +open_browser = true +# Project-specific sanitizer [[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" | "all" - -[[analysis.languages.javascript.rules]] -matchers = ["dangerouslySetHTML"] -kind = "sink" -cap = "html_escape" +kind = "sanitizer" +cap = "html_escape" ``` -Rules can also be added interactively via `nyx config add-rule` and `nyx config add-terminator`. - -A fully documented `nyx.conf` is generated automatically on first run. +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`, `code_exec`, `crypto`, `unauthorized_id`, `all`. Full schema: [`docs/configuration.md`](docs/configuration.md). --- -## Architecture in Brief +## Status -Nyx uses a **two-pass architecture** to enable cross-file analysis without sacrificing parallelism: +Under active development. APIs, detector behavior, and configuration options may change between releases. Rule-level F1 on the 305-case corpus is the CI regression floor; per-language detail lives in [`tests/benchmark/RESULTS.md`](tests/benchmark/RESULTS.md). -1. **File enumeration** -- A parallel walker (Rayon + `ignore` crate) applies gitignore rules, size limits, and user exclusions. -2. **Pass 1 -- Summary extraction** -- Each file is parsed via tree-sitter, an intra-procedural CFG is built (petgraph), and a `FuncSummary` is exported per function capturing source/sanitizer/sink capabilities (bitflags), taint propagation behavior, and callee lists. Summaries are persisted to SQLite. -3. **Summary merge** -- All per-file summaries are merged into a `GlobalSummaries` map with conservative conflict resolution (union caps, OR booleans). -4. **Pass 2 -- Analysis** -- Files are re-parsed and analyzed with the full cross-file context: a monotone forward dataflow engine resolves callees against local and global summaries and propagates taint through a bounded lattice with guaranteed convergence. CFG analysis checks for auth gaps, unguarded sinks, resource leaks, and more. -5. **Reporting** -- Findings are scored, ranked, deduplicated, and emitted to the console or serialized as JSON. +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`. -With indexing enabled, Pass 1 skips files whose blake3 content hash is unchanged, and cached findings are served directly for AST-only results. - ---- - -## Roadmap - -### Phase 1 -- Deep Static Engine (Complete) - -| Feature | Status | Description | -|---|--------|---| -| Interprocedural call graph | Done | Precise symbol resolution via `FuncKey`, language-scoped namespaces, cross-module linking. Full call graph with SCC and topological analysis. | -| Path-sensitive analysis | Done | Track path predicates and conditional constraints. Detect infeasible paths and validation-only-in-one-branch patterns. Monotone predicate summaries with contradiction pruning. | -| Dataflow & state modeling | Done | Resource state machines (init -> use -> close), auth state transitions, privilege level tracking. Generic `Transfer` trait over bounded lattices with guaranteed convergence. | -| Monotone taint analysis | Done | Replaced BFS taint engine with a forward worklist dataflow analysis over a finite `TaintState` lattice. Multi-origin tracking, dual validated-must/may sets, JS/TS two-level solve. Guaranteed termination via lattice finiteness. | -| Attack surface ranking | Done | Deterministic post-analysis scoring of findings by severity, analysis kind, evidence strength, source-kind exploitability, and validation state. Findings sorted by score before truncation so `max_results` keeps the most important results. | -| Inline suppressions | Done | `nyx:ignore` and `nyx:ignore-next-line` comments with wildcard matching, all 10 languages supported. `--show-suppressed` flag for visibility. | -| Low-noise prioritization | Done | Category filtering, rollup grouping for high-frequency rules, configurable LOW budgets. Quality-category findings hidden by default. | -| Pattern-level confidence | Done | Explicit High/Medium/Low confidence on every AST pattern. Confidence flows into output alongside severity and rank score. | -| AST pattern overhaul | Done | 30+ new patterns across all languages, 11 broken query fixes, namespaced IDs, severity recalibration. | - -### Phase 2 -- Dynamic Capability - -| Feature | Description | -|---|---| -| Controlled dynamic execution | Local sandbox: identify entry points, spin up test harnesses, inject payloads, detect runtime crashes and command execution. Deterministic automated exploit validation -- static finds `exec(user_input)`, dynamic confirms it with `; id`. | -| Fuzzing integration | libFuzzer (C/C++), cargo-fuzz (Rust), go-fuzz, HTTP fuzzing harness. Static engine identifies interesting functions, fuzzer targets only those. | - -### Phase 3 -- Intelligent Reasoning Layer - -| Feature | Description | -|---|---| -| Semantic similarity | Embeddings for finding similar vulnerability patterns across codebases. | -| LLM reasoning | AI-assisted detection of non-obvious logic bugs. | -| Exploit refinement | Automated loops to refine and validate exploit chains. | - -### Other planned improvements - -| Area | Details | -|---|---| -| Output formats | JUnit XML, HTML report generator | -| Language coverage | Expanded taint rules per language | -| Rule updates | Remote rule feed with signature verification | -| UX | Smart file-watch re-scan | - -Community feedback shapes priorities -- please [open an issue](https://github.com/elicpeter/nyx/issues) to discuss proposed changes. +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. +- Rust is experimental tier; C/C++ are preview tier. Pair them 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 -Full documentation is available in the [`docs/`](docs/index.md) directory: - -- [Installation](docs/installation.md) — cargo, binaries, CI tips -- [Quick Start](docs/quickstart.md) — Your first scan in 60 seconds -- [CLI Reference](docs/cli.md) — Every flag and subcommand -- [Configuration](docs/configuration.md) — Config file schema, custom rules -- [Output Formats](docs/output.md) — Console, JSON, SARIF; exit codes -- [Detector Overview](docs/detectors.md) — How the four detector families work - - [Taint Analysis](docs/detectors/taint.md) — Cross-file source-to-sink dataflow - - [CFG Structural](docs/detectors/cfg.md) — Auth gaps, unguarded sinks, resource leaks - - [State Model](docs/detectors/state.md) — Resource lifecycle, authentication state - - [AST Patterns](docs/detectors/patterns.md) — Tree-sitter structural matching -- [Rule Reference](docs/rules/index.md) — Per-language rule listings with examples +- [Quick Start](docs/quickstart.md) · [CLI Reference](docs/cli.md) · [Installation](docs/installation.md) +- [`nyx serve`](docs/serve.md) · [Output Formats](docs/output.md) · [Configuration](docs/configuration.md) +- [How it works](docs/how-it-works.md) · [Detectors](docs/detectors.md) ([Taint](docs/detectors/taint.md), [CFG](docs/detectors/cfg.md), [State](docs/detectors/state.md), [AST Patterns](docs/detectors/patterns.md)) +- [Rule Reference](docs/rules.md) · [Language Maturity](docs/language-maturity.md) · [Advanced Analysis](docs/advanced-analysis.md) · [Auth Analysis](docs/auth.md) --- ## Contributing -Pull requests are welcome. To contribute: +Contributions are welcome. -1. Fork the repository and create a feature branch. -2. Adhere to `rustfmt` and ensure `cargo clippy --all -- -D warnings` passes. -3. Add unit and/or integration tests where applicable (`cargo test` should remain green). -4. Submit a concise, well-documented pull request. +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. -Please open an issue for any crash, panic, or suspicious result -- attach the minimal code snippet and mention the Nyx version. +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. -See [`CONTRIBUTING.md`](CONTRIBUTING.md) for full guidelines, including how to add new rules and support new languages. +--- + +## 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 -Nyx is licensed under the **GNU General Public License v3.0 (GPL-3.0)**. - -This ensures that all modified versions of the scanner remain free and open-source, protecting the integrity and transparency of security tools. - -See [LICENSE](./LICENSE) for full details. +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/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..94137664 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,22 @@ +# Roadmap + +Nyx today is a static-only multi-language vulnerability scanner. The roadmap below extends it into a hybrid scanner that combines static analysis with controlled execution and AI-assisted reasoning. + +## Phase 1 — Static Analysis (current) + +The shipped scanner. Multi-language taint tracking on a pruned SSA IR, cross-file function summaries, points-to and abstract interpretation, symbolic execution with an optional SMT backend, and a local web UI for triage. See the [Changelog](CHANGELOG.md) for the full breakdown of what's landed through 0.5.0. + +## Phase 2 — Dynamic Capability + +| Feature | Description | +| --- | --- | +| Controlled dynamic execution | Local sandbox: identify entry points, spin up test harnesses, inject payloads, detect runtime crashes and command execution. Deterministic automated exploit validation — static finds `exec(user_input)`, dynamic confirms it with `; id`. | +| Fuzzing integration | libFuzzer (C/C++), cargo-fuzz (Rust), go-fuzz, HTTP fuzzing harness. Static engine identifies interesting functions, fuzzer targets only those. | + +## Phase 3 — Intelligent Reasoning Layer + +| Feature | Description | +| --- | --- | +| Semantic similarity | Embeddings for finding similar vulnerability patterns across codebases. | +| LLM reasoning | AI-assisted detection of non-obvious logic bugs. | +| Exploit refinement | Automated loops to refine and validate exploit chains. | diff --git a/SECURITY.md b/SECURITY.md index 06c4ecc8..e730bacc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,9 +4,9 @@ | Version | Supported | Notes | |---------|-----------|----------------------| -| 0.4.x | ✅ | Latest stable line | -| 0.3.x | ✅ | Critical fixes only | -| < 0.3 | ❌ | End-of-life | +| 0.5.x | ✅ | Latest stable line | +| 0.4.x | ✅ | Critical fixes only | +| < 0.4 | ❌ | End-of-life | We follow [Semantic Versioning] as soon as we hit **1.0.0**. Before that, breaking changes may land in any minor release. diff --git a/THIRDPARTY-LICENSES.html b/THIRDPARTY-LICENSES.html new file mode 100644 index 00000000..84ca7a8c --- /dev/null +++ b/THIRDPARTY-LICENSES.html @@ -0,0 +1,6671 @@ + + + + + + + +
+
+

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
    +                     https://www.apache.org/licenses/LICENSE-2.0
    +
    +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +1. Definitions.
    +
    +   "License" shall mean the terms and conditions for use, reproduction,
    +   and distribution as defined by Sections 1 through 9 of this document.
    +
    +   "Licensor" shall mean the copyright owner or entity authorized by
    +   the copyright owner that is granting the License.
    +
    +   "Legal Entity" shall mean the union of the acting entity and all
    +   other entities that control, are controlled by, or are under common
    +   control with that entity. For the purposes of this definition,
    +   "control" means (i) the power, direct or indirect, to cause the
    +   direction or management of such entity, whether by contract or
    +   otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +   outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +   "You" (or "Your") shall mean an individual or Legal Entity
    +   exercising permissions granted by this License.
    +
    +   "Source" form shall mean the preferred form for making modifications,
    +   including but not limited to software source code, documentation
    +   source, and configuration files.
    +
    +   "Object" form shall mean any form resulting from mechanical
    +   transformation or translation of a Source form, including but
    +   not limited to compiled object code, generated documentation,
    +   and conversions to other media types.
    +
    +   "Work" shall mean the work of authorship, whether in Source or
    +   Object form, made available under the License, as indicated by a
    +   copyright notice that is included in or attached to the work
    +   (an example is provided in the Appendix below).
    +
    +   "Derivative Works" shall mean any work, whether in Source or Object
    +   form, that is based on (or derived from) the Work and for which the
    +   editorial revisions, annotations, elaborations, or other modifications
    +   represent, as a whole, an original work of authorship. For the purposes
    +   of this License, Derivative Works shall not include works that remain
    +   separable from, or merely link (or bind by name) to the interfaces of,
    +   the Work and Derivative Works thereof.
    +
    +   "Contribution" shall mean any work of authorship, including
    +   the original version of the Work and any modifications or additions
    +   to that Work or Derivative Works thereof, that is intentionally
    +   submitted to Licensor for inclusion in the Work by the copyright owner
    +   or by an individual or Legal Entity authorized to submit on behalf of
    +   the copyright owner. For the purposes of this definition, "submitted"
    +   means any form of electronic, verbal, or written communication sent
    +   to the Licensor or its representatives, including but not limited to
    +   communication on electronic mailing lists, source code control systems,
    +   and issue tracking systems that are managed by, or on behalf of, the
    +   Licensor for the purpose of discussing and improving the Work, but
    +   excluding communication that is conspicuously marked or otherwise
    +   designated in writing by the copyright owner as "Not a Contribution."
    +
    +   "Contributor" shall mean Licensor and any individual or Legal Entity
    +   on behalf of whom a Contribution has been received by Licensor and
    +   subsequently incorporated within the Work.
    +
    +2. Grant of Copyright License. Subject to the terms and conditions of
    +   this License, each Contributor hereby grants to You a perpetual,
    +   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +   copyright license to reproduce, prepare Derivative Works of,
    +   publicly display, publicly perform, sublicense, and distribute the
    +   Work and such Derivative Works in Source or Object form.
    +
    +3. Grant of Patent License. Subject to the terms and conditions of
    +   this License, each Contributor hereby grants to You a perpetual,
    +   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +   (except as stated in this section) patent license to make, have made,
    +   use, offer to sell, sell, import, and otherwise transfer the Work,
    +   where such license applies only to those patent claims licensable
    +   by such Contributor that are necessarily infringed by their
    +   Contribution(s) alone or by combination of their Contribution(s)
    +   with the Work to which such Contribution(s) was submitted. If You
    +   institute patent litigation against any entity (including a
    +   cross-claim or counterclaim in a lawsuit) alleging that the Work
    +   or a Contribution incorporated within the Work constitutes direct
    +   or contributory patent infringement, then any patent licenses
    +   granted to You under this License for that Work shall terminate
    +   as of the date such litigation is filed.
    +
    +4. Redistribution. You may reproduce and distribute copies of the
    +   Work or Derivative Works thereof in any medium, with or without
    +   modifications, and in Source or Object form, provided that You
    +   meet the following conditions:
    +
    +   (a) You must give any other recipients of the Work or
    +       Derivative Works a copy of this License; and
    +
    +   (b) You must cause any modified files to carry prominent notices
    +       stating that You changed the files; and
    +
    +   (c) You must retain, in the Source form of any Derivative Works
    +       that You distribute, all copyright, patent, trademark, and
    +       attribution notices from the Source form of the Work,
    +       excluding those notices that do not pertain to any part of
    +       the Derivative Works; and
    +
    +   (d) If the Work includes a "NOTICE" text file as part of its
    +       distribution, then any Derivative Works that You distribute must
    +       include a readable copy of the attribution notices contained
    +       within such NOTICE file, excluding those notices that do not
    +       pertain to any part of the Derivative Works, in at least one
    +       of the following places: within a NOTICE text file distributed
    +       as part of the Derivative Works; within the Source form or
    +       documentation, if provided along with the Derivative Works; or,
    +       within a display generated by the Derivative Works, if and
    +       wherever such third-party notices normally appear. The contents
    +       of the NOTICE file are for informational purposes only and
    +       do not modify the License. You may add Your own attribution
    +       notices within Derivative Works that You distribute, alongside
    +       or as an addendum to the NOTICE text from the Work, provided
    +       that such additional attribution notices cannot be construed
    +       as modifying the License.
    +
    +   You may add Your own copyright statement to Your modifications and
    +   may provide additional or different license terms and conditions
    +   for use, reproduction, or distribution of Your modifications, or
    +   for any such Derivative Works as a whole, provided Your use,
    +   reproduction, and distribution of the Work otherwise complies with
    +   the conditions stated in this License.
    +
    +5. Submission of Contributions. Unless You explicitly state otherwise,
    +   any Contribution intentionally submitted for inclusion in the Work
    +   by You to the Licensor shall be under the terms and conditions of
    +   this License, without any additional terms or conditions.
    +   Notwithstanding the above, nothing herein shall supersede or modify
    +   the terms of any separate license agreement you may have executed
    +   with Licensor regarding such Contributions.
    +
    +6. Trademarks. This License does not grant permission to use the trade
    +   names, trademarks, service marks, or product names of the Licensor,
    +   except as required for reasonable and customary use in describing the
    +   origin of the Work and reproducing the content of the NOTICE file.
    +
    +7. Disclaimer of Warranty. Unless required by applicable law or
    +   agreed to in writing, Licensor provides the Work (and each
    +   Contributor provides its Contributions) on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +   implied, including, without limitation, any warranties or conditions
    +   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +   PARTICULAR PURPOSE. You are solely responsible for determining the
    +   appropriateness of using or redistributing the Work and assume any
    +   risks associated with Your exercise of permissions under this License.
    +
    +8. Limitation of Liability. In no event and under no legal theory,
    +   whether in tort (including negligence), contract, or otherwise,
    +   unless required by applicable law (such as deliberate and grossly
    +   negligent acts) or agreed to in writing, shall any Contributor be
    +   liable to You for damages, including any direct, indirect, special,
    +   incidental, or consequential damages of any character arising as a
    +   result of this License or out of the use or inability to use the
    +   Work (including but not limited to damages for loss of goodwill,
    +   work stoppage, computer failure or malfunction, or any and all
    +   other commercial damages or losses), even if such Contributor
    +   has been advised of the possibility of such damages.
    +
    +9. Accepting Warranty or Additional Liability. While redistributing
    +   the Work or Derivative Works thereof, You may choose to offer,
    +   and charge a fee for, acceptance of support, warranty, indemnity,
    +   or other liability obligations and/or rights consistent with this
    +   License. However, in accepting such obligations, You may act only
    +   on Your own behalf and on Your sole responsibility, not on behalf
    +   of any other Contributor, and only if You agree to indemnify,
    +   defend, and hold each Contributor harmless for any liability
    +   incurred by, or claims asserted against, such Contributor by reason
    +   of your accepting any such warranty or additional liability.
    +
    +END OF TERMS AND CONDITIONS
    +
    +APPENDIX: How to apply the Apache License to your work.
    +
    +   To apply the Apache License to your work, attach the following
    +   boilerplate notice, with the fields enclosed by brackets "[]"
    +   replaced with your own identifying information. (Don't include
    +   the brackets!)  The text should be enclosed in the appropriate
    +   comment syntax for the file format. We also recommend that a
    +   file or class name and description of purpose be included on the
    +   same "printed page" as the copyright notice for easier
    +   identification within third-party archives.
    +
    +Copyright [yyyy] [name of copyright owner]
    +
    +Licensed under the Apache License, Version 2.0 (the "License");
    +you may not use this file except in compliance with the License.
    +You may obtain a copy of the License at
    +
    +	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) 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) 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:

    + +
    MIT License
    +
    +Copyright (c) [2021] [Marvin Countryman]
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +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.toml b/about.toml index e9e60887..4304a25b 100644 --- a/about.toml +++ b/about.toml @@ -1,12 +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", - "Unicode-3.0", "BSD-2-Clause", - "Unlicense", + "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", - "GPL-3.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..fb9c8d4a --- /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.5.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..17cee50c --- /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.5.0). "latest" is accepted but discouraged, pinning to a specific tag protects against upstream compromise.' + required: false + default: 'v0.5.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/nyx-logo-text.png b/assets/nyx-logo-text.png new file mode 100644 index 00000000..e0fc04e2 Binary files /dev/null and b/assets/nyx-logo-text.png differ diff --git a/assets/nyx-logo.png b/assets/nyx-logo.png new file mode 100644 index 00000000..e261bf28 Binary files /dev/null and b/assets/nyx-logo.png differ diff --git a/assets/nyx-wordmark.svg b/assets/nyx-wordmark.svg new file mode 100644 index 00000000..b989eb3c --- /dev/null +++ b/assets/nyx-wordmark.svg @@ -0,0 +1,10 @@ + + nyx + diff --git a/assets/screenshots/cli-scan.png b/assets/screenshots/cli-scan.png new file mode 100644 index 00000000..0c030d6f Binary files /dev/null and b/assets/screenshots/cli-scan.png differ diff --git a/assets/screenshots/demo.gif b/assets/screenshots/demo.gif new file mode 100644 index 00000000..14fc2e55 Binary files /dev/null and b/assets/screenshots/demo.gif differ diff --git a/assets/screenshots/docs/cli-configshow.png b/assets/screenshots/docs/cli-configshow.png new file mode 100644 index 00000000..7949dd62 Binary files /dev/null and b/assets/screenshots/docs/cli-configshow.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..a8ac4419 Binary files /dev/null and b/assets/screenshots/docs/cli-explain-engine.png differ diff --git a/assets/screenshots/docs/cli-failon.png b/assets/screenshots/docs/cli-failon.png new file mode 100644 index 00000000..087c4916 Binary files /dev/null and b/assets/screenshots/docs/cli-failon.png differ diff --git a/assets/screenshots/docs/cli-idxstatus.png b/assets/screenshots/docs/cli-idxstatus.png new file mode 100644 index 00000000..338b5871 Binary files /dev/null and b/assets/screenshots/docs/cli-idxstatus.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..8e6a0662 Binary files /dev/null and b/assets/screenshots/docs/cli-rollup-tail.png differ diff --git a/assets/screenshots/docs/cli-scan-quickstart.png b/assets/screenshots/docs/cli-scan-quickstart.png new file mode 100644 index 00000000..ae189dea Binary files /dev/null and b/assets/screenshots/docs/cli-scan-quickstart.png differ diff --git a/assets/screenshots/docs/serve-config.png b/assets/screenshots/docs/serve-config.png new file mode 100644 index 00000000..6d918741 Binary files /dev/null and b/assets/screenshots/docs/serve-config.png differ diff --git a/assets/screenshots/docs/serve-explorer.png b/assets/screenshots/docs/serve-explorer.png new file mode 100644 index 00000000..6a83d99f Binary files /dev/null and b/assets/screenshots/docs/serve-explorer.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..169815cd Binary files /dev/null and b/assets/screenshots/docs/serve-finding-detail.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..a7722f73 Binary files /dev/null and b/assets/screenshots/docs/serve-findings-list.png differ diff --git a/assets/screenshots/docs/serve-overview.png b/assets/screenshots/docs/serve-overview.png new file mode 100644 index 00000000..70690ad8 Binary files /dev/null and b/assets/screenshots/docs/serve-overview.png differ diff --git a/assets/screenshots/docs/serve-rules.png b/assets/screenshots/docs/serve-rules.png new file mode 100644 index 00000000..b52fcb22 Binary files /dev/null and b/assets/screenshots/docs/serve-rules.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..7f6d7abf Binary files /dev/null and b/assets/screenshots/docs/serve-scan-detail.png differ diff --git a/assets/screenshots/docs/serve-scans.png b/assets/screenshots/docs/serve-scans.png new file mode 100644 index 00000000..524406c1 Binary files /dev/null and b/assets/screenshots/docs/serve-scans.png differ diff --git a/assets/screenshots/docs/serve-triage.png b/assets/screenshots/docs/serve-triage.png new file mode 100644 index 00000000..5fb3471b Binary files /dev/null and b/assets/screenshots/docs/serve-triage.png differ diff --git a/assets/screenshots/explorer.png b/assets/screenshots/explorer.png new file mode 100644 index 00000000..44f62968 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..90ae41cd 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..9dbb739d 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..c6807db6 Binary files /dev/null and b/assets/screenshots/triage.png differ 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/scan_bench.rs b/benches/scan_bench.rs index df5eaf48..d11f1fac 100644 --- a/benches/scan_bench.rs +++ b/benches/scan_bench.rs @@ -73,6 +73,47 @@ fn bench_full_scan(c: &mut Criterion) { }); } +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"); @@ -86,6 +127,40 @@ fn bench_single_file_parse_and_cfg(c: &mut Criterion) { }); } +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(), + ) + }); + }); +} + fn bench_classify(c: &mut Criterion) { c.bench_function("classify_hit", |b| { b.iter(|| nyx_scanner::labels::classify("rust", "std::env::var", None)); @@ -100,7 +175,9 @@ 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, ); criterion_main!(benches); diff --git a/book.toml b/book.toml new file mode 100644 index 00000000..a3e541da --- /dev/null +++ b/book.toml @@ -0,0 +1,20 @@ +[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/" + +[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..074b0730 --- /dev/null +++ b/build.rs @@ -0,0 +1,72 @@ +use std::path::Path; +use std::process::Command; + +fn main() { + // 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." + ); +} diff --git a/default-nyx.conf b/default-nyx.conf index 097890f1..919635e4 100644 --- a/default-nyx.conf +++ b/default-nyx.conf @@ -8,16 +8,20 @@ [scanner] -## If full uses both ast patterns and cfg taint analysis, -## Possible values: full | ast | cfg +## 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 = [ @@ -34,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. @@ -54,28 +58,44 @@ 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 "taint" (needs CFG). Default: off. -enable_state_analysis = false +## 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 + +## 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: console | json | sarif +## Default output format: console | json | sarif +## Used when --format is not specified on the command line. default_format = "console" ## Suppress all human-readable status output (stderr) @@ -120,13 +140,13 @@ 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 @@ -139,16 +159,165 @@ batch_size = 100 channel_multiplier = 4 ## Maximum stack size for Rayon threads (bytes) -rayon_thread_stack_size = 8 * 1024 * 1024 # 8 MiB +rayon_thread_stack_size = 8388608 # 8 MiB -## Timeout on individual files (seconds); null = none (UNIMPLEMENTED) +## 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 + + # ─── 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. @@ -171,4 +340,6 @@ memory_limit_mb = 512 # # Valid `kind` values: "source", "sanitizer", "sink" # Valid `cap` values: "env_var", "html_escape", "shell_escape", -# "url_encode", "json_parse", "file_io", "all" +# "url_encode", "json_parse", "file_io", +# "fmt_string", "sql_query", "deserialize", +# "ssrf", "code_exec", "crypto", "all" diff --git a/deny.toml b/deny.toml index 47f90fb6..644d9a1e 100644 --- a/deny.toml +++ b/deny.toml @@ -1,13 +1,68 @@ [licenses] allow = [ + # --- Apache / MIT / BSD / permissive --- "Apache-2.0", "MIT", "MIT-0", - "Unicode-3.0", "BSD-2-Clause", - "Unlicense", + "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..faad2b38 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,29 @@ +# Summary + +# Getting started + +- [Quickstart](quickstart.md) +- [Installation](installation.md) + +# Using nyx + +- [CLI reference](cli.md) +- [Browser UI](serve.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) diff --git a/docs/advanced-analysis.md b/docs/advanced-analysis.md new file mode 100644 index 00000000..0444007c --- /dev/null +++ b/docs/advanced-analysis.md @@ -0,0 +1,221 @@ +# Advanced Analysis + +Nyx ships four optional analysis passes that layer on top of the core SSA +taint engine. Each pass is independently switchable via config +(`[analysis.engine]` in `nyx.conf` / `nyx.local`), a matching CLI flag pair, +or; as a legacy last-resort override for library users with no CLI entry +point; a `NYX_*` environment variable. All four are **on by default**: turning +them off trades precision for speed. + +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. + +**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. + +**Source**: [`src/taint/ssa_transfer.rs`](https://github.com/elicpeter/nyx/blob/master/src/taint/ssa_transfer.rs) +(`ArgTaintSig`, `InlineCache`, `inline_analyse_callee`). + +--- + +## 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 (first cut).** Reverse call-graph expansion past a +`ReachedParam` is deferred; 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. + +**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..f0db642b --- /dev/null +++ b/docs/auth.md @@ -0,0 +1,91 @@ +# Auth analysis + +**Rust today.** Other languages have rule scaffolding in [`src/auth_analysis/config.rs`](https://github.com/elicpeter/nyx/blob/master/src/auth_analysis/config.rs) (Python, Ruby, Go, Java, JavaScript, TypeScript), but only Rust has benchmark corpus coverage and the precision work to back it. Treat findings on other languages as preview; the rule prefix (`py.auth.*`, `js.auth.*`, `rb.auth.*`, `go.auth.*`, `java.auth.*`) is reserved but the matchers haven't been validated against real codebases yet. + +## 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 five 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 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). + +## 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. + +### 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 is in [`src/auth_analysis/checks.rs`](https://github.com/elicpeter/nyx/blob/master/src/auth_analysis/checks.rs) under `extract_validation_target` and friends. + +### 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

+ +## Where the work was done + +The remediation work is documented release-by-release in `tests/benchmark/RESULTS.md` under the Rust auth row. Phases A1 through B5 (precision and structural improvements) and Phase C (taint-based variant) all landed on the 0.5.0 release branch. The benchmark corpus at [`tests/benchmark/corpus/rust/auth/`](https://github.com/elicpeter/nyx/tree/master/tests/benchmark/corpus/rust/auth/) is 10 fixtures covering the five FP patterns plus a true-positive control. 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 index 82e24715..79936b42 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -53,8 +53,15 @@ nyx scan [PATH] [OPTIONS] | Flag | Default | Description | |------|---------|-------------| | `-f, --format ` | `console` | Output format: `console`, `json`, or `sarif` | -| `--quiet` | off | Suppress status messages (stderr); stdout stays clean | +| `--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 @@ -63,10 +70,11 @@ nyx scan [PATH] [OPTIONS] | `--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 — show everything | +| `--all` | off | Disable category filtering, rollups, and LOW budgets -- show 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 | @@ -85,6 +93,65 @@ nyx scan [PATH] [OPTIONS] **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

+ ### Examples ```bash @@ -148,6 +215,8 @@ 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` @@ -185,7 +254,9 @@ Manage configuration. ### `nyx config show` -Print the effective merged configuration as TOML. +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` @@ -204,7 +275,7 @@ Add a custom taint rule. Written to `nyx.local`. | `--lang` | `rust`, `javascript`, `typescript`, `python`, `go`, `java`, `c`, `cpp`, `php`, `ruby` | | `--matcher` | Function or property name to match | | `--kind` | `source`, `sanitizer`, `sink` | -| `--cap` | `env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `all` | +| `--cap` | `env_var`, `html_escape`, `shell_escape`, `url_encode`, `json_parse`, `file_io`, `fmt_string`, `sql_query`, `deserialize`, `ssrf`, `code_exec`, `crypto`, `unauthorized_id`, `all` | ### `nyx config add-terminator` @@ -216,19 +287,30 @@ Add a terminator function (e.g. `process.exit`). Written to `nyx.local`. --- -## Exit Codes +## Exit codes -| Code | Meaning | -|------|---------| -| `0` | Scan completed; no findings matched `--fail-on` threshold (or no `--fail-on` specified) | -| `1` | Scan completed but at least one finding met or exceeded the `--fail-on` severity | -| Non-zero | Error during scan (I/O error, config parse error, database error, etc.) | +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 +## 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 index 2d884b01..a001bf9b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,8 @@ # Configuration -Nyx uses TOML configuration files. A default config is auto-generated on first run. +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 @@ -14,8 +16,8 @@ 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) +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. @@ -24,8 +26,10 @@ Both files are optional. CLI flags take precedence over both. | Type | Behavior | |------|----------| | Scalars (`mode`, `min_severity`, booleans) | User value wins | -| Arrays (`excluded_extensions`, `excluded_directories`) | Union + deduplicate | +| 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 @@ -36,7 +40,7 @@ excluded_extensions = ["jpg", "png", "exe"] excluded_extensions = ["foo", "jpg"] # Effective result: -# ["exe", "foo", "jpg", "png"] — sorted, deduped union +# ["exe", "foo", "jpg", "png"] -- sorted, deduped union ``` --- @@ -49,30 +53,33 @@ excluded_extensions = ["foo", "jpg"] |-------|------|---------|-------------| | `mode` | `"full"` \| `"ast"` \| `"cfg"` \| `"taint"` | `"full"` | Analysis mode | | `min_severity` | `"Low"` \| `"Medium"` \| `"High"` | `"Low"` | Minimum severity to report | -| `max_file_size_mb` | int \| null | null | Max file size in MiB; null = unlimited | +| `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 | +| `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 | `false` | 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 = "cfg"`. | +| `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"`. | ### `[database]` | Field | Type | Default | Description | |-------|------|---------|-------------| -| `path` | string | `""` | Custom SQLite DB path; empty = platform default | +| `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 | +| `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 | @@ -89,11 +96,122 @@ excluded_extensions = ["foo", "jpg"] | 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) | -| `prune` | bool | `false` | Stop traversing into matching directories | +| `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 | + +### `[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_PARSE_TIMEOUT_MS`, +`NYX_SMT`); those env vars are still honored as a last-resort override 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 | + +**`[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` | + +**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. ### `[analysis.languages.]` @@ -112,7 +230,9 @@ Per-language custom rules. `` is one of: `rust`, `javascript`, `typescript matchers = ["escapeHtml"] kind = "sanitizer" # "source" | "sanitizer" | "sink" cap = "html_escape" # "env_var" | "html_escape" | "shell_escape" | - # "url_encode" | "json_parse" | "file_io" | "all" + # "url_encode" | "json_parse" | "file_io" | + # "fmt_string" | "sql_query" | "deserialize" | + # "ssrf" | "code_exec" | "crypto" | "all" ``` --- @@ -146,6 +266,26 @@ default_format = "sarif" 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 @@ -181,3 +321,93 @@ 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–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 so 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 (0.4.0 → 0.5.0), 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. This allows forward-compatible config files; settings will activate when the feature is implemented without requiring config changes. diff --git a/docs/detectors.md b/docs/detectors.md index 32070724..f3f95fdf 100644 --- a/docs/detectors.md +++ b/docs/detectors.md @@ -1,81 +1,68 @@ -# Detector Overview +# Detectors -Nyx uses four independent detector families. Each targets different vulnerability classes and operates at a different level of analysis depth. Findings from all active detectors are merged, deduplicated, ranked, and presented in a single result set. +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. -## The Four Detector Families +| 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 | -| Family | Rule prefix | Analysis depth | What it finds | -|--------|------------|----------------|---------------| -| [**Taint Analysis**](detectors/taint.md) | `taint-*` | Cross-file dataflow | Unsanitized data flowing from sources to sinks | -| [**CFG Structural**](detectors/cfg.md) | `cfg-*` | Intra-procedural CFG | Auth gaps, unguarded sinks, resource leaks, error fallthrough | -| [**State Model**](detectors/state.md) | `state-*` | Intra-procedural lattice | Use-after-close, double-close, resource leaks, unauthenticated access | -| [**AST Patterns**](detectors/patterns.md) | `.*.*` | Structural (no flow) | Dangerous function calls, banned APIs, weak crypto | +For Rust auth-specific rules (`rs.auth.*`), see [auth.md](auth.md). -## How They Combine +## How they combine -In `--mode full` (default), all four families run. Findings are deduplicated: +In `--mode full`: -1. **Taint supersedes AST**: If a taint finding and an AST pattern both fire at the same location (e.g. both flag `eval(userInput)`), both are kept with distinct rule IDs. The taint finding ranks higher due to the analysis-kind bonus. +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. -2. **State supersedes CFG**: If a state-model finding (e.g. `state-resource-leak`) fires at the same location as a CFG finding (e.g. `cfg-resource-leak`), the CFG finding is suppressed. +## Modes -3. **Location-level dedup**: Exact duplicates (same line, column, rule ID, severity) are removed. +| Mode | Active detectors | +|---|---| +| `full` (default) | All four | +| `ast` | AST patterns only | +| `cfg` | Taint + CFG + State (no AST patterns) | +| `taint` | Taint + State | -## Analysis Modes +## Attack-surface ranking -| Mode | CLI flag | Active detectors | -|------|----------|-----------------| -| Full | `--mode full` | All four | -| AST-only | `--mode ast` | AST patterns only | -| CFG/Taint | `--mode cfg` | Taint + CFG + State | - -## Attack-Surface Ranking - -Every finding receives a deterministic **attack-surface score** estimating exploitability. Findings are sorted by descending score. - -### Scoring Formula +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 | Purpose | -|-----------|--------|---------| -| **Severity base** | High=60, Medium=30, Low=10 | Primary signal | -| **Analysis kind** | taint=+10, state=+8, cfg(with evidence)=+5, cfg(no evidence)=+3, ast=+0 | Confidence of analysis | -| **Evidence strength** | +1 per evidence item (max 4), +2-6 for source kind | Specificity of finding | -| **State bonus** | use-after-close/unauthed=+6, double-close=+3, must-leak=+2, may-leak=+1 | State rule severity | -| **Validation penalty** | -5 if path-validated | Guard reduces exploitability | +| Component | Values | +|---|---| +| Severity base | High=60, Medium=30, Low=10 | +| Analysis kind | taint=+10, 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 | -### Source-kind priority +Source-kind contributions (taint only): -| Source type | Bonus | Examples | -|-------------|-------|---------| -| User input | +6 | `req.body`, `argv`, `stdin`, `form`, `query`, `params` | -| Environment | +5 | `env::var`, `getenv`, `process.env` | -| Unknown | +4 | Conservative default | -| File system | +3 | `fs::read_to_string`, `fgets` | -| Database | +2 | Query results | +| 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 | -### Score ranges (approximate) +Approximate score ranges: -| Finding type | Score range | -|-------------|------------| -| High taint + user input | ~76-80 | +| Finding type | Score | +|---|---| +| High taint with user input | 76 to 81 | | High state (use-after-close) | ~74 | -| High CFG structural | ~63-68 | -| Medium taint + env source | ~45-50 | +| High CFG structural | 63 to 68 | +| Medium taint with env source | 45 to 50 | | Medium state (resource leak) | ~40 | | Low AST-only pattern | ~10 | -Ranking is enabled by default. Disable with `--no-rank` or `output.attack_surface_ranking = false`. - -## Two-Pass Architecture - -Nyx's taint analysis requires cross-file context, achieved via two passes: - -1. **Pass 1 — Summary extraction**: Each file is parsed, a CFG is built, and a `FuncSummary` is extracted per function. Summaries capture source/sanitizer/sink capabilities (bitflags), taint propagation behavior, and callee lists. Summaries are persisted to SQLite. - -2. **Pass 2 — Analysis**: All summaries are merged into a global map. Files are re-parsed and analyzed with full cross-file context. The taint engine resolves callees against local summaries (more precise) first, then falls back to global summaries. - -With indexing enabled, Pass 1 skips files whose content hash hasn't changed since the last scan. +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 index 5f9415ce..7cc8e1c4 100644 --- a/docs/detectors/cfg.md +++ b/docs/detectors/cfg.md @@ -1,161 +1,130 @@ -# CFG Structural Analysis +# CFG structural analysis -## Summary +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. -Nyx builds an intra-procedural control-flow graph (CFG) for each function and analyzes 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-handling code terminates properly. - -These detectors use **dominator analysis** — they check whether a guard node dominates (must execute before) a sink node on the CFG. +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 | Description | -|---------|----------|-------------| -| `cfg-unguarded-sink` | High/Medium | Sink reachable without a dominating guard or sanitizer | -| `cfg-auth-gap` | High | Web handler reaches privileged sink without auth check | -| `cfg-unreachable-sink` | Medium | Dangerous function in unreachable code | -| `cfg-unreachable-sanitizer` | Low | Sanitizer in unreachable code | -| `cfg-unreachable-source` | Low | Source in unreachable code | -| `cfg-error-fallthrough` | High/Medium | Error check doesn't terminate; dangerous code follows | -| `cfg-resource-leak` | Medium | Resource acquired but not released on all exit paths | -| `cfg-lock-not-released` | Medium | Lock acquired but not released on all exit paths | +| 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 +## What it detects -### Unguarded sinks (`cfg-unguarded-sink`) -A sink call (e.g. `system()`, `eval()`, `Command::new()`) is reachable from the function entry without passing through a guard or sanitizer that matches the sink's capability. +**`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. -### Auth gaps (`cfg-auth-gap`) -A function identified as a web handler (by parameter naming conventions like `req`, `res`, `ctx`, `request`) reaches a privileged sink (shell execution, file I/O) without a prior call to an authentication function (`is_authenticated`, `require_auth`, `check_permission`, etc.). +**`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. -### Unreachable security code (`cfg-unreachable-*`) -Sinks, sanitizers, or sources in dead code branches. This often indicates a refactoring error where security-critical code was accidentally made unreachable. +**`cfg-unreachable-*`**: Sinks, sanitizers, or sources in dead code. Usually signals a refactoring error that silently disabled security-relevant logic. -### Error fallthrough (`cfg-error-fallthrough`) -An error check (null check, error return check) does not terminate the function or loop back. Execution continues to a dangerous operation on the error path. +**`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. -### Resource leaks (`cfg-resource-leak`, `cfg-lock-not-released`) -A resource acquisition call (e.g. `File::open`, `fopen`, `socket`, `Lock`) is not matched by a release call (e.g. `close`, `fclose`, `unlock`) on all exit paths from the function. +**`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 Cannot Detect +## What it can't detect -- **Inter-procedural guards**: If authentication is checked in a middleware function that calls this handler, the CFG detector cannot see it. It only analyzes one function at a time. -- **Dynamic dispatch**: Virtual method calls, function pointers, and closures are opaque to the CFG. -- **Complex guard patterns**: Only recognized guard function names are checked. Custom validation logic (e.g. `if password == expected`) is not recognized as a guard. -- **Correct sanitization**: The detector checks that *some* guard dominates the sink, not that the guard is *correct*. A guard that always passes would suppress the finding. -- **Cross-function resource flows**: If a file handle is opened in one function and closed in another, the detector will report a leak in the first function. +- **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 +## Common false positives -| Scenario | Why it fires | Mitigation | -|----------|-------------|------------| -| Framework-level auth middleware | Handler doesn't call auth directly | Document as expected; suppress with severity filter | -| Resource closed via RAII/defer | Implicit cleanup not visible to CFG | Currently not detected; known limitation | -| Custom guard function name | Function not in the recognized guard list | Add the function name as a sanitizer in config | -| Test handlers | Intentionally skip auth in tests | Default non-prod downgrade reduces severity; or exclude test dirs | +| 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 +## Common false negatives -| Scenario | Why it's missed | -|----------|----------------| -| Auth in called function | Cross-function guards not tracked | -| Guard via type system | Type-level guarantees (e.g. Rust's `AuthenticatedUser` wrapper) not analyzed | -| Resource closed in finally/defer | Some cleanup patterns not recognized | +| 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 | -## Confidence Signals +## Tuning -| Signal | Meaning | -|--------|---------| -| **Evidence lists guard nodes** | Shows which guards were checked and found missing | -| **Sink has high capability** | Shell execution or file I/O sinks are higher risk | -| **Handler detection matched** | Web handler identification is based on conventional parameter names | +### Recognised guard names -## Tuning and Noise Controls +Nyx accepts these patterns as dominating guards: -### Add custom guards/sanitizers +| 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" +cap = "all" ``` -### Add auth rules - -Auth checks are recognized by function name. If your codebase uses non-standard names: +### Custom auth functions ```toml [[analysis.languages.javascript.rules]] matchers = ["ensureLoggedIn", "requirePermission"] kind = "sanitizer" -cap = "all" -``` - -### Filter results - -```bash -# Skip low-severity unreachable findings -nyx scan . --severity ">=MEDIUM" -``` - -### Disable CFG analysis - -```bash -nyx scan . --mode ast # AST patterns only +cap = "all" ``` ## Examples -### Unguarded sink +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: no guard dominates + exec.Command("sh", "-c", cmd).Run() // cfg-unguarded-sink } ``` -### Auth gap +Auth gap: ```javascript app.get('/admin/delete', (req, res) => { - // No is_authenticated() call - db.execute("DELETE FROM users WHERE id = " + req.params.id); - // cfg-auth-gap: web handler reaches privileged sink without auth + // No auth call + db.execute("DELETE FROM users WHERE id = " + req.params.id); // cfg-auth-gap }); ``` -### Resource leak +Resource leak: ```c void process() { - FILE *f = fopen("data.txt", "r"); // acquire + FILE *f = fopen("data.txt", "r"); if (error) { - return; // cfg-resource-leak: f not closed on this path + return; // cfg-resource-leak: f not closed on this path } fclose(f); } ``` - -## Guard Rules - -Nyx recognizes these function name patterns as guards: - -| Pattern | Applies to | -|---------|-----------| -| `validate*`, `sanitize*` | All sinks | -| `check_*`, `verify_*`, `assert_*` | All sinks | -| `shell_escape` | Shell execution sinks | -| `html_escape` | HTML/XSS sinks | -| `url_encode` | URL sinks | -| `which` | Shell execution (binary lookup) | - -### Auth rules - -| Pattern | Category | -|---------|----------| -| `is_authenticated`, `require_auth`, `check_permission` | Common | -| `authorize`, `authenticate`, `require_login` | Common | -| `check_auth`, `verify_token`, `validate_token` | Common | -| `middleware.auth`, `auth.required` | Go | -| `isAuthenticated`, `checkPermission`, `hasAuthority`, `hasRole` | Java | diff --git a/docs/detectors/patterns.md b/docs/detectors/patterns.md index 4b4c99f4..adf50b23 100644 --- a/docs/detectors/patterns.md +++ b/docs/detectors/patterns.md @@ -1,111 +1,84 @@ -# AST Pattern Matching +# AST patterns -## Summary +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. -AST patterns are tree-sitter queries that match specific structural code constructs. They are the simplest and fastest detector family — no dataflow, no CFG, just structural presence. A match means the dangerous construct exists in the code; it does not prove the code is exploitable. - -AST patterns run in all analysis modes, including `--mode ast` (where they are the only active detector). +Patterns run in every analysis mode. In `--mode ast` they're the only active detector. ## Rule IDs -Pattern rule IDs follow the format `..`: - ``` -rs.memory.transmute -js.code_exec.eval -py.deser.pickle_loads -c.memory.gets -java.sqli.execute_concat +.. ``` -See the [Rule Reference](../rules/index.md) for a complete listing per language. +Examples: `js.code_exec.eval`, `py.deser.pickle_loads`, `c.memory.gets`, `java.sqli.execute_concat`. -## Pattern Tiers +Full list: [rules.md](../rules.md). -| Tier | Meaning | Examples | -|------|---------|---------| -| **A** | Structural presence alone is high-signal | `gets()`, `eval()`, `pickle.loads()`, `mem::transmute` | -| **B** | Query includes a heuristic guard | SQL `execute` with concatenated arg, `printf(var)` with non-literal format | +## Tiers -Tier B patterns use additional tree-sitter predicates to reduce false positives. For example, `java.sqli.execute_concat` only fires when `executeQuery()` receives a `binary_expression` (string concatenation) as its argument, not when it receives a literal or parameter placeholder. +| 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 | -## What It Detects +## Categories -### By category +| 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` | -| Category | What it matches | Example languages | -|----------|----------------|-------------------| -| **CommandExec** | Shell command execution functions | C (`system`), Python (`os.system`), Ruby (backticks) | -| **CodeExec** | Dynamic code evaluation | JS (`eval`, `new Function()`), Python (`exec`), PHP (`eval`) | -| **Deserialization** | Unsafe object deserialization | Java (`readObject`), Python (`pickle.loads`), Ruby (`Marshal.load`) | -| **SqlInjection** | SQL with string concatenation | Java, Go, Python, PHP (Tier B heuristic) | -| **PathTraversal** | File inclusion with variable path | PHP (`include $var`) | -| **Xss** | XSS sink functions | JS (`document.write`, `outerHTML`), Java (`getWriter().print`) | -| **Crypto** | Weak cryptographic algorithms | All languages (`md5`, `sha1`, `Math.random()`) | -| **Secrets** | Hardcoded credentials | Go (variable name matching) | -| **InsecureTransport** | Unencrypted communication | Go (`InsecureSkipVerify`), JS (`fetch("http://")`) | -| **Reflection** | Dynamic class/method dispatch | Java (`Class.forName`, `Method.invoke`), Ruby (`send`, `constantize`) | -| **MemorySafety** | Memory safety violations | Rust (`transmute`, `unsafe`), C (`gets`, `strcpy`, `sprintf`) | -| **Prototype** | Prototype pollution | JS/TS (`__proto__` assignment) | -| **CodeQuality** | Panic/abort/type-safety issues | Rust (`unwrap`, `panic!`), TS (`as any`) | +## What patterns can't tell you -## What It Cannot Detect +- **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. -- **Dataflow**: Patterns don't track whether the dangerous function receives tainted input. `eval("hello")` (safe) and `eval(userInput)` (dangerous) both match `js.code_exec.eval`. -- **Context**: Patterns don't understand whether the code is reachable, guarded, or inside a test. -- **Semantics**: `strcpy(dst, src)` always matches — it cannot determine buffer sizes. -- **Indirect calls**: Function pointers, dynamic dispatch, and aliased references are invisible. +## Common false positives -## 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 | Suppress with `--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 | -| Scenario | Why it fires | Mitigation | -|----------|-------------|------------| -| `eval()` with a hardcoded string literal | Pattern matches structural presence | Taint analysis won't flag this — use `--mode cfg` for fewer false positives | -| `unsafe` block in Rust with sound justification | All unsafe blocks match | Filter with `--severity ">=MEDIUM"` (unsafe_block is Medium) | -| `.unwrap()` in test code | Acceptable in tests | Default non-prod downgrade reduces severity | -| `md5()` used for checksums (not security) | Pattern doesn't know usage intent | Filter Low severity or add to exclusions | -| SQL concatenation with trusted data | Tier B heuristic can't verify data source | Taint analysis is more precise here | +## Confidence levels -## Common False Negatives +Every AST pattern carries an explicit confidence: -| Scenario | Why it's missed | -|----------|----------------| -| `eval` called via alias (`let e = eval; e(input)`) | Pattern matches the identifier `eval`, not the resolved function | -| Dangerous function in a macro expansion | Tree-sitter parses the macro call, not the expansion | -| SQL injection via ORM query builder | No pattern for ORM-specific query building | -| Imported function under different name | `from os import system as s; s(cmd)` — pattern looks for `system` | +| 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` | -## Confidence Signals +`--min-confidence medium` (or `output.min_confidence = "medium"`) drops Low-confidence matches. -| Signal | Meaning | -|--------|---------| -| **Tier A** | High confidence — the function itself is dangerous | -| **Tier B** | Moderate confidence — heuristic guard reduces false positives | -| **High severity** | Critical vulnerability class (command exec, deserialization) | -| **Low severity** | Informational (weak crypto, code quality) | -| **Non-prod path** | Finding in test/vendor code — downgraded by default | - -## Tuning and Noise Controls - -### Severity filtering +## Tuning ```bash -# Skip code-quality and weak-crypto findings -nyx scan . --severity ">=MEDIUM" - -# Only critical findings -nyx scan . --severity HIGH +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 ``` -### Use taint for precision - -```bash -# Taint-only mode: only report findings with confirmed dataflow -nyx scan . --mode cfg -``` - -### Exclude directories - ```toml [scanner] excluded_directories = ["node_modules", "vendor", "generated"] @@ -113,37 +86,29 @@ excluded_directories = ["node_modules", "vendor", "generated"] ## Examples -### Tier A — structural presence +Tier A, structural presence: -**C: Banned function** ```c char buf[64]; -gets(buf); // c.memory.gets — always dangerous, no safe usage +gets(buf); // c.memory.gets ``` -**Python: Unsafe deserialization** ```python import pickle -data = pickle.loads(user_input) # py.deser.pickle_loads +data = pickle.loads(user_input) // py.deser.pickle_loads ``` -### Tier B — heuristic-guarded +Tier B, heuristic guard: -**Java: SQL concatenation** ```java // Fires: concatenated argument -stmt.executeQuery("SELECT * FROM users WHERE id=" + userId); -// java.sqli.execute_concat +stmt.executeQuery("SELECT * FROM users WHERE id=" + userId); // java.sqli.execute_concat -// Does NOT fire: parameterized query +// Does not fire: parameterized stmt.executeQuery(preparedSql); ``` -**C: Format string** ```c -// Fires: variable as first argument -printf(user_input); // c.memory.printf_no_fmt - -// Does NOT fire: literal format string -printf("%s", user_input); +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 index 52500e67..e123a23a 100644 --- a/docs/detectors/state.md +++ b/docs/detectors/state.md @@ -1,26 +1,22 @@ -# State Model Analysis +# State model analysis -## Summary +Tracks resource lifecycle and authentication state through a function. Detects use-after-close, double-close, leaks, and unauthenticated access to privileged operations. -Nyx's state model analysis tracks **resource lifecycle** and **authentication state** through a function using monotone dataflow over bounded lattices. It detects use-after-close bugs, double-close bugs, resource leaks, and unauthenticated access to privileged operations. - -State analysis is **opt-in** — enable it with `scanner.enable_state_analysis = true` in config. It requires `mode = "full"` or `mode = "cfg"`. +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 | Description | -|---------|----------|-------------| -| `state-use-after-close` | High | Variable used after being closed/released | -| `state-double-close` | Medium | Resource closed twice | -| `state-resource-leak` | Medium | Resource opened but never closed (definite) | -| `state-resource-leak-possible` | Low | Resource may not be closed on all paths | -| `state-unauthed-access` | High | Privileged operation reached without authentication | +| 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 +## What it detects -### Use-after-close (`state-use-after-close`) - -A resource transitions to the CLOSED state (via `close()`, `fclose()`, `disconnect()`, etc.), then a use operation (`read`, `write`, `send`, `recv`, `query`, etc.) is performed on it. +**`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"); @@ -28,147 +24,108 @@ fclose(f); fread(buf, 1, 100, f); // state-use-after-close ``` -### Double-close (`state-double-close`) +**`state-double-close`**: Resource closed twice. Crashes or undefined behaviour on most runtimes. -A resource is closed twice. This can cause crashes or undefined behavior. +**`state-resource-leak`**: Resource opened but never closed on any path through the function. Definite leak. -```python -f = open("data.txt") -f.close() -f.close() # state-double-close -``` +**`state-resource-leak-possible`**: Resource closed on some paths but not others. Lower confidence; often an early-return error path. -### Resource leak (`state-resource-leak`) +**`state-unauthed-access`**: A function recognised as a web handler reaches a privileged sink without an auth call on the path. -A resource is opened but never closed on any path through the function. This is a definite leak. +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. -```java -FileInputStream fis = new FileInputStream("data.txt"); -process(fis); -// function exits without fis.close() — state-resource-leak -``` +## Managed-resource suppression -### Possible resource leak (`state-resource-leak-possible`) +Several language-specific cleanup patterns suppress leak findings: -A resource is closed on some paths but not others. +| 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 | -```go -f, err := os.Open("data.txt") -if err != nil { - return // f not closed here -} -f.Close() // closed here -// state-resource-leak-possible on the error path -``` +## What it can't detect -### Unauthenticated access (`state-unauthed-access`) +- **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. -A function identified as a web handler reaches a privileged sink (shell execution, file I/O) without any authentication check on the path. +## Common false positives -A function is identified as a web handler if: -1. Its name starts with `handle_`, `route_`, or `api_` (strong match — sufficient on its own), OR -2. Its name starts with `serve_` or `process_` AND any function in the file has web-like parameter names (`request`, `req`, `ctx`, `res`, `response`, `w`, `writer`, etc., varying by language). +| 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 | -The function name `main` is explicitly excluded. +## Per-language detection -```javascript -app.post('/admin/exec', (req, res) => { - // No auth check - exec(req.body.command); // state-unauthed-access -}); -``` +| 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 | -## What It Cannot Detect - -- **Cross-function resource management**: Resources opened in one function and closed in another are not tracked. This is the most common source of false positives for leak detection. -- **RAII / defer / try-with-resources**: Implicit cleanup via language-level constructs (Rust's `Drop`, Go's `defer`, Java's try-with-resources, Python's `with`) is not recognized. These patterns will produce false-positive leak findings. -- **Dynamic dispatch**: If `close()` is called through a trait object or interface, it may not be recognized. -- **Authentication via type system**: Rust's type-state pattern (e.g. `AuthenticatedRequest`) is not recognized as an auth check. -- **Complex authorization logic**: Only recognized function name patterns are checked. - -## Common False Positives - -| Scenario | Why it fires | Mitigation | -|----------|-------------|------------| -| RAII / Drop / defer cleanup | Implicit cleanup not visible | Known limitation; filter by severity | -| Resource returned to caller | Ownership transferred, not leaked | Known limitation | -| Framework-managed resources | Web framework manages connection lifecycle | Exclude framework-generated handlers | -| Try-with-resources (Java) | Language construct not parsed | Known limitation | -| Context manager (Python `with`) | Block construct not tracked | Known limitation | - -## Common False Negatives - -| Scenario | Why it's missed | -|----------|----------------| -| Resource closed in helper function | Cross-function tracking not implemented | -| Auth in middleware | Auth check happens before handler is called | -| Double-close via aliased reference | Alias analysis not performed | - -## Confidence Signals - -| Signal | Meaning | -|--------|---------| -| **Definite leak (state-resource-leak)** | Resource is never closed on any path — high confidence | -| **Use-after-close** | Read/write operation after explicit close — high confidence | -| **Web handler detected** | Entry point matched by parameter naming convention | -| **Possible leak (state-resource-leak-possible)** | Resource closed on some but not all paths — lower confidence | - -## Tuning and Noise Controls - -### Enable state analysis - -```toml -[scanner] -enable_state_analysis = true -``` - -### Severity filtering +## Tuning ```bash -# Skip possible-leak findings (Low severity) -nyx scan . --severity ">=MEDIUM" +nyx scan . --severity ">=MEDIUM" # Skip "possible" leaks (Low) ``` -### Exclude test files - ```toml [scanner] -excluded_directories = ["tests", "test", "spec"] +enable_state_analysis = true # default +excluded_directories = ["tests", "test", "spec"] ``` -## Resource Pairs +## Recognised pairs -The state engine recognizes these acquire/release pairs per language: +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 | Resource | -|---------|---------|----------| -| `fopen` | `fclose` | File handle | -| `open` | `close` | File descriptor | -| `socket` | `close` | Socket | -| `malloc`, `calloc`, `realloc` | `free` | Heap memory | -| `pthread_mutex_lock` | `pthread_mutex_unlock` | Mutex | +**C / C++** -### Rust -| Acquire | Release | Resource | -|---------|---------|----------| -| `File::open`, `File::create` | `drop`, `close` | File handle | -| `TcpStream::connect` | `shutdown` | TCP connection | -| `lock`, `read`, `write` (on Mutex/RwLock) | `drop` | Lock guard | +| Acquire | Release | +|---|---| +| `fopen` | `fclose` | +| `open` | `close` | +| `socket` | `close` | +| `malloc`, `calloc`, `realloc` | `free` | +| `pthread_mutex_lock` | `pthread_mutex_unlock` | +| `new`, `new[]` *(C++)* | `delete`, `delete[]` | -### Java -| Acquire | Release | Resource | -|---------|---------|----------| -| `new FileInputStream` | `close` | File stream | -| `getConnection` | `close` | DB connection | -| `new Socket` | `close` | Socket | +**Rust** -### Go, Python, JavaScript, Ruby, PHP -Similar patterns with language-specific function names. +| Acquire | Release | +|---|---| +| `File::open`, `File::create` | `drop`, `close` | +| `TcpStream::connect` | `shutdown` | +| `lock`, `read`, `write` (Mutex/RwLock) | `drop` | -## Use Patterns (Trigger use-after-close) +**Java** -The following operations on a closed resource trigger `state-use-after-close`: +| 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, @@ -177,28 +134,3 @@ ungetc, query, execute, fetch, sendto, recvfrom, ioctl, fcntl, strcpy, strncpy, strcat, strncat, memcpy, memmove, memset, memcmp, strcmp, strncmp, strlen, sprintf, snprintf ``` - -## Technical Details - -### Resource Lifecycle Lattice - -``` -UNINIT → OPEN → CLOSED - → MOVED -``` - -States are tracked as bitflags, allowing the lattice to represent uncertainty (e.g. OPEN|CLOSED means the resource is open on some paths and closed on others). - -### Leak Detection Scope - -Resource leaks are checked at the file-level exit node and the **synthesized** function exit node (a single Return node that all early returns feed into). Early-return nodes are **not** checked individually — only the merged state at the function's synthesized exit is inspected. This prevents duplicate findings where an early-return path reports a definite leak while the merged exit correctly reports a possible leak. - -This per-function exit inspection ensures that a variable leaked inside one function is not masked by a same-named variable that is properly closed in a subsequent function. - -### Auth Level Lattice - -``` -Unauthed < Authed < Admin -``` - -Join semantics: take the minimum (conservative). If any path is unauthenticated, the result is unauthenticated. diff --git a/docs/detectors/taint.md b/docs/detectors/taint.md index ffbf5043..473af0e6 100644 --- a/docs/detectors/taint.md +++ b/docs/detectors/taint.md @@ -1,10 +1,8 @@ -# Taint Analysis +# Taint analysis -## Summary +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. -Nyx's taint analysis tracks the flow of untrusted data from **sources** (where data enters the program) through **assignments and function calls** to **sinks** (where dangerous operations happen). If the data reaches a sink without passing through a **sanitizer** with matching capabilities, a finding is emitted. - -The engine uses a monotone forward dataflow analysis over a finite lattice with guaranteed termination. Analysis is **intra-procedural with cross-file function summaries** — it does not follow calls into other functions but uses pre-computed summaries of their behavior. +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 @@ -12,191 +10,135 @@ The engine uses a monotone forward dataflow analysis over a finite lattice with taint-unsanitised-flow (source :) ``` -One rule ID covers all taint findings. The parenthetical identifies the specific source location. +One rule ID, parameterized by the source location. Suppressions can target either the base ID or the full string. -## What It Detects +## What it detects -- Environment variables flowing to shell execution (`env::var` → `Command::new`) -- User input flowing to code evaluation (`req.body` → `eval()`) -- File contents flowing to SQL queries (`fs::read_to_string` → `db.execute()`) -- Request parameters flowing to HTML output (`req.query` → `innerHTML`) -- Any source-to-sink flow where the sink's required capability is not stripped by a sanitizer +- 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 Cannot Detect +## What it can't detect -- **Inter-procedural flows without summaries**: If a function isn't summarized (e.g. from a third-party library without source), the taint engine cannot track data through it. It conservatively treats unknown callees as neither propagating nor sanitizing. -- **Flows through data structures**: Taint is tracked per-variable, not per-field. `obj.field = tainted; sink(obj.other_field)` may produce a false positive because taint attaches to `obj` as a whole. -- **Aliasing**: `let y = &x; sink(*y)` — the engine tracks `y` as a fresh variable, not an alias of `x`. This can cause false negatives. -- **Complex control flow**: The analysis is flow-sensitive (respects control flow within a function) but does not track taint through arbitrary loops with complex exit conditions. -- **Implicit flows**: Taint only follows explicit data flow, not information flow through branching (e.g. `if (secret) { x = 1 } else { x = 0 }` does not taint `x`). +- **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. +- **Taint through struct fields and containers.** Taint attaches to whole variables. `obj.field = tainted; sink(obj.other_field)` can produce a false positive because `obj` itself is tainted. +- **Aliasing.** `let y = &x; sink(*y)` tracks `y` separately from `x`. Can cause FNs. +- **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 +## Common false positives -| Scenario | Why it happens | Mitigation | -|----------|---------------|------------| -| Custom sanitizer not recognized | Nyx only knows built-in and configured sanitizers | Add a custom sanitizer rule in config | -| Taint through struct fields | Variable-level (not field-level) tracking | No current mitigation; field sensitivity is planned | -| Dead code paths | The engine is path-insensitive within a function (it considers all paths) | Contradiction pruning catches some cases; path-validated findings score lower | -| Library wrappers | A wrapper around a dangerous function may re-introduce taint that was sanitized by the wrapper | Summarize the wrapper function or add it as a sanitizer | +| Scenario | Why | Mitigation | +|---|---|---| +| Custom sanitizer not recognised | Only built-in + configured sanitizers match | Add a custom sanitizer rule in config | +| Taint through struct fields | Variable-level tracking, not field-level | No fix yet; field-sensitivity is planned | +| 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 +## Common false negatives -| Scenario | Why it's missed | -|----------|----------------| -| Third-party library calls | No summary available; callee treated as opaque | -| Taint through global/static variables | Not tracked across function boundaries | -| Taint through closures/callbacks in some languages | Closure capture analysis is limited (JS/TS/Ruby/Go anonymous functions ARE analyzed) | -| Flows spanning more than two files | Summary approximation loses precision at depth | +| 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 +## Confidence signals -These signals in the output indicate higher-confidence findings: +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`). -| Signal | What it means | -|--------|--------------| -| **Evidence: Source + Sink** | Both endpoints identified with specific function names and locations | -| **Source kind = user input** | Source is directly controllable by an attacker (req.body, argv, etc.) | -| **path_validated = false** | No validation guard on the path — higher exploitability | -| **No guard_kind** | No dominating predicate check (null check, error check, etc.) | -| **High rank_score** | Multiple confidence signals combined | +Lower confidence: +- Path-validated taint (`path_validated: true`). +- Source is a database read or internal file (pre-validated at insertion is common). +- Engine note `ForwardBailed` / `PathWidened`. Use `--require-converged` to drop these in strict gates. -Lower-confidence: +## Tuning -| Signal | What it means | -|--------|--------------| -| **path_validated = true** | A validation predicate guards the path — may not be exploitable | -| **guard_kind = "ValidationCall"** | An explicit validation function was called before the sink | -| **Source kind = database** | Data from DB — may already be validated at insertion time | - -## Tuning and Noise Controls - -### Add custom sanitizers - -If your codebase has a custom sanitizer that Nyx doesn't recognize: +### Custom sanitizer ```toml # nyx.local [[analysis.languages.javascript.rules]] matchers = ["escapeHtml", "sanitizeInput"] -kind = "sanitizer" -cap = "html_escape" +kind = "sanitizer" +cap = "html_escape" ``` -Or via CLI: -```bash -nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape -``` +Or: `nyx config add-rule --lang javascript --matcher escapeHtml --kind sanitizer --cap html_escape`. -### Filter by severity +### Filter by severity or confidence ```bash -nyx scan . --severity HIGH # Only high-severity taint findings -nyx scan . --severity ">=MEDIUM" # Skip low-severity +nyx scan . --severity HIGH +nyx scan . --min-confidence medium ``` -### Skip non-production code - -By default, findings in `tests/`, `vendor/`, `build/` paths are downgraded one severity tier. To exclude them entirely, add to config: - -```toml -[scanner] -excluded_directories = ["tests", "vendor", "build", "examples"] -``` - -### Disable taint (AST-only mode) +### 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 -**Vulnerable code** (Rust): +Rust: + ```rust use std::env; use std::process::Command; fn main() { - let cmd = env::var("USER_CMD").unwrap(); // line 5: source - Command::new("sh").arg("-c").arg(&cmd).output(); // line 6: sink + let cmd = env::var("USER_CMD").unwrap(); // source + Command::new("sh").arg("-c").arg(&cmd).output(); // sink } ``` -**Finding**: +Finding: + ``` -[HIGH] taint-unsanitised-flow (source 5:15) src/main.rs:6:5 - Source: env::var("USER_CMD") at 5:15 - Sink: Command::new("sh").arg("-c") - Score: 76 +[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 alternative**: -```rust -use std::env; -use std::process::Command; +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. -fn main() { - let cmd = env::var("USER_CMD").unwrap(); - // Use the value as a direct argument, not a shell command - Command::new(&cmd).output(); - // Or validate against an allowlist -} -``` +## Capabilities -## Technical Details +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 System +| 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` | `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`, `HttpClient.send` | +| `code_exec` | | | `eval`, `exec`, `Function` | +| `crypto` | | | weak-algorithm constructors | +| `unauthorized_id` | request-bound scoped IDs (Rust auth analysis) | ownership check | row-level write | +| `all` | Sources typically use `all` so they match any sink | | | -Taint uses a bitflag capability system to match sources with appropriate sanitizers and sinks: - -| Capability | Bit | Sources | Sanitizers | Sinks | -|-----------|-----|---------|------------|-------| -| `ENV_VAR` | 0x01 | `env::var`, `getenv` | — | — | -| `HTML_ESCAPE` | 0x02 | — | `html_escape`, `DOMPurify.sanitize` | `innerHTML`, `document.write` | -| `SHELL_ESCAPE` | 0x04 | — | `shell_escape` | `Command::new`, `system()`, `eval()` | -| `URL_ENCODE` | 0x08 | — | `encodeURIComponent` | `location.href` | -| `JSON_PARSE` | 0x10 | — | `JSON.parse` | — | -| `FILE_IO` | 0x20 | — | `filepath.Clean`, `basename`, `os.path.realpath` | `fopen`, `open`, `send_file`, `fs::read_to_string` | -| `FMT_STRING` | 0x40 | — | — | `printf(var)` | - -Sources typically use `Cap::all()` to match any sink. A sanitizer strips specific capability bits. A finding fires when a tainted variable reaches a sink and the taint still has the matching capability bit set. - -### Nested Function Analysis - -The CFG builder recursively discovers function expressions nested inside call arguments: - -- **JavaScript/TypeScript**: `function_expression`, `arrow_function` inside call arguments (e.g., Express route handlers) -- **Ruby**: `do_block` and `block` nodes (e.g., Sinatra `get '/path' do...end`) -- **Go**: `func_literal` (anonymous function literals) - -Each nested function is walked as a separate scope and receives a unique identifier (``) to prevent collisions when multiple anonymous functions exist in the same file. - -### Chained Call Classification - -Method chains like `r.URL.Query().Get("host")` are normalized by stripping internal `()` segments between `.` separators. The classifier matches against both the original text and the normalized form, enabling rules like `r.URL` to match within `r.URL.Query.Get`. - -### Nested Call Fallback - -When the outermost call in an expression doesn't classify as a source/sink, the engine tries all nested inner calls. This handles patterns like `str(eval(expr))` where `str` is not a sink but the inner `eval` is. - -### Rust `if let` / `while let` Pattern Bindings - -The CFG builder recognizes Rust `let_condition` nodes inside `if` and `while` expressions. The value expression is classified for source/sink labels, and the pattern binding is extracted as a variable definition: - -```rust -if let Ok(cmd) = env::var("CMD") { - // cmd is tainted — env::var is a source, cmd is the binding - Command::new("sh").arg("-c").arg(&cmd).output(); // taint-unsanitised-flow -} -``` - -This also works for `while let` patterns. - -### JS/TS Two-Level Solve - -For JavaScript and TypeScript, taint analysis uses a two-level approach: - -1. **Level 1**: Solve top-level code (module scope) -2. **Level 2**: Solve each function seeded with the converged top-level state - -This prevents false positives from cross-function taint leakage while preserving global-to-function flows. +Sources typically use `cap = "all"` so they match every sink. Sinks declare the specific cap they need. Sanitizers only clear the cap they name. diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 00000000..42c6b329 --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,46 @@ +# 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. + +**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. + +## 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 | +| 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, 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/index.md b/docs/index.md deleted file mode 100644 index fff14f10..00000000 --- a/docs/index.md +++ /dev/null @@ -1,32 +0,0 @@ -# Nyx Documentation - -Welcome to the Nyx documentation. Nyx is a multi-language static vulnerability scanner built in Rust. - -## User Guide - -- [Installation](installation.md) — Install via cargo, prebuilt binaries, or from source -- [Quick Start](quickstart.md) — Your first scan in 60 seconds -- [CLI Reference](cli.md) — Every flag, subcommand, and option -- [Configuration](configuration.md) — Config file schema, precedence, custom rules -- [Output Formats](output.md) — Console, JSON, SARIF; exit codes; evidence fields - -## Detector Reference - -- [Detector Overview](detectors.md) — How the four detector families work together -- [Taint Analysis](detectors/taint.md) — Cross-file source-to-sink dataflow tracking -- [CFG Structural Analysis](detectors/cfg.md) — Auth gaps, unguarded sinks, resource leaks -- [State Model Analysis](detectors/state.md) — Resource lifecycle and authentication state -- [AST Patterns](detectors/patterns.md) — Tree-sitter structural pattern matching - -## Rule Reference - -- [Rule Index](rules/index.md) — How rules are organized -- [Rust](rules/rust.md) | [C](rules/c.md) | [C++](rules/cpp.md) | [Java](rules/java.md) | [Go](rules/go.md) -- [JavaScript](rules/javascript.md) | [TypeScript](rules/typescript.md) | [Python](rules/python.md) -- [PHP](rules/php.md) | [Ruby](rules/ruby.md) - -## Contributing - -- [Contributing Guide](../CONTRIBUTING.md) — Development setup, adding rules, PR guidelines -- [Security Policy](../SECURITY.md) — Responsible disclosure -- [Code of Conduct](../CODE_OF_CONDUCT.md) diff --git a/docs/installation.md b/docs/installation.md index b73eb73a..73334fd3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,42 +1,42 @@ # Installation -## Install from crates.io +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 -cargo install nyx-scanner +# 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 ``` -This installs the `nyx` binary into `~/.cargo/bin/`. +If `sha256sum` is missing on macOS, `shasum -a 256 -c SHA256SUMS --ignore-missing` is equivalent. -## Install from GitHub releases +## Windows -1. Go to the [Releases](https://github.com/elicpeter/nyx/releases) page. -2. Download the binary for your platform: - - | 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` | - -3. Extract and install: - - ```bash - # Linux / macOS - unzip nyx-*.zip - chmod +x nyx - sudo mv nyx /usr/local/bin/ - - # Windows (PowerShell) - Expand-Archive -Path nyx-*.zip -DestinationPath . - Move-Item -Path .\nyx.exe -Destination "C:\Program Files\Nyx\" - ``` - -4. Verify: - ```bash - nyx --version - ``` +```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 @@ -44,33 +44,34 @@ This installs the `nyx` binary into `~/.cargo/bin/`. git clone https://github.com/elicpeter/nyx.git cd nyx cargo build --release -cargo install --path . +# Binary at target/release/nyx ``` -Requires **Rust 1.85+** (edition 2024). +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`. -## CI Integration +Optional features: -### GitHub Actions +| 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 | -```yaml -- name: Install Nyx - run: cargo install nyx-scanner +## Upgrading -- name: Run security scan - run: nyx scan . --format sarif --fail-on medium > results.sarif +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: -- name: Upload SARIF - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: results.sarif +``` +engine version changed (0.4.0 → 0.5.0), rebuilding index ``` -### Generic CI +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 -# Fail the build if any High or Medium finding is detected -nyx scan . --severity ">=MEDIUM" --fail-on medium --quiet --format json +rm "$(nyx config path)"/.sqlite* ``` -The `--fail-on` flag causes Nyx to exit with code **1** if any finding meets or exceeds the given severity. Exit code **0** means no findings matched. +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..2a6c15d1 --- /dev/null +++ b/docs/language-maturity.md @@ -0,0 +1,266 @@ +# 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 305-case + corpus (267 synthetic + 14 real-CVE pairs + 10 auth fixtures) in + [`tests/benchmark/RESULTS.md`](https://github.com/elicpeter/nyx/blob/master/tests/benchmark/RESULTS.md), + last measured 2026-04-23 with scanner version 0.5.0. +3. **Known weak spots**: FPs and FNs the maintainers have deliberately left + in the benchmark rather than suppressed, documented release-by-release in + [`RESULTS.md`](https://github.com/elicpeter/nyx/blob/master/tests/benchmark/RESULTS.md). + +All parser integrations use tree-sitter and are stable; parsing is not a +differentiator between tiers. The differentiators are rule depth, cross-file +confidence, and modeled idioms. + +--- + +## Tier Summary + +| Tier | Languages | What to expect | +|------|-----------|----------------| +| **Stable** | Python, JavaScript, TypeScript | Deep rule sets, gated sinks (argument-role-aware), framework detection, extensive fixtures, and the bulk of advanced-analysis (SSA, context-sensitivity, symbolic execution) coverage. Safe to depend on in CI gates. | +| **Beta** | Go, Java, Ruby, PHP | Solid mid-depth rule sets with known narrower class coverage. No gated sinks yet. Cross-file flows work; some idioms (variable-typed method receivers, framework context, string interpolation) are incomplete. Usable in CI, but review FP/FN lists before tightening gates. | +| **Preview** | C, C++ | Pattern-only coverage. Pointer aliasing, function pointers, array-element taint, and STL container flows are not modeled. Suitable for finding obvious unsafe API uses; do not use as a sole SAST gate. Pair with clang-tidy / Clang Static Analyzer / Infer. | +| **Experimental** | Rust | Full source coverage relative to the framework ecosystem, but several FPs persist on adversarial safe cases pending engine work (match-arm guards, structural sinks with type facts). Appropriate for spot-checks and contribution but not yet recommended as a sole SAST dependency. | + +--- + +## Per-Language Detail + +### Stable tier + +#### Python: 100% P / 100% R / 100% F1 *(29-case corpus)* + +- **Rule depth**: 5 source families, 7 sanitizer families, 21 sink matchers + 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**: 125 under `tests/fixtures/` plus 30 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: 93.8% P / 100% R / 96.8% F1 *(27-case corpus)* + +- **Rule depth**: 3 source families, 10 sanitizer families, 24 sink matchers + 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. +- **Framework context**: Express, Koa, Fastify (via in-file import scan when + `package.json` is absent). +- **Fixtures**: 238 under `tests/fixtures/`; the largest corpus 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: 100% P / 100% R / 100% F1 *(35-case corpus, most recent measurement)* + +- **Rule depth**: Shares the JS ruleset (3 sources, 10 sanitizers, 24 sinks) + plus TS-specific grammar handling. +- **Advanced analysis**: TSX and JSX grammars wired as of 2026-04-20; + 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**: 39 test fixtures plus 35 benchmark cases. +- **Blind spots**: 0 known open weak spots as of 2026-04-20. `as any` casts + and `any`-typed flows are handled conservatively (treated as tainted). + +### Beta tier + +#### Go: 94.1% P / 100% R / 97.0% F1 *(28-case corpus)* + +- **Rule depth**: 4 source families, 4 sanitizer families, 9 sink matchers + covering HTML, URL, Shell, SQL, SSRF, Crypto, and File I/O. +- **Framework context**: Gin, Echo source matchers. +- **Known gaps**: no gated sinks, no deserialization class, allowlist + early-return patterns in path-pruning benchmark cases still produce FPs + (`go-pathprune-safe-001`). `fmt.Sprintf` is deliberately not a sink. + +#### Java: 92.9% P / 100% R / 96.3% F1 *(23-case corpus)* + +- **Rule depth**: 3 source families, 8 sanitizer families, 10 sink matchers + 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 missed (`java-ssrf-002` historically persisted as + FN; closed via type facts but fragile on unusual builder chains). + +#### Ruby: 100% P / 92.3% R / 96.0% F1 *(24-case corpus)* + +- **Rule depth**: 3 source families, 7 sanitizer families, 15 sink matchers + covering HTML, Shell, SQL, Code, SSRF, File I/O, and Deserialization. +- **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 documented as deferred + (structurally incompatible with `build_try()`). One FN persists on an + interprocedural taint propagation case due to rule-ID mismatch, not a + missed flow (`rb-interproc-001`). + +#### PHP: 86.7% P / 100% R / 92.9% F1 *(24-case corpus)* + +- **Rule depth**: 3 source families (`$_GET`, `$_POST`, `$_REQUEST` + superglobals), 7 sanitizer families, 10 sink matchers covering HTML, URL, + Shell, SQL, Code, SSRF, File I/O, and Deserialization. +- **Known gaps**: no gated sinks. Limited framework context (Laravel raw + methods only). Interprocedural sanitizer-wrapping case + (`php-interproc-safe-001`) persists as FP. `echo` language-construct + detection is wired but its inner-argument propagation is narrower than + function-call sinks. + +### Preview tier + +C and C++ are labeled **Preview** (not Experimental) to convey a specific +shape of limitation: the parser and existing rules produce useful findings +on obvious unsafe-API uses, but the engine **structurally cannot model** +several pervasive C/C++ constructs. Running Nyx on a C/C++ codebase and +seeing a clean report should not be read as a clean audit. Pair Nyx with +clang-tidy, the Clang Static Analyzer, or Infer for production use. + +**Not modeled** (common to both C and C++): + +- Pointer aliasing. Taint through `*p`, `p->field`, arbitrary pointer + arithmetic, and aliased writes are not tracked. +- Function pointers and callback dispatch. Indirect calls through + `void (*fn)(char *)` resolve to no callee. +- Array-element taint. Writes to `buf[i]` do not propagate taint to `buf` + in the general case; structural taint chains involving `fgets` → array → + `system` have rule-ID matching issues (`c-cmdi-004`). +- STL container operations (C++ only). `std::vector`, `std::map`, + `std::string` methods are not taint-aware; `c_str()` breaks taint chains + (`cpp-cmdi-003`). +- Lambdas and nested classes (C++ only). Not modeled. +- Complex socket setup (C++ only). E.g. `connect()` chains are not detected + (`cpp-ssrf-002`). + +#### C: 85.7% P / 100% R / 92.3% F1 *(20-case corpus)* + +- **Rule depth**: 3 source families, **2** sanitizer families (prefix-based + only), 5 sink matchers spanning Shell, File, SSRF, and Format-String. +- **Known gaps**: no framework rules, no gated sinks. Path-validation via + `strstr()` is not recognized as a guard (`c-safe-006`). Forward-declared + sanitizers are not tracked (`c-safe-008`). + +#### C++: 80.0% P / 100% R / 88.9% F1 *(20-case corpus)* + +- **Rule depth**: Clones the C ruleset (3 sources, 2 sanitizers, 5 sinks) and + adds `std::cin` / `std::getline` sources. +- **Known gaps**: same sanitizer-recognition gaps as C. See the "Not + modeled" list above for structural gaps (STL containers, `c_str()`, + `connect()`, lambdas, nested classes). + +### Experimental tier + +#### Rust: 76.0% P / 100% R / 86.4% F1 *(31-case adversarial corpus)* + +- **Rule depth**: 6 source families, **2** sanitizer families (prefix and + type-coercion), 11 sink matchers 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. +- **Recent additions (2026-04-20)**: new SQL class (`rusqlite`, `sqlx`, + `diesel`, `postgres`), new Deserialization class (`serde_yaml`, + `bincode`, `rmp_serde`, `ciborium`, `ron`, `toml`), expanded file I/O + (`fs::remove_file/dir/rename/copy`), `reqwest` SSRF builder chain. +- **Known gaps**: + - `rs-safe-003`: structural `cfg-unguarded-sink` fires when a tainted + variable is *declared* in scope but not used in the sink; intentional + for high-risk sinks. + - `rs-safe-009`: match-arm guards don't surface as `StmtKind::If`, so + `classify_condition` never sees the character-class validation. + - `safe_direct_sanitizer.rs`: still FP because the SSA lowering for + an OR-chain rejection (`if a || b || c { return X }`) joins both + return paths into a single block, losing the early-return + semantics. Distinct from the merged-return-block defect closed in + 2026-04-24; tracked separately. +- **Closed by the 2026-04-23 PathFact domain** + (`src/abstract_interp/path_domain.rs`): `rs-safe-007` (`.replace("..", + "")` sanitiser), `rs-safe-008` (negative-validation return pattern), + `rs-safe-010` (static-map lookup; still handled by the dedicated + static-map analysis, but PathFact does not interfere), new `rs-safe-012` + (`.contains("..")` + `.starts_with('/')` intraprocedural rejection), + new `rs-safe-015` (`Path::new(p).is_absolute()` typed rejection), plus a + new `rs-path-006` negative-guard to prevent over-suppression. +- **Closed by the 2026-04-24 per-return-path PathFact landing** + (`PathFactReturnEntry` on `SsaFuncSummary` + structural + variant-wrapper transparency + non-data-return skipping + + path-fact-proven leaf detection in + `trace_tainted_leaf_values`): + `rs-safe-014` (Option-returning user sanitiser), + new `rs-safe-016` (cross-function `.contains("..")` rejection), + `CVE-2018-20997` patched (tar-rs zip-slip), + `CVE-2022-36113` patched (cargo `.cargo-ok` symlink), + `CVE-2024-24576` patched (BatBadBut argv injection). +- **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), and Rocket / + Actix positive cases (rules exist but no benchmark fixtures yet). + +--- + +## How the tiers were assigned + +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 gating. +- Benchmark F1 ≥ 95% on a corpus of ≥ 25 cases. +- Advanced analysis (SSA lowering, context-sensitivity, symbolic-execution) + is exercised by fixtures for the language. + +A language lands in **Beta** when benchmark F1 ≥ 90% but at least one of the +Stable criteria fails; usually narrower cap coverage or absence of gated +sinks. + +A language lands in **Preview** when the engine structurally cannot model +constructs that are pervasive in typical codebases for that language +(pointer aliasing, function pointers, array-element taint, STL containers +for C/C++). Pattern-only coverage is useful but not sufficient as a sole +SAST gate. + +A language lands in **Experimental** when rule depth is clearly narrower +(≤ 5 sinks and ≤ 2 sanitizers), or benchmark F1 < 90%, or documented weak +spots require engine changes rather than rule additions to close, but the +engine does not have the pervasive structural blind spots of the Preview +tier. + +--- + +## 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; the weak-spot lists + above tell you exactly what to skim for. On Preview- and Experimental-tier, + treat Nyx findings as a starting point for manual review rather than + authoritative; Preview-tier languages in particular have structural + blind spots that a clean report will not disclose. +- **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, C++, or Rust, Nyx will + surface real findings, but you should budget for review time and consider + combining Nyx with a language-specific tool (e.g. `cargo-audit`, + `clang-tidy`) until those tiers mature. + +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/output.md b/docs/output.md index 091432e5..dc2d9936 100644 --- a/docs/output.md +++ b/docs/output.md @@ -19,9 +19,9 @@ Human-readable, color-coded output to stdout. Status messages go to stderr. | 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 | +| `[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 @@ -139,9 +139,9 @@ Fields marked "no" are omitted when empty/null/false to keep output compact. | 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 | +| `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 @@ -192,12 +192,12 @@ 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` and `rollup.count` -- **Related locations** — Rollup findings include example locations in `relatedLocations` -- **Artifacts** — File paths referenced by findings +- **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` and `rollup.count` +- **Related locations** -- Rollup findings include example locations in `relatedLocations` +- **Artifacts** -- File paths referenced by findings ### GitHub Code Scanning integration @@ -229,9 +229,9 @@ Without `--fail-on`, Nyx always exits `0` on a successful scan regardless of fin | 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 | +| **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 @@ -265,8 +265,8 @@ x = dangerous() # nyx:ignore taint-unsanitised-flow ← suppresses this lin x = dangerous() ← suppresses this line ``` -- `nyx:ignore ` — suppresses findings on the **same line** as the comment. -- `nyx:ignore-next-line ` — suppresses findings on the **next line**. +- `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 @@ -312,4 +312,4 @@ Suppressed findings do **not** trigger `--fail-on`. A scan with only suppressed | `state-*` | State model | `state-use-after-close`, `state-resource-leak` | | `.*.*` | AST patterns | `rs.memory.transmute`, `js.code_exec.eval` | -See the [Rule Reference](rules/index.md) for a complete listing. +See the [Rule Reference](rules.md) for a complete listing. diff --git a/docs/quickstart.md b/docs/quickstart.md index 69ddf371..267ea8c0 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,103 +1,101 @@ -# Quick Start +# Quick start -## Your first scan +After `cargo install nyx-scanner` (or dropping a release binary on your PATH), point Nyx at a directory: ```bash -# Scan the current directory -nyx scan - -# Scan a specific path nyx scan ./my-project ``` -Nyx automatically creates an SQLite index on first run. Subsequent scans skip unchanged files. +First run builds a SQLite index under `.nyx/`; later runs skip files whose content hash hasn't changed. -## Understanding the output +## What a finding looks like -A typical console output looks like: +

nyx scan output: two HIGH taint flows (Python os.system, JavaScript document.write) framed by the brand purple gradient

+ +The same scan in console form: ``` -[HIGH] taint-unsanitised-flow (source 5:11) src/handler.rs:12:5 - Source: env::var("CMD") at 5:11 - Sink: Command::new("sh").arg("-c") - Score: 76 +/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 -[MEDIUM] cfg-unguarded-sink src/handler.rs:12:5 - Score: 35 + Source: request.args.get (5:11) + Sink: os.system -[MEDIUM] rs.quality.unsafe_block src/lib.rs:44:5 - Score: 30 + 6:5 ✖ [HIGH] py.cmdi.os_system (Score: 64, Confidence: High) + Os.system() — shell command execution + +/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 + + 5:5 ⚠ [MEDIUM] js.xss.document_write (Score: 34, Confidence: High) + Document.write() — XSS sink + +warning 'demo' generated 10 issues. +Finished in 0.054s. ``` -Each finding shows: +Each finding is one line of header plus evidence. Fields that matter: | Field | Meaning | -|-------|---------| -| **Severity tag** | `[HIGH]`, `[MEDIUM]`, or `[LOW]` | -| **Rule ID** | Identifies the detector and specific rule | -| **Location** | `file:line:col` | -| **Evidence** | Source, Sink, and guard details (taint findings only) | -| **Score** | Attack-surface ranking score (higher = more exploitable) | +|---|---| +| `[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 | -## Common workflows +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. -### CI gate — fail on high-severity findings +## Fail a CI job on High findings ```bash -nyx scan . --fail-on high --quiet -# Exit code 1 if any HIGH finding exists, 0 otherwise +nyx scan . --fail-on HIGH --quiet ``` -### Export for tooling +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 -# JSON for scripting -nyx scan . --format json > findings.json - -# SARIF for GitHub Code Scanning nyx scan . --format sarif > results.sarif ``` -### Fast structural scan (no dataflow) +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 dataflow for a fast first pass ```bash nyx scan . --mode ast ``` -AST-only mode runs tree-sitter pattern queries without building CFGs or running taint analysis. Much faster, but misses dataflow vulnerabilities. +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. -### Filter by severity +## Next -```bash -# Only high-severity -nyx scan . --severity HIGH - -# High and medium -nyx scan . --severity ">=MEDIUM" - -# Specific set -nyx scan . --severity "HIGH,MEDIUM" -``` - -### Skip the index - -```bash -nyx scan . --index off -``` - -Useful for one-off scans or when you don't want to write to disk. - -### Scan without non-production noise - -By default, findings in test/vendor/build paths are downgraded one severity tier. To keep original severity: - -```bash -nyx scan . --keep-nonprod-severity -``` - -## Next steps - -- [CLI Reference](cli.md) — All flags and options -- [Configuration](configuration.md) — Customize rules, exclusions, and behavior -- [Detector Overview](detectors.md) — How the analysis engines work -- [Rule Reference](rules/index.md) — Browse all rules by language +- [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..23457261 --- /dev/null +++ b/docs/rules.md @@ -0,0 +1,258 @@ +# Rule reference + +Every finding Nyx emits has a rule ID. This page enumerates the IDs that ship with scanner 0.5.0, 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 + +One rule covers every source-to-sink flow. The parenthetical identifies the source location. + +| Rule ID | Severity | +|---|---| +| `taint-unsanitised-flow (source L:C)` | Varies by source kind and sink capability | + +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: 8 patterns + +| Rule ID | Severity | Tier | Confidence | +|---|---|---|---| +| `java.cmdi.runtime_exec` | High | A | High | +| `java.deser.readobject` | High | A | High | +| `java.reflection.class_forname` | Medium | A | High | +| `java.reflection.method_invoke` | Medium | A | High | +| `java.sqli.execute_concat` | Medium | B | Medium | +| `java.xss.getwriter_print` | Medium | A | High | +| `java.crypto.insecure_random` | Low | A | Medium | +| `java.crypto.weak_digest` | Low | A | Medium | + +### JavaScript: 22 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.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: 13 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.xss.jinja_from_string` | Medium | A | High | +| `py.crypto.md5` | Low | A | Medium | +| `py.crypto.sha1` | Low | A | Medium | + +### 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: 22 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.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`, `all` + +Source for both the enum and the `to_cap` mapping: [`src/labels/mod.rs`](https://github.com/elicpeter/nyx/blob/master/src/labels/mod.rs) (`Cap`) and [`src/utils/config.rs`](https://github.com/elicpeter/nyx/blob/master/src/utils/config.rs) (`CapName`). diff --git a/docs/rules/c.md b/docs/rules/c.md deleted file mode 100644 index e66f706b..00000000 --- a/docs/rules/c.md +++ /dev/null @@ -1,89 +0,0 @@ -# C Rules - -Nyx detects C vulnerabilities through AST patterns (banned functions, format strings) and taint analysis (user input → shell execution, buffer overflow sinks). - -## Taint Sources - -| Function | Capability | Source Kind | -|----------|-----------|-------------| -| `getenv` | `all` | EnvironmentConfig | -| `fgets`, `scanf`, `fscanf`, `gets`, `read` | `all` | UserInput | - -## Taint Sinks - -| Function | Required Capability | -|----------|-------------------| -| `system`, `popen`, `exec*` family | `SHELL_ESCAPE` | -| `sprintf`, `strcpy`, `strcat` | `HTML_ESCAPE` | -| `printf`, `fprintf` | `FMT_STRING` | -| `fopen`, `open` | `FILE_IO` | - ---- - -## AST Pattern Rules - -### Memory Safety (Banned Functions) - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `c.memory.gets` | High | A | `gets()` — no bounds checking, always exploitable | -| `c.memory.strcpy` | High | A | `strcpy()` — no bounds checking on destination buffer | -| `c.memory.strcat` | High | A | `strcat()` — no bounds checking on destination buffer | -| `c.memory.sprintf` | High | A | `sprintf()` — no length limit on output buffer | -| `c.memory.scanf_percent_s` | High | A | `scanf("%s")` — unbounded string read | -| `c.memory.printf_no_fmt` | High | B | `printf(var)` — format-string vulnerability (non-literal first arg) | - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `c.cmdi.system` | High | A | `system()` — shell command execution | -| `c.cmdi.popen` | Medium | A | `popen()` — shell command execution with pipe | - ---- - -## Examples - -### `c.memory.gets` — Banned function - -**Vulnerable:** -```c -char buf[64]; -gets(buf); // No bounds checking — buffer overflow -``` - -**Safe alternative:** -```c -char buf[64]; -fgets(buf, sizeof(buf), stdin); -``` - -### `c.memory.printf_no_fmt` — Format string - -**Vulnerable:** -```c -char *user_input = get_input(); -printf(user_input); // Format string vulnerability -``` - -**Safe alternative:** -```c -char *user_input = get_input(); -printf("%s", user_input); -``` - -### `c.cmdi.system` — Shell execution - -**Vulnerable:** -```c -char cmd[256]; -snprintf(cmd, sizeof(cmd), "ls %s", user_dir); -system(cmd); // Command injection if user_dir contains shell metacharacters -``` - -**Safe alternative:** -```c -// Use execvp with explicit argument array -char *args[] = {"ls", user_dir, NULL}; -execvp("ls", args); -``` diff --git a/docs/rules/cpp.md b/docs/rules/cpp.md deleted file mode 100644 index 5178a920..00000000 --- a/docs/rules/cpp.md +++ /dev/null @@ -1,66 +0,0 @@ -# C++ Rules - -C++ rules inherit C banned-function concerns and add C++-specific patterns like dangerous casts. - -## Taint Labels - -C++ shares taint labels with C. See [C Rules](c.md) for the full source/sink/sanitizer listing. - ---- - -## AST Pattern Rules - -### Memory Safety - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `cpp.memory.gets` | High | A | `gets()` — no bounds checking, always exploitable | -| `cpp.memory.strcpy` | High | A | `strcpy()` — no bounds checking on destination | -| `cpp.memory.strcat` | High | A | `strcat()` — no bounds checking on destination | -| `cpp.memory.sprintf` | High | A | `sprintf()` — no length limit on output | -| `cpp.memory.reinterpret_cast` | Medium | A | `reinterpret_cast` — type-punning cast | -| `cpp.memory.const_cast` | Medium | A | `const_cast` — removes const/volatile qualifier | -| `cpp.memory.printf_no_fmt` | High | B | `printf(var)` — format-string vulnerability | - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `cpp.cmdi.system` | High | A | `system()` — shell command execution | -| `cpp.cmdi.popen` | High | A | `popen()` — shell command execution | - ---- - -## Examples - -### `cpp.memory.reinterpret_cast` — Type-punning cast - -**Flagged:** -```cpp -int x = 42; -float* fp = reinterpret_cast(&x); // Type-punning, may violate strict aliasing -``` - -**Safe alternative:** -```cpp -int x = 42; -float f; -std::memcpy(&f, &x, sizeof(f)); // Well-defined type punning -``` - -### `cpp.memory.const_cast` — Removing const - -**Flagged:** -```cpp -void process(const std::string& s) { - char* p = const_cast(s.c_str()); // Removes const - p[0] = 'X'; // Undefined behavior -} -``` - -**Safe alternative:** -```cpp -void process(std::string s) { // Take by value - s[0] = 'X'; -} -``` diff --git a/docs/rules/go.md b/docs/rules/go.md deleted file mode 100644 index 391d763b..00000000 --- a/docs/rules/go.md +++ /dev/null @@ -1,148 +0,0 @@ -# Go Rules - -Nyx detects Go vulnerabilities through AST patterns and taint analysis, covering command execution, unsafe pointer usage, TLS misconfiguration, weak crypto, SQL injection, hardcoded secrets, and deserialization. - -## Taint Labels - -Go has moderate taint label coverage. Sources, sinks, and sanitizers are defined in `src/labels/go.rs`. - -### Sources - -| Matcher | Cap | -|---------|-----| -| `os.Getenv` | all | -| `http.Request`, `r.FormValue`, `r.URL`, `r.Body`, `r.Header` | all | -| `r.URL.Query`, `r.URL.Query.Get`, `Request.FormValue`, `Request.URL` | all | - -### Sanitizers - -| Matcher | Cap | -|---------|-----| -| `html.EscapeString`, `template.HTMLEscapeString` | HTML_ESCAPE | -| `url.QueryEscape`, `url.PathEscape` | URL_ENCODE | -| `filepath.Clean`, `filepath.Base` | FILE_IO | - -### Sinks - -| Matcher | Cap | -|---------|-----| -| `exec.Command` | SHELL_ESCAPE | -| `db.Query`, `db.Exec`, `db.QueryRow`, `db.Prepare` | SHELL_ESCAPE | -| `fmt.Fprintf`, `fmt.Sprintf`, `fmt.Printf` | FMT_STRING | -| `os.Open`, `os.OpenFile`, `os.Create`, `ioutil.ReadFile`, `os.ReadFile` | FILE_IO | -| `template.HTML` | HTML_ESCAPE | - -> **Note:** Chained calls like `r.URL.Query().Get("host")` are normalized by stripping internal `()` segments before matching, so `r.URL.Query.Get` matches the source rule. - ---- - -## AST Pattern Rules - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.cmdi.exec_command` | High | A | `exec.Command()` — arbitrary process execution | - -### Memory Safety - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.memory.unsafe_pointer` | Medium | A | `unsafe.Pointer` — bypasses Go type system | - -### Insecure Transport - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.transport.insecure_skip_verify` | High | A | `InsecureSkipVerify: true` — disables TLS certificate validation | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.crypto.md5` | Low | A | `md5.New()` / `md5.Sum()` — weak hash algorithm | -| `go.crypto.sha1` | Low | A | `sha1.New()` / `sha1.Sum()` — weak hash algorithm | - -### SQL Injection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.sqli.query_concat` | Medium | B | `db.Query`/`Exec`/`QueryRow` with concatenated string | - -### Secrets - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.secrets.hardcoded_key` | Medium | A | Variable with secret-like name assigned a string literal | - -### Deserialization - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `go.deser.gob_decode` | Medium | A | `gob.NewDecoder` — Go binary deserialization | - ---- - -## Examples - -### `go.transport.insecure_skip_verify` — TLS misconfiguration - -**Vulnerable:** -```go -tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, // Disables certificate verification - }, -} -``` - -**Safe alternative:** -```go -tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - // Use proper CA certificates - RootCAs: certPool, - }, -} -``` - -### `go.sqli.query_concat` — SQL concatenation - -**Vulnerable:** -```go -rows, err := db.Query("SELECT * FROM users WHERE id=" + userID) -``` - -**Safe alternative:** -```go -rows, err := db.Query("SELECT * FROM users WHERE id=$1", userID) -``` - -### `go.secrets.hardcoded_key` — Hardcoded secret - -**Flagged:** -```go -apiKey := "sk-1234567890abcdef" -password := "hunter2" -``` - -**Safe alternative:** -```go -apiKey := os.Getenv("API_KEY") -password := os.Getenv("DB_PASSWORD") -``` - -### `go.cmdi.exec_command` — Command execution - -**Vulnerable:** -```go -cmd := exec.Command("sh", "-c", userInput) -cmd.Run() -``` - -**Safe alternative:** -```go -// Use explicit command and arguments, not shell -cmd := exec.Command("ls", "-la", safeDir) -cmd.Run() -``` diff --git a/docs/rules/index.md b/docs/rules/index.md deleted file mode 100644 index 52e08e82..00000000 --- a/docs/rules/index.md +++ /dev/null @@ -1,79 +0,0 @@ -# Rule Reference - -This section lists every detection rule in Nyx, organized by language. - -## Rule ID Format - -| Prefix | Detector Family | Example | -|--------|----------------|---------| -| `taint-*` | [Taint analysis](../detectors/taint.md) | `taint-unsanitised-flow (source 5:11)` | -| `cfg-*` | [CFG structural](../detectors/cfg.md) | `cfg-unguarded-sink`, `cfg-auth-gap` | -| `state-*` | [State model](../detectors/state.md) | `state-use-after-close`, `state-resource-leak` | -| `.*.*` | [AST patterns](../detectors/patterns.md) | `rs.memory.transmute`, `js.code_exec.eval` | - -## Cross-Language Rules - -These rules apply to all supported languages: - -### Taint Rules - -| Rule ID | Severity | Description | -|---------|----------|-------------| -| `taint-unsanitised-flow (source L:C)` | Varies by source kind | Unsanitized data flows from source to sink | - -### CFG Structural Rules - -| Rule ID | Severity | Description | -|---------|----------|-------------| -| `cfg-unguarded-sink` | High/Medium | Sink without dominating guard | -| `cfg-auth-gap` | High | Web handler reaches privileged sink without auth | -| `cfg-unreachable-sink` | Medium | Dangerous function in unreachable code | -| `cfg-unreachable-sanitizer` | Low | Sanitizer in unreachable code | -| `cfg-unreachable-source` | Low | Source in unreachable code | -| `cfg-error-fallthrough` | High/Medium | Error path doesn't terminate before dangerous code | -| `cfg-resource-leak` | Medium | Resource not released on all exit paths | -| `cfg-lock-not-released` | Medium | Lock not released on all exit paths | - -### State Model Rules - -| Rule ID | Severity | Description | -|---------|----------|-------------| -| `state-use-after-close` | High | Variable used after being closed | -| `state-double-close` | Medium | Resource closed twice | -| `state-resource-leak` | Medium | Resource never closed (definite) | -| `state-resource-leak-possible` | Low | Resource may not close on all paths | -| `state-unauthed-access` | High | Privileged operation without authentication | - -## Per-Language AST Pattern Rules - -Each language page lists all AST pattern rules with examples: - -- [Rust](rust.md) — 12 rules (memory safety, code quality) -- [C](c.md) — 8 rules (banned functions, command execution, format strings) -- [C++](cpp.md) — 9 rules (banned functions, dangerous casts, command execution) -- [Java](java.md) — 8 rules (deserialization, command execution, reflection, SQL, crypto, XSS) -- [Go](go.md) — 8 rules (command execution, unsafe pointer, TLS, crypto, SQL, secrets, deserialization) -- [JavaScript](javascript.md) — 12 rules (code execution, XSS, prototype pollution, crypto, transport) -- [TypeScript](typescript.md) — 10 rules (mirrors JS + type-safety escapes) -- [Python](python.md) — 12 rules (code execution, command execution, deserialization, SQL, crypto, XSS) -- [PHP](php.md) — 11 rules (code execution, command execution, deserialization, SQL, path traversal, crypto) -- [Ruby](ruby.md) — 10 rules (code execution, command execution, deserialization, reflection, SSRF, crypto) - -## Taint Label Coverage - -Taint analysis uses language-specific source/sink/sanitizer labels. Coverage varies by language: - -| Language | Sources | Sinks | Sanitizers | Coverage | -|----------|---------|-------|------------|----------| -| Rust | Complete | Complete | Complete | Full | -| JavaScript | Complete | Complete | Partial | Full | -| TypeScript | Partial | Partial | Partial | Moderate | -| Python | Partial | Complete | Partial | Moderate | -| C | Partial | Complete | Minimal | Moderate | -| C++ | Partial | Complete | Minimal | Moderate | -| Java | Partial | Partial | Partial | Moderate | -| Go | Complete | Complete | Partial | Full | -| PHP | Complete | Complete | Partial | Full | -| Ruby | Partial | Partial | Partial | Moderate | - -"Starter" coverage means basic rules exist but many common library functions are not yet labeled. Contributions welcome. diff --git a/docs/rules/java.md b/docs/rules/java.md deleted file mode 100644 index 99ab5a10..00000000 --- a/docs/rules/java.md +++ /dev/null @@ -1,135 +0,0 @@ -# Java Rules - -Nyx detects Java vulnerabilities through AST patterns and taint analysis, covering deserialization, command execution, reflection, SQL injection, weak crypto, and XSS. - -## Taint Labels - -Java has moderate taint label coverage. Sources, sinks, and sanitizers are defined in `src/labels/java.rs`. - -### Sources - -| Matcher | Cap | -|---------|-----| -| `System.getenv` | all | -| `getParameter`, `getInputStream`, `getHeader`, `getCookies`, `getReader`, `getQueryString`, `getPathInfo` | all | -| `readObject`, `readLine` | all | - -### Sanitizers - -| Matcher | Cap | -|---------|-----| -| `HtmlUtils.htmlEscape`, `StringEscapeUtils.escapeHtml4` | HTML_ESCAPE | - -### Sinks - -| Matcher | Cap | -|---------|-----| -| `Runtime.exec`, `ProcessBuilder` | SHELL_ESCAPE | -| `executeQuery`, `executeUpdate`, `prepareStatement` | SHELL_ESCAPE | -| `Class.forName` | SHELL_ESCAPE | -| `println`, `print`, `write` | HTML_ESCAPE | - ---- - -## AST Pattern Rules - -### Deserialization - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `java.deser.readobject` | High | A | `ObjectInputStream.readObject()` — unsafe deserialization | - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `java.cmdi.runtime_exec` | High | A | `Runtime.getRuntime().exec()` — shell command execution | - -### Reflection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `java.reflection.class_forname` | Medium | A | `Class.forName()` — dynamic class loading | -| `java.reflection.method_invoke` | Medium | A | `Method.invoke()` — reflective method invocation | - -### SQL Injection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `java.sqli.execute_concat` | Medium | B | SQL `execute*()` with concatenated string argument | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `java.crypto.insecure_random` | Low | A | `new Random()` — `java.util.Random` is not cryptographically secure | -| `java.crypto.weak_digest` | Low | A | `MessageDigest.getInstance("MD5"/"SHA1")` | - -### XSS - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `java.xss.getwriter_print` | Medium | A | `response.getWriter().print/println/write` — direct output | - ---- - -## Examples - -### `java.deser.readobject` — Unsafe deserialization - -**Vulnerable:** -```java -ObjectInputStream ois = new ObjectInputStream(request.getInputStream()); -Object obj = ois.readObject(); // Arbitrary object instantiation -``` - -**Safe alternative:** -```java -// Use a safe format like JSON -ObjectMapper mapper = new ObjectMapper(); -MyType obj = mapper.readValue(request.getInputStream(), MyType.class); -``` - -### `java.sqli.execute_concat` — SQL concatenation - -**Vulnerable:** -```java -String query = "SELECT * FROM users WHERE id=" + userId; -stmt.executeQuery(query); // SQL injection -``` - -**Safe alternative:** -```java -PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id=?"); -ps.setString(1, userId); -ResultSet rs = ps.executeQuery(); -``` - -### `java.cmdi.runtime_exec` — Command execution - -**Vulnerable:** -```java -Runtime.getRuntime().exec("cmd /c " + userCommand); -``` - -**Safe alternative:** -```java -ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "dir"); -// Use explicit argument list, never concatenate user input -``` - -### `java.reflection.class_forname` — Dynamic class loading - -**Flagged:** -```java -Class cls = Class.forName(className); -Object obj = cls.getDeclaredConstructor().newInstance(); -``` - -**Safe alternative:** -```java -// Use an allowlist of permitted class names -Map> allowed = Map.of("User", User.class, "Order", Order.class); -Class cls = allowed.get(className); -if (cls != null) { /* ... */ } -``` diff --git a/docs/rules/javascript.md b/docs/rules/javascript.md deleted file mode 100644 index 9971ed6d..00000000 --- a/docs/rules/javascript.md +++ /dev/null @@ -1,138 +0,0 @@ -# JavaScript Rules - -JavaScript has the most complete taint label coverage alongside Rust. Nyx detects code execution, XSS, prototype pollution, command injection, and weak crypto. - -## Taint Sources - -| Function | Capability | Source Kind | -|----------|-----------|-------------| -| `document.location`, `window.location` | `all` | UserInput | -| `req.body`, `req.query`, `req.params` | `all` | UserInput | -| `req.headers`, `req.cookies` | `all` | UserInput | -| `process.env` | `all` | EnvironmentConfig | - -## Taint Sinks - -| Function | Required Capability | -|----------|-------------------| -| `eval` | `SHELL_ESCAPE` | -| `innerHTML` | `HTML_ESCAPE` | -| `location.href`, `window.location.href` | `URL_ENCODE` | -| `child_process.exec`, `child_process.execSync` | `SHELL_ESCAPE` | -| `child_process.spawn` | `SHELL_ESCAPE` | - -## Taint Sanitizers - -| Function | Strips Capability | -|----------|------------------| -| `JSON.parse` | `JSON_PARSE` | -| `encodeURIComponent`, `encodeURI` | `URL_ENCODE` | -| `DOMPurify.sanitize` | `HTML_ESCAPE` | - -> **Note:** Anonymous function expressions and arrow functions passed as callback arguments (e.g., Express `app.get('/path', function(req, res) { ... })`) are automatically walked as separate function scopes for taint analysis. Each anonymous function gets a unique scope identifier to prevent cross-function taint leakage. - ---- - -## AST Pattern Rules - -### Code Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `js.code_exec.eval` | High | A | `eval()` — dynamic code execution | -| `js.code_exec.new_function` | High | A | `new Function()` — eval equivalent | -| `js.code_exec.settimeout_string` | Medium | A | `setTimeout`/`setInterval` with string argument | - -### XSS Sinks - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `js.xss.document_write` | Medium | A | `document.write()` / `document.writeln()` | -| `js.xss.outer_html` | Medium | A | Assignment to `.outerHTML` | -| `js.xss.insert_adjacent_html` | Medium | A | `insertAdjacentHTML()` | -| `js.xss.location_assign` | Medium | A | Assignment to `location`/`location.href` — open redirect | -| `js.xss.cookie_write` | Medium | A | Write to `document.cookie` | - -### Prototype Pollution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `js.prototype.proto_assignment` | Medium | A | Assignment to `__proto__` | -| `js.prototype.extend_object` | Medium | A | Assignment to `Object.prototype.*` | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `js.crypto.weak_hash` | Low | A | `crypto.createHash("md5"/"sha1")` | -| `js.crypto.math_random` | Low | A | `Math.random()` — not cryptographically secure | - -### Insecure Transport - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `js.transport.fetch_http` | Low | A | `fetch("http://...")` — plaintext HTTP | - ---- - -## Examples - -### `js.code_exec.eval` — Dynamic code execution - -**Vulnerable:** -```javascript -const code = req.query.code; -eval(code); // Remote code execution -``` - -**Safe alternative:** -```javascript -// Use a sandboxed interpreter or avoid eval entirely -const allowed = { add: (a, b) => a + b }; -const result = allowed[req.query.operation]?.(req.query.a, req.query.b); -``` - -### `js.xss.document_write` — XSS sink - -**Vulnerable:** -```javascript -document.write("

" + userName + "

"); -``` - -**Safe alternative:** -```javascript -const el = document.createElement("h1"); -el.textContent = userName; -document.body.appendChild(el); -``` - -### `js.prototype.proto_assignment` — Prototype pollution - -**Vulnerable:** -```javascript -function merge(target, source) { - for (let key in source) { - target[key] = source[key]; // If key is "__proto__", pollutes prototype - } -} -``` - -**Safe alternative:** -```javascript -function merge(target, source) { - for (let key in source) { - if (key === "__proto__" || key === "constructor") continue; - target[key] = source[key]; - } -} -``` - -### Taint: `req.body` → `eval()` - -**Finding:** -``` -[HIGH] taint-unsanitised-flow (source 2:18) src/handler.js:3:5 - Source: req.body at 2:18 - Sink: eval() - Score: 78 -``` diff --git a/docs/rules/php.md b/docs/rules/php.md deleted file mode 100644 index 50bfe659..00000000 --- a/docs/rules/php.md +++ /dev/null @@ -1,138 +0,0 @@ -# PHP Rules - -Nyx detects PHP vulnerabilities through AST patterns and taint analysis, covering code execution, command injection, deserialization, SQL injection, path traversal, and weak crypto. - -## Taint Labels - -PHP has moderate taint label coverage. Sources, sinks, and sanitizers are defined in `src/labels/php.rs`. - -### Sources - -| Matcher | Cap | -|---------|-----| -| `$_GET` / `_GET`, `$_POST` / `_POST`, `$_REQUEST` / `_REQUEST`, `$_COOKIE` / `_COOKIE`, `$_FILES` / `_FILES`, `$_SERVER` / `_SERVER`, `$_ENV` / `_ENV` | all | -| `file_get_contents`, `fread` | all | - -> **Note:** PHP superglobal names are matched both with and without the `$` prefix because the CFG's `collect_idents` strips the leading `$` from variable names. Subscript access like `$_GET['cmd']` is handled via `element_reference` / `subscript_expression` node detection. - -### Sanitizers - -| Matcher | Cap | -|---------|-----| -| `htmlspecialchars`, `htmlentities` | HTML_ESCAPE | -| `escapeshellarg`, `escapeshellcmd` | SHELL_ESCAPE | -| `basename` | FILE_IO | - -### Sinks - -| Matcher | Cap | -|---------|-----| -| `system`, `exec`, `passthru`, `shell_exec`, `proc_open`, `popen` | SHELL_ESCAPE | -| `eval`, `assert` | SHELL_ESCAPE | -| `include`, `include_once`, `require`, `require_once` | FILE_IO | -| `unserialize` | SHELL_ESCAPE | -| `move_uploaded_file`, `copy`, `file_put_contents`, `fwrite` | FILE_IO | -| `echo`, `print` | HTML_ESCAPE | -| `mysqli_query`, `pg_query`, `query` | SHELL_ESCAPE | - ---- - -## AST Pattern Rules - -### Code Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `php.code_exec.eval` | High | A | `eval()` — dynamic code execution | -| `php.code_exec.create_function` | High | A | `create_function()` — deprecated eval-like constructor | -| `php.code_exec.preg_replace_e` | High | A | `preg_replace` with `/e` modifier — code execution via regex | -| `php.code_exec.assert_string` | High | A | `assert()` with string argument — evaluates PHP code | - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `php.cmdi.system` | High | A | `system`/`shell_exec`/`exec`/`passthru`/`proc_open`/`popen` | - -### Deserialization - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `php.deser.unserialize` | High | A | `unserialize()` — PHP object injection | - -### SQL Injection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `php.sqli.query_concat` | Medium | B | `mysql_query`/`mysqli_query` with concatenated SQL | - -### Path Traversal - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `php.path.include_variable` | High | B | `include`/`require` with variable path — file inclusion | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `php.crypto.md5` | Low | A | `md5()` — weak hash function | -| `php.crypto.sha1` | Low | A | `sha1()` — weak hash function | -| `php.crypto.rand` | Low | A | `rand()`/`mt_rand()` — not cryptographically secure | - ---- - -## Examples - -### `php.code_exec.eval` — Dynamic code execution - -**Vulnerable:** -```php -eval($_GET['code']); -``` - -**Safe alternative:** -```php -// Never use eval with user input -// Use a template engine or allowlisted operations -``` - -### `php.deser.unserialize` — Object injection - -**Vulnerable:** -```php -$obj = unserialize($_COOKIE['data']); -``` - -**Safe alternative:** -```php -$data = json_decode($_COOKIE['data'], true); -``` - -### `php.path.include_variable` — File inclusion - -**Vulnerable:** -```php -include($_GET['page']); // Local/remote file inclusion -``` - -**Safe alternative:** -```php -$allowed = ['home', 'about', 'contact']; -$page = in_array($_GET['page'], $allowed) ? $_GET['page'] : 'home'; -include("pages/{$page}.php"); -``` - -### `php.sqli.query_concat` — SQL concatenation - -**Vulnerable:** -```php -mysqli_query($conn, "SELECT * FROM users WHERE id=" . $_GET['id']); -``` - -**Safe alternative:** -```php -$stmt = $conn->prepare("SELECT * FROM users WHERE id=?"); -$stmt->bind_param("i", $_GET['id']); -$stmt->execute(); -``` diff --git a/docs/rules/python.md b/docs/rules/python.md deleted file mode 100644 index cfc1c556..00000000 --- a/docs/rules/python.md +++ /dev/null @@ -1,142 +0,0 @@ -# Python Rules - -Nyx detects Python vulnerabilities through AST patterns and taint analysis, covering code execution, command injection, deserialization, SQL injection, and weak crypto. - -## Taint Labels - -Python has moderate taint label coverage. Sources, sinks, and sanitizers are defined in `src/labels/python.rs`. - -### Sources - -| Matcher | Cap | -|---------|-----| -| `os.getenv`, `os.environ` | all | -| `request.args`, `request.form`, `request.json`, `request.headers`, `request.cookies`, `input` | all | -| `sys.argv` | all | -| `argparse.parse_args`, `urllib.request.urlopen`, `requests.get`, `requests.post` | all | - -### Sanitizers - -| Matcher | Cap | -|---------|-----| -| `html.escape` | HTML_ESCAPE | -| `shlex.quote` | SHELL_ESCAPE | -| `os.path.realpath` | FILE_IO | - -### Sinks - -| Matcher | Cap | -|---------|-----| -| `eval`, `exec` | SHELL_ESCAPE | -| `os.system`, `os.popen`, `subprocess.call`, `subprocess.run`, `subprocess.Popen`, `subprocess.check_output`, `subprocess.check_call` | SHELL_ESCAPE | -| `cursor.execute`, `cursor.executemany` | SHELL_ESCAPE | -| `send_file`, `send_from_directory` | FILE_IO | -| `open` | FILE_IO | - ---- - -## AST Pattern Rules - -### Code Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `py.code_exec.eval` | High | A | `eval()` — dynamic code execution | -| `py.code_exec.exec` | High | A | `exec()` — dynamic code execution | -| `py.code_exec.compile` | Medium | A | `compile()` with exec/eval mode | - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `py.cmdi.os_system` | High | A | `os.system()` — shell command execution | -| `py.cmdi.os_popen` | High | A | `os.popen()` — shell command execution | -| `py.cmdi.subprocess_shell` | High | B | `subprocess.*` with `shell=True` | - -### Deserialization - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `py.deser.pickle_loads` | High | A | `pickle.loads()` / `pickle.load()` — arbitrary object deserialization | -| `py.deser.yaml_load` | High | A | `yaml.load()` without SafeLoader | -| `py.deser.shelve_open` | Medium | A | `shelve.open()` — pickle-backed deserialization | - -### SQL Injection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `py.sqli.execute_format` | Medium | B | `cursor.execute()` with string concatenation | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `py.crypto.md5` | Low | A | `hashlib.md5()` — weak hash algorithm | -| `py.crypto.sha1` | Low | A | `hashlib.sha1()` — weak hash algorithm | - -### Template Injection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `py.xss.jinja_from_string` | Medium | A | `jinja2.Template.from_string()` — template injection | - ---- - -## Examples - -### `py.deser.pickle_loads` — Unsafe deserialization - -**Vulnerable:** -```python -import pickle -data = pickle.loads(request.body) # Arbitrary code execution -``` - -**Safe alternative:** -```python -import json -data = json.loads(request.body) # JSON is safe -``` - -### `py.cmdi.subprocess_shell` — Shell execution - -**Vulnerable:** -```python -import subprocess -subprocess.call(user_input, shell=True) # Command injection -``` - -**Safe alternative:** -```python -import subprocess -import shlex -subprocess.call(shlex.split(user_input), shell=False) -# Or better: use an explicit command list -subprocess.call(["ls", "-la", user_dir]) -``` - -### `py.deser.yaml_load` — Unsafe YAML - -**Vulnerable:** -```python -import yaml -config = yaml.load(user_data) # Can instantiate arbitrary objects -``` - -**Safe alternative:** -```python -import yaml -config = yaml.safe_load(user_data) # Only basic Python types -``` - -### `py.sqli.execute_format` — SQL concatenation - -**Vulnerable:** -```python -cursor.execute("SELECT * FROM users WHERE id=" + user_id) -``` - -**Safe alternative:** -```python -cursor.execute("SELECT * FROM users WHERE id=?", (user_id,)) -``` diff --git a/docs/rules/ruby.md b/docs/rules/ruby.md deleted file mode 100644 index 0b9a6f19..00000000 --- a/docs/rules/ruby.md +++ /dev/null @@ -1,132 +0,0 @@ -# Ruby Rules - -Nyx detects Ruby vulnerabilities through AST patterns and taint analysis, covering code execution, command injection, deserialization, reflection, SSRF, and weak crypto. - -## Taint Labels - -Ruby has moderate taint label coverage. Sources, sinks, and sanitizers are defined in `src/labels/ruby.rs`. - -### Sources - -| Matcher | Cap | -|---------|-----| -| `ENV`, `gets` | all | -| `params` | all | - -> **Note:** Ruby's `params[:cmd]` subscript access is detected via `element_reference` node handling in the CFG. Sinatra/Rails `do...end` blocks are walked as function scopes. - -### Sanitizers - -| Matcher | Cap | -|---------|-----| -| `CGI.escapeHTML`, `ERB::Util.html_escape` | HTML_ESCAPE | -| `Shellwords.escape`, `Shellwords.shellescape` | SHELL_ESCAPE | - -### Sinks - -| Matcher | Cap | -|---------|-----| -| `system`, `exec` | SHELL_ESCAPE | -| `eval` | SHELL_ESCAPE | -| `puts`, `print` | HTML_ESCAPE | - ---- - -## AST Pattern Rules - -### Code Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rb.code_exec.eval` | High | A | `Kernel#eval` — dynamic code execution | -| `rb.code_exec.instance_eval` | High | A | `instance_eval` — evaluates string in object context | -| `rb.code_exec.class_eval` | High | A | `class_eval` / `module_eval` — evaluates string in class context | - -### Command Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rb.cmdi.backtick` | High | A | Backtick shell execution (`` `cmd` ``) | -| `rb.cmdi.system_interp` | High | A | `system`/`exec` call — command execution risk | - -### Deserialization - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rb.deser.yaml_load` | High | A | `YAML.load` — arbitrary object deserialization | -| `rb.deser.marshal_load` | High | A | `Marshal.load` — arbitrary Ruby object deserialization | - -### Reflection - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rb.reflection.send_dynamic` | Medium | B | `send()` with non-symbol argument — arbitrary method dispatch | -| `rb.reflection.constantize` | Medium | A | `constantize` / `safe_constantize` — dynamic class resolution | - -### SSRF - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rb.ssrf.open_uri` | Medium | A | `Kernel#open` with HTTP URL — SSRF via open-uri | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rb.crypto.md5` | Low | A | `Digest::MD5` — weak hash algorithm | - ---- - -## Examples - -### `rb.deser.yaml_load` — Unsafe YAML deserialization - -**Vulnerable:** -```ruby -data = YAML.load(params[:config]) # Arbitrary object instantiation -``` - -**Safe alternative:** -```ruby -data = YAML.safe_load(params[:config]) # Only basic Ruby types -``` - -### `rb.cmdi.backtick` — Backtick shell execution - -**Vulnerable:** -```ruby -output = `ls #{user_dir}` # Command injection via interpolation -``` - -**Safe alternative:** -```ruby -require 'open3' -output, status = Open3.capture2('ls', user_dir) -``` - -### `rb.reflection.send_dynamic` — Dynamic method dispatch - -**Vulnerable:** -```ruby -obj.send(params[:method], params[:arg]) # Arbitrary method invocation -``` - -**Safe alternative:** -```ruby -allowed = %w[name email phone] -if allowed.include?(params[:method]) - obj.send(params[:method]) -end -``` - -### `rb.deser.marshal_load` — Marshal deserialization - -**Vulnerable:** -```ruby -obj = Marshal.load(request.body.read) -``` - -**Safe alternative:** -```ruby -data = JSON.parse(request.body.read) -``` diff --git a/docs/rules/rust.md b/docs/rules/rust.md deleted file mode 100644 index 03ef12af..00000000 --- a/docs/rules/rust.md +++ /dev/null @@ -1,105 +0,0 @@ -# Rust Rules - -Nyx detects Rust vulnerabilities through AST patterns (memory safety, code quality) and taint analysis (command injection via `env::var` → `Command::new`). - -## Taint Sources - -| Function | Capability | Source Kind | -|----------|-----------|-------------| -| `std::env::var`, `env::var` | `all` | EnvironmentConfig | - -## Taint Sinks - -| Function | Required Capability | -|----------|-------------------| -| `Command::new`, `Command::arg`, `Command::args` | `SHELL_ESCAPE` | -| `Command::status`, `Command::output` | `SHELL_ESCAPE` | -| `fs::read_to_string`, `fs::write`, `fs::read`, `File::open`, `File::create` | `FILE_IO` | - -## Taint Sanitizers - -| Function | Strips Capability | -|----------|------------------| -| `html_escape::encode_safe`, `sanitize_html` | `HTML_ESCAPE` | -| `shell_escape::unix::escape`, `sanitize_shell` | `SHELL_ESCAPE` | - -> **Note:** `fs::read_to_string` was moved from taint sources to sinks to support path traversal detection (`env::var` → `fs::read_to_string`). - ---- - -## AST Pattern Rules - -### Memory Safety - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rs.memory.transmute` | High | A | `std::mem::transmute` — unchecked type reinterpretation | -| `rs.memory.copy_nonoverlapping` | High | A | `ptr::copy_nonoverlapping` — raw pointer memcpy | -| `rs.memory.get_unchecked` | High | A | `get_unchecked` / `get_unchecked_mut` — unchecked indexing | -| `rs.memory.mem_zeroed` | High | A | `std::mem::zeroed` — may be UB for non-POD types | -| `rs.memory.ptr_read` | High | A | `ptr::read` / `ptr::read_volatile` — raw pointer dereference | -| `rs.memory.narrow_cast` | Low | A | `as u8`/`i8`/`u16`/`i16` — possible truncation | -| `rs.memory.mem_forget` | Low | A | `std::mem::forget` — may leak resources | - -### Code Quality - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `rs.quality.unsafe_block` | Medium | A | `unsafe { }` block — manual memory safety obligation | -| `rs.quality.unsafe_fn` | Medium | A | `unsafe fn` declaration | -| `rs.quality.unwrap` | Low | A | `.unwrap()` — panics on `None`/`Err` | -| `rs.quality.expect` | Low | A | `.expect()` — panics on `None`/`Err` | -| `rs.quality.panic_macro` | Low | A | `panic!()` macro invocation | -| `rs.quality.todo` | Low | A | `todo!()` / `unimplemented!()` placeholder | - ---- - -## Examples - -### `rs.memory.transmute` — Unchecked type reinterpretation - -**Vulnerable:** -```rust -let x: u32 = 42; -let y: f32 = unsafe { std::mem::transmute(x) }; -``` - -**Safe alternative:** -```rust -let x: u32 = 42; -let y: f32 = f32::from_bits(x); -``` - -### `rs.quality.unsafe_block` — Unsafe block - -**Flagged:** -```rust -unsafe { - let ptr = &x as *const i32; - println!("{}", *ptr); -} -``` - -**Safe alternative:** -```rust -// Use safe abstractions when possible -println!("{}", x); -``` - -### Taint: `env::var` → `Command::new` - -**Vulnerable:** -```rust -let cmd = std::env::var("USER_CMD").unwrap(); -Command::new("sh").arg("-c").arg(&cmd).output()?; -``` - -**Safe alternative:** -```rust -let cmd = std::env::var("USER_CMD").unwrap(); -// Validate against allowlist -let allowed = ["ls", "whoami", "date"]; -if allowed.contains(&cmd.as_str()) { - Command::new(&cmd).output()?; -} -``` diff --git a/docs/rules/typescript.md b/docs/rules/typescript.md deleted file mode 100644 index b86427b8..00000000 --- a/docs/rules/typescript.md +++ /dev/null @@ -1,81 +0,0 @@ -# TypeScript Rules - -TypeScript rules mirror JavaScript patterns plus TypeScript-specific type-safety escape detectors. Taint labels are shared with JavaScript (see [JavaScript Rules](javascript.md)). - ---- - -## AST Pattern Rules - -### Code Execution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `ts.code_exec.eval` | High | A | `eval()` — dynamic code execution | -| `ts.code_exec.new_function` | High | A | `new Function()` — eval equivalent | -| `ts.code_exec.settimeout_string` | Medium | A | `setTimeout`/`setInterval` with string argument | - -### XSS Sinks - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `ts.xss.document_write` | Medium | A | `document.write()` / `document.writeln()` | -| `ts.xss.outer_html` | Medium | A | Assignment to `.outerHTML` | -| `ts.xss.insert_adjacent_html` | Medium | A | `insertAdjacentHTML()` | -| `ts.xss.location_assign` | Medium | A | Assignment to `location`/`location.href` | -| `ts.xss.cookie_write` | Low | A | Write to `document.cookie` | - -### Prototype Pollution - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `ts.prototype.proto_assignment` | Medium | A | Assignment to `__proto__` | - -### Weak Crypto - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `ts.crypto.math_random` | Low | A | `Math.random()` — not cryptographically secure | - -### Code Quality (TypeScript-specific) - -| Rule ID | Severity | Tier | Description | -|---------|----------|------|-------------| -| `ts.quality.any_annotation` | Low | A | Type annotation of `any` — disables type checking | -| `ts.quality.as_any` | Low | A | Type assertion `as any` — type-safety escape hatch | - ---- - -## Examples - -### `ts.quality.any_annotation` — `any` type - -**Flagged:** -```typescript -function process(data: any) { // ts.quality.any_annotation - data.whatever(); // No type checking -} -``` - -**Safe alternative:** -```typescript -interface UserData { name: string; email: string; } -function process(data: UserData) { - console.log(data.name); -} -``` - -### `ts.quality.as_any` — Type assertion escape - -**Flagged:** -```typescript -const result = someValue as any; // ts.quality.as_any -result.nonexistentMethod(); -``` - -**Safe alternative:** -```typescript -if (isValidType(someValue)) { - const result = someValue as KnownType; - result.knownMethod(); -} -``` diff --git a/docs/serve.md b/docs/serve.md new file mode 100644 index 00000000..a2afd4d3 --- /dev/null +++ b/docs/serve.md @@ -0,0 +1,124 @@ +# `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`. + +

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 at the HTTP layer ([`src/server/security.rs`](https://github.com/elicpeter/nyx/blob/master/src/server/security.rs)): + +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 ''`. +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. +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. + +### 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`, `PathWidened`, `ForwardBailed`, etc.). Anything tagged `under-report` means the emitted flow is real and the result set is a lower bound; `over-report` means widening or bail. `--require-converged` in the CLI drops the over-report ones for strict gates. + +### 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/examples/cfg_analysis/example.js b/examples/cfg_analysis/example.js deleted file mode 100644 index a65d4dd4..00000000 --- a/examples/cfg_analysis/example.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - EXPECTED OUTPUT (high-level): - - 1) cfg-unguarded-sink (High / High confidence) - - handler(req,res): source req.body.cmd flows to child_process.exec(cmd) without sanitizer/guard. - - Should rank high (entry-point-ish function name 'handler', close to entry). - - 2) cfg-auth-gap (High / Medium) - - handler is entry-point-ish (name matches handler/route/api conventions). - - No auth guard dominates sink (require_auth / is_authenticated / is_admin / authorize). - - 3) cfg-error-fallthrough (Medium / Medium) - - Example: if (err) { console.log(err); } then exec(...) still runs. - - This is the JS analogue of your Go heuristic. If your implementation only targets Go, this should be NO finding. - If you later generalize, this file includes a pattern you can test against. - - 4) cfg-unguarded-sink (HTML) (Medium/High) - - req.query.html is written into innerHTML without DOMPurify.sanitize - - 5) No findings for safe paths: - - safeHandler uses encodeURIComponent before exec (URL_ENCODE sanitizer) OR uses a dedicated sanitizer you map to SHELL_ESCAPE. - NOTE: encodeURIComponent is URL_ENCODE, not SHELL_ESCAPE — so for SHELL_ESCAPE sinks, it may still be flagged depending on your caps logic. - The “definitely safe” case here uses a dummy sanitize_shell() wrapper to match your Rust-style naming if you add it for JS later. - - safeHtml uses DOMPurify.sanitize before innerHTML (HTML_ESCAPE). - - Taint / dataflow: - - should find taint from req.body / req.query / process.env sources to exec/eval/innerHTML sinks. - */ - -const child_process = require("child_process"); - -// ─── Entry-point-ish + unguarded shell sink + auth gap ──────────────────────────── -function handler(req, res) { - // Source (Cap::all): req.body - const cmd = req.body.cmd; - - // Vulnerable sink (Cap::SHELL_ESCAPE): child_process.exec - child_process.exec(cmd); - - res.end("ok"); -} - -// ─── Guarded HTML sink (should NOT be flagged) ──────────────────────────────────── -function safeHtml(req, res, DOMPurify) { - const html = req.query.html; // Source - const cleaned = DOMPurify.sanitize(html); // Sanitizer(HTML_ESCAPE) - document.getElementById("app").innerHTML = cleaned; // Sink(HTML_ESCAPE) - res.end("ok"); -} - -// ─── Unguarded HTML sink (should be flagged) ───────────────────────────────────── -function unsafeHtml(req, res) { - const html = req.query.html; // Source - document.getElementById("app").innerHTML = html; // Sink(HTML_ESCAPE) without sanitizer - res.end("ok"); -} - -// ─── Heuristic error fallthrough pattern (JS analogue) ─────────────────────────── -// If your error-handling analysis is Go-only, ignore this for now. -// If generalized later, it should be flagged. -function errFallthrough(req, res) { - const err = req.query.err; - if (err) { - console.log(err); - } - child_process.exec(req.body.cmd); - res.end("ok"); -} - -// ─── Optional: eval sink (should be flagged) ───────────────────────────────────── -function evalSink(req) { - const payload = process.env.PAYLOAD; // Source - eval(payload); // Sink(SHELL_ESCAPE) per your rules -} \ No newline at end of file diff --git a/examples/cfg_analysis/example.rs b/examples/cfg_analysis/example.rs deleted file mode 100644 index 4e420800..00000000 --- a/examples/cfg_analysis/example.rs +++ /dev/null @@ -1,99 +0,0 @@ -/*! -EXPECTED OUTPUT (high-level): - -1) cfg-unguarded-sink (High / High confidence) - - In handle_request(): user input from std::env::var("INPUT") flows to std::process::Command::new("sh").arg(&input) - - No dominating SHELL_ESCAPE sanitizer or validation guard for that value. - - This should rank very high in scoring (entry-point-ish name + close to entry + shell sink). - -2) cfg-auth-gap (High / Medium confidence) - - handle_request() looks like an entry-point (name matches handle_*) - - Contains a shell sink without an auth guard (require_auth / is_authenticated / is_admin etc.) - -3) cfg-resource-leak (Medium / High or Medium confidence) - - alloc_then_return_leak(): malloc without free on an early return path. - -4) cfg-unreachable-sanitizer or cfg-unreachable-guard (Medium/Low) - - unreachable_sanitizer(): sanitizer call in unreachable block. - -5) taint / dataflow (existing BFS taint engine): - - should detect at least one taint finding for: - env::var source -> Command sink - - should NOT flag safe_shell() because it uses shell_escape::unix::escape(&input) and passes `safe`. - -Notes: -- This fixture intentionally contains both vulnerable and safe patterns, plus unreachable code and resource misuse, - to exercise cfg_analysis::{unreachable, guards, auth, resources, scoring}. -*/ - -use std::process::Command; - -// ─── CFG: Entry-point-ish + unguarded sink + auth gap ───────────────────────────── - -pub fn handle_request() { - // Source (Cap::all) - let input = std::env::var("INPUT").unwrap(); - - // Vulnerable sink (Cap::SHELL_ESCAPE) - Command::new("sh").arg(&input).status().unwrap(); -} - -// ─── CFG: Guarded sink (should NOT produce cfg-unguarded-sink) ──────────────────── - -pub fn safe_shell() { - let input = std::env::var("INPUT").unwrap(); - - // Sanitizer (Cap::SHELL_ESCAPE) - let safe = shell_escape::unix::escape(&input); - - // Sink, but guarded by dominating sanitizer - Command::new("sh").arg(&safe).status().unwrap(); -} - -// ─── CFG: Unreachable sanitizer (should report unreachable sanitizer/guard) ─────── - -pub fn unreachable_sanitizer() { - let input = std::env::var("INPUT").unwrap(); - - return; - - // This block is unreachable; should produce an unreachable finding for sanitizer call. - let _safe = shell_escape::unix::escape(&input); -} - -// ─── CFG: Resource misuse (malloc without free on some exit path) ───────────────── - -extern "C" { - fn malloc(size: usize) -> *mut u8; - fn free(ptr: *mut u8); -} - -pub fn alloc_then_return_leak(flag: bool) { - unsafe { - let p = malloc(128); - - // Early return leaks `p` on this path. - if flag { - return; - } - - free(p); - } -} - -// ─── Extra: HTML sink labeling sanity (optional) ────────────────────────────────── - -// `sink_html` is a test marker recognized as Sink(HTML_ESCAPE) by the label rules. -// In real code this would be something like response.body(), template.render(), etc. -fn sink_html(_s: &str) {} - -pub fn html_print() { - let raw = std::env::var("HTML").unwrap(); - sink_html(&raw); -} - -pub fn html_print_sanitized() { - let raw = std::env::var("HTML").unwrap(); - let safe = html_escape::encode_safe(&raw); - sink_html(&safe); -} \ No newline at end of file diff --git a/examples/cross-file/config.rs b/examples/cross-file/config.rs deleted file mode 100644 index ead5abd8..00000000 --- a/examples/cross-file/config.rs +++ /dev/null @@ -1,36 +0,0 @@ -// ───────────────────────────────────────────────────────────────────────────── -// examples/cross-file/config.rs — Sources -// -// This module reads untrusted data from the environment and filesystem. -// Every public function here acts as a **source** — its return value -// carries taint. -// -// ┌─────────────────────────────────────────────────────────────────────────┐ -// │ FuncSummary produced by pass 1: │ -// │ │ -// │ get_user_command → source_caps: ALL, sink: 0, sanitizer: 0 │ -// │ get_config_path → source_caps: ALL, sink: 0, sanitizer: 0 │ -// │ load_template → source_caps: ALL, sink: 0, sanitizer: 0 │ -// └─────────────────────────────────────────────────────────────────────────┘ -// ───────────────────────────────────────────────────────────────────────────── - -use std::env; -use std::fs; - -/// Reads a user-supplied command from the environment. -/// Taint: SOURCE(ALL) — caller must sanitise before passing to any sink. -pub fn get_user_command() -> String { - env::var("USER_CMD").unwrap_or_default() -} - -/// Reads a path from the environment. -/// Taint: SOURCE(ALL) -pub fn get_config_path() -> String { - env::var("CONFIG_PATH").unwrap_or_default() -} - -/// Reads an HTML template from disk (path is trusted, *content* is not). -/// Taint: SOURCE(ALL) -pub fn load_template(path: &str) -> String { - fs::read_to_string(path).unwrap_or_default() -} diff --git a/examples/cross-file/exec.rs b/examples/cross-file/exec.rs deleted file mode 100644 index d35d6e9b..00000000 --- a/examples/cross-file/exec.rs +++ /dev/null @@ -1,41 +0,0 @@ -// ───────────────────────────────────────────────────────────────────────────── -// examples/cross-file/exec.rs — Sinks -// -// Functions that perform dangerous operations. Passing tainted data to -// these without the matching sanitiser is a vulnerability. -// -// ┌─────────────────────────────────────────────────────────────────────────┐ -// │ FuncSummary produced by pass 1: │ -// │ │ -// │ run_command → sink_caps: SHELL_ESCAPE, tainted_sink_params: [0] │ -// │ render_page → sink_caps: HTML_ESCAPE, tainted_sink_params: [0] │ -// │ log_and_execute → sink_caps: SHELL_ESCAPE, source_caps: ALL │ -// │ (both a source AND a sink!) │ -// └─────────────────────────────────────────────────────────────────────────┘ -// ───────────────────────────────────────────────────────────────────────────── - -use std::env; -use std::process::Command; - -/// Executes a shell command. -/// Taint: SINK(SHELL_ESCAPE) on `cmd` (param 0). -pub fn run_command(cmd: &str) { - Command::new("sh").arg(cmd).status().unwrap(); -} - -/// Renders user content into an HTML page. -/// Taint: SINK(HTML_ESCAPE) on `body` (param 0). -pub fn render_page(body: &str) { - println!("{body}"); -} - -/// Reads an env var *and* shells out — a function that is simultaneously -/// a source (return value) and a sink (cmd parameter). -/// -/// This exercises the "independent caps" design: source_caps and sink_caps -/// are both non-zero on the same summary. -pub fn log_and_execute(cmd: &str) -> String { - let log_path = env::var("LOG_PATH").unwrap_or_default(); - Command::new("sh").arg(cmd).status().unwrap(); - log_path -} diff --git a/examples/cross-file/main.rs b/examples/cross-file/main.rs deleted file mode 100644 index abe49134..00000000 --- a/examples/cross-file/main.rs +++ /dev/null @@ -1,148 +0,0 @@ -// ───────────────────────────────────────────────────────────────────────────── -// examples/cross-file/main.rs — The caller -// -// This file calls functions from config.rs, sanitize.rs, and exec.rs. -// It never directly touches std::env, std::fs, or std::process — every -// source, sanitiser, and sink lives in another file. -// -// Nyx's two-pass cross-file taint analysis should: -// • Pass 1: summarise config.rs, sanitize.rs, exec.rs -// • Pass 2: resolve calls in main.rs against those summaries -// -// ───────────────────────────────────────────────────────────────────────────── -// -// EXPECTED NYX OUTPUT -// =================== -// -// examples/cross-file/main.rs -// 12:5 [High] taint-unsanitised-flow ← case_1_direct_source_to_sink -// 22:5 [High] taint-unsanitised-flow ← case_3_wrong_sanitiser -// 34:5 [High] taint-unsanitised-flow ← case_5_passthrough_preserves_taint -// 40:5 [High] taint-unsanitised-flow ← case_6_taint_through_branch -// 50:5 [High] taint-unsanitised-flow ← case_8_source_and_sink_same_fn -// -// examples/cross-file/exec.rs -// 30:5 [High] taint-unsanitised-flow ← log_and_execute internal vuln -// -// NO findings expected for: -// case_2 (correct sanitiser applied) -// case_4 (correct html sanitiser applied) -// case_7 (sanitised before branch) -// -// ───────────────────────────────────────────────────────────────────────────── - -// ─── Case 1: Direct source → sink (UNSAFE) ────────────────────────────────── -// -// get_user_command() returns tainted(ALL) -// run_command() is a sink(SHELL_ESCAPE) -// No sanitiser in between → FINDING -// -fn case_1_direct_source_to_sink() { - let cmd = get_user_command(); // tainted(ALL) via cross-file source - run_command(&cmd); // FINDING: taint reaches shell sink -} - -// ─── Case 2: Correctly sanitised (SAFE) ───────────────────────────────────── -// -// get_user_command() returns tainted(ALL) -// sanitize_shell() strips SHELL_ESCAPE -// run_command() sinks SHELL_ESCAPE → bit is gone → no finding -// -fn case_2_sanitised_before_sink() { - let cmd = get_user_command(); // tainted(ALL) - let safe = sanitize_shell(&cmd); // SHELL_ESCAPE bit stripped - run_command(&safe); // SAFE — no finding -} - -// ─── Case 3: Wrong sanitiser for the sink (UNSAFE) ────────────────────────── -// -// get_user_command() returns tainted(ALL) -// sanitize_html() strips HTML_ESCAPE — but NOT SHELL_ESCAPE -// run_command() sinks SHELL_ESCAPE → bit still set → FINDING -// -fn case_3_wrong_sanitiser() { - let cmd = get_user_command(); // tainted(ALL) - let wrong = sanitize_html(&cmd); // strips HTML_ESCAPE only - run_command(&wrong); // FINDING: SHELL_ESCAPE still set -} - -// ─── Case 4: Correct HTML sanitiser (SAFE) ────────────────────────────────── -// -// load_template() returns tainted(ALL) from file read -// sanitize_html() strips HTML_ESCAPE -// render_page() sinks HTML_ESCAPE → bit is gone → no finding -// -fn case_4_html_sanitised() { - let tpl = load_template("page.html"); // tainted(ALL) via cross-file source - let safe = sanitize_html(&tpl); // HTML_ESCAPE bit stripped - render_page(&safe); // SAFE — no finding -} - -// ─── Case 5: Passthrough preserves taint (UNSAFE) ─────────────────────────── -// -// get_user_command() returns tainted(ALL) -// passthrough() propagates taint unchanged (propagates_taint = true) -// run_command() sinks SHELL_ESCAPE → still tainted → FINDING -// -fn case_5_passthrough_preserves_taint() { - let cmd = get_user_command(); // tainted(ALL) - let same = passthrough(&cmd); // taint flows through - run_command(&same); // FINDING: still tainted -} - -// ─── Case 6: Taint flows through only one branch (UNSAFE) ─────────────────── -// -// One branch sanitises, the other does not. -// The unsanitised branch reaches the sink → FINDING on that path. -// -fn case_6_taint_through_branch() { - let cmd = get_user_command(); // tainted(ALL) - if cmd.len() > 10 { - run_command(&cmd); // FINDING: unsanitised path - } else { - let safe = sanitize_shell(&cmd); - run_command(&safe); // SAFE path - } -} - -// ─── Case 7: Sanitised before branch (SAFE) ───────────────────────────────── -// -// Sanitisation happens before the branch → both paths are clean. -// -fn case_7_sanitised_before_branch() { - let cmd = get_user_command(); // tainted(ALL) - let safe = sanitize_shell(&cmd); // SHELL_ESCAPE stripped - if safe.len() > 10 { - run_command(&safe); // SAFE - } else { - run_command(&safe); // SAFE - } -} - -// ─── Case 8: Source-and-sink function (UNSAFE) ────────────────────────────── -// -// log_and_execute() is both: -// • a SINK(SHELL_ESCAPE) on its cmd parameter -// • a SOURCE(ALL) in its return value (reads env var) -// -// Passing tainted data to it → FINDING for the sink. -// Its return value is freshly tainted, but we don't pass it anywhere -// dangerous here — so only one finding. -// -fn case_8_source_and_sink_same_fn() { - let cmd = get_user_command(); // tainted(ALL) - let _log = log_and_execute(&cmd); // FINDING: tainted arg hits shell sink - // _log is now tainted(ALL) from log_and_execute's source behaviour, - // but we don't use it — no second finding. -} - -fn main() { - case_1_direct_source_to_sink(); - case_2_sanitised_before_sink(); - case_3_wrong_sanitiser(); - case_4_html_sanitised(); - case_5_passthrough_preserves_taint(); - case_6_taint_through_branch(); - case_7_sanitised_before_branch(); - case_8_source_and_sink_same_fn(); -} diff --git a/examples/cross-file/sanitize.rs b/examples/cross-file/sanitize.rs deleted file mode 100644 index c64b1006..00000000 --- a/examples/cross-file/sanitize.rs +++ /dev/null @@ -1,30 +0,0 @@ -// ───────────────────────────────────────────────────────────────────────────── -// examples/cross-file/sanitize.rs — Sanitizers -// -// Functions that clean specific taint capabilities. After passing through -// one of these, the corresponding Cap bit is stripped. -// -// ┌─────────────────────────────────────────────────────────────────────────┐ -// │ FuncSummary produced by pass 1: │ -// │ │ -// │ sanitize_shell → sanitizer_caps: SHELL_ESCAPE, propagates: true │ -// │ sanitize_html → sanitizer_caps: HTML_ESCAPE, propagates: true │ -// │ passthrough → sanitizer: 0, source: 0, sink: 0, propagates: true │ -// └─────────────────────────────────────────────────────────────────────────┘ -// ───────────────────────────────────────────────────────────────────────────── - -/// Escapes shell metacharacters. Strips the SHELL_ESCAPE cap bit. -pub fn sanitize_shell(input: &str) -> String { - shell_escape::unix::escape(input.into()).to_string() -} - -/// Escapes HTML entities. Strips the HTML_ESCAPE cap bit. -pub fn sanitize_html(input: &str) -> String { - html_escape::encode_safe(input).to_string() -} - -/// Does nothing security-relevant — just returns a copy. -/// Taint passes straight through (propagates_taint = true). -pub fn passthrough(input: &str) -> String { - input.to_string() -} diff --git a/examples/sanatize/example.rs b/examples/sanatize/example.rs deleted file mode 100644 index c01f2923..00000000 --- a/examples/sanatize/example.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! demo.rs — realistic taint-tracking playground -//! `cargo add html-escape shell-escape` before compiling. - -use std::{env, process::Command, fs}; - -#[derive(Default)] -struct UserCtx { - query: String, // potentially tainted - sanitized: String, // should remain clean -} - -/// ---------- helper wrappers so we get nice Source / Sink labels ---------- -fn source_env(var: &str) -> String { - env::var(var).unwrap_or_default() // Source(env-var) -} - -fn source_file(path: &str) -> String { - fs::read_to_string(path).unwrap_or_default() // Source(file-io) -} - -fn sink_shell(arg: &str) { - Command::new("sh").arg(arg).status().unwrap(); // Sink(process-spawn) -} - -fn sink_html(out: &str) { - println!("{out}"); // Sink(html-out) -} - -fn sanitize_html(s: &str) -> String { - html_escape::encode_safe(s) // Sanitizer(html-escape) -} - -fn sanitize_shell(s: &str) -> String { - shell_escape::unix::escape(s.into()).into_owned() // Sanitizer(shell-escape) -} - -/// ---------- 1. Main demo fuction ---------- -fn main() { - // FLOW A ──────────────────────────────────────────────────────────────── - // env → sanitized → safe shell - let raw = source_env("USER_CMD"); - let clean = sanitize_shell(&raw); - sink_shell(&clean); // EXPECT: SAFE - - // FLOW B ──────────────────────────────────────────────────────────────── - // env → if-else, only one branch escapes - let arg = source_env("ANOTHER"); - if arg.len() > 5 { - sink_shell(&arg); // EXPECT: UNSAFE (branch tainted) - } else { - let escaped = sanitize_shell(&arg); - sink_shell(&escaped); // safe - } - - // FLOW C ──────────────────────────────────────────────────────────────── - // file → while loop → HTML sanitizer cleared - let mut data = source_file("/tmp/input.txt"); - while data.len() < 32 { - data.push('x'); - } - let html_ok = sanitize_html(&data); - sink_html(&html_ok); // safe - - // FLOW D ──────────────────────────────────────────────────────────────── - // file → struct field → match → unsanitised HTML - let mut ctx = UserCtx::default(); - ctx.query = source_file("/tmp/q.txt"); - // overwrite the clean field; `ctx.sanitized` is *not* tainted - ctx.sanitized = sanitize_html("constant"); - match ctx { - UserCtx { query, sanitized } if query.contains("DROP") => { - sink_html(&query); // EXPECT: UNSAFE - } - _ => { - sink_html(&ctx.sanitized); // safe - } - } - - // FLOW E ──────────────────────────────────────────────────────────────── - // source → function call → reassignment clears taint - let mut name = source_env("USER"); // tainted - greet(&name); // just prints - name = "anonymous".into(); // kills taint - greet(&name); // safe - - // FLOW F ──────────────────────────────────────────────────────────────── - // Multiple sanitizers, only the *right* one matters - let cmd = source_env("MIXED"); - let partly = sanitize_html(&cmd); // wrong sanitizer - sink_shell(&partly); // EXPECT: UNSAFE -} - -/// helper (non-sink) function -fn greet(who: &str) { - println!("Hello, {who}"); -} \ No newline at end of file diff --git a/examples/single-func/example.rs b/examples/single-func/example.rs deleted file mode 100644 index ca0642c9..00000000 --- a/examples/single-func/example.rs +++ /dev/null @@ -1,8 +0,0 @@ -fn source_env(var: &str) -> String { - env::var(var).unwrap_or_default() // Source(env-var) -} - -fn main() { - let raw = source_env("USER_CMD"); - Command::new("sh").arg(raw).status().unwrap(); -} \ No newline at end of file diff --git a/examples/standard/test.rs b/examples/standard/test.rs deleted file mode 100644 index 170b6f5c..00000000 --- a/examples/standard/test.rs +++ /dev/null @@ -1,30 +0,0 @@ -fn source_env(var: &str) -> String { - env::var(var).unwrap_or_default() // Source(env-var) -} - -fn source_file(path: &str) -> String { - fs::read_to_string(path).unwrap_or_default() // Source(file-io) -} - -fn sink_shell(arg: &str) { - Command::new("sh").arg(arg).status().unwrap(); // Sink(process-spawn) -} - -fn sink_html(out: &str) { - println!("{out}"); // Sink(html-out) -} - -fn main() { - let raw = source_env("USER_CMD"); - let raw2 = source_file("ANOTHER"); - let x = source_env("ANOTHER"); - if x.len() > 5 { - sink_shell(&x); // EXPECT: UNSAFE - return; - } else { - let escaped = sanitize_shell(&x); - sink_shell(&escaped); // safe - } - sink_shell(raw); // EXPECT: UNSAFE - sink_html(raw2); -} \ No newline at end of file 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..56c058b4 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + Nyx Scanner + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..3fffd592 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5485 @@ +{ + "name": "nyx-frontend", + "version": "0.5.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nyx-frontend", + "version": "0.5.0", + "license": "GPL-3.0-or-later", + "dependencies": { + "@tanstack/react-query": "^5.62.0", + "elkjs": "^0.11.1", + "graphology": "^0.26.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "sigma": "^3.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.1.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "license-checker-rseidelsohn": "^4.4.2", + "prettier": "^3.8.1", + "typescript": "~5.6.2", + "typescript-eslint": "^8.57.2", + "vite": "^6.0.0", + "vitest": "^4.1.1" + } + }, + "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.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "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-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "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.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "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.1.1" + }, + "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.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "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/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/fs/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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz", + "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", + "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.95.2" + }, + "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/@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/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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/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/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "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.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "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/@typescript-eslint/visitor-keys/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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz", + "integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.1", + "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.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.1", + "vitest": "4.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", + "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", + "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.1", + "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.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", + "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", + "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "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/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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", + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "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/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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/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/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/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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "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/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, + "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": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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/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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "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/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "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", + "optional": true + }, + "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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@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.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "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.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.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/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/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": "4.4.2", + "resolved": "https://registry.npmjs.org/license-checker-rseidelsohn/-/license-checker-rseidelsohn-4.4.2.tgz", + "integrity": "sha512-Sf8WaJhd2vELvCne+frS9AXqnY/vv591s2/nZcJDwTnoNgltG4mAmoenffVb8L2YPRYbxARLyrHJBC38AVfpuA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "4.1.2", + "debug": "^4.3.4", + "lodash.clonedeep": "^4.5.0", + "mkdirp": "^1.0.4", + "nopt": "^7.2.0", + "read-installed-packages": "^2.0.1", + "semver": "^7.3.5", + "spdx-correct": "^3.1.1", + "spdx-expression-parse": "^3.0.1", + "spdx-satisfies": "^5.0.1", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker-rseidelsohn": "bin/license-checker-rseidelsohn.js" + }, + "engines": { + "node": ">=18", + "npm": ">=8" + } + }, + "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/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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "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/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/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/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.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": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "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.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "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/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": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "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-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-installed-packages": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/read-installed-packages/-/read-installed-packages-2.0.1.tgz", + "integrity": "sha512-t+fJOFOYaZIjBpTVxiV8Mkt7yQyy4E6MSrrnt5FmPd4enYvpU/9DYGirDmN1XQwkfeuWIhM/iu0t2rm6iSr0CA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "debug": "^4.3.4", + "read-package-json": "^6.0.0", + "semver": "2 || 3 || 4 || 5 || 6 || 7", + "slide": "~1.1.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "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.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz", + "integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==", + "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/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "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-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-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": "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-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": "5.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz", + "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.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.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "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.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.27" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", + "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/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/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": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "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.0.0" + } + }, + "node_modules/undici": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", + "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/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", + "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "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.0.3", + "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.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", + "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/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/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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "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..e78f4d18 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "nyx-frontend", + "private": true, + "version": "0.5.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.62.0", + "elkjs": "^0.11.1", + "graphology": "^0.26.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "sigma": "^3.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.1.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "license-checker-rseidelsohn": "^4.4.2", + "prettier": "^3.8.1", + "typescript": "~5.6.2", + "typescript-eslint": "^8.57.2", + "vite": "^6.0.0", + "vitest": "^4.1.1" + } +} 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..ed0cd3c0 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,17 @@ +import { QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import { queryClient } from './api/queryClient'; +import { SSEProvider } from './contexts/SSEContext'; +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..365a1bf1 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,104 @@ +const BASE = '/api'; +const CSRF_HEADER = 'X-Nyx-CSRF'; +let csrfTokenPromise: Promise | null = null; + +export class ApiError extends Error { + constructor( + public status: number, + message: string, + ) { + super(message); + this.name = 'ApiError'; + } +} + +async function getCsrfToken(): Promise { + if (!csrfTokenPromise) { + csrfTokenPromise = fetch(`${BASE}/session`) + .then(async (res) => { + if (!res.ok) { + throw new ApiError( + res.status, + await res.text().catch(() => res.statusText), + ); + } + + 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'); + } + + 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'; + } + const res = await fetch(url, { + ...rest, + headers, + }); + + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new ApiError(res.status, text); + } + + // 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 apiDelete(path: string, signal?: AbortSignal): Promise { + return request(path, { method: 'DELETE', signal }); +} diff --git a/frontend/src/api/mutations/config.ts b/frontend/src/api/mutations/config.ts new file mode 100644 index 00000000..368d8641 --- /dev/null +++ b/frontend/src/api/mutations/config.ts @@ -0,0 +1,157 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiPost, 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'] }); + }, + }); +} + +export function useDeleteSource() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => apiDelete('/config/sources'), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sources'] }); + }, + }); +} + +// --- Sinks --- + +export function useAddSink() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiPost('/config/sinks', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sinks'] }); + }, + }); +} + +export function useDeleteSink() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => apiDelete('/config/sinks'), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sinks'] }); + }, + }); +} + +// --- Sanitizers --- + +export function useAddSanitizer() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => + apiPost('/config/sanitizers', body), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sanitizers'] }); + }, + }); +} + +export function useDeleteSanitizer() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddLabelBody) => apiDelete('/config/sanitizers'), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'sanitizers'] }); + }, + }); +} + +// --- 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'] }); + }, + }); +} + +export function useDeleteTerminator() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: AddTerminatorBody) => + apiDelete('/config/terminators'), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['config', 'terminators'] }); + }, + }); +} + +// --- 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'] }); + }, + }); +} + +// --- 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..faf413ce --- /dev/null +++ b/frontend/src/api/mutations/scans.ts @@ -0,0 +1,34 @@ +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 interface StartScanBody { + scan_root?: string; + mode?: ScanMode; + engine_profile?: EngineProfile; +} + +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..e26c3b4f --- /dev/null +++ b/frontend/src/api/queries/config.ts @@ -0,0 +1,48 @@ +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 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..ee2d713b --- /dev/null +++ b/frontend/src/api/queries/debug.ts @@ -0,0 +1,111 @@ +import { useQuery } from '@tanstack/react-query'; +import { apiGet } from '../client'; +import type { + FunctionInfo, + CfgGraphView, + SsaBodyView, + TaintAnalysisView, + AbstractInterpView, + SymexView, + CallGraphView, + FuncSummaryView, +} 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), + }); +} 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..b7e39f40 --- /dev/null +++ b/frontend/src/api/queries/findings.ts @@ -0,0 +1,63 @@ +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; + 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/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..149f5b06 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,593 @@ +// Evidence types (from src/evidence.rs) +export type Confidence = 'Low' | 'Medium' | 'High'; +export type FlowStepKind = 'source' | 'assignment' | 'call' | 'phi' | 'sink'; + +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[]; +} + +// 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; +} + +export interface FindingView { + index: number; + fingerprint: string; + portable_fingerprint?: string; + 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; + 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[]; +} + +// Scan types +export interface TimingBreakdown { + walk_ms: number; + pass1_ms: number; + call_graph_ms: number; + pass2_ms: number; + post_process_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; +} + +// 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[]; +} + +// 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[]; +} + +// 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; + 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; + kind: 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; + 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; + 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; +} 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/charts/HorizontalBarChart.tsx b/frontend/src/components/charts/HorizontalBarChart.tsx new file mode 100644 index 00000000..73a5d5a5 --- /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 = 22; + const gap = 4; + const labelW = 110; + const valueW = 45; + const barAreaW = width - labelW - valueW - 16; + const totalH = items.length * (barH + gap); + const maxVal = maxValue ?? Math.max(...items.map((i) => i.value), 1); + + return ( +
+ + {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..a79ef472 --- /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 = 160, +}: 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..078fa316 --- /dev/null +++ b/frontend/src/components/icons/Icons.tsx @@ -0,0 +1,170 @@ +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 SettingsIcon({ className, size = 18 }: IconProps) { + return ( + + + + + ); +} + +export function FolderIcon({ className, size = 14 }: IconProps) { + return ( + + + + ); +} + +export function TagIcon({ className, size = 14 }: 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, + settings: SettingsIcon, + folder: FolderIcon, + tag: TagIcon, +}; diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx new file mode 100644 index 00000000..02c78124 --- /dev/null +++ b/frontend/src/components/layout/AppLayout.tsx @@ -0,0 +1,67 @@ +import { useState, useCallback } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import { Sidebar } from './Sidebar'; +import { HeaderBar } from './HeaderBar'; +import { NewScanModal } from '../../modals/NewScanModal'; +import { OverviewPage } from '../../pages/OverviewPage'; +import { FindingsPage } from '../../pages/FindingsPage'; +import { FindingDetailPage } from '../../pages/FindingDetailPage'; +import { ScansPage } from '../../pages/ScansPage'; +import { ScanDetailPage } from '../../pages/ScanDetailPage'; +import { ScanComparePage } from '../../pages/ScanComparePage'; +import { RulesPage } from '../../pages/RulesPage'; +import { TriagePage } from '../../pages/TriagePage'; +import { ConfigPage } from '../../pages/ConfigPage'; +import { StubPage } from '../../pages/StubPage'; +import { ExplorerPage } from '../../pages/ExplorerPage'; +import { DebugLayout } from '../../pages/debug/DebugLayout'; +import { CallGraphPage } from '../../pages/debug/CallGraphPage'; +import { SummaryExplorerPage } from '../../pages/debug/SummaryExplorerPage'; + +export function AppLayout() { + const [scanModalOpen, setScanModalOpen] = useState(false); + + const handleStartScan = useCallback(() => { + setScanModalOpen(true); + }, []); + + return ( +
+ +
+ +
+ + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + }> + } + /> + } /> + } /> + + } /> + +
+
+ setScanModalOpen(false)} + /> +
+ ); +} diff --git a/frontend/src/components/layout/HeaderBar.tsx b/frontend/src/components/layout/HeaderBar.tsx new file mode 100644 index 00000000..ba6c2fc3 --- /dev/null +++ b/frontend/src/components/layout/HeaderBar.tsx @@ -0,0 +1,90 @@ +import { Link, useLocation } from 'react-router-dom'; + +const SECTION_TITLES: Record = { + overview: 'Overview', + findings: 'Findings', + scans: 'Scans', + rules: 'Rules', + triage: 'Triage', + config: 'Config', + explorer: 'Explorer', + debug: 'Debug', + settings: 'Settings', +}; + +const ROUTE_TITLES: Record = { + '/debug/cfg': 'CFG Viewer', + '/debug/ssa': 'SSA Viewer', + '/debug/call-graph': 'Call Graph', + '/debug/taint': 'Taint Debugger', +}; + +function pathToSection(pathname: string): string { + if (pathname === '/') return 'overview'; + const first = pathname.split('/')[1]; + return first || 'overview'; +} + +function buildBreadcrumbs(pathname: string) { + const section = pathToSection(pathname); + const sectionTitle = SECTION_TITLES[section] ?? section; + const crumbs: Array<{ label: string; path?: string }> = []; + + // Always show section as root breadcrumb + const sectionPath = section === 'overview' ? '/' : `/${section}`; + crumbs.push({ label: sectionTitle, path: sectionPath }); + + // If we have a sub-route, show it + if (ROUTE_TITLES[pathname]) { + crumbs.push({ label: ROUTE_TITLES[pathname] }); + } else { + const parts = pathname.split('/').filter(Boolean); + if (parts.length > 1) { + // e.g. /findings/123 or /scans/compare/1/2 + const sub = parts.slice(1).join('/'); + crumbs.push({ label: sub }); + } + } + + return crumbs; +} + +interface HeaderBarProps { + onStartScan?: () => void; +} + +export function HeaderBar({ onStartScan }: HeaderBarProps) { + const { pathname } = useLocation(); + const crumbs = buildBreadcrumbs(pathname); + + return ( +
+
+ +
+
+ {onStartScan && ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 00000000..41b57199 --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,178 @@ +import { NavLink } from 'react-router-dom'; +import { + OverviewIcon, + FindingsIcon, + ScansIcon, + RulesIcon, + TriageIcon, + ConfigIcon, + ExplorerIcon, + DebugIcon, + SettingsIcon, + FolderIcon, + TagIcon, +} from '../icons/Icons'; +import type { FC } from 'react'; +import type { IconProps } from '../icons/Icons'; +import { useHealth } from '../../api/queries/health'; +import { useSSE } from '../../contexts/SSEContext'; + +interface NavItem { + id: string; + label: string; + path: string; + Icon: FC; + group: 'primary' | 'secondary' | 'footer'; +} + +const NAV_SECTIONS: NavItem[] = [ + { + id: 'overview', + label: 'Overview', + path: '/', + Icon: OverviewIcon, + group: 'primary', + }, + { + id: 'findings', + label: 'Findings', + path: '/findings', + Icon: FindingsIcon, + group: 'primary', + }, + { + id: 'scans', + label: 'Scans', + path: '/scans', + Icon: ScansIcon, + group: 'primary', + }, + { + id: 'rules', + label: 'Rules', + path: '/rules', + Icon: RulesIcon, + group: 'primary', + }, + { + id: 'triage', + label: 'Triage', + path: '/triage', + Icon: TriageIcon, + group: 'primary', + }, + { + id: 'config', + label: 'Config', + path: '/config', + Icon: ConfigIcon, + group: 'secondary', + }, + { + id: 'explorer', + label: 'Explorer', + path: '/explorer', + Icon: ExplorerIcon, + group: 'secondary', + }, + { + id: 'debug', + label: 'Debug', + path: '/debug', + Icon: DebugIcon, + group: 'secondary', + }, + { + id: 'settings', + label: 'Settings', + path: '/settings', + Icon: SettingsIcon, + group: 'footer', + }, +]; + +function navLinkClass({ isActive }: { isActive: boolean }) { + return `nav-link${isActive ? ' active' : ''}`; +} + +export function Sidebar() { + const { data: health } = useHealth(); + const { isScanRunning } = useSSE(); + + const primary = NAV_SECTIONS.filter((n) => n.group === 'primary'); + const secondary = NAV_SECTIONS.filter((n) => n.group === 'secondary'); + const footer = NAV_SECTIONS.filter((n) => n.group === 'footer'); + + return ( + + ); +} diff --git a/frontend/src/components/ui/Dropdown.tsx b/frontend/src/components/ui/Dropdown.tsx new file mode 100644 index 00000000..ddc0b967 --- /dev/null +++ b/frontend/src/components/ui/Dropdown.tsx @@ -0,0 +1,103 @@ +import { + useCallback, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; + +interface DropdownProps { + trigger: (opts: { open: boolean }) => ReactNode; + children: (opts: { close: () => void }) => ReactNode; + align?: 'left' | 'right'; + className?: string; +} + +export function Dropdown({ + trigger, + children, + align = 'left', + className, +}: DropdownProps) { + const [open, setOpen] = useState(false); + const rootRef = useRef(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..c7ab5a6f --- /dev/null +++ b/frontend/src/components/ui/ErrorState.tsx @@ -0,0 +1,13 @@ +interface ErrorStateProps { + title?: string; + message: string; +} + +export function ErrorState({ title = 'Error', message }: ErrorStateProps) { + return ( +
+

{title}

+

{message}

+
+ ); +} diff --git a/frontend/src/components/ui/LoadingState.tsx b/frontend/src/components/ui/LoadingState.tsx new file mode 100644 index 00000000..a6f25cfc --- /dev/null +++ b/frontend/src/components/ui/LoadingState.tsx @@ -0,0 +1,7 @@ +interface LoadingStateProps { + message?: string; +} + +export function LoadingState({ message = 'Loading...' }: LoadingStateProps) { + return
{message}
; +} 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/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/contexts/SSEContext.tsx b/frontend/src/contexts/SSEContext.tsx new file mode 100644 index 00000000..ef157963 --- /dev/null +++ b/frontend/src/contexts/SSEContext.tsx @@ -0,0 +1,109 @@ +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; + 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>(); + + 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'] }); + }); + + 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'] }); + }); + + es.addEventListener('scan_failed', () => { + setScanProgress(null); + setIsScanRunning(false); + queryClient.invalidateQueries({ queryKey: ['scans'] }); + }); + + 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/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/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/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..1ae2ce39 --- /dev/null +++ b/frontend/src/graph/layout/elk.ts @@ -0,0 +1,288 @@ +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', + }, +}; + +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..1339943b --- /dev/null +++ b/frontend/src/graph/layout/text.ts @@ -0,0 +1,119 @@ +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, + }, +}; + +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..45181d16 --- /dev/null +++ b/frontend/src/graph/styles.ts @@ -0,0 +1,258 @@ +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: '#ffffff', + backgroundSecondary: '#f7f7f8', + text: '#1a1a1a', + textSecondary: '#6b6b76', + textTertiary: '#9b9ba7', + border: '#e5e5ea', + borderLight: '#f0f0f4', + accent: '#5856d6', + accentSoft: '#ededfc', + success: '#2ecc71', + warning: '#e67e22', + danger: '#e74c3c', + neutral: '#607187', + neutralSoft: '#8c99ab', +}; + +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: '#4f78c2', + stroke: '#3c5f9a', + textFill: '#ffffff', + secondaryFill: withAlpha('#ffffff', 0.8), + shape: 'rect', + strokeWidth: 2.1, + accentFill: palette.accent, + neighborFill: withAlpha('#4f78c2', 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 callGraphNodeStyle( + palette: GraphThemePalette, + metadata?: GraphMetadata, +): NodeStyle { + const isRecursive = metadata?.isRecursive === true; + const fill = isRecursive ? '#7d6450' : palette.neutral; + const stroke = isRecursive ? '#6a5444' : 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 { + return graphKind === 'callgraph' + ? callGraphNodeStyle(palette, metadata) + : 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: [], + }; + } + + 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: '#4f78c2', 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..5869bed7 --- /dev/null +++ b/frontend/src/graph/types.ts @@ -0,0 +1,111 @@ +export type GraphViewKind = 'callgraph' | 'cfg'; + +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/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..bf6330e0 --- /dev/null +++ b/frontend/src/hooks/useFindingsURLState.ts @@ -0,0 +1,117 @@ +import { useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +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; + search: string; +} + +const FINDINGS_DEFAULTS: FindingsURLState = { + page: '1', + per_page: '50', + sort_by: '', + sort_dir: 'asc', + severity: '', + category: '', + confidence: '', + language: '', + rule_id: '', + status: '', + search: '', +}; + +const FILTER_KEYS: ReadonlySet = new Set([ + 'severity', + 'category', + 'confidence', + 'language', + 'rule_id', + 'status', + '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 state: FindingsURLState = useMemo(() => { + const s = {} as FindingsURLState; + for (const key of Object.keys( + FINDINGS_DEFAULTS, + ) as (keyof FindingsURLState)[]) { + s[key] = searchParams.get(key) || FINDINGS_DEFAULTS[key]; + } + return s; + }, [searchParams]); + + 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/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..86922d62 --- /dev/null +++ b/frontend/src/modals/NewScanModal.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Modal } from '../components/ui/Modal'; +import { useHealth } from '../api/queries/health'; +import { + useStartScan, + type ScanMode, + type EngineProfile, + 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. ~2–3× slower.', +}; + +export function NewScanModal({ open, onClose }: NewScanModalProps) { + const { data: health } = useHealth(); + const startScan = useStartScan(); + const navigate = useNavigate(); + const defaultRoot = health?.scan_root || ''; + const [scanRoot, setScanRoot] = useState(''); + const [mode, setMode] = useState('full'); + const [engineProfile, setEngineProfile] = useState('balanced'); + + 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; + const payload = Object.keys(body).length ? body : undefined; + try { + await startScan.mutateAsync(payload); + onClose(); + navigate('/scans'); + } catch (e) { + alert(e instanceof Error ? e.message : 'Failed to start scan'); + } + }; + + if (!open) return null; + + return ( + +
+

Start New Scan

+
+
+ + setScanRoot(e.target.value)} + placeholder="/path/to/project" + /> +
+
+ + + {MODE_HINTS[mode]} +
+
+ + + {PROFILE_HINTS[engineProfile]} +
+
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx new file mode 100644 index 00000000..5c4b8d92 --- /dev/null +++ b/frontend/src/pages/ConfigPage.tsx @@ -0,0 +1,519 @@ +import { useState, useCallback } from 'react'; +import { + useConfig, + useSources, + useSinks, + useSanitizers, + useTerminators, + useProfiles, +} from '../api/queries/config'; +import { + useAddSource, + useDeleteSource, + useAddSink, + useDeleteSink, + useAddSanitizer, + useDeleteSanitizer, + useAddTerminator, + useDeleteTerminator, + useAddProfile, + useDeleteProfile, + useActivateProfile, + useToggleTriageSync, +} from '../api/mutations/config'; +import { LoadingState } from '../components/ui/LoadingState'; +import { ErrorState } from '../components/ui/ErrorState'; +import type { LabelEntryView, TerminatorView, ProfileView } from '../api/types'; + +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', +]; + +// ── Collapsible Config Section ─────────────────────────────────────────────── + +function ConfigSection({ + title, + id, + children, +}: { + title: string; + id: string; + children: React.ReactNode; +}) { + const [collapsed, setCollapsed] = useState(false); + + return ( +
+
setCollapsed(!collapsed)} + > + + ▼ + {' '} + {title} +
+
+ {children} +
+
+ ); +} + +// ── Label Table (Source/Sink/Sanitizer) ────────────────────────────────────── + +function LabelSection({ + title, + id, + kind, + entries, + onAdd, + onDelete, +}: { + title: string; + id: string; + kind: string; + 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 builtins = entries.filter((e) => e.is_builtin); + const custom = entries.filter((e) => !e.is_builtin); + + const handleAdd = useCallback(() => { + if (!lang || !matcher) return; + onAdd({ lang, matchers: [matcher], cap }); + setMatcher(''); + }, [lang, matcher, cap, onAdd]); + + return ( + +
+
+ + +
+
+ + setMatcher(e.target.value)} + /> +
+
+ + +
+ +
+
+ {entries.length === 0 ? ( +
+

No {kind} rules

+
+ ) : ( + + + + + + + + + + + {builtins.map((e, i) => ( + + + + + + + ))} + {custom.map((e, i) => ( + + + + + + + ))} + +
LanguageMatchersCap
{e.lang} + {e.matchers.join(', ')} + {e.cap} + built-in +
{e.lang} + {e.matchers.join(', ')} + {e.cap} + +
+ )} +
+
+ ); +} + +// ── Config Page ────────────────────────────────────────────────────────────── + +export function ConfigPage() { + const { + data: config, + isLoading: configLoading, + error: configError, + } = useConfig(); + const { data: sources } = useSources(); + const { data: sinks } = useSinks(); + const { data: sanitizers } = useSanitizers(); + const { data: terminators } = useTerminators(); + const { data: profiles } = useProfiles(); + + const addSource = useAddSource(); + const deleteSource = useDeleteSource(); + const addSink = useAddSink(); + const deleteSink = useDeleteSink(); + const addSanitizer = useAddSanitizer(); + const deleteSanitizer = useDeleteSanitizer(); + const addTerminator = useAddTerminator(); + const deleteTerminator = useDeleteTerminator(); + const addProfile = useAddProfile(); + const deleteProfile = useDeleteProfile(); + const activateProfile = useActivateProfile(); + const toggleTriageSync = useToggleTriageSync(); + + const [termLang, setTermLang] = useState(''); + const [termName, setTermName] = useState(''); + const [profileName, setProfileName] = useState(''); + + const handleAddTerminator = useCallback(() => { + if (!termLang || !termName) return; + addTerminator.mutate({ lang: termLang, name: termName }); + setTermName(''); + }, [termLang, termName, addTerminator]); + + const handleSaveProfile = useCallback(() => { + if (!profileName) return; + addProfile.mutate({ name: profileName, settings: {} }); + setProfileName(''); + }, [profileName, addProfile]); + + if (configLoading) return ; + if (configError) return ; + + // Extract config fields (config is typed as unknown since it's the raw NyxConfig) + const cfg = config as Record> | undefined; + const scanner = cfg?.scanner as Record | undefined; + const output = cfg?.output as Record | undefined; + const server = cfg?.server as Record | undefined; + + return ( + <> +
+

Config

+
+ + {/* General Section */} + +
+
+ Analysis Mode: {String(scanner?.mode || 'full')} +
+
+ Min Severity:{' '} + {String(scanner?.min_severity || 'Low')} +
+
+ Max File Size:{' '} + {scanner?.max_file_size_mb + ? String(scanner.max_file_size_mb) + ' MB' + : 'unlimited'} +
+
+ Excluded Dirs:{' '} + {((scanner?.excluded_directories as string[]) || []).join(', ')} +
+
+ Excluded Exts:{' '} + {((scanner?.excluded_extensions as string[]) || []).join(', ')} +
+
+ Attack Surface Ranking:{' '} + {output?.attack_surface_ranking ? 'Enabled' : 'Disabled'} +
+
+
+
+ + toggleTriageSync.mutate({ enabled: e.target.checked }) + } + /> + +
+
+
+ + {/* Sources */} + addSource.mutate(body)} + onDelete={(e) => + deleteSource.mutate({ + lang: e.lang, + matchers: e.matchers, + cap: e.cap, + }) + } + /> + + {/* Sinks */} + addSink.mutate(body)} + onDelete={(e) => + deleteSink.mutate({ lang: e.lang, matchers: e.matchers, cap: e.cap }) + } + /> + + {/* Sanitizers */} + addSanitizer.mutate(body)} + onDelete={(e) => + deleteSanitizer.mutate({ + lang: e.lang, + matchers: e.matchers, + cap: e.cap, + }) + } + /> + + {/* Terminators */} + +
+
+ + +
+
+ + setTermName(e.target.value)} + /> +
+ +
+
+ {!terminators || terminators.length === 0 ? ( +
+

No terminators configured

+
+ ) : ( + + + + + + + + + + {(terminators as TerminatorView[]).map((t, i) => ( + + + + + + ))} + +
LanguageName
{t.lang}{t.name} + +
+ )} +
+
+ + {/* Profiles */} + +
+ {!profiles || profiles.length === 0 ? ( +
+

No profiles configured

+
+ ) : ( + + + + + + + + + + + {(profiles as ProfileView[]).map((p) => ( + + + + + + + ))} + +
NameTypeSettings
+ {p.name} + + {p.is_builtin ? ( + built-in + ) : ( + custom + )} + + {JSON.stringify(p.settings)} + + + {!p.is_builtin && ( + + )} +
+ )} +
+
+
+ + setProfileName(e.target.value)} + /> +
+ +
+
+ + ); +} diff --git a/frontend/src/pages/ExplorerPage.tsx b/frontend/src/pages/ExplorerPage.tsx new file mode 100644 index 00000000..6d356529 --- /dev/null +++ b/frontend/src/pages/ExplorerPage.tsx @@ -0,0 +1,878 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { + useExplorerSymbols, + useExplorerFindings, +} from '../api/queries/explorer'; +import { useFinding } from '../api/queries/findings'; +import { useDebugFunctions } from '../api/queries/debug'; +import { ApiError } from '../api/client'; +import { FileTree } from '../components/data-display/FileTree'; +import { CodeViewer } from '../components/data-display/CodeViewer'; +import { LoadingState } from '../components/ui/LoadingState'; +import { EmptyState } from '../components/ui/EmptyState'; +import { ExplorerIcon } from '../components/icons/Icons'; +import { useFileTree } from '../hooks/useFileTree'; +import { FunctionSelector } from './debug/FunctionSelector'; +import { CfgAnalysisPanel } from './debug/CfgViewerPage'; +import { SsaAnalysisPanel } from './debug/SsaViewerPage'; +import { TaintAnalysisPanel } from './debug/TaintViewerPage'; +import { SummaryAnalysisPanel } from './debug/SummaryExplorerPage'; +import { AbstractInterpAnalysisPanel } from './debug/AbstractInterpPage'; +import { SymexAnalysisPanel } from './debug/SymexPage'; +import type { TreeEntry, FlowStep, FindingView } from '../api/types'; + +type ExplorerMode = 'tree' | 'symbols' | 'hotspots'; +type ExplorerView = + | 'code' + | 'cfg' + | 'ssa' + | 'taint' + | 'summaries' + | 'abstract-interp' + | 'symex'; + +const FLOW_KIND_COLORS: Record = { + source: 'var(--success)', + assignment: 'var(--accent)', + call: 'var(--sev-medium)', + phi: 'var(--text-tertiary)', + sink: 'var(--sev-high)', +}; + +const FLOW_KIND_LABELS: Record = { + source: 'Source', + assignment: 'Assign', + call: 'Call', + phi: 'Phi', + sink: 'Sink', +}; + +const VIEW_CONFIG: Array<{ + id: ExplorerView; + label: string; + requiresFunction?: boolean; + supportsFunction?: boolean; +}> = [ + { id: 'code', label: 'Code' }, + { id: 'cfg', label: 'CFG', requiresFunction: true, supportsFunction: true }, + { id: 'ssa', label: 'SSA', requiresFunction: true, supportsFunction: true }, + { + id: 'taint', + label: 'Taint', + requiresFunction: true, + supportsFunction: true, + }, + { id: 'summaries', label: 'Summaries', supportsFunction: true }, + { + id: 'abstract-interp', + label: 'Abstract Interp', + requiresFunction: true, + supportsFunction: true, + }, + { + id: 'symex', + label: 'Symex', + requiresFunction: true, + supportsFunction: true, + }, +]; + +const VIEW_CONFIG_BY_ID = new Map(VIEW_CONFIG.map((view) => [view.id, view])); + +export function ExplorerPage() { + const [params, setParams] = useSearchParams(); + const [explorerMode, setExplorerMode] = useState('tree'); + const [highlightLine, setHighlightLine] = useState(); + const [selectedFindingIndex, setSelectedFindingIndex] = useState< + number | null + >(null); + const [invalidFunctionNotice, setInvalidFunctionNotice] = useState< + string | null + >(null); + const codeScrollPositionsRef = useRef>({}); + + const rawView = params.get('view'); + const rawFile = params.get('file') || null; + const rawFunction = params.get('function') || null; + const currentView: ExplorerView = isExplorerView(rawView) ? rawView : 'code'; + const currentViewConfig = VIEW_CONFIG_BY_ID.get(currentView)!; + const isCodeView = currentView === 'code'; + + const updateExplorerParams = useCallback( + ( + updates: Partial>, + replace = false, + ) => { + setParams( + (prev) => { + const next = new URLSearchParams(prev); + for (const [key, value] of Object.entries(updates)) { + if (value) { + next.set(key, value); + } else { + next.delete(key); + } + } + return next; + }, + { replace }, + ); + }, + [setParams], + ); + + useEffect(() => { + if (rawView !== currentView) { + updateExplorerParams({ view: currentView }, true); + } + }, [currentView, rawView, updateExplorerParams]); + + const { data: symbolEntries, error: symbolsError } = + useExplorerSymbols(rawFile); + const hasInvalidFile = Boolean( + rawFile && isPathResolutionError(symbolsError), + ); + const hasFileLookupError = Boolean( + rawFile && symbolsError && !hasInvalidFile, + ); + const selectedFile = rawFile && !hasInvalidFile ? rawFile : null; + + const handleFileSelect = useCallback( + (path: string) => { + setHighlightLine(undefined); + setSelectedFindingIndex(null); + setInvalidFunctionNotice(null); + updateExplorerParams({ file: path, function: null }); + }, + [updateExplorerParams], + ); + + const { + rootEntries, + isLoading: treeLoading, + expandedPaths, + loadedChildren, + selectedPath, + handleToggleExpand, + handleSelectFile, + } = useFileTree(selectedFile, handleFileSelect); + + const { data: functions, isLoading: functionsLoading } = + useDebugFunctions(selectedFile); + const selectedFunction = + rawFunction && functions?.some((fn) => fn.name === rawFunction) + ? rawFunction + : null; + const hasFunctionOptions = (functions?.length ?? 0) > 0; + + useEffect(() => { + if (!rawFunction) { + return; + } + + if (!selectedFile) { + setInvalidFunctionNotice( + `Function "${rawFunction}" was cleared because no valid file is selected.`, + ); + updateExplorerParams({ function: null }, true); + return; + } + + if (!functions) { + return; + } + + if (!functions.some((fn) => fn.name === rawFunction)) { + setInvalidFunctionNotice( + `Function "${rawFunction}" was not found in ${selectedFile}.`, + ); + updateExplorerParams({ function: null }, true); + } + }, [functions, rawFunction, selectedFile, updateExplorerParams]); + + const { data: findings } = useExplorerFindings(selectedFile); + const { data: fullFinding } = useFinding(selectedFindingIndex ?? ''); + + const handleSelectFinding = useCallback((index: number, line: number) => { + setSelectedFindingIndex(index); + setHighlightLine(line); + }, []); + + const handleViewSelect = useCallback( + (view: ExplorerView) => { + updateExplorerParams({ view }); + }, + [updateExplorerParams], + ); + + const handleFunctionChange = useCallback( + (fnName: string | null) => { + setInvalidFunctionNotice(null); + updateExplorerParams({ function: fnName }); + }, + [updateExplorerParams], + ); + + const selectedEntry = findEntry(rootEntries, loadedChildren, selectedFile); + const language = selectedEntry?.language || ''; + const hotspotFiles = useMemo( + () => buildHotspotList(rootEntries, loadedChildren), + [loadedChildren, rootEntries], + ); + + const sevBreakdown = findings + ? findings.reduce( + (acc, finding) => { + const key = finding.severity.toUpperCase(); + acc[key] = (acc[key] || 0) + 1; + return acc; + }, + {} as Record, + ) + : {}; + + const evidence = fullFinding?.evidence; + const flowSteps = evidence?.flow_steps; + const hasFlow = flowSteps && flowSteps.length > 0; + const hasStateEvidence = + fullFinding?.rule_id.startsWith('state-') && evidence?.state; + + const codeHighlights = + selectedFindingIndex != null && evidence + ? { + sourceLine: evidence.source?.line, + sinkLine: evidence.sink?.line, + findingLine: fullFinding?.line, + } + : undefined; + + const flowLineSet = new Set(); + if (hasFlow) { + for (const step of flowSteps) { + if (step.line) { + flowLineSet.add(step.line); + } + } + } + + const analysisContent = renderAnalysisContent({ + currentView, + currentViewLabel: currentViewConfig.label, + selectedFile, + selectedFunction, + functions, + functionsLoading, + onBrowseFiles: () => handleViewSelect('code'), + }); + + return ( +
+
+
+
+ {(['tree', 'symbols', 'hotspots'] as ExplorerMode[]).map((mode) => ( + + ))} +
+
+
+ {explorerMode === 'tree' && ( + <> + {treeLoading && } + {rootEntries && ( + + )} + + )} + + {explorerMode === 'symbols' && ( +
+ {!selectedFile && ( +
+ Select a file to view symbols +
+ )} + {selectedFile && symbolEntries && symbolEntries.length === 0 && ( +
No symbols found
+ )} + {selectedFile && + symbolEntries?.map((sym, index) => ( +
+ + {sym.kind === 'function' ? 'ƒ' : 'm'} + + {sym.name} + {sym.arity !== undefined && sym.arity !== null && ( + ({sym.arity}) + )} + {sym.finding_count > 0 && ( + + {sym.finding_count} + + )} +
+ ))} +
+ )} + + {explorerMode === 'hotspots' && ( +
+ {hotspotFiles.length === 0 && ( +
+ No findings in scanned files +
+ )} + {hotspotFiles.map((entry) => ( +
handleSelectFile(entry.path)} + > + + {entry.name} + + + + {entry.finding_count} + + +
+ ))} +
+ )} +
+
+ +
+
+
+
+ File + + {selectedFile || 'Select a file in Explorer'} + +
+ {selectedFile && currentViewConfig.supportsFunction && ( +
+ +
+ )} +
+
+ {VIEW_CONFIG.map((view) => ( + + ))} +
+ {hasInvalidFile && rawFile && ( +
+ The requested file {rawFile} could not be found. + Choose another file in Explorer. +
+ )} + {hasFileLookupError && ( +
+ Explorer could not validate the selected file right now. +
+ )} + {invalidFunctionNotice && ( +
+ {invalidFunctionNotice} +
+ )} +
+ +
+ {isCodeView ? ( + <> + {!selectedFile && ( + } + message={ + hasInvalidFile + ? 'Choose a file from the Explorer to continue.' + : 'Select a file from the tree to view its contents.' + } + /> + )} + {selectedFile && ( + 0 ? flowLineSet : undefined} + language={language} + initialScrollTop={ + codeScrollPositionsRef.current[selectedFile] + } + onScrollPositionChange={(scrollTop) => { + codeScrollPositionsRef.current[selectedFile] = scrollTop; + }} + /> + )} + + ) : ( + analysisContent + )} +
+
+ + {isCodeView && ( +
+ {!selectedFile && ( +
+
+ Select a file to view analysis details +
+
+ )} + + {selectedFile && ( + <> +
+

File Summary

+
+ {language && {language}} + + {findings ? findings.length : 0} finding + {findings?.length !== 1 ? 's' : ''} + +
+ {findings && findings.length > 0 && ( +
+ {Object.entries(sevBreakdown) + .sort(([a], [b]) => sevOrder(a) - sevOrder(b)) + .map(([sev, count]) => ( + + {sev}: {count} + + ))} +
+ )} +
+ +
+

Symbols

+ {symbolEntries && symbolEntries.length === 0 && ( +
No symbols found
+ )} + {symbolEntries?.map((sym, index) => ( +
+ + {sym.kind === 'function' ? 'ƒ' : 'm'} + + {sym.name} +
+ ))} +
+ +
+

Findings

+ {findings && findings.length === 0 && ( +
No findings in this file
+ )} +
+ {findings?.map((finding) => ( +
+ handleSelectFinding(finding.index, finding.line) + } + > + + L{finding.line} + {finding.rule_id} + {finding.message && ( + + {finding.message} + + )} +
+ ))} +
+
+ + {hasFlow && ( +
+

Taint Flow

+ setHighlightLine(line)} + /> +
+ )} + + {hasStateEvidence && fullFinding && ( + + )} + + )} +
+ )} +
+ ); +} + +function renderAnalysisContent({ + currentView, + currentViewLabel, + selectedFile, + selectedFunction, + functions, + functionsLoading, + onBrowseFiles, +}: { + currentView: ExplorerView; + currentViewLabel: string; + selectedFile: string | null; + selectedFunction: string | null; + functions: Array<{ name: string }> | undefined; + functionsLoading: boolean; + onBrowseFiles: () => void; +}) { + if (!selectedFile) { + return ( + } + message="Select a file from the tree to view its contents." + /> + ); + } + + if (currentView === 'summaries') { + return ( +
+ +
+ ); + } + + if (functionsLoading) { + return ; + } + + if ((functions?.length ?? 0) === 0) { + return ( + + ); + } + + if (!selectedFunction) { + return ( + + ); + } + + switch (currentView) { + case 'cfg': + return ( + + ); + case 'ssa': + return ( +
+ +
+ ); + case 'taint': + return ( +
+ +
+ ); + case 'abstract-interp': + return ( +
+ +
+ ); + case 'symex': + return ( +
+ +
+ ); + case 'code': + return null; + } +} + +function AnalysisEmptyState({ + title, + message, + onBrowseFiles, +}: { + title: string; + message: string; + onBrowseFiles?: () => void; +}) { + return ( + +

{title}

+

{message}

+ {onBrowseFiles && ( + + )} +
+ ); +} + +function ExplorerFlowTimeline({ + steps, + onStepClick, +}: { + steps: FlowStep[]; + onStepClick: (line: number) => void; +}) { + return ( +
+ {steps.map((step, index) => { + const color = FLOW_KIND_COLORS[step.kind] || 'var(--text-secondary)'; + const label = FLOW_KIND_LABELS[step.kind] || step.kind; + const isLast = index === steps.length - 1; + + return ( +
step.line && onStepClick(step.line)} + > +
+
+ {!isLast &&
} +
+
+
+ + {label} + + {step.variable && ( + {step.variable} + )} + {step.callee && ( + {step.callee} + )} +
+
+ L{step.line}:{step.col} + {step.function ? ` in ${step.function}` : ''} +
+ {step.snippet && ( +
{step.snippet}
+ )} +
+
+ ); + })} +
+ ); +} + +const STATE_REMEDIATION_HINTS: Record = { + 'state-use-after-close': + 'Ensure the resource is not accessed after calling close/free.', + 'state-double-close': + 'Remove the duplicate close call, or guard with a null/closed check.', + 'state-resource-leak': + 'Add a close/free call before the function exits, or use defer/with/try-with-resources/RAII.', + 'state-resource-leak-possible': + 'Ensure the resource is closed on all code paths, including error/early-return paths.', + 'state-unauthed-access': + 'Add an authentication check before this operation, or move it behind auth middleware.', +}; + +function ExplorerStateDetail({ finding }: { finding: FindingView }) { + const state = finding.evidence?.state; + if (!state) { + return null; + } + + const isAuth = state.machine === 'auth'; + const machineLabel = isAuth ? 'Authentication State' : 'Resource Lifecycle'; + const hint = STATE_REMEDIATION_HINTS[finding.rule_id]; + const acquireLocation = + finding.rule_id.includes('leak') && finding.evidence?.sink + ? `L${finding.evidence.sink.line}:${finding.evidence.sink.col}` + : null; + + return ( +
+

State Analysis

+
+
{machineLabel}
+ {state.subject && ( +
+ Variable: + {state.subject} +
+ )} +
+ {state.from_state} + + {state.to_state} +
+ {acquireLocation && ( +
+ Acquired at: {acquireLocation} +
+ )} +
+ {hint && ( +
+
Remediation
+ {hint} +
+ )} +
+ ); +} + +function findEntry( + rootEntries: TreeEntry[] | undefined, + loadedChildren: Map, + path: string | null, +): TreeEntry | undefined { + if (!path) { + return undefined; + } + + if (rootEntries) { + const found = rootEntries.find((entry) => entry.path === path); + if (found) { + return found; + } + } + + for (const children of loadedChildren.values()) { + const found = children.find((entry) => entry.path === path); + if (found) { + return found; + } + } + + return undefined; +} + +function buildHotspotList( + rootEntries: TreeEntry[] | undefined, + loadedChildren: Map, +): TreeEntry[] { + const files: TreeEntry[] = []; + + function collect(entries: TreeEntry[]) { + for (const entry of entries) { + if (entry.entry_type === 'file' && entry.finding_count > 0) { + files.push(entry); + } + if (entry.entry_type === 'dir') { + const children = loadedChildren.get(entry.path); + if (children) { + collect(children); + } + } + } + } + + if (rootEntries) { + collect(rootEntries); + } + files.sort((a, b) => b.finding_count - a.finding_count); + return files; +} + +function sevOrder(sev: string): number { + switch (sev) { + case 'HIGH': + return 0; + case 'MEDIUM': + return 1; + case 'LOW': + return 2; + default: + return 3; + } +} + +function isExplorerView(value: string | null): value is ExplorerView { + return VIEW_CONFIG_BY_ID.has(value as ExplorerView); +} + +function isPathResolutionError(error: unknown): boolean { + return ( + error instanceof ApiError && (error.status === 403 || error.status === 404) + ); +} diff --git a/frontend/src/pages/FindingDetailPage.tsx b/frontend/src/pages/FindingDetailPage.tsx new file mode 100644 index 00000000..b2463e44 --- /dev/null +++ b/frontend/src/pages/FindingDetailPage.tsx @@ -0,0 +1,1024 @@ +import { useState, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useFinding } from '../api/queries/findings'; +import { useBulkTriage } from '../api/mutations/triage'; +import { truncPath } from '../utils/truncPath'; +import { escapeHtml, highlightSyntax } from '../utils/syntaxHighlight'; +import { parseNoteText } from '../utils/parseNote'; +import { findingToMarkdown } from '../utils/findingMarkdown'; +import { CopyMarkdownButton } from '../components/CopyMarkdownButton'; +import { Dropdown, DropdownItem } from '../components/ui/Dropdown'; +import { CodeViewerModal } from '../modals/CodeViewerModal'; +import type { + FindingView, + Evidence, + FlowStep, + SpanEvidence, + RelatedFindingView, +} from '../api/types'; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +function formatTriageState(state: string): string { + return (state || 'open').replace(/_/g, ' '); +} + +interface StatusOption { + value: string; + label: string; +} + +const STATUS_GROUPS: { heading: string; options: StatusOption[] }[] = [ + { + heading: 'Active', + options: [ + { value: 'open', label: 'Open' }, + { value: 'investigating', label: 'Investigating' }, + ], + }, + { + heading: 'Resolved', + options: [ + { value: 'fixed', label: 'Fixed' }, + { value: 'false_positive', label: 'False Positive' }, + { value: 'accepted_risk', label: 'Accepted Risk' }, + { value: 'suppressed', label: 'Suppressed' }, + ], + }, +]; + +function isStateFinding(f: FindingView): boolean { + return f.rule_id.startsWith('state-'); +} + +const STATE_REMEDIATION_HINTS: Record = { + 'state-use-after-close': [ + 'Do not access the resource after calling close/free.', + 'Restructure so every use happens before release.', + 'Consider a language-native cleanup pattern (defer, with, try-with-resources, RAII).', + ], + 'state-double-close': [ + 'Remove the duplicate close call, or guard with a null/closed check.', + 'Centralize cleanup in a single code path to avoid repeats.', + ], + 'state-resource-leak': [ + 'Add a close/free call before every function exit.', + 'Prefer a language-native cleanup pattern (defer, with, try-with-resources, RAII).', + ], + 'state-resource-leak-possible': [ + 'Ensure the resource is closed on all code paths — including error and early-return paths.', + 'Put cleanup in a finally/defer block rather than after the happy path.', + ], + 'state-unauthed-access': [ + 'Add an authentication check before the sensitive operation.', + 'Move this handler behind an auth middleware or guard.', + ], +}; + +const STATE_RULE_DESCRIPTIONS: Record = { + 'state-use-after-close': 'Variable used after its resource handle was closed', + 'state-double-close': 'Resource handle closed more than once', + 'state-resource-leak': 'Resource acquired but never closed', + 'state-resource-leak-possible': 'Resource may not be closed on all paths', + 'state-unauthed-access': 'Sensitive operation reached without authentication', +}; + +// ── Collapsible Section ───────────────────────────────────────────────────── + +interface CollapsibleSectionProps { + title: string; + defaultOpen?: boolean; + children: React.ReactNode; +} + +function CollapsibleSection({ + title, + defaultOpen = true, + children, +}: CollapsibleSectionProps) { + const [open, setOpen] = useState(defaultOpen); + + return ( +
+
setOpen((v) => !v)}> + + ▼ + {' '} + {title} +
+
+ {children} +
+
+ ); +} + +// ── Evidence Cards ────────────────────────────────────────────────────────── + +function EvidenceCard({ + kind, + color, + span, +}: { + kind: string; + color: string; + span: SpanEvidence; +}) { + return ( +
+
+ {kind} +
+
+ {span.path}:{span.line}:{span.col} +
+ {span.snippet &&
{span.snippet}
} +
+ ); +} + +function StateTransitionCard({ + evidence, + ruleId, +}: { + evidence: Evidence; + ruleId: string; +}) { + const st = evidence.state; + if (!st) return null; + + const isAuth = st.machine === 'auth'; + const machineLabel = isAuth ? 'Authentication State' : 'Resource Lifecycle'; + const acquireLocation = + ruleId.includes('leak') && evidence.sink + ? `${evidence.sink.path}:${evidence.sink.line}:${evidence.sink.col}` + : null; + + return ( +
+
{machineLabel}
+ {st.subject && ( +
+ Variable: + {st.subject} +
+ )} +
+ {st.from_state} + + {st.to_state} +
+ {acquireLocation && ( +
+ Acquired at: {acquireLocation} +
+ )} +
+ ); +} + +function EvidenceSection({ + evidence, + skipStateCard, +}: { + evidence: Evidence; + skipStateCard?: boolean; +}) { + const cards: React.ReactNode[] = []; + + if (evidence.source) { + cards.push( + , + ); + } + + if (evidence.sink) { + cards.push( + , + ); + } + + for (let i = 0; i < (evidence.guards?.length ?? 0); i++) { + cards.push( + , + ); + } + + for (let i = 0; i < (evidence.sanitizers?.length ?? 0); i++) { + cards.push( + , + ); + } + + if (evidence.state && !skipStateCard) { + const st = evidence.state; + cards.push( +
+
State: {st.machine}
+
+ {st.subject ? `${st.subject}: ` : ''} + {st.from_state} → {st.to_state} +
+
, + ); + } + + if (cards.length === 0) return null; + return <>{cards}; +} + +// ── Notes Section ─────────────────────────────────────────────────────────── + +function NotesSection({ evidence }: { evidence: Evidence }) { + if (!evidence.notes || evidence.notes.length === 0) return null; + + return ( +
    + {evidence.notes.map((note, i) => ( +
  • + {parseNoteText(note)} +
  • + ))} +
+ ); +} + +// ── Confidence Section ────────────────────────────────────────────────────── + +function ConfidenceSection({ finding }: { finding: FindingView }) { + if (!finding.confidence) return null; + + const limiters = finding.evidence?.confidence_limiters; + const showLimiters = + limiters && limiters.length > 0 && finding.confidence !== 'High'; + + return ( + <> + + {finding.confidence} + + {finding.rank_score != null && ( + + Score: {finding.rank_score.toFixed(1)} + + )} + {finding.rank_reason && finding.rank_reason.length > 0 && ( +
+ {finding.rank_reason.map(([k, v], i) => ( +
+ {k}: {v} +
+ ))} +
+ )} + {showLimiters && ( +
+ + Why not higher confidence? + +
    + {limiters!.map((l, i) => ( +
  • {l}
  • + ))} +
+
+ )} + + ); +} + +// ── Structured Explanation ────────────────────────────────────────────────── + +function describeSpan(span: SpanEvidence): string { + const name = + span.snippet?.trim() || + span.kind || + span.path.split('/').pop() || + span.path; + return `${name} (line ${span.line})`; +} + +function StructuredExplanation({ + finding, + evidence, +}: { + finding: FindingView; + evidence: Evidence; +}) { + const rows: { label: string; value: React.ReactNode }[] = []; + + if (evidence.source) { + rows.push({ + label: 'From', + value: ( + + {describeSpan(evidence.source)} + + ), + }); + } + + if (evidence.sink) { + rows.push({ + label: 'Into', + value: ( + {describeSpan(evidence.sink)} + ), + }); + } + + rows.push({ + label: 'Risk', + value: riskSummary(finding, evidence), + }); + + const contextNote = buildContextNote(finding, evidence); + if (contextNote) { + rows.push({ label: 'Notes', value: contextNote }); + } + + if (rows.length === 0) return null; + + return ( +
+ {rows.map((r, i) => ( +
+
{r.label}
+
{r.value}
+
+ ))} +
+ ); +} + +function riskSummary(finding: FindingView, evidence: Evidence): string { + if (evidence.explanation) return evidence.explanation; + if (finding.message) return finding.message; + const category = finding.category?.toLowerCase() || ''; + if (category.includes('security')) { + return 'Potential injection or unsafe-operation vulnerability.'; + } + return `${finding.category} issue.`; +} + +function buildContextNote( + finding: FindingView, + evidence: Evidence, +): React.ReactNode { + const parts: string[] = []; + const hasCrossFile = evidence.flow_steps?.some((s) => s.is_cross_file); + if (hasCrossFile) { + parts.push('Crosses function boundaries via summary resolution.'); + } + if (finding.sanitizer_status === 'none') { + parts.push('No sanitizer was applied to this flow.'); + } else if (finding.sanitizer_status === 'bypassed') { + parts.push('A sanitizer was present but was bypassed.'); + } + if (finding.guard_kind) { + parts.push(`Guard: ${finding.guard_kind}.`); + } + return parts.length ? parts.join(' ') : null; +} + +// ── Taint Flow Timeline ───────────────────────────────────────────────────── + +const FLOW_KIND_COLORS: Record = { + source: 'var(--success)', + assignment: 'var(--accent)', + call: 'var(--sev-medium)', + phi: 'var(--text-tertiary)', + sink: 'var(--sev-high)', +}; + +const FLOW_KIND_LABELS: Record = { + source: 'Source', + assignment: 'Assign', + call: 'Call', + phi: 'Phi', + sink: 'Sink', +}; + +const FLOW_COLLAPSE_THRESHOLD = 5; + +function FlowTimeline({ steps }: { steps: FlowStep[] }) { + const [expanded, setExpanded] = useState( + steps.length <= FLOW_COLLAPSE_THRESHOLD, + ); + + if (steps.length === 0) return null; + + const isLong = steps.length > FLOW_COLLAPSE_THRESHOLD; + const visibleSteps: FlowStep[] = (() => { + if (!isLong || expanded) return steps; + const firstIdx = steps.findIndex((s) => s.kind === 'source'); + const lastSinkIdx = [...steps] + .map((s, i) => ({ s, i })) + .reverse() + .find(({ s }) => s.kind === 'sink')?.i; + const picked = new Set(); + if (firstIdx >= 0) picked.add(firstIdx); + if (lastSinkIdx != null) picked.add(lastSinkIdx); + picked.add(0); + picked.add(steps.length - 1); + return [...picked].sort((a, b) => a - b).map((i) => steps[i]); + })(); + + return ( +
+ {visibleSteps.map((s, i) => { + const color = FLOW_KIND_COLORS[s.kind] || 'var(--text-secondary)'; + const label = FLOW_KIND_LABELS[s.kind] || s.kind; + const isLast = i === visibleSteps.length - 1; + const isEndpoint = s.kind === 'source' || s.kind === 'sink'; + + return ( +
+
+
+ {!isLast &&
} +
+
+
+ + {label} + + #{s.step} + {s.variable && ( + {s.variable} + )} + {s.callee && ( + {s.callee} + )} +
+
+ {s.file}:{s.line}:{s.col} + {s.function ? ` in ${s.function}` : ''} +
+ {s.snippet && ( +
{s.snippet}
+ )} +
+
+ ); + })} + {isLong && ( + + )} +
+ ); +} + +// ── Related Findings ──────────────────────────────────────────────────────── + +function RelatedFindings({ findings }: { findings: RelatedFindingView[] }) { + const navigate = useNavigate(); + + if (findings.length === 0) return null; + + return ( + <> + {findings.map((r) => ( +
navigate(`/findings/${r.index}`)} + > + + {r.severity.charAt(0)} + + {r.rule_id} + + {truncPath(r.path, 30)}:{r.line} + +
+ ))} + + ); +} + +// ── Code Preview ──────────────────────────────────────────────────────────── + +function CodePreview({ + lines, + startLine, + highlightLine, + language, +}: { + lines: string[]; + startLine: number; + highlightLine: number; + language?: string; +}) { + const lang = (language || '').toLowerCase(); + + return ( +
+ {lines.map((line, i) => { + const lineNum = startLine + i; + const isHighlight = lineNum === highlightLine; + return ( +
+ {lineNum} + +
+ ); + })} +
+ ); +} + +// ── How to Fix ────────────────────────────────────────────────────────────── + +function sinkCapKey(finding: FindingView): string | null { + const snippet = (finding.evidence?.sink?.snippet || '').toLowerCase(); + const rule = finding.rule_id.toLowerCase(); + + if ( + /innerhtml|outerhtml|document\.write|dangerouslysetinnerhtml/.test(snippet) + ) + return 'xss'; + if (/\beval\b|new function|settimeout\s*\(\s*["'`]/.test(snippet)) + return 'code-exec'; + if ( + /\bexec\b|\bspawn\b|\bsystem\b|\bpopen\b|shell_exec|subprocess/.test( + snippet, + ) + ) + return 'cmd-inject'; + if ( + /query|execute|raw|prepare.*%|select\s|insert\s|update\s|delete\s/i.test( + snippet, + ) + ) + return 'sql'; + if (/readfile|fs\.|open\s*\(|path\.join/.test(snippet)) return 'path'; + if (/\bfetch\b|\baxios\b|http\.|request\.|urlopen|curl/.test(snippet)) + return 'ssrf'; + + if (rule.includes('xss')) return 'xss'; + if (rule.includes('sql')) return 'sql'; + if (rule.includes('cmd') || rule.includes('command')) return 'cmd-inject'; + if (rule.includes('ssrf')) return 'ssrf'; + if (rule.includes('path') || rule.includes('traversal')) return 'path'; + if (rule.includes('deserial')) return 'deserialize'; + if (rule.includes('eval') || rule.includes('codeexec')) return 'code-exec'; + + return null; +} + +const TAINT_REMEDIATION: Record = { + xss: [ + 'Avoid writing user input into innerHTML / outerHTML / document.write.', + 'Use textContent, or framework-native binding (React props, Vue {{ }}, etc.).', + 'If HTML is unavoidable, run input through a well-maintained sanitizer (DOMPurify, Bleach).', + ], + sql: [ + 'Use parameterized queries or a prepared statement — never concatenate user input into SQL.', + 'Prefer an ORM or query builder that escapes parameters automatically.', + 'Validate input type (integer, enum, allowlist) before the query.', + ], + 'cmd-inject': [ + 'Avoid passing user input to shell/exec APIs.', + 'Use the argv-array form of exec (no shell interpretation).', + 'Validate against a strict allowlist of commands and arguments.', + ], + ssrf: [ + 'Validate and allowlist outbound hostnames before making the request.', + 'Resolve and check the target IP is not internal / metadata (169.254.169.254, 127.0.0.0/8, 10.0.0.0/8, RFC1918).', + 'Use a dedicated HTTP client that disables redirects to private addresses.', + ], + path: [ + 'Normalize the path and verify it stays within an expected root directory.', + 'Reject inputs containing "..", null bytes, or absolute paths.', + 'Use a safe-join helper rather than string concatenation.', + ], + deserialize: [ + 'Do not deserialize untrusted input with dangerous formats (pickle, ObjectInputStream).', + 'Use a schema-constrained format (JSON with a validator, Protobuf).', + 'If unavoidable, run deserialization in a locked-down process and validate types post-hoc.', + ], + 'code-exec': [ + 'Do not pass user input to eval / new Function / exec.', + 'Replace dynamic code generation with a parser over an allowlisted grammar.', + 'If scripting is required, sandbox it (VM / Web Worker with no DOM, seccomp).', + ], +}; + +const DEFAULT_TAINT_REMEDIATION: string[] = [ + 'Validate user input against an allowlist (length, character set, format).', + 'Encode or escape data appropriately for the target sink.', + 'Prefer parameterized / structured APIs over string concatenation.', +]; + +function HowToFix({ finding }: { finding: FindingView }) { + const isState = isStateFinding(finding); + + const bullets: string[] = (() => { + if (isState) { + return STATE_REMEDIATION_HINTS[finding.rule_id] || []; + } + const key = sinkCapKey(finding); + if (key && TAINT_REMEDIATION[key]) return TAINT_REMEDIATION[key]; + return DEFAULT_TAINT_REMEDIATION; + })(); + + if (bullets.length === 0) return null; + + return ( +
    + {bullets.map((b, i) => ( +
  • {b}
  • + ))} +
+ ); +} + +// ── Status Control ────────────────────────────────────────────────────────── + +function StatusControl({ + finding, + onTriage, + isPending, +}: { + finding: FindingView; + onTriage: (state: string, note: string) => void; + isPending: boolean; +}) { + const [noteDraft, setNoteDraft] = useState(''); + const [noteOpen, setNoteOpen] = useState(false); + + const currentState = finding.triage_state || 'open'; + + const chooseStatus = (state: string, close: () => void) => { + if (state === currentState) { + close(); + return; + } + onTriage(state, noteDraft.trim()); + setNoteDraft(''); + setNoteOpen(false); + close(); + }; + + return ( +
+
+ + ( + + )} + > + {({ close }) => ( + <> + {STATUS_GROUPS.map((group) => ( +
+
{group.heading}
+ {group.options.map((opt) => ( + chooseStatus(opt.value, close)} + > + {opt.label} + + ))} +
+ ))} + + )} +
+ {!noteOpen && ( + + )} +
+ {finding.triage_note && !noteOpen && ( +
+ Note: {finding.triage_note} +
+ )} + {noteOpen && ( +
+