diff --git a/.env.example b/.env.example index 20e8a9b..fc87660 100644 --- a/.env.example +++ b/.env.example @@ -78,6 +78,14 @@ ZAP_PORT=8090 # Nuclei Configuration NUCLEI_TEMPLATES_DIR="/root/nuclei-templates" +# ═══════════════════════════════════════════════════════════ +# SSRF / Domain Allowlist +# ═══════════════════════════════════════════════════════════ + +# Comma-separated list of domains that the scanner is allowed to contact. +# Add every target domain you want to scan here. +ALLOWED_DOMAINS="example.com" + # ═══════════════════════════════════════════════════════════ # Development Only # ═══════════════════════════════════════════════════════════ diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c994576..60d858b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,381 +1,21 @@ -# GitHub Copilot Instructions +# GitHub Copilot Instructions - Web Check -This repository is **Web-Check**, a Docker-based security scanning toolkit. +Always use context7 when I need code generation, setup or +configuration steps, or library/API documentation. This means +you should automatically use the Context7 MCP tools to resolve +library id and get library docs without me having to +explicitly ask. -## Context +## Agent Profile -- **Language**: Python 3.12+ -- **Framework**: FastAPI 0.115+ with Uvicorn -- **Architecture**: Docker-first with docker-compose -- **Purpose**: Web security analysis and vulnerability scanning -- **Database**: SQLAlchemy 2.0 + Alembic + SQLite (async) -- **Logging**: structlog for structured logs -- **HTTP**: httpx for async requests -- **Tooling**: Ruff (linting/formatting), Ty (type checking), Pytest (testing) +You are an expert in web development and CLI tools, with a focus on Python, FastAPI. You have experience building tools for website analysis and optimization. When generating code, prioritize best practices for performance, accessibility, and maintainability. -## Code Style +## Best practises -- Use **type hints** on all functions (mandatory) -- Use **Pydantic v2** for validation and settings -- Use **async/await** for all I/O operations -- Follow **PEP 8** with max line length **100** -- Use **Google-style docstrings** -- Use **datetime.now(UTC)** instead of `datetime.utcnow()` (deprecated) -- Use **structlog** for all logs, never `print()` -- Use **httpx.AsyncClient** instead of requests -- Use **type aliases** for Literal types (e.g., `Severity`, `ScanStatus`) - -## Patterns to Follow - -### API Endpoints - -```python -@router.get("/scan", response_model=CheckResult) -async def perform_scan( - url: str = Query(..., description="Target URL to scan"), - timeout: int = Query(300, ge=30, le=600, description="Timeout in seconds"), -) -> CheckResult: - """ - Run security scan on target URL. - - Average duration: 2-5 minutes. - """ - if not url.startswith(("http://", "https://")): - raise HTTPException(status_code=400, detail="URL must start with http:// or https://") - - return await run_scan(url, timeout) -``` - -### Service Functions - -```python -async def run_scanner(target: str, timeout: int = 300) -> CheckResult: - """ - Run scanner against a target. - - Args: - target: URL or domain to scan - timeout: Timeout in seconds - - Returns: - CheckResult with findings - """ - start = time.time() - findings: list[Finding] = [] - - try: - result = await docker_run( - image="scanner/image:latest", - command=["--target", target], - timeout=timeout, - container_name="security-scanner-tool", - ) - - if result["timeout"]: - return CheckResult( - module="scanner", - category="quick", - target=target, - timestamp=datetime.now(UTC), - duration_ms=int((time.time() - start) * 1000), - status="timeout", - data=None, - findings=[], - error="Scan timed out", - ) - - findings = _parse_output(result["stdout"]) - - logger.info( - "scan_completed", - target=target, - findings_count=len(findings), - ) - - return CheckResult( - module="scanner", - category="quick", - target=target, - timestamp=datetime.now(UTC), - duration_ms=int((time.time() - start) * 1000), - status="success", - data={"findings_count": len(findings)}, - findings=findings, - error=None, - ) - - except Exception as e: - logger.error("scan_failed", target=target, error=str(e)) - return CheckResult( - module="scanner", - category="quick", - target=target, - timestamp=datetime.now(UTC), - duration_ms=int((time.time() - start) * 1000), - status="error", - data=None, - findings=[], - error=str(e), - ) -``` - -### Docker Container Execution - -```python -async def docker_run( - image: str, - command: list[str], - volumes: dict[str, str] | None = None, - timeout: int = 300, - container_name: str | None = None, - network: str | None = None, -) -> dict[str, Any]: - """Run Docker container and return results.""" - if container_name: - # Use existing container with docker exec - cmd = ["docker", "exec", container_name] + command - else: - # Run new container - cmd = ["docker", "run", "--rm"] - if volumes: - for host_path, container_path in volumes.items(): - cmd.extend(["-v", f"{host_path}:{container_path}"]) - if network: - cmd.extend(["--network", network]) - cmd.append(image) - cmd.extend(command) - - logger.info("running_docker_command", command=" ".join(cmd)) - # ... implementation -``` - -## Key Models - -### Finding Model - -```python -from typing import Literal -from pydantic import BaseModel, Field - -Severity = Literal["critical", "high", "medium", "low", "info"] - -class Finding(BaseModel): - """Security finding from a scan.""" - severity: Severity = Field(..., description="Severity level") - title: str = Field(..., description="Short title") - description: str = Field(..., description="Detailed description") - reference: str | None = Field(None, description="URL or reference") - cve: str | None = Field(None, description="CVE identifier if applicable") - cvss_score: float | None = Field(None, ge=0.0, le=10.0, description="CVSS score") -``` - -### CheckResult Model - -```python -from datetime import UTC, datetime - -ScanStatus = Literal["success", "error", "timeout", "running"] -ScanCategory = Literal["quick", "deep", "security"] - -class CheckResult(BaseModel): - """Result from a security check.""" - module: str = Field(..., description="Name of the scanning module") - category: ScanCategory = Field(..., description="Category of the scan") - target: str = Field(..., description="Target URL or domain") - timestamp: datetime = Field( - default_factory=lambda: datetime.now(UTC), - description="When the scan was performed", - ) - duration_ms: int = Field(..., ge=0, description="Scan duration in milliseconds") - status: ScanStatus = Field(..., description="Status of the scan") - data: dict[str, Any] | None = Field(None, description="Raw scan data and metadata") - findings: list[Finding] = Field(default_factory=list, description="Security findings") - error: str | None = Field(None, description="Error message if scan failed") -``` - -## File Organization - -``` -api/ -├── main.py # FastAPI app, middleware, lifespan -├── database.py # SQLAlchemy setup -├── routers/ # FastAPI route handlers -│ ├── health.py # Health check endpoint -│ ├── quick.py # Quick scans (nuclei, nikto, dns) -│ ├── deep.py # Deep analysis (ZAP, SSLyze) -│ ├── security.py # Security scans (SQLMap, Wapiti) -│ ├── advanced.py # Advanced scans (XSStrike) -│ └── scans.py # Scan management (CRUD) -├── services/ # Business logic -│ ├── nikto.py # Nikto scanner service -│ ├── nuclei.py # Nuclei scanner service -│ ├── zap_native.py # OWASP ZAP service (Python API) -│ ├── sslyze_scanner.py# SSLyze service -│ ├── sqlmap_scanner.py# SQLMap service -│ ├── wapiti_scanner.py# Wapiti service -│ ├── xsstrike_scanner.py # XSStrike service -│ ├── docker_runner.py # Docker execution utilities -│ ├── db_service.py # Database operations -│ └── log_streamer.py # SSE log streaming -├── models/ # Pydantic models -│ ├── findings.py # Finding, Severity types -│ ├── results.py # CheckResult, ScanStatus types -│ └── db_models.py # SQLAlchemy models -└── utils/ - └── config.py # Settings with pydantic-settings - -alembic/ # Database migrations -config/ # Scanner configuration -outputs/ # Scan outputs (HTML, JSON) -``` - -## Configuration - -Settings via `pydantic-settings`: - -```python -from pydantic_settings import BaseSettings, SettingsConfigDict -from pathlib import Path - -class Settings(BaseSettings): - """Application settings.""" - api_title: str = "Web-Check Security Scanner" - api_version: str = "0.1.0" - debug: bool = False - docker_network: str = "scanner-net" - output_base_dir: Path = Path("outputs") - default_timeout: int = 300 - max_timeout: int = 3600 - log_level: str = "INFO" - - model_config = SettingsConfigDict( - env_file=".env", - env_file_encoding="utf-8", - case_sensitive=False, - ) - -@lru_cache -def get_settings() -> Settings: - return Settings() -``` - -## Logging - -Use structlog with structured logs: - -```python -import structlog - -logger = structlog.get_logger() - -# Good -logger.info("scan_completed", target=url, findings_count=len(findings)) -logger.error("scan_failed", target=url, error=str(e)) - -# Bad -logger.info(f"Scan completed for {url}") # ❌ No string formatting -print("Scan completed") # ❌ Never use print() -``` - -## Database - -SQLAlchemy 2.0 with async SQLite: - -```python -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -from sqlalchemy.orm import DeclarativeBase - -engine = create_async_engine("sqlite+aiosqlite:///./web-check.db") - -class Base(DeclarativeBase): - pass - -# In services -async def create_scan(db: AsyncSession, scan_data: dict): - scan = Scan(**scan_data) - db.add(scan) - await db.commit() - await db.refresh(scan) - return scan -``` - -## Current Scanner Modules - -1. **Nuclei** (`api/services/nuclei.py`) - - Image: `projectdiscovery/nuclei:latest` - - Fast CVE and vulnerability scanning - - Timeout: 300s by default - -2. **Nikto** (`api/services/nikto.py`) - - Image: `alpine/nikto:latest` - - Scan web server misconfigurations - - HTML output in `/outputs/` - - Timeout: 600s by default - -3. **OWASP ZAP** (`api/services/zap_native.py`) - - Uses Python API (zapv2) - - Comprehensive security scan - - Timeout: 900s by default - -4. **SSLyze** (`api/services/sslyze_scanner.py`) - - Native Python library - - SSL/TLS configuration analysis - - Timeout: 300s by default - -5. **SQLMap** (`api/services/sqlmap_scanner.py`) - - Native Python library - - SQL injection detection - - Timeout: 900s by default - -6. **Wapiti** (`api/services/wapiti_scanner.py`) - - Docker-based web scanner - - Web application vulnerabilities - - Timeout: 900s by default - -7. **XSStrike** (`api/services/xsstrike_scanner.py`) - - Installed from GitHub - - Advanced XSS detection - - Timeout: 600s by default - -## Don'ts - -- ❌ Don't use `requests` - use `httpx.AsyncClient` -- ❌ Don't use `datetime.utcnow()` - use `datetime.now(UTC)` -- ❌ Don't block event loop - use async throughout -- ❌ Don't hardcode timeouts/paths - use Settings -- ❌ Don't ignore errors - always return CheckResult with error field -- ❌ Don't use `print()` - use `structlog.get_logger()` -- ❌ Don't use bare Exception - catch specific exceptions -- ❌ Don't forget type hints - they're mandatory -- ❌ Don't use `default_factory=lambda: []` - use `default_factory=list` - -## Testing - -```python -import pytest -from httpx import ASGITransport, AsyncClient -from api.main import app - -@pytest.mark.asyncio -async def test_nuclei_scan(): - transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get( - "/api/quick/nuclei", - params={"url": "https://example.com", "timeout": 300} - ) - assert response.status_code == 200 - data = response.json() - assert data["module"] == "nuclei" - assert data["status"] in ["success", "error", "timeout"] -``` - -## Commit Messages - -Use conventional commits: -- `feat:` - New feature -- `fix:` - Bug fix -- `docs:` - Documentation -- `refactor:` - Refactoring without behavior change -- `test:` - Add/modify tests -- `chore:` - Maintenance tasks +- Makefile for common tasks (e.g. setup, run, test) / Maximum 150 lines +- Use FastAPI for the CLI tool, with Typer for command-line interface +- Use Python 3.12+ features (e.g. dataclasses, type hints) +- Write modular code with clear separation of concerns (e.g. separate modules for crawling, analysis, reporting) +- Include error handling and logging for better debugging and user experience +- Write documentation for the CLI tool, including usage instructions and examples in docs/ directory +- Use pytest for testing, with a focus on unit tests for core functionality and integration tests for the CLI workflow \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index 9565c3d..0000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: CD - Release Please - -on: - push: - branches: - - main - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - release-please: - runs-on: ubuntu-latest - outputs: - release_created: ${{ steps.release.outputs.release_created }} - tag_name: ${{ steps.release.outputs.tag_name }} - version: ${{ steps.release.outputs.version }} - steps: - - name: Release Please - id: release - uses: googleapis/release-please-action@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - config-file: .github/release/release-please-config.json - manifest-file: .github/release/.release-please-manifest.json - - build-and-push: - needs: release-please - if: ${{ needs.release-please.outputs.release_created }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push API image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: true - tags: | - ghcr.io/${{ github.repository }}/api:${{ needs.release-please.outputs.version }} - ghcr.io/${{ github.repository }}/api:latest - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push Web image - uses: docker/build-push-action@v6 - with: - context: ./web - file: ./web/Dockerfile - push: true - tags: | - ghcr.io/${{ github.repository }}/web:${{ needs.release-please.outputs.version }} - ghcr.io/${{ github.repository }}/web:latest - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..70ef109 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,95 @@ +name: CI / CD + +on: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + branches: + - "**" + - "!release/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # ── CI — Python (lint, format, typecheck, test) ─────────────────────────── + python: + name: Python CI + if: github.event_name == 'pull_request' + uses: KevinDeBenedetti/github-workflows/.github/workflows/ci-python.yml@main + with: + python-version: "3.12" + working-directory: "." + run-lint: true + run-format: true + run-typecheck: true + run-test: true + run-coverage: false + + # ── CI — Security (secret scan + Python audit) ──────────────────────────── + security: + name: Security + if: github.event_name == 'pull_request' + uses: KevinDeBenedetti/github-workflows/.github/workflows/security.yml@main + with: + run-node-audit: false + run-python-audit: true + python-working-directory: "." + run-secret-scan: true + run-codeql: false + permissions: + security-events: write + actions: read + contents: read + pull-requests: read + + # ── CI — Bot commit guard ───────────────────────────────────────────────── + bot-check: + name: Bot check + if: github.event_name == 'pull_request' + uses: KevinDeBenedetti/github-workflows/.github/workflows/check-bot-commits.yml@main + with: + allowed-bots: '["dependabot[bot]"]' + permissions: + contents: read + pull-requests: read + + # ── CI — Gate (all required checks passed) ──────────────────────────────── + ci-passed: + name: CI Passed + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + needs: [python] + steps: + - name: All checks passed + run: echo "✅ All CI checks passed successfully!" + + # ── CD — Release Please ─────────────────────────────────────────────────── + release: + name: Release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: KevinDeBenedetti/github-workflows/.github/workflows/release.yml@main + secrets: inherit + + # ── CD — Build & push API Docker image (on release) ─────────────────────── + deploy-api: + name: Deploy API + needs: release + if: needs.release.outputs.released == 'true' + uses: KevinDeBenedetti/github-workflows/.github/workflows/cd-docker.yml@main + with: + image-name: web-check-api + context: "." + dockerfile: Dockerfile + tag-latest: true + version: ${{ needs.release.outputs.tag }} + secrets: inherit + + # ── CD — Sync docs → kevindebenedetti.github.io ─────────────────────────── + deploy-docs: + name: Sync docs → central repo + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: KevinDeBenedetti/github-workflows/.github/workflows/cd-docs.yml@main + secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index beeccb9..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,235 +0,0 @@ - -name: CI - -permissions: - contents: read - -on: - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - branches: - - "**" - - "!release/**" - -jobs: - # 1. Secret scanning with Gitleaks - gitleaks: - name: Gitleaks Secret Scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - name: Run Gitleaks - uses: gitleaks/gitleaks-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} - - # 2. Python jobs (lint, format, type-check, test, build) - python-lint: - name: Python Lint (Ruff) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install Ruff - run: pipx install ruff - - name: Lint code with Ruff - run: ruff check --output-format=github --target-version=py312 api/ - - python-format: - name: Python Format Check (Ruff) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install Ruff - run: pipx install ruff - - name: Check code formatting with Ruff - run: ruff format --check --target-version=py312 api/ - - python-typecheck: - name: Python Type Check (ty) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v5 - - name: Set up Python - run: uv python install 3.12 - - name: Install dependencies - run: uv sync --all-groups - - name: Type check with ty - run: uv run ty check api/ - - python-test: - name: Python Tests (Pytest) - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.12", "3.13"] - steps: - - uses: actions/checkout@v5 - - name: Install uv - uses: astral-sh/setup-uv@v5 - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - name: Install dependencies - run: uv sync --all-groups - - name: Run tests with pytest - run: uv run pytest api/tests/ --cov=api --cov-report=xml --junitxml=junit/test-results-${{ matrix.python-version }}.xml - - name: Upload pytest test results - uses: actions/upload-artifact@v4 - with: - name: pytest-results-${{ matrix.python-version }} - path: junit/test-results-${{ matrix.python-version }}.xml - if: ${{ always() }} - - name: Upload coverage reports - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.python-version }} - path: coverage.xml - if: ${{ always() }} - - python-build: - name: Python Build (Docker) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: false - load: true - tags: web-check:test - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: false - - # 3. React jobs (lint, format, type-check, test, build) - react-lint: - name: React Lint (oxlint) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./web - steps: - - uses: actions/checkout@v5 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install dependencies - run: bun install - - name: Lint with oxlint - run: bun run lint - - react-format: - name: React Format Check (oxfmt) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./web - steps: - - uses: actions/checkout@v5 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install dependencies - run: bun install - - name: Check formatting with oxfmt - run: bun run format:check - - react-typecheck: - name: React Type Check (TypeScript) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./web - steps: - - uses: actions/checkout@v5 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install dependencies - run: bun install - - name: Type check with TypeScript - run: bun run tsc --noEmit - - react-build: - name: React Build (Vite) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./web - steps: - - uses: actions/checkout@v5 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install dependencies - run: bun install - - name: Build with Vite - run: bun run build - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: react-build - path: web/dist/ - - react-docker-build: - name: React Build (Docker) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build Docker image - uses: docker/build-push-action@v6 - with: - context: ./web - file: ./web/Dockerfile - push: false - load: true - tags: web-check-ui:test - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: false - - # 4. Final check - all jobs passed - ci-passed: - name: CI Passed - runs-on: ubuntu-latest - needs: - - gitleaks - - python-lint - - python-format - - python-typecheck - - python-test - - python-build - - react-lint - - react-format - - react-typecheck - - react-build - - react-docker-build - steps: - - name: All checks passed - run: echo "✅ All CI checks passed successfully!" diff --git a/CLI.md b/CLI.md new file mode 100644 index 0000000..2d7f2e0 --- /dev/null +++ b/CLI.md @@ -0,0 +1,254 @@ +# Web-Check CLI + +A command-line interface for Web-Check security scanning toolkit. This is a self-hosted, CLI-only tool for performing security assessments on web applications. + +## Installation + +```bash +# Install with dependencies +uv sync --all-extras --dev + +# Or using pip +pip install -e . +``` + +## Quick Start + +### 1. Check CLI Configuration + +```bash +web-check config show +``` + +### 2. Verify API Connection + +```bash +web-check config validate +``` + +This assumes the API is running locally on `http://localhost:8000`. You can customize this with environment variables: + +```bash +export WEB_CHECK_CLI_API_URL=http://your-api:8000 +web-check config validate +``` + +### 3. Run a Scan + +```bash +# Quick vulnerability scan +web-check scan quick https://example.com + +# Nuclei vulnerability scan +web-check scan nuclei https://example.com + +# Nikto web server scan +web-check scan nikto https://example.com + +# SSL/TLS assessment +web-check scan ssl https://example.com +``` + +### 4. View Results + +```bash +# List recent scans +web-check results list + +# View specific scan +web-check results show + +# Clear all results +web-check results clear +``` + +## Commands + +### Scan Operations + +```bash +web-check scan nuclei # Run Nuclei vulnerability scan +web-check scan nikto # Run Nikto web server scan +web-check scan quick # Run quick security scan +web-check scan ssl # Run SSL/TLS assessment +``` + +**Options:** +- `--timeout` - Timeout in seconds (default: varies by scanner) +- `--output-format` - Output format: `table` or `json` (default: table) + +### Results Operations + +```bash +web-check results list # List recent scan results +web-check results show # Show specific result +web-check results clear # Clear all results +``` + +**Options:** +- `--limit` - Number of results to display (default: 10) +- `--status` - Filter by status: success, error, timeout +- `--output-format` - Output format: `table` or `json` + +### Configuration Operations + +```bash +web-check config show # Display current configuration +web-check config validate # Validate API connection +``` + +## Configuration + +Configure via environment variables: + +```bash +export WEB_CHECK_CLI_API_URL=http://localhost:8000 +export WEB_CHECK_CLI_API_TIMEOUT=600 +export WEB_CHECK_CLI_OUTPUT_FORMAT=json +export WEB_CHECK_CLI_DEBUG=false +export WEB_CHECK_CLI_LOG_LEVEL=INFO +``` + +Or create a `.env` file in your working directory: + +```env +WEB_CHECK_CLI_API_URL=http://localhost:8000 +WEB_CHECK_CLI_API_TIMEOUT=600 +WEB_CHECK_CLI_OUTPUT_FORMAT=table +``` + +## Output Formats + +### Table Format (Default) + +Human-readable table output with color highlighting: + +``` +✓ Scan Result (nuclei - 1523ms) + +Status: success + +Found 3 Finding(s) + +[red][1] CRITICAL[/red] + Title: SQL Injection + Description: Application is vulnerable to SQL injection + CVE: CVE-2024-1234 + CVSS: 9.8 +``` + +### JSON Format + +Complete JSON output for programmatic processing: + +```bash +web-check scan nuclei https://example.com --output-format json +``` + +Returns full scan result including all metadata and findings. + +## Self-Hosted Setup + +The CLI is designed for self-hosted deployments: + +1. **Start the API locally:** + ```bash + cd /path/to/web-check + uv run uvicorn apps.api.main:app --host 0.0.0.0 --port 8000 + ``` + +2. **Or use Docker:** + ```bash + docker compose up -d api + ``` + +3. **Run CLI commands:** + ```bash + web-check scan nuclei https://example.com + ``` + +## Examples + +### Basic Vulnerability Scan + +```bash +web-check scan quick https://example.com +``` + +### Output to JSON + +```bash +web-check scan nuclei https://example.com --output-format json > results.json +``` + +### Custom Timeout + +```bash +web-check scan nikto https://example.com --timeout 900 +``` + +### List Results with Filtering + +```bash +# Show last 20 results +web-check results list --limit 20 + +# Show only failed scans +web-check results list --status error +``` + +## Troubleshooting + +### API Connection Refused + +Ensure the API is running: +```bash +web-check config validate +``` + +### Change API URL + +```bash +export WEB_CHECK_CLI_API_URL=http://your-server:8000 +web-check config validate +``` + +### Enable Debug Mode + +```bash +web-check --debug scan quick https://example.com +``` + +### Check Logs + +The CLI uses structured logging. View logs with: +```bash +web-check --debug scan quick https://example.com 2>&1 | grep -i error +``` + +## Development + +### Running Tests + +```bash +uv run pytest apps/api/tests/ -v +``` + +### Code Quality + +```bash +# Format code +uv run ruff format apps/ + +# Lint code +uv run ruff check apps/ + +# Type check +uv run ty check apps/api +``` + +## Version + +```bash +web-check --version +``` diff --git a/Dockerfile b/Dockerfile index c7f13b5..271a5a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,9 +25,9 @@ COPY uv.lock ./ RUN uv sync --frozen --no-install-project --no-dev # Copy application code -COPY api/ ./api/ -COPY alembic/ ./alembic/ -COPY alembic.ini ./ +COPY apps/api/ ./api/ +COPY apps/alembic/ ./alembic/ +COPY apps/alembic.ini ./ # Install project RUN uv sync --frozen --no-dev @@ -40,7 +40,7 @@ WORKDIR /app # Create outputs directory and copy config RUN mkdir -p outputs/temp -COPY config/ ./config/ +COPY apps/config/ ./config/ # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" diff --git a/Makefile b/Makefile index f16537f..5205db5 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,19 @@ -.PHONY: help install dev run test lint format check start stop restart logs \ - clean clean-all +.PHONY: help install run test lint format check ci \ + start stop restart logs logs-api status \ + clean clean-all cli # ============================================================================== # Variables # ============================================================================== PYTHON_VERSION ?= 3.12 -# Colors for display -RED = \033[0;31m -GREEN = \033[0;32m +# Colors +RED = \033[0;31m +GREEN = \033[0;32m YELLOW = \033[1;33m -BLUE = \033[0;34m -CYAN = \033[0;36m -NC = \033[0m +BLUE = \033[0;34m +CYAN = \033[0;36m +NC = \033[0m # ============================================================================== ##@ Help @@ -21,183 +22,106 @@ NC = \033[0m help: ## Display this help @echo "" @echo "$(BLUE)╔═══════════════════════════════════════════════════════════════╗$(NC)" - @echo "$(BLUE)║ 🔒 Web-Check Security Scanner ║$(NC)" + @echo "$(BLUE)║ 🔒 Web-Check Security Scanner ║$(NC)" @echo "$(BLUE)╚═══════════════════════════════════════════════════════════════╝$(NC)" @echo "" @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make $(CYAN)$(NC)\n\n"} /^[a-zA-Z_-]+:.*?##/ { printf " $(CYAN)%-18s$(NC) %s\n", $$1, $$2 } /^##@/ { printf "\n$(YELLOW)%s$(NC)\n", substr($$0, 5) } ' $(MAKEFILE_LIST) @echo "" - @echo "$(YELLOW)Quick Start:$(NC)" - @echo " 1. Copy .env.example to .env" - @echo " 2. make start # Start production environment" - @echo " 3. Open http://localhost:3000" - @echo "" - @echo "$(YELLOW)Development:$(NC)" - @echo " make dev # Start with hot-reload" - @echo " make logs # View logs" - @echo " make stop # Stop containers" + @echo "$(YELLOW)Quick start:$(NC)" + @echo " 1. cp .env.example .env" + @echo " 2. make start" + @echo " 3. make cli ARGS=\"scan quick https://example.com\"" @echo "" # ============================================================================== -##@ Docker - Quick Start +##@ Docker # ============================================================================== -start: ## Start production environment (web + api + scanners) - @echo "$(GREEN)🚀 Starting Web-Check in production mode...$(NC)" - @docker compose --profile prod up -d - @echo "$(GREEN)✅ Web-Check is ready!$(NC)" - @echo "" - @echo "$(CYAN)Access:$(NC)" - @echo " Web UI: http://localhost:3000" - @echo " API: http://localhost:8000" - @echo " API Docs: http://localhost:8000/docs" - @echo "" - -dev: ## Start development environment (hot-reload enabled) - @echo "$(GREEN)🚀 Starting Web-Check in development mode...$(NC)" - @docker compose --profile dev up -d - @echo "$(GREEN)✅ Development environment ready!$(NC)" - @echo "" - @echo "$(YELLOW)Hot-reload enabled for web and API$(NC)" - @echo "" - @echo "$(CYAN)Access:$(NC)" - @echo " Web UI: http://localhost:3000" - @echo " API: http://localhost:8000" - @echo " API Docs: http://localhost:8000/docs" - @echo "" - @echo "$(CYAN)View logs: make logs$(NC)" +start: ## Start all services (API + scanners) + @echo "$(GREEN)🚀 Starting services...$(NC)" + @docker compose up -d + @echo "$(GREEN)✅ Ready — API: http://localhost:8000/docs$(NC)" stop: ## Stop all containers - @echo "$(YELLOW)🛑 Stopping Web-Check...$(NC)" - @docker compose --profile prod --profile dev down + @docker compose down @echo "$(GREEN)✅ Stopped$(NC)" -restart: stop start ## Restart production environment +restart: stop start ## Restart all services -logs: ## View logs (all containers) +logs: ## Stream logs (all containers) @docker compose logs -f -logs-api: ## View API logs only +logs-api: ## Stream API logs @docker compose logs -f api -logs-web: ## View web logs only - @docker compose --profile prod logs -f web || docker compose --profile dev logs -f web-dev - status: ## Show container status - @echo "$(BLUE)📊 Container Status:$(NC)" @docker compose ps # ============================================================================== -##@ Development Tools +##@ Development # ============================================================================== -install: ## Install/setup development environment - @echo "$(GREEN)📦 Setting up development environment...$(NC)" - @command -v uv >/dev/null 2>&1 || { echo "$(RED)❌ uv not found. Install: curl -LsSf https://astral.sh/uv/install.sh | sh$(NC)"; exit 1; } - @command -v bun >/dev/null 2>&1 || { echo "$(RED)❌ Bun not found. Install: curl -fsSL https://bun.sh/install | bash$(NC)"; exit 1; } +install: ## Install all development dependencies + @command -v uv >/dev/null 2>&1 || { echo "$(RED)❌ uv not found: curl -LsSf https://astral.sh/uv/install.sh | sh$(NC)"; exit 1; } @uv python install $(PYTHON_VERSION) - @uv sync --all-extras --dev - @cd web && bun install - @echo "$(GREEN)✅ Development environment ready!$(NC)" + @uv sync --all-groups + @echo "$(GREEN)✅ Development environment ready$(NC)" -run: ## Run API locally (outside Docker) - @echo "$(GREEN)🚀 Starting API locally...$(NC)" +run: ## Run API locally (without Docker) @uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload -test: ## Run tests - @echo "$(GREEN)🧪 Running tests...$(NC)" - @uv run pytest api/tests/ -v +ARGS ?= guide +cli: ## Run CLI wizard (or: make cli ARGS="scan quick https://example.com") + @uv run python -m cli.main $(ARGS) -lint: ## Lint code - @echo "$(GREEN)🔍 Linting...$(NC)" - @uv run ruff check api/ +test: ## Run Python tests + @uv run pytest -q -format: ## Format code - @echo "$(GREEN)✨ Formatting code...$(NC)" - @uv run ruff format api/ +lint: ## Lint all code (ruff) + @uv run ruff check . -check: ## Run all code quality checks - @echo "$(GREEN)✅ Running all checks...$(NC)" - @uv run ruff format --check api/ - @uv run ruff check api/ - @uv run ty check api/ - @echo "$(GREEN)✅ All checks passed!$(NC)" +format: ## Format all code (ruff) + @uv run ruff format . -ci: ## Test all CI workflow steps locally - @echo "$(BLUE)╔═══════════════════════════════════════════════════════════════╗$(NC)" - @echo "$(BLUE)║ 🧪 Running CI Workflow Locally ║$(NC)" - @echo "$(BLUE)╚═══════════════════════════════════════════════════════════════╝$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 1/11: Gitleaks Secret Scan$(NC)" - @command -v gitleaks >/dev/null 2>&1 || { echo "$(YELLOW)⚠️ Gitleaks not installed. Install: brew install gitleaks$(NC)"; } - @command -v gitleaks >/dev/null 2>&1 && gitleaks detect --no-banner --verbose || echo "$(YELLOW)⏭️ Skipped (gitleaks not installed)$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 2/11: Python Lint (Ruff)$(NC)" - @uv run ruff check --output-format=github --target-version=py312 api/ - @echo "$(GREEN)✅ Python lint passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 3/11: Python Format Check (Ruff)$(NC)" - @uv run ruff format --check --target-version=py312 api/ - @echo "$(GREEN)✅ Python format check passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 4/11: Python Type Check (ty)$(NC)" - @uv run ty check api/ - @echo "$(GREEN)✅ Python type check passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 5/11: Python Tests (Pytest)$(NC)" - @uv run pytest api/tests/ -m "not slow" --cov=api --cov-report=term-missing -v - @echo "$(GREEN)✅ Python tests passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 6/11: Python Build (Docker)$(NC)" - @docker buildx build -t web-check:test -f Dockerfile . --load - @echo "$(GREEN)✅ Python Docker build passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 7/11: React Lint (oxlint)$(NC)" - @test -d web/node_modules || { echo "$(YELLOW)⚠️ Installing web dependencies...$(NC)"; cd web && bun install; } - @cd web && bun run lint - @echo "$(GREEN)✅ React lint passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 8/11: React Format Check (oxfmt)$(NC)" - @cd web && bun run format:check - @echo "$(GREEN)✅ React format check passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 9/11: React Type Check (TypeScript)$(NC)" - @cd web && bun run tsc --noEmit - @echo "$(GREEN)✅ React type check passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 10/11: React Build (Vite)$(NC)" - @cd web && bun run build - @echo "$(GREEN)✅ React build passed$(NC)" - @echo "" - @echo "$(YELLOW)📋 Step 11/11: React Build (Docker)$(NC)" - @docker buildx build -t web-check-ui:test -f web/Dockerfile web/ --load - @echo "$(GREEN)✅ React Docker build passed$(NC)" +check: ## Run all quality checks (format + lint + typecheck) + @echo "$(CYAN)▶ ruff format --check$(NC)" + @uv run ruff format --check . + @echo "$(CYAN)▶ ruff check$(NC)" + @uv run ruff check . + @echo "$(CYAN)▶ ty check$(NC)" + @uv run ty check + @echo "$(GREEN)✅ All checks passed$(NC)" + +ci: ## Simulate CI pipeline locally (matches ci-cd.yml) + @echo "$(BLUE)🧪 CI — Python$(NC)" + @uv run ruff format --check . + @uv run ruff check . + @uv run ty check + @uv run pytest -q + @echo "$(GREEN)✅ Python OK$(NC)" @echo "" - @echo "$(BLUE)╔═══════════════════════════════════════════════════════════════╗$(NC)" - @echo "$(BLUE)║ $(GREEN)✅ All CI Checks Passed Successfully!$(BLUE) ║$(NC)" - @echo "$(BLUE)╚═══════════════════════════════════════════════════════════════╝$(NC)" + @echo "$(BLUE)🧪 CI — Docker build$(NC)" + @docker build -t web-check-api:ci . -q + @echo "$(GREEN)✅ Docker OK$(NC)" @echo "" + @echo "$(GREEN)✅ All CI checks passed$(NC)" # ============================================================================== ##@ Cleanup # ============================================================================== -clean: ## Clean output files - @echo "$(YELLOW)🧹 Cleaning outputs...$(NC)" - @rm -rf outputs/* - @mkdir -p outputs +clean: ## Remove scan outputs + @rm -rf outputs/* && mkdir -p outputs @echo "$(GREEN)✅ Outputs cleaned$(NC)" -clean-all: ## Remove all containers, volumes, and outputs - @echo "$(RED)⚠️ This will remove ALL containers, volumes, and outputs!$(NC)" - @read -p "Are you sure? [y/N] " -n 1 -r; \ - echo; \ +clean-all: ## Remove containers, volumes, and outputs + @echo "$(RED)⚠️ This will remove all containers, volumes and outputs.$(NC)" + @read -p "Continue? [y/N] " -n 1 -r; echo; \ if [[ $$REPLY =~ ^[Yy]$$ ]]; then \ - echo "$(YELLOW)🧹 Cleaning everything...$(NC)"; \ - docker compose --profile prod --profile dev down -v; \ + docker compose down -v; \ docker system prune -f; \ rm -rf outputs/*; \ - rm -rf web/dist web/node_modules; \ - echo "$(GREEN)✅ Complete cleanup done$(NC)"; \ + echo "$(GREEN)✅ Clean$(NC)"; \ fi .DEFAULT_GOAL := help diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5218a92 --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +# TODO + +- FEAT: add documentation for web-check +- FEAT: update CLI to have a complete workflow for checking a website +- FEAT: use copilot sdk for the CLI tool \ No newline at end of file diff --git a/alembic/versions/1dd0c571c561_initial_database_schema.py b/alembic/versions/1dd0c571c561_initial_database_schema.py deleted file mode 100644 index 6320add..0000000 --- a/alembic/versions/1dd0c571c561_initial_database_schema.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Initial database schema - -Revision ID: 1dd0c571c561 -Revises: -Create Date: 2026-01-09 18:34:28.830369 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '1dd0c571c561' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('findings', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('scan_result_id', sa.Integer(), nullable=False), - sa.Column('scan_id', sa.String(length=50), nullable=False), - sa.Column('severity', sa.String(length=20), nullable=False), - sa.Column('title', sa.String(length=500), nullable=False), - sa.Column('description', sa.Text(), nullable=False), - sa.Column('reference', sa.String(length=500), nullable=True), - sa.Column('cve', sa.String(length=50), nullable=True), - sa.Column('cvss_score', sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_findings_scan_id'), 'findings', ['scan_id'], unique=False) - op.create_index(op.f('ix_findings_scan_result_id'), 'findings', ['scan_result_id'], unique=False) - op.create_table('scan_results', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('scan_id', sa.String(length=50), nullable=False), - sa.Column('module', sa.String(length=50), nullable=False), - sa.Column('category', sa.String(length=20), nullable=False), - sa.Column('target', sa.String(length=500), nullable=False), - sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False), - sa.Column('duration_ms', sa.Integer(), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('data', sa.JSON(), nullable=True), - sa.Column('error', sa.Text(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_scan_results_scan_id'), 'scan_results', ['scan_id'], unique=False) - op.create_table('scans', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('scan_id', sa.String(length=50), nullable=False), - sa.Column('target', sa.String(length=500), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('started_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('modules', sa.JSON(), nullable=True), - sa.Column('timeout', sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_scans_scan_id'), 'scans', ['scan_id'], unique=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_scans_scan_id'), table_name='scans') - op.drop_table('scans') - op.drop_index(op.f('ix_scan_results_scan_id'), table_name='scan_results') - op.drop_table('scan_results') - op.drop_index(op.f('ix_findings_scan_result_id'), table_name='findings') - op.drop_index(op.f('ix_findings_scan_id'), table_name='findings') - op.drop_table('findings') - # ### end Alembic commands ### diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..20ba29f --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1 @@ +"""Web-Check applications.""" diff --git a/alembic.ini b/apps/alembic.ini similarity index 100% rename from alembic.ini rename to apps/alembic.ini diff --git a/alembic/README b/apps/alembic/README similarity index 100% rename from alembic/README rename to apps/alembic/README diff --git a/alembic/env.py b/apps/alembic/env.py similarity index 89% rename from alembic/env.py rename to apps/alembic/env.py index e5fa623..073f020 100644 --- a/alembic/env.py +++ b/apps/alembic/env.py @@ -1,13 +1,10 @@ from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool - from alembic import context # Import your models here for autogenerate support from api.database import Base -from api.models.db_models import Scan, ScanResult, Finding +from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -66,9 +63,7 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/alembic/script.py.mako b/apps/alembic/script.py.mako similarity index 100% rename from alembic/script.py.mako rename to apps/alembic/script.py.mako diff --git a/apps/alembic/versions/1dd0c571c561_initial_database_schema.py b/apps/alembic/versions/1dd0c571c561_initial_database_schema.py new file mode 100644 index 0000000..a4bd512 --- /dev/null +++ b/apps/alembic/versions/1dd0c571c561_initial_database_schema.py @@ -0,0 +1,82 @@ +"""Initial database schema + +Revision ID: 1dd0c571c561 +Revises: +Create Date: 2026-01-09 18:34:28.830369 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "1dd0c571c561" +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "findings", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("scan_result_id", sa.Integer(), nullable=False), + sa.Column("scan_id", sa.String(length=50), nullable=False), + sa.Column("severity", sa.String(length=20), nullable=False), + sa.Column("title", sa.String(length=500), nullable=False), + sa.Column("description", sa.Text(), nullable=False), + sa.Column("reference", sa.String(length=500), nullable=True), + sa.Column("cve", sa.String(length=50), nullable=True), + sa.Column("cvss_score", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_findings_scan_id"), "findings", ["scan_id"], unique=False) + op.create_index( + op.f("ix_findings_scan_result_id"), "findings", ["scan_result_id"], unique=False + ) + op.create_table( + "scan_results", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("scan_id", sa.String(length=50), nullable=False), + sa.Column("module", sa.String(length=50), nullable=False), + sa.Column("category", sa.String(length=20), nullable=False), + sa.Column("target", sa.String(length=500), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("duration_ms", sa.Integer(), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("data", sa.JSON(), nullable=True), + sa.Column("error", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_scan_results_scan_id"), "scan_results", ["scan_id"], unique=False) + op.create_table( + "scans", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("scan_id", sa.String(length=50), nullable=False), + sa.Column("target", sa.String(length=500), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("modules", sa.JSON(), nullable=True), + sa.Column("timeout", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_scans_scan_id"), "scans", ["scan_id"], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_scans_scan_id"), table_name="scans") + op.drop_table("scans") + op.drop_index(op.f("ix_scan_results_scan_id"), table_name="scan_results") + op.drop_table("scan_results") + op.drop_index(op.f("ix_findings_scan_result_id"), table_name="findings") + op.drop_index(op.f("ix_findings_scan_id"), table_name="findings") + op.drop_table("findings") + # ### end Alembic commands ### diff --git a/api/__init__.py b/apps/api/__init__.py similarity index 100% rename from api/__init__.py rename to apps/api/__init__.py diff --git a/api/database.py b/apps/api/database.py similarity index 99% rename from api/database.py rename to apps/api/database.py index 9c8cd7f..f5aeb19 100644 --- a/api/database.py +++ b/apps/api/database.py @@ -3,11 +3,10 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager +from api.utils.config import get_settings from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase -from api.utils.config import get_settings - settings = get_settings() # Ensure database directory exists diff --git a/api/main.py b/apps/api/main.py similarity index 98% rename from api/main.py rename to apps/api/main.py index b253c23..f171978 100644 --- a/api/main.py +++ b/apps/api/main.py @@ -5,13 +5,12 @@ from contextlib import asynccontextmanager import structlog +from api.database import Base, engine +from api.routers import advanced, deep, health, quick, scans, security from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from api.database import Base, engine -from api.routers import advanced, deep, health, quick, scans, security - logger = structlog.get_logger() @@ -39,7 +38,7 @@ async def lifespan(app: FastAPI): # CORS middleware app.add_middleware( - CORSMiddleware, # ty: ignore[invalid-argument-type] + CORSMiddleware, allow_origins=["*"], # Configure appropriately for production allow_credentials=True, allow_methods=["*"], diff --git a/api/models/__init__.py b/apps/api/models/__init__.py similarity index 100% rename from api/models/__init__.py rename to apps/api/models/__init__.py diff --git a/api/models/db_models.py b/apps/api/models/db_models.py similarity index 99% rename from api/models/db_models.py rename to apps/api/models/db_models.py index 59a1311..7267a8a 100644 --- a/api/models/db_models.py +++ b/apps/api/models/db_models.py @@ -3,11 +3,10 @@ from datetime import UTC, datetime from typing import Any +from api.database import Base from sqlalchemy import JSON, DateTime, Float, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column -from api.database import Base - class Scan(Base): """Scan database model.""" diff --git a/api/models/findings.py b/apps/api/models/findings.py similarity index 100% rename from api/models/findings.py rename to apps/api/models/findings.py diff --git a/api/models/results.py b/apps/api/models/results.py similarity index 99% rename from api/models/results.py rename to apps/api/models/results.py index 5ecaf88..bfa4eb4 100644 --- a/api/models/results.py +++ b/apps/api/models/results.py @@ -3,9 +3,8 @@ from datetime import UTC, datetime from typing import Any, Literal -from pydantic import BaseModel, Field - from api.models.findings import Finding +from pydantic import BaseModel, Field ScanStatus = Literal["success", "error", "timeout", "running"] ScanCategory = Literal["quick", "deep", "security"] diff --git a/api/routers/__init__.py b/apps/api/routers/__init__.py similarity index 100% rename from api/routers/__init__.py rename to apps/api/routers/__init__.py diff --git a/api/routers/advanced.py b/apps/api/routers/advanced.py similarity index 99% rename from api/routers/advanced.py rename to apps/api/routers/advanced.py index 81fdb48..bd8e1a1 100644 --- a/api/routers/advanced.py +++ b/apps/api/routers/advanced.py @@ -1,11 +1,10 @@ """Advanced security scanning endpoints (SQLMap, Wapiti, XSStrike).""" -from fastapi import APIRouter, Query - from api.models import CheckResult from api.services.sqlmap_scanner import run_sqlmap_scan from api.services.wapiti_scanner import run_wapiti_scan from api.services.xsstrike_scanner import run_xsstrike_scan +from fastapi import APIRouter, Query router = APIRouter() diff --git a/api/routers/deep.py b/apps/api/routers/deep.py similarity index 99% rename from api/routers/deep.py rename to apps/api/routers/deep.py index cf973c9..b35986d 100644 --- a/api/routers/deep.py +++ b/apps/api/routers/deep.py @@ -1,10 +1,9 @@ """Deep scan endpoints.""" -from fastapi import APIRouter, HTTPException, Query - from api.models import CheckResult from api.services.sslyze_scanner import run_sslyze_scan from api.services.zap_native import run_zap_scan +from fastapi import APIRouter, HTTPException, Query router = APIRouter() diff --git a/api/routers/health.py b/apps/api/routers/health.py similarity index 100% rename from api/routers/health.py rename to apps/api/routers/health.py diff --git a/api/routers/quick.py b/apps/api/routers/quick.py similarity index 98% rename from api/routers/quick.py rename to apps/api/routers/quick.py index df9aca6..8a98520 100644 --- a/api/routers/quick.py +++ b/apps/api/routers/quick.py @@ -5,12 +5,12 @@ from urllib.parse import urlparse import httpx -from fastapi import APIRouter, HTTPException, Query -from httpx_secure import httpx_ssrf_protection - from api.models import CheckResult from api.services.nikto import run_nikto_scan from api.services.nuclei import run_nuclei_scan +from api.utils.config import get_settings +from fastapi import APIRouter, HTTPException, Query +from httpx_secure import httpx_ssrf_protection router = APIRouter() @@ -74,7 +74,7 @@ def _extract_hostname(value: str) -> str: # Domains that this endpoint is allowed to contact. # Replace or extend this tuple with the domains that are acceptable in your deployment. - ALLOWED_DOMAINS = ("example.com",) + ALLOWED_DOMAINS = tuple(get_settings().get_allowed_domains()) def _custom_ssrf_validator(hostname: str, ip: IPv4Address | IPv6Address, port: int) -> bool: """ diff --git a/api/routers/scans.py b/apps/api/routers/scans.py similarity index 99% rename from api/routers/scans.py rename to apps/api/routers/scans.py index c9fe849..6815e3b 100644 --- a/api/routers/scans.py +++ b/apps/api/routers/scans.py @@ -4,14 +4,13 @@ from datetime import UTC, datetime from typing import cast -from fastapi import APIRouter, Depends, HTTPException -from fastapi.responses import StreamingResponse -from sqlalchemy.ext.asyncio import AsyncSession - from api.database import get_session from api.models.results import CheckResult, ScanRequest, ScanResponse from api.services import db_service from api.services.log_streamer import log_streamer +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter() diff --git a/api/routers/security.py b/apps/api/routers/security.py similarity index 99% rename from api/routers/security.py rename to apps/api/routers/security.py index 0d06390..cef77e4 100644 --- a/api/routers/security.py +++ b/apps/api/routers/security.py @@ -2,9 +2,8 @@ from datetime import UTC -from fastapi import APIRouter, HTTPException, Query - from api.models import CheckResult +from fastapi import APIRouter, HTTPException, Query router = APIRouter() diff --git a/api/services/__init__.py b/apps/api/services/__init__.py similarity index 100% rename from api/services/__init__.py rename to apps/api/services/__init__.py diff --git a/api/services/db_service.py b/apps/api/services/db_service.py similarity index 99% rename from api/services/db_service.py rename to apps/api/services/db_service.py index a858680..b83b8de 100644 --- a/api/services/db_service.py +++ b/apps/api/services/db_service.py @@ -2,11 +2,10 @@ from datetime import UTC, datetime -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - from api.models import CheckResult from api.models.db_models import Finding, Scan, ScanResult +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession async def create_scan( diff --git a/api/services/docker_runner.py b/apps/api/services/docker_runner.py similarity index 100% rename from api/services/docker_runner.py rename to apps/api/services/docker_runner.py diff --git a/api/services/log_streamer.py b/apps/api/services/log_streamer.py similarity index 100% rename from api/services/log_streamer.py rename to apps/api/services/log_streamer.py diff --git a/api/services/nikto.py b/apps/api/services/nikto.py similarity index 99% rename from api/services/nikto.py rename to apps/api/services/nikto.py index 23314e2..c97df5f 100644 --- a/api/services/nikto.py +++ b/apps/api/services/nikto.py @@ -4,7 +4,6 @@ from datetime import UTC, datetime import structlog - from api.models import CheckResult, Finding from api.services.docker_runner import docker_run diff --git a/api/services/nuclei.py b/apps/api/services/nuclei.py similarity index 95% rename from api/services/nuclei.py rename to apps/api/services/nuclei.py index 0ae9fe8..a627151 100644 --- a/api/services/nuclei.py +++ b/apps/api/services/nuclei.py @@ -6,7 +6,6 @@ from typing import Any import structlog - from api.models import CheckResult, Finding from api.services.docker_runner import docker_run, load_jsonl_output @@ -124,9 +123,11 @@ def _parse_nuclei_output(data: list[dict[str, Any]]) -> list[Finding]: severity_str: str = str(info.get("severity", "info")).lower() finding = Finding( - severity=severity_str - if severity_str in ["critical", "high", "medium", "low", "info"] - else "info", # type: ignore[arg-type] + severity=( + severity_str + if severity_str in ["critical", "high", "medium", "low", "info"] + else "info" + ), # ty: ignore[invalid-argument-type] title=str(info.get("name", "Nuclei Finding")), description=str(info.get("description", "No description available")), reference=str(info.get("reference")) if info.get("reference") else None, diff --git a/api/services/sqlmap_scanner.py b/apps/api/services/sqlmap_scanner.py similarity index 99% rename from api/services/sqlmap_scanner.py rename to apps/api/services/sqlmap_scanner.py index 761f134..535b9a2 100644 --- a/api/services/sqlmap_scanner.py +++ b/apps/api/services/sqlmap_scanner.py @@ -6,7 +6,6 @@ from pathlib import Path import structlog - from api.models import CheckResult, Finding logger = structlog.get_logger() diff --git a/api/services/sslyze_scanner.py b/apps/api/services/sslyze_scanner.py similarity index 99% rename from api/services/sslyze_scanner.py rename to apps/api/services/sslyze_scanner.py index b8dbd82..e5a9bc8 100644 --- a/api/services/sslyze_scanner.py +++ b/apps/api/services/sslyze_scanner.py @@ -6,14 +6,13 @@ from typing import Any import structlog +from api.models import CheckResult, Finding from sslyze.plugins.scan_commands import ScanCommand from sslyze.scanner.models import ServerScanRequest, ServerScanStatusEnum from sslyze.scanner.scan_command_attempt import ScanCommandAttemptStatusEnum from sslyze.scanner.scanner import Scanner from sslyze.server_setting import ServerNetworkLocation -from api.models import CheckResult, Finding - logger = structlog.get_logger() diff --git a/api/services/wapiti_scanner.py b/apps/api/services/wapiti_scanner.py similarity index 96% rename from api/services/wapiti_scanner.py rename to apps/api/services/wapiti_scanner.py index ec8810c..312aab9 100644 --- a/api/services/wapiti_scanner.py +++ b/apps/api/services/wapiti_scanner.py @@ -7,8 +7,7 @@ from pathlib import Path import structlog - -from api.models import CheckResult, Finding +from api.models import CheckResult, Finding, Severity logger = structlog.get_logger() @@ -92,7 +91,7 @@ async def run_wapiti_scan( severity_str = _map_wapiti_severity(vuln.get("level", 1)) findings.append( Finding( - severity=severity_str, # type: ignore[arg-type] + severity=severity_str, title=f"Wapiti: {vuln_type}", description=vuln.get("info", "No description available"), reference=vuln.get("wstg", [None])[0] if vuln.get("wstg") else None, @@ -156,7 +155,7 @@ async def run_wapiti_scan( ) -def _map_wapiti_severity(level: int) -> str: +def _map_wapiti_severity(level: int) -> "Severity": """Map Wapiti severity level to our severity levels.""" if level == 3: return "critical" diff --git a/api/services/xsstrike_scanner.py b/apps/api/services/xsstrike_scanner.py similarity index 99% rename from api/services/xsstrike_scanner.py rename to apps/api/services/xsstrike_scanner.py index d670abf..dfc8834 100644 --- a/api/services/xsstrike_scanner.py +++ b/apps/api/services/xsstrike_scanner.py @@ -6,7 +6,6 @@ from pathlib import Path import structlog - from api.models import CheckResult, Finding logger = structlog.get_logger() diff --git a/api/services/zap_native.py b/apps/api/services/zap_native.py similarity index 99% rename from api/services/zap_native.py rename to apps/api/services/zap_native.py index 660fee6..8eb35b9 100644 --- a/api/services/zap_native.py +++ b/apps/api/services/zap_native.py @@ -6,9 +6,8 @@ from typing import Any import structlog -from zapv2 import ZAPv2 - from api.models import CheckResult, Finding +from zapv2 import ZAPv2 logger = structlog.get_logger() diff --git a/api/tests/__init__.py b/apps/api/tests/__init__.py similarity index 100% rename from api/tests/__init__.py rename to apps/api/tests/__init__.py diff --git a/api/tests/conftest.py b/apps/api/tests/conftest.py similarity index 100% rename from api/tests/conftest.py rename to apps/api/tests/conftest.py diff --git a/api/tests/test_api.py b/apps/api/tests/test_api.py similarity index 99% rename from api/tests/test_api.py rename to apps/api/tests/test_api.py index 6b12167..580370c 100644 --- a/api/tests/test_api.py +++ b/apps/api/tests/test_api.py @@ -1,9 +1,8 @@ """Tests for Web-Check Security Scanner.""" import pytest -from httpx import ASGITransport, AsyncClient - from api.main import app +from httpx import ASGITransport, AsyncClient @pytest.fixture diff --git a/api/tests/test_models.py b/apps/api/tests/test_models.py similarity index 96% rename from api/tests/test_models.py rename to apps/api/tests/test_models.py index 8649648..a05878e 100644 --- a/api/tests/test_models.py +++ b/apps/api/tests/test_models.py @@ -3,9 +3,8 @@ from datetime import datetime import pytest -from pydantic import ValidationError - from api.models import CheckResult, Finding, ScanRequest +from pydantic import ValidationError def test_finding_model(): @@ -28,7 +27,7 @@ def test_finding_invalid_severity(): """Test that invalid severity is rejected.""" with pytest.raises(ValidationError): Finding( - severity="invalid", # type: ignore[arg-type] + severity="invalid", # ty: ignore[invalid-argument-type] title="Test", description="Test", reference=None, diff --git a/api/tests/test_scanners.py b/apps/api/tests/test_scanners.py similarity index 99% rename from api/tests/test_scanners.py rename to apps/api/tests/test_scanners.py index 0b57ec9..e9a11f5 100644 --- a/api/tests/test_scanners.py +++ b/apps/api/tests/test_scanners.py @@ -1,9 +1,8 @@ """Tests for security scanner endpoints.""" import pytest -from httpx import ASGITransport, AsyncClient - from api.main import app +from httpx import ASGITransport, AsyncClient @pytest.fixture diff --git a/api/utils/__init__.py b/apps/api/utils/__init__.py similarity index 100% rename from api/utils/__init__.py rename to apps/api/utils/__init__.py diff --git a/api/utils/config.py b/apps/api/utils/config.py similarity index 65% rename from api/utils/config.py rename to apps/api/utils/config.py index 4f5e2ba..1b3ed40 100644 --- a/api/utils/config.py +++ b/apps/api/utils/config.py @@ -1,5 +1,6 @@ """Configuration management for Web-Check.""" +import json from functools import lru_cache from pathlib import Path @@ -28,6 +29,17 @@ class Settings(BaseSettings): # Logging log_level: str = "INFO" + # SSRF / domain allowlist — comma-separated string. + # Set ALLOWED_DOMAINS="example.com,yourdomain.com" in .env or environment. + allowed_domains: str = "example.com" + + def get_allowed_domains(self) -> list[str]: + """Return allowed_domains as a parsed list (comma-separated or JSON array).""" + v = self.allowed_domains.strip() + if v.startswith("["): + return json.loads(v) + return [d.strip() for d in v.split(",") if d.strip()] + model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", diff --git a/apps/cli/__init__.py b/apps/cli/__init__.py new file mode 100644 index 0000000..c267652 --- /dev/null +++ b/apps/cli/__init__.py @@ -0,0 +1,3 @@ +"""Web-Check CLI - Command line interface for security scanning.""" + +__version__ = "0.1.1" diff --git a/apps/cli/commands/__init__.py b/apps/cli/commands/__init__.py new file mode 100644 index 0000000..09948d4 --- /dev/null +++ b/apps/cli/commands/__init__.py @@ -0,0 +1,7 @@ +"""Commands subpackage.""" + +from .config import config_app +from .results import results_app +from .scan import scan_app + +__all__ = ["config_app", "results_app", "scan_app"] diff --git a/apps/cli/commands/config.py b/apps/cli/commands/config.py new file mode 100644 index 0000000..c86abc2 --- /dev/null +++ b/apps/cli/commands/config.py @@ -0,0 +1,59 @@ +"""Configuration command implementation.""" + +import structlog +import typer +from cli.utils import CLISettings +from rich.console import Console +from rich.table import Table + +logger = structlog.get_logger() +console = Console() + +config_app = typer.Typer(help="Configuration operations") + + +@config_app.command() +def show() -> None: + """Display current CLI configuration.""" + settings = CLISettings() + + table = Table(title="Web-Check CLI Configuration", show_header=True) + table.add_column("Setting", style="cyan") + table.add_column("Value", style="green") + + table.add_row("API URL", settings.api_url) + table.add_row("API Timeout", f"{settings.api_timeout}s") + table.add_row("Output Format", settings.output_format) + table.add_row("Debug", "Yes" if settings.debug else "No") + table.add_row("Log Level", settings.log_level) + + console.print(table) + console.print("\n[dim]Environment Variables:[/dim]") + console.print(" WEB_CHECK_CLI_API_URL") + console.print(" WEB_CHECK_CLI_API_TIMEOUT") + console.print(" WEB_CHECK_CLI_OUTPUT_FORMAT") + console.print(" WEB_CHECK_CLI_DEBUG") + console.print(" WEB_CHECK_CLI_LOG_LEVEL") + + +@config_app.command() +def validate() -> None: + """Validate API connection.""" + settings = CLISettings() + console.print(f"[cyan]Testing connection to {settings.api_url}...[/cyan]") + + try: + import httpx + + with httpx.Client(timeout=5) as client: + response = client.get(f"{settings.api_url}/api/health") + response.raise_for_status() + + console.print("[green]✓ API connection successful[/green]") + health_data = response.json() + console.print(f" Status: {health_data.get('status', 'unknown')}") + + except Exception as e: + logger.error("api_connection_failed", api_url=settings.api_url, error=str(e)) + console.print(f"[red]✗ API connection failed: {e}[/red]") + raise typer.Exit(1) from None diff --git a/apps/cli/commands/results.py b/apps/cli/commands/results.py new file mode 100644 index 0000000..334ceec --- /dev/null +++ b/apps/cli/commands/results.py @@ -0,0 +1,144 @@ +"""Results command implementation.""" + +import builtins + +import structlog +import typer +from cli.utils import APIClient, CLISettings, format_json, format_table +from rich.console import Console + +logger = structlog.get_logger() +console = Console() + +results_app = typer.Typer(help="Results operations") + + +@results_app.command() +def list( + limit: int = typer.Option(10, help="Number of results to return"), + status: str = typer.Option(None, help="Filter by status (success, error, timeout)"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """List recent scan results.""" + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Fetching results..."): + response = client.get("/api/scans") + + # API returns a list directly + scans: builtins.list = response if isinstance(response, builtins.list) else [] + if status: + scans = [s for s in scans if s.get("status") == status] + scans = scans[:limit] + + if not scans: + console.print("[yellow]No scan results found[/yellow]") + return + + if output_format == "json": + format_json(scans) + else: + display_data = [ + { + "ID": s.get("scan_id", "N/A"), + "Target": s.get("target", "N/A"), + "Status": s.get("status", "N/A"), + "Modules": len(s.get("results", [])), + "Started": s.get("started_at", "N/A")[:19] if s.get("started_at") else "N/A", + } + for s in scans + ] + format_table("Scan Results", display_data) + + except Exception as e: + logger.error("fetch_results_failed", error=str(e)) + console.print(f"[red]✗ Failed to fetch results: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +@results_app.command() +def show( + scan_id: str = typer.Argument(..., help="Scan ID to display"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """Display details of a specific scan result.""" + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Fetching scan result..."): + result = client.get(f"/api/scans/{scan_id}") + + if not result: + console.print("[yellow]Scan result not found[/yellow]") + raise typer.Exit(1) from None + + if output_format == "json": + format_json(result) + else: + console.print("\n[bold cyan]Scan Details[/bold cyan]") + console.print(f"ID: {result.get('scan_id', 'N/A')}") + console.print(f"Target: {result.get('target', 'N/A')}") + console.print(f"Status: {result.get('status', 'N/A')}") + console.print(f"Started: {result.get('started_at', 'N/A')}") + + results = result.get("results", []) + if results: + console.print(f"\n[bold]Modules run ({len(results)})[/bold]") + for r in results: + findings_n = len(r.get("findings", [])) + icon = "✓" if r.get("status") == "success" else "✗" + console.print( + f" [{icon}] {r.get('module', '?'):10} " + f"{r.get('duration_ms', 0)}ms " + f"{findings_n} finding(s)" + ) + + except Exception as e: + logger.error("fetch_result_failed", scan_id=scan_id, error=str(e)) + console.print(f"[red]✗ Failed to fetch scan result: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +@results_app.command() +def clear( + confirm: bool = typer.Option(False, "--confirm", help="Confirm deletion without prompt"), +) -> None: + """Clear all scan results from the local database.""" + if not confirm: + if not typer.confirm("Are you sure you want to delete all results?"): + console.print("[yellow]Operation cancelled[/yellow]") + return + + import subprocess + + try: + result = subprocess.run( + [ + "docker", + "exec", + "web-check-api", + "python3", + "-c", + "import sqlite3; db=sqlite3.connect('/app/data/web-check.db'); " + "[db.execute(f'DELETE FROM {t}') for t in ('findings','scan_results','scans')]; " + "db.commit(); print('cleared')", + ], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode == 0: + console.print("[green]✓ All results cleared[/green]") + else: + console.print(f"[red]✗ {result.stderr.strip()}[/red]") + raise typer.Exit(1) from None + except Exception as e: + console.print(f"[red]✗ Failed to clear results: {e}[/red]") + raise typer.Exit(1) from None diff --git a/apps/cli/commands/scan.py b/apps/cli/commands/scan.py new file mode 100644 index 0000000..4524a8b --- /dev/null +++ b/apps/cli/commands/scan.py @@ -0,0 +1,304 @@ +"""Scan command implementation.""" + +import time + +import structlog +import typer +from cli.utils import APIClient, CLISettings, format_findings, format_json +from rich.console import Console +from rich.table import Table + +logger = structlog.get_logger() +console = Console() + +scan_app = typer.Typer(help="Scan operations") + +# All modules supported by /api/scans/start +_ALL_MODULES = ["nuclei", "nikto", "zap", "testssl", "sqlmap", "wapiti", "xsstrike"] +_DEFAULT_MODULES = ["nuclei", "nikto", "zap"] + + +@scan_app.command() +def nuclei( + url: str = typer.Argument(..., help="Target URL to scan"), + timeout: int = typer.Option(300, help="Timeout in seconds (30-600)"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """Run Nuclei vulnerability scan. + + Fast vulnerability and CVE detection scan using Nuclei templates. + """ + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Running Nuclei scan..."): + result = client.get( + "/api/quick/nuclei", + url=url, + timeout=timeout, + ) + + _display_result(result, output_format) + except Exception as e: + logger.error("nuclei_scan_failed", error=str(e)) + console.print(f"[red]✗ Nuclei scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +@scan_app.command() +def nikto( + url: str = typer.Argument(..., help="Target URL to scan"), + timeout: int = typer.Option(600, help="Timeout in seconds (30-600)"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """Run Nikto web server scan. + + Comprehensive web server misconfiguration and vulnerability detection. + """ + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Running Nikto scan..."): + result = client.get( + "/api/quick/nikto", + url=url, + timeout=timeout, + ) + + _display_result(result, output_format) + except Exception as e: + logger.error("nikto_scan_failed", error=str(e)) + console.print(f"[red]✗ Nikto scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +@scan_app.command() +def quick( + url: str = typer.Argument(..., help="Target URL to scan"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """Run quick DNS + reachability check.""" + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Running quick scan..."): + result = client.get("/api/quick/dns", url=url) + + _display_result(result, output_format) + except Exception as e: + logger.error("quick_scan_failed", error=str(e)) + console.print(f"[red]✗ Quick scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +@scan_app.command() +def ssl( + url: str = typer.Argument(..., help="Target URL to scan"), + timeout: int = typer.Option(300, help="Timeout in seconds (30-600)"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """Run SSL/TLS security assessment. + + Comprehensive SSL/TLS configuration analysis using SSLyze. + """ + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Running SSL scan..."): + result = client.get( + "/api/deep/sslyze", + url=url, + timeout=timeout, + ) + + _display_result(result, output_format) + except Exception as e: + logger.error("ssl_scan_failed", error=str(e)) + console.print(f"[red]✗ SSL scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +@scan_app.command() +def full( + url: str = typer.Argument(..., help="Target URL to scan"), + modules: str = typer.Option( + ",".join(_DEFAULT_MODULES), + help=f"Comma-separated modules. Available: {', '.join(_ALL_MODULES)}", + ), + all_modules: bool = typer.Option(False, "--all", help="Run every available module"), + timeout: int = typer.Option(300, help="Timeout per module in seconds (30-3600)"), + output_format: str = typer.Option("table", help="Output format (table, json)"), +) -> None: + """Run a complete multi-module security scan (async, with live progress). + + Default modules: nuclei, nikto, zap. + Use --all to run all 7 modules (much slower). + """ + module_list = ( + _ALL_MODULES if all_modules else [m.strip() for m in modules.split(",") if m.strip()] + ) + + invalid = [m for m in module_list if m not in _ALL_MODULES] + if invalid: + console.print(f"[red]✗ Unknown module(s): {', '.join(invalid)}[/red]") + console.print(f" Available: {', '.join(_ALL_MODULES)}") + raise typer.Exit(1) from None + + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + # Start async scan + console.print(f"\n[bold cyan]🔍 Starting full scan on {url}[/bold cyan]") + console.print(f" Modules : [cyan]{', '.join(module_list)}[/cyan]") + console.print(f" Timeout : {timeout}s per module\n") + + response = client.post( + "/api/scans/start", + json={"target": url, "modules": module_list, "timeout": timeout}, + ) + scan_id = response.get("scan_id") + if not scan_id: + console.print("[red]✗ Failed to start scan — no scan_id returned[/red]") + raise typer.Exit(1) from None + + console.print(f"[dim]Scan ID: {scan_id}[/dim]\n") + + # Poll until complete + poll_interval = 5 + max_wait = timeout * len(module_list) + 60 + elapsed = 0 + last_count = 0 + + with console.status("[bold green]Scanning…") as status: + while elapsed < max_wait: + time.sleep(poll_interval) + elapsed += poll_interval + + scan = client.get(f"/api/scans/{scan_id}") + current_status = scan.get("status", "running") + results = scan.get("results", []) + + if len(results) > last_count: + for r in results[last_count:]: + icon = "✓" if r.get("status") == "success" else "✗" + findings_n = len(r.get("findings", [])) + color = "green" if r.get("status") == "success" else "yellow" + status.update( + f"[{color}][{icon}] {r.get('module', '?'):10} " + f"{r.get('duration_ms', 0)}ms " + f"{findings_n} finding(s)[/{color}]" + ) + console.log( + f"[{color}][{icon}][/{color}] [bold]{r.get('module', '?')}[/bold] " + f"[dim]{r.get('duration_ms', 0)}ms[/dim] " + f"[yellow]{findings_n} finding(s)[/yellow]" + ) + last_count = len(results) + + if current_status != "running": + break + + # Final results + scan = client.get(f"/api/scans/{scan_id}") + results = scan.get("results", []) + + if output_format == "json": + format_json(scan) + return + + console.print() + _display_full_summary(scan_id, url, results) + + except Exception as e: + logger.error("full_scan_failed", error=str(e)) + console.print(f"[red]✗ Full scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +def _display_full_summary(scan_id: str, url: str, results: list) -> None: + """Print a consolidated findings table for a full scan.""" + all_findings = [] + for r in results: + for f in r.get("findings", []): + all_findings.append({**f, "_module": r.get("module", "?")}) + + # Module summary table + summary = Table(title=f"Full Scan — {url}", show_header=True, header_style="bold magenta") + summary.add_column("Module", style="cyan") + summary.add_column("Status") + summary.add_column("Duration", justify="right") + summary.add_column("Findings", justify="right") + + for r in results: + status = r.get("status", "?") + color = "green" if status == "success" else "red" + findings_n = len(r.get("findings", [])) + summary.add_row( + r.get("module", "?"), + f"[{color}]{status}[/{color}]", + f"{r.get('duration_ms', 0)}ms", + f"[yellow]{findings_n}[/yellow]" if findings_n else "0", + ) + + console.print(summary) + + if all_findings: + console.print( + f"\n[bold red]⚠ {len(all_findings)} Finding(s) across all modules[/bold red]\n" + ) + format_findings(all_findings) + else: + console.print("\n[bold green]✓ No security findings detected[/bold green]") + + console.print( + f"\n[dim]Scan ID: {scan_id} — run 'make cli ARGS=\"results show {scan_id}\"' to review later[/dim]" + ) + + +def _display_result(result: dict, output_format: str) -> None: + """Display scan result in requested format. + + Args: + result: Scan result dictionary + output_format: Format to display (table, json) + """ + status = result.get("status", "unknown") + module = result.get("module", "unknown") + duration = result.get("duration_ms", 0) + + if output_format == "json": + format_json(result) + else: + # Display summary + status_icon = "✓" if status == "success" else "✗" + console.print(f"\n[bold]{status_icon} Scan Result[/bold] ({module} - {duration}ms)\n") + + if result.get("error"): + console.print(f"[red]Error: {result['error']}[/red]") + else: + console.print(f"[green]Status: {status}[/green]") + + # Display findings + if result.get("findings"): + format_findings(result["findings"]) + + # Display metadata + if result.get("data"): + console.print("\n[bold cyan]Metadata:[/bold cyan]") + for key, value in result["data"].items(): + console.print(f" {key}: {value}") diff --git a/apps/cli/main.py b/apps/cli/main.py new file mode 100644 index 0000000..456d845 --- /dev/null +++ b/apps/cli/main.py @@ -0,0 +1,217 @@ +"""Web-Check CLI main application.""" + +import structlog +import typer +from cli import __version__ +from cli.commands import config_app, results_app, scan_app +from cli.commands.scan import _display_result +from cli.utils import APIClient, CLISettings +from rich.console import Console +from rich.prompt import Prompt +from rich.rule import Rule + +logger = structlog.get_logger() +console = Console() + +app = typer.Typer( + help="Web-Check Security Scanner CLI", + pretty_exceptions_enable=False, + invoke_without_command=True, +) + +# Register subcommands +app.add_typer(scan_app, name="scan", help="Scan operations") +app.add_typer(results_app, name="results", help="Results operations") +app.add_typer(config_app, name="config", help="Configuration operations") + + +@app.callback() +def main( + ctx: typer.Context, + version: bool = typer.Option( + None, + "--version", + "-v", + help="Show version and exit", + callback=lambda x: _show_version(x) if x else None, + ), + debug: bool = typer.Option(False, "--debug", help="Enable debug mode"), +) -> None: + """Web-Check Security Scanner - Self-hosted vulnerability detection tool.""" + if debug: + structlog.configure( + processors=[ + structlog.processors.JSONRenderer(), + ] + ) + # No subcommand given → launch interactive guide + if ctx.invoked_subcommand is None: + guide() + + +def _show_version(value: bool) -> None: + """Display version and exit.""" + if value: + console.print(f"Web-Check CLI v{__version__}") + raise typer.Exit() + + +_SCAN_DESCRIPTIONS = { + "full": "Complete scan — nuclei + nikto + zap (async, with live progress)", + "quick": "DNS + reachability check (fast)", + "ssl": "SSL/TLS configuration analysis", + "nuclei": "Nuclei CVE & vulnerability templates", + "nikto": "Web server misconfiguration scan", +} + +_SCAN_ENDPOINTS = { + "quick": "/api/quick/dns", + "ssl": "/api/deep/sslyze", + "nuclei": "/api/quick/nuclei", + "nikto": "/api/quick/nikto", +} + + +@app.command() +def guide() -> None: + """Interactive guided wizard — choose a scan or check health.""" + console.print() + console.print(Rule("[bold cyan]🔒 Web-Check Security Scanner[/bold cyan]")) + console.print() + + # ── Step 1: top-level action ────────────────────────────────────────── + action = Prompt.ask( + "[bold]What would you like to do?[/bold]", + choices=["scan", "health", "quit"], + default="scan", + ) + + if action == "quit": + console.print("[dim]Bye![/dim]") + raise typer.Exit() + + if action == "health": + _run_health() + return + + # ── Step 2: scan type ───────────────────────────────────────────────── + console.print() + for name, desc in _SCAN_DESCRIPTIONS.items(): + console.print(f" [cyan]{name:<8}[/cyan] {desc}") + console.print() + + scan_type = Prompt.ask( + "[bold]Select scan type[/bold]", + choices=list(_SCAN_DESCRIPTIONS), + default="full", + ) + + # ── Step 3: target URL ──────────────────────────────────────────────── + url = Prompt.ask("[bold]Target URL[/bold]", default="https://example.com") + + # ── Step 4: output format ───────────────────────────────────────────── + fmt = Prompt.ask( + "[bold]Output format[/bold]", + choices=["table", "json"], + default="table", + ) + + # ── Execute ─────────────────────────────────────────────────────────── + console.print() + + # Full scan delegates to the `scan full` command logic + if scan_type == "full": + import time + + from cli.commands.scan import _DEFAULT_MODULES, _display_full_summary + + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + try: + console.print(f"[bold cyan]🔍 Starting full scan on {url}[/bold cyan]") + console.print(f" Modules : [cyan]{', '.join(_DEFAULT_MODULES)}[/cyan]\n") + + response = client.post( + "/api/scans/start", + json={"target": url, "modules": _DEFAULT_MODULES, "timeout": 300}, + ) + scan_id = response.get("scan_id") + if not scan_id: + console.print("[red]✗ Failed to start scan[/red]") + raise typer.Exit(1) + + last_count = 0 + with console.status("[bold green]Scanning…") as status: + for _ in range(300): + time.sleep(5) + scan = client.get(f"/api/scans/{scan_id}") + results = scan.get("results", []) + if len(results) > last_count: + for r in results[last_count:]: + icon = "✓" if r.get("status") == "success" else "✗" + color = "green" if r.get("status") == "success" else "yellow" + findings_n = len(r.get("findings", [])) + console.log( + f"[{color}][{icon}][/{color}] [bold]{r.get('module', '?')}[/bold] " + f"[dim]{r.get('duration_ms', 0)}ms[/dim] " + f"[yellow]{findings_n} finding(s)[/yellow]" + ) + status.update(f"[green]Completed {len(results)} module(s)…[/green]") + last_count = len(results) + if scan.get("status") != "running": + break + + scan = client.get(f"/api/scans/{scan_id}") + if fmt == "json": + _display_result(scan, "json") + else: + _display_full_summary(scan_id, url, scan.get("results", [])) + except Exception as e: + console.print(f"[red]✗ Scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + return + + endpoint = _SCAN_ENDPOINTS[scan_type] + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status(f"[bold green]Running {scan_type} scan on {url}…"): + result = client.get(endpoint, url=url) + _display_result(result, fmt) + except Exception as e: + console.print(f"[red]✗ Scan failed: {e}[/red]") + raise typer.Exit(1) from None + finally: + client.close() + + +def _run_health() -> None: + """Health check helper (shared by guide and health command).""" + settings = CLISettings() + try: + import httpx + + with httpx.Client(timeout=5) as client: + response = client.get(f"{settings.api_url}/api/health") + response.raise_for_status() + health_data = response.json() + + status = health_data.get("status", "unknown") + color = "green" if status == "healthy" else "yellow" + console.print(f"[{color}]● API Status: {status}[/{color}]") + except Exception as e: + console.print(f"[red]✗ API unreachable: {e}[/red]") + raise typer.Exit(1) from None + + +@app.command() +def health() -> None: + """Check API health status.""" + _run_health() + + +if __name__ == "__main__": + app() diff --git a/apps/cli/utils/__init__.py b/apps/cli/utils/__init__.py new file mode 100644 index 0000000..bb6a4d8 --- /dev/null +++ b/apps/cli/utils/__init__.py @@ -0,0 +1,13 @@ +"""CLI utilities package.""" + +from .config import CLISettings, get_settings +from .http_client import APIClient, format_findings, format_json, format_table + +__all__ = [ + "CLISettings", + "get_settings", + "APIClient", + "format_findings", + "format_json", + "format_table", +] diff --git a/apps/cli/utils/config.py b/apps/cli/utils/config.py new file mode 100644 index 0000000..e723865 --- /dev/null +++ b/apps/cli/utils/config.py @@ -0,0 +1,25 @@ +"""CLI configuration and settings.""" + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class CLISettings(BaseSettings): + """CLI application settings.""" + + api_url: str = "http://localhost:8000" + api_timeout: int = 600 + output_format: str = "table" # table, json, yaml + debug: bool = False + log_level: str = "INFO" + + model_config = SettingsConfigDict( + env_file=".env", + env_prefix="WEB_CHECK_CLI_", + case_sensitive=False, + extra="ignore", + ) + + +def get_settings() -> CLISettings: + """Get CLI settings instance.""" + return CLISettings() diff --git a/apps/cli/utils/http_client.py b/apps/cli/utils/http_client.py new file mode 100644 index 0000000..a7cd821 --- /dev/null +++ b/apps/cli/utils/http_client.py @@ -0,0 +1,150 @@ +"""HTTP client utilities for API communication.""" + +from typing import Any + +import httpx +import structlog +from rich.console import Console +from rich.table import Table + +logger = structlog.get_logger() +console = Console() + + +class APIClient: + """HTTP client for API communication.""" + + def __init__(self, base_url: str, timeout: int = 600): + """Initialize API client. + + Args: + base_url: Base URL of the API + timeout: Request timeout in seconds + """ + self.base_url = base_url.rstrip("/") + self.timeout = timeout + self.client = httpx.Client(timeout=timeout, follow_redirects=True) + + def post(self, endpoint: str, json: dict | None = None, **params: Any) -> dict[str, Any]: + """Make POST request to API. + + Args: + endpoint: API endpoint path + json: Optional JSON body + **params: Query parameters + + Returns: + Response JSON + """ + url = f"{self.base_url}/{endpoint.lstrip('/')}" + try: + response = self.client.post(url, json=json, params=params if params else None) + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + logger.error("api_request_failed", url=url, error=str(e)) + console.print(f"[red]Error: {e}[/red]") + raise + + def get(self, endpoint: str, **params: Any) -> Any: + """Make GET request to API. + + Args: + endpoint: API endpoint path + **params: Query parameters + + Returns: + Response JSON (dict or list depending on endpoint) + """ + url = f"{self.base_url}/{endpoint.lstrip('/')}" + try: + response = self.client.get(url, params=params) + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + logger.error("api_request_failed", url=url, error=str(e)) + console.print(f"[red]Error: {e}[/red]") + raise + + def close(self) -> None: + """Close the HTTP client.""" + self.client.close() + + def __enter__(self) -> "APIClient": + """Context manager entry.""" + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Context manager exit.""" + self.close() + + +def format_table(title: str, data: list[dict[str, Any]]) -> None: + """Format and print data as a table. + + Args: + title: Table title + data: List of dictionaries to display + """ + if not data: + console.print("[yellow]No data to display[/yellow]") + return + + table = Table(title=title, show_header=True, header_style="bold magenta") + + # Add columns from first row + keys = list(data[0].keys()) + for key in keys: + table.add_column(key, style="cyan") + + # Add rows + for row in data: + values = [str(row.get(key, "")) for key in keys] + table.add_row(*values) + + console.print(table) + + +def format_json(data: Any) -> None: + """Format and print data as JSON. + + Args: + data: Data to display + """ + console.print_json(data=data) + + +def format_findings(findings: list[dict[str, Any]]) -> None: + """Format and print security findings. + + Args: + findings: List of findings + """ + if not findings: + console.print("[green]✓ No security findings detected[/green]") + return + + console.print(f"\n[bold red]Found {len(findings)} Finding(s)[/bold red]\n") + + for i, finding in enumerate(findings, 1): + severity = finding.get("severity", "unknown").upper() + severity_color = { + "CRITICAL": "red", + "HIGH": "red", + "MEDIUM": "yellow", + "LOW": "blue", + "INFO": "cyan", + }.get(severity, "white") + + console.print(f"[{severity_color}][{i}] {severity}[/{severity_color}]") + console.print(f" Title: {finding.get('title', 'N/A')}") + console.print(f" Description: {finding.get('description', 'N/A')}") + + if finding.get("cve"): + console.print(f" CVE: {finding['cve']}") + if finding.get("cvss_score") is not None: + console.print(f" CVSS: {finding['cvss_score']}") + if finding.get("reference"): + console.print(f" Reference: {finding['reference']}") + + console.print() diff --git a/config/settings.conf b/apps/config/settings.conf similarity index 100% rename from config/settings.conf rename to apps/config/settings.conf diff --git a/config/wordlists/common.txt b/apps/config/wordlists/common.txt similarity index 100% rename from config/wordlists/common.txt rename to apps/config/wordlists/common.txt diff --git a/docker-compose.yml b/docker-compose.yml index 0d2b614..7bbd694 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,73 +2,34 @@ # Web-Check Security Scanner - Docker Compose Configuration # ============================================================================== # Usage: -# Production: docker compose up -d -# Development: docker compose --profile dev up -d +# Start: docker compose up -d +# Tools: docker compose --profile tools up -d # # Copy .env.example to .env and customize as needed services: - # ========================================================================== - # Web Interface (Production Build) - # ========================================================================== - web: - build: - context: ./web - dockerfile: Dockerfile - container_name: web-check-web - ports: - - "${WEB_PORT:-3000}:80" - environment: - - VITE_API_URL=${VITE_API_URL:-http://localhost:8000} - networks: - - scanner-net - restart: unless-stopped - depends_on: - - api - profiles: - - prod - - # ========================================================================== - # Web Interface (Development with Hot-Reload) - # ========================================================================== - web-dev: - image: oven/bun:1.1-alpine - container_name: web-check-web-dev - working_dir: /app - ports: - - "${WEB_PORT:-3000}:3000" - volumes: - - ./web:/app - - /app/node_modules - command: sh -c "bun install && bun run dev -- --host" - environment: - - VITE_API_URL=http://api:8000 - networks: - - scanner-net - depends_on: - - api - profiles: - - dev - # ========================================================================== # API Server # ========================================================================== api: - build: . + build: + context: . + dockerfile: Dockerfile container_name: web-check-api ports: - "${API_PORT:-8000}:8000" volumes: - - ./api:/app/api:ro + - ./apps/api:/app/api:ro - ./outputs:/app/outputs:rw - - ./config:/app/config:ro - - ./web-check.db:/app/web-check.db:rw - - ./alembic:/app/alembic:ro - - ./alembic.ini:/app/alembic.ini:ro + - ./apps/config:/app/config:ro + - ./data:/app/data:rw + - ./apps/alembic:/app/alembic:ro + - ./apps/alembic.ini:/app/alembic.ini:ro environment: - DEBUG=${DEBUG:-false} - LOG_LEVEL=${LOG_LEVEL:-INFO} - DOCKER_NETWORK=${DOCKER_NETWORK:-scanner-net} + - ALLOWED_DOMAINS=${ALLOWED_DOMAINS:-example.com} networks: - scanner-net restart: unless-stopped @@ -118,9 +79,10 @@ services: - scanner-net restart: unless-stopped - # Nikto (Web Server Scanner) + # Nikto (Web Server Scanner) — amd64 only, runs via Rosetta on Apple Silicon nikto: image: alpine/nikto:latest + platform: linux/amd64 container_name: security-scanner-nikto volumes: - ./outputs:/output:rw diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..57cb759 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,55 @@ +# Architecture + +Web Check is a **monorepo** containing a Python/FastAPI backend, a React/Vite frontend, and Docker Compose orchestration for a suite of security scanner sidecars. + +## Repository layout + +``` +web-check/ +├── apps/ +│ ├── api/ # FastAPI application (Python 3.12, uv) +│ ├── cli/ # Typer CLI (thin wrapper around the API) +│ ├── alembic/ # Database migrations +│ ├── alembic.ini # Alembic config (SQLite by default) +│ ├── config/ # Static config (wordlists, settings) +│ └── web/ # React + Vite + Bun frontend +├── Dockerfile # API image (multi-stage, uv + Python 3.12) +├── docker-compose.yml +├── pyproject.toml # Python project config (uv, ruff, pytest, ty) +└── docs/ # This documentation +``` + +## Services + +| Service | Image | Purpose | +|---------|-------|---------| +| `api` | Custom (repo Dockerfile) | FastAPI REST API + scan orchestrator | +| `web` | Custom (`apps/web/Dockerfile`) | Production Nginx-served React UI | +| `web-dev` | `oven/bun:1.1-alpine` | Hot-reload dev server | +| `zap` | `zaproxy/zap-stable` | OWASP ZAP dynamic analysis proxy | +| `nuclei` | `projectdiscovery/nuclei` | Template-based vulnerability scanner | +| `nikto` | `alpine/nikto` | Web server misconfiguration scanner | +| `ffuf` | `secsi/ffuf` | Directory/path fuzzer (optional profile) | + +## Networking + +All containers share the `scanner-net` bridge network. The API communicates with scanners by their container name (e.g. `http://zap:8090`). The `DOCKER_NETWORK` environment variable allows overriding the network name for external integration. + +## API design + +The FastAPI app exposes scan endpoints under `/api/`: + +| Prefix | Description | +|--------|-------------| +| `/api/health` | Liveness / readiness checks | +| `/api/quick` | Fast, low-impact scans | +| `/api/deep` | Thorough scans (longer runtime) | +| `/api/security` | Dedicated security tool integrations | +| `/api/advanced` | Advanced / multi-tool chained scans | +| `/api/scans` | Scan history and results management | + +Database: SQLite via SQLAlchemy async + Alembic migrations (auto-run on startup). + +## Frontend + +React 18 SPA built with Vite, styled with Tailwind CSS and Radix UI primitives. Communicates with the API via `VITE_API_URL` (defaults to `http://localhost:8000`). Production: served by Nginx. Development: Vite dev server with HMR. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..1ac2680 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,64 @@ +# Configuration + +## Environment variables + +Copy `.env.example` to `.env` and adjust as needed. + +### API + +| Variable | Default | Description | +|----------|---------|-------------| +| `DEBUG` | `false` | Enable debug mode (verbose errors, auto-reload) | +| `LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `DOCKER_NETWORK` | `scanner-net` | Docker network name used to reach scanner sidecars | + +### Ports + +| Variable | Default | Description | +|----------|---------|-------------| +| `API_PORT` | `8000` | Host port for the FastAPI server | +| `WEB_PORT` | `3000` | Host port for the React UI | + +### Frontend + +| Variable | Default | Description | +|----------|---------|-------------| +| `VITE_API_URL` | `http://localhost:8000` | URL of the FastAPI backend (must be reachable from the browser) | + +## Docker Compose profiles + +| Profile | Services started | +|---------|-----------------| +| *(none)* | `api`, `zap`, `nuclei`, `nikto` | +| `dev` | + `web-dev` (Vite hot-reload) | +| `prod` | + `web` (Nginx production build) | +| `tools` | + `ffuf` (directory fuzzer) | + +Example: + +```bash +# API + scanners + dev UI + fuzzer +docker compose --profile dev --profile tools up -d +``` + +## Scanner configuration + +### ZAP + +ZAP runs in daemon mode with the REST API enabled (`api.disablekey=true`). Scan output is written to `./outputs/`. The API connects to ZAP via `http://zap:8090` on the Docker network. + +### Nuclei + +Nuclei templates are stored in the `nuclei-templates` Docker volume and persist between restarts. The container stays idle until the API dispatches a scan command. + +### Nikto + +Nikto runs in on-demand mode (idle container). The API calls nikto via `docker exec` or the internal Docker socket when a scan is triggered. + +### Wordlists (FFuf) + +Custom wordlists go in `apps/config/wordlists/`. They are mounted at `/wordlists` inside the `ffuf` container. + +## Release configuration + +Release Please is configured in `.github/release/release-please-config.json` (Python release type). The version is tracked in `.github/release/.release-please-manifest.json`. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..3616360 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,105 @@ +# Development + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) + Docker Compose v2 +- [uv](https://docs.astral.sh/uv/) — Python package manager +- [Bun](https://bun.sh/) — JavaScript runtime & package manager +- Python 3.12+ + +## Quick start (Docker) + +```bash +# Copy and customise environment +cp .env.example .env + +# Start API + all scanners (development mode with hot-reload) +docker compose --profile dev up -d + +# Start API + all scanners (production build) +docker compose --profile prod up -d +``` + +| URL | Service | +|-----|---------| +| http://localhost:3000 | React UI | +| http://localhost:8000 | FastAPI (Swagger at `/docs`) | +| http://localhost:8090 | ZAP API | + +## Local API development (without Docker) + +```bash +# Install dependencies +uv sync --all-groups + +# Run database migrations +uv run alembic -c apps/alembic.ini upgrade head + +# Start the API +uv run uvicorn api.main:app --reload --host 0.0.0.0 --port 8000 +``` + +> **Note:** Scanner sidecars (ZAP, Nuclei, Nikto) must be running via Docker for scan features to work. + +## Local frontend development + +```bash +cd apps/web + +# Install dependencies +bun install + +# Start dev server (connects to API at VITE_API_URL) +VITE_API_URL=http://localhost:8000 bun run dev +``` + +## Running tests + +```bash +# All tests +uv run pytest + +# With coverage +uv run pytest --cov --cov-report=term-missing + +# Single file +uv run pytest apps/api/tests/test_health.py -v +``` + +## Code quality + +```bash +# Lint +uv run ruff check . + +# Format check +uv run ruff format --check . + +# Auto-fix +uv run ruff check --fix . && uv run ruff format . + +# Type check +uv run ty check + +# Frontend lint + format +cd apps/web && bun run lint && bun run format:check +``` + +Pre-commit hooks are configured via `.pre-commit-config.yaml`: + +```bash +pre-commit install +``` + +## Database migrations + +```bash +# Create a new migration +uv run alembic -c apps/alembic.ini revision --autogenerate -m "description" + +# Apply migrations +uv run alembic -c apps/alembic.ini upgrade head + +# Rollback one step +uv run alembic -c apps/alembic.ini downgrade -1 +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b1f4cca --- /dev/null +++ b/docs/index.md @@ -0,0 +1,25 @@ +--- +title: "Web Check" +description: "Docker-based security scanning toolkit for web applications. Orchestrates ZAP, Nuclei, Nikto and more behind a FastAPI + React UI." +repo: KevinDeBenedetti/web-check +order: 4 +--- + +# Web Check + +Docker-based security scanning toolkit for web applications. Orchestrates ZAP, Nuclei, Nikto and more behind a FastAPI + React UI. + +View on GitHub → + +## Source repository + +| | | +|---|---| +| Repository | [KevinDeBenedetti/web-check](https://github.com/KevinDeBenedetti/web-check) | +| Source docs | [`docs/`](https://github.com/KevinDeBenedetti/web-check/tree/main/docs) | + +## Documentation + +- [Architecture](./architecture.md) +- [Development](./development.md) +- [Configuration](./configuration.md) diff --git a/pyproject.toml b/pyproject.toml index c64c25f..2cc3722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,30 +8,35 @@ authors = [ readme = "README.md" requires-python = ">=3.12" dependencies = [ - "fastapi>=0.115.13", - "uvicorn[standard]>=0.34.0", + "fastapi>=0.135.3", + "uvicorn[standard]>=0.43.0", "pydantic>=2.12.5", - "pydantic-settings>=2.7.1", + "pydantic-settings>=2.13.1", "httpx>=0.28.1", "httpx-secure>=1.2.0", "structlog>=25.5.0", - "python-json-logger>=4.0.0", - "sqlalchemy>=2.0.28,<2.1", - "alembic>=1.18.0", + "python-json-logger>=4.1.0", + "sqlalchemy>=2.0.49,<2.1", + "alembic>=1.18.4", "aiosqlite>=0.20.0", "python-owasp-zap-v2.4>=0.0.22", - "sslyze>=6.0.0", - "sqlmap>=1.8.11", + "sslyze>=6.3.1", + "sqlmap>=1.10.3", + "typer>=0.12.0", + "rich>=13.7.0", ] +[project.scripts] +web-check = "cli.main:app" + [dependency-groups] dev = [ "pytest>=9.0.2", "pytest-asyncio>=1.3.0", - "pytest-cov>=6.0.0", - "ruff>=0.8.5", - "ty>=0.0.11", - "prek>=0.2.23", + "pytest-cov>=7.1.0", + "ruff>=0.15.9", + "ty>=0.0.28", + "prek>=0.3.8", ] [build-system] @@ -39,7 +44,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["api"] +packages = ["apps/api", "apps/cli"] [tool.ruff] line-length = 100 @@ -69,7 +74,7 @@ line-ending = "auto" python-version = "3.12" python = ".venv" python-platform = "all" -root = ["api"] +root = ["apps/api"] [tool.ty.rules] # Critical errors @@ -83,13 +88,13 @@ unused-ignore-comment = "warn" redundant-cast = "warn" [tool.ty.src] -include = ["api"] +include = ["apps/api"] exclude = ["**/__pycache__", "**/node_modules", ".venv", "outputs"] respect-ignore-files = true [tool.pytest.ini_options] asyncio_mode = "auto" -testpaths = ["api/tests"] +testpaths = ["apps/api/tests"] markers = [ "slow: marks tests as slow (deselected by default in CI)", "integration: marks tests as integration tests", diff --git a/uv.lock b/uv.lock index 16c635a..78a5b25 100644 --- a/uv.lock +++ b/uv.lock @@ -18,16 +18,16 @@ wheels = [ [[package]] name = "alembic" -version = "1.18.0" +version = "1.18.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/a5/57f989c26c078567a08f1d88c337acfcb69c8c9cac6876a34054f35b8112/alembic-1.18.0.tar.gz", hash = "sha256:0c4c03c927dc54d4c56821bdcc988652f4f63bf7b9017fd9d78d63f09fd22b48", size = 2043788, upload-time = "2026-01-09T21:22:23.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/fd/68773667babd452fb48f974c4c1f6e6852c6e41bcf622c745faca1b06605/alembic-1.18.0-py3-none-any.whl", hash = "sha256:3993fcfbc371aa80cdcf13f928b7da21b1c9f783c914f03c3c6375f58efd9250", size = 260967, upload-time = "2026-01-09T21:22:25.333Z" }, + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] [[package]] @@ -316,17 +316,18 @@ wheels = [ [[package]] name = "fastapi" -version = "0.128.0" +version = "0.135.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, ] [[package]] @@ -473,6 +474,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -536,6 +549,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "nassl" version = "5.4.0" @@ -581,26 +603,26 @@ wheels = [ [[package]] name = "prek" -version = "0.2.27" +version = "0.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/0b/2a0509d2d8881811e4505227df9ca31b3a4482497689b5c2b7f38faab1e5/prek-0.2.27.tar.gz", hash = "sha256:dfd2a1b040f55402c2449ae36ea28e8c1bb05ca900490d5c0996b1b72297cc0e", size = 283076, upload-time = "2026-01-07T14:23:17.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/ee/03e8180e3fda9de25b6480bd15cc2bde40d573868d50648b0e527b35562f/prek-0.3.8.tar.gz", hash = "sha256:434a214256516f187a3ab15f869d950243be66b94ad47987ee4281b69643a2d9", size = 400224, upload-time = "2026-03-23T08:23:35.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/03/01dd50c89aa38bc194bb14073468bcbd1fec1621150967b7d424d2f043a7/prek-0.2.27-py3-none-linux_armv6l.whl", hash = "sha256:3c7ce590289e4fc0119524d0f0f187133a883d6784279b6a3a4080f5851f1612", size = 4799872, upload-time = "2026-01-07T14:23:15.5Z" }, - { url = "https://files.pythonhosted.org/packages/51/86/807267659e4775c384e755274a214a45461266d6a1117ec059fbd245731b/prek-0.2.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:df35dee5dcf09a9613c8b9c6f3d79a3ec894eb13172f569773d529a5458887f8", size = 4903805, upload-time = "2026-01-07T14:23:35.199Z" }, - { url = "https://files.pythonhosted.org/packages/1b/5b/cc3c13ed43e7523f27a2f9b14d18c9b557fb1090e7a74689f934cb24d721/prek-0.2.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:772d84ebe19b70eba1da0f347d7d486b9b03c0a33fe19c2d1bf008e72faa13b3", size = 4629083, upload-time = "2026-01-07T14:23:12.204Z" }, - { url = "https://files.pythonhosted.org/packages/34/d9/86eafc1d7bddf9236263d4428acca76b7bfc7564ccc2dc5e539d1be22b5e/prek-0.2.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:571aab2e9c0eace30a51b0667533862f4bdc0a81334d342f6f516796a63fd1e4", size = 4825005, upload-time = "2026-01-07T14:23:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/44/cf/83004be0a9e8ac3c8c927afab5948d9e31760e15442a0fff273f158cae51/prek-0.2.27-py3-none-manylinux_2_24_armv7l.whl", hash = "sha256:cc7a47f40f36c503e77eb6209f7ad5979772f9c7c5e88ba95cf20f0d24ece926", size = 4724850, upload-time = "2026-01-07T14:23:18.276Z" }, - { url = "https://files.pythonhosted.org/packages/73/8c/5c754f4787fc07e7fa6d2c25ac90931cd3692b51f03c45259aca2ea6fd3f/prek-0.2.27-py3-none-manylinux_2_24_i686.whl", hash = "sha256:cd87b034e56f610f9cafd3b7d554dca69f1269a511ad330544d696f08c656eb3", size = 5042584, upload-time = "2026-01-07T14:23:37.892Z" }, - { url = "https://files.pythonhosted.org/packages/4d/80/762283280ae3d2aa35385ed2db76c39518ed789fbaa0b6fb52352764d41c/prek-0.2.27-py3-none-manylinux_2_24_s390x.whl", hash = "sha256:638b4e942dd1cea6fc0ddf4ce5b877e5aa97c6c142b7bf28e9ce6db8f0d06a4a", size = 5511089, upload-time = "2026-01-07T14:23:23.121Z" }, - { url = "https://files.pythonhosted.org/packages/e0/78/1b53b604c188f4054346b237ec1652489718fedc0d465baadecf7907dc42/prek-0.2.27-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:769b13d7bd11fbb4a5fc5fffd2158aea728518ec9aca7b36723b10ad8b189810", size = 5100175, upload-time = "2026-01-07T14:23:19.643Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/a9dc29598e664e6e663da316338e1e980e885072107876a3ca8d697f4d65/prek-0.2.27-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6c0bc38806caf14d47d44980d936ee0cb153bccea703fb141c16bb9be49fb778", size = 4833004, upload-time = "2026-01-07T14:23:36.467Z" }, - { url = "https://files.pythonhosted.org/packages/04/b7/56ca9226f20375519d84a2728a985cc491536f0b872f10cb62bcc55ccea0/prek-0.2.27-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:77c8ac95a0bb1156159edcb3c52b5f852910a7d2ed53d6136ecc1d9d6dc39fe1", size = 4842559, upload-time = "2026-01-07T14:23:31.691Z" }, - { url = "https://files.pythonhosted.org/packages/87/20/71ef2c558daabbe2a4cfe6567597f7942dbbad1a3caca0d786b4ec1304cb/prek-0.2.27-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:5e8d56b386660266c2a31e12af8b52a0901fe21fb71ab05768fdd41b405794ac", size = 4709053, upload-time = "2026-01-07T14:23:26.602Z" }, - { url = "https://files.pythonhosted.org/packages/e8/14/7376117d0e91e35ce0f6581d4427280f634b9564c86615f74b79f242fa79/prek-0.2.27-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3fdeaa1b9f97e21d870ba091914bc7ccf85106a9ef74d81f362a92cdbfe33569", size = 4927803, upload-time = "2026-01-07T14:23:30Z" }, - { url = "https://files.pythonhosted.org/packages/fb/81/87f36898ec2ac1439468b20e9e7061b4956ce0cf518c7cc15ac0457f2971/prek-0.2.27-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:20dd04fe33b9fcfbc2069f4e523ec8d9b4813c1ca4ac9784fe2154dcab42dacb", size = 5210701, upload-time = "2026-01-07T14:23:24.87Z" }, - { url = "https://files.pythonhosted.org/packages/50/5a/53f7828543c09cb70ed35291818ec145a42ef04246fa4f82c128b26abd4f/prek-0.2.27-py3-none-win32.whl", hash = "sha256:15948cacbbccd935f57ca164b36c4c5d7b03c58cd5a335a6113cdbd149b6e50d", size = 4623511, upload-time = "2026-01-07T14:23:33.472Z" }, - { url = "https://files.pythonhosted.org/packages/73/21/3a079075a4d4db58f909eedfd7a79517ba90bb12f7b61f6e84c3c29d4d61/prek-0.2.27-py3-none-win_amd64.whl", hash = "sha256:8225dc8523e7a0e95767b3d3e8cfb3bc160fe6af0ee5115fc16c68428c4e0779", size = 5312713, upload-time = "2026-01-07T14:23:21.116Z" }, - { url = "https://files.pythonhosted.org/packages/39/79/d1c3d96ed4f7dff37ed11101d8336131e8108315c3078246007534dcdd27/prek-0.2.27-py3-none-win_arm64.whl", hash = "sha256:f9192bfb6710db2be10f0e28ff31706a2648c1eb8a450b20b2f55f70ba05e769", size = 4978272, upload-time = "2026-01-07T14:23:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/00/84/40d2ddf362d12c4cd4a25a8c89a862edf87cdfbf1422aa41aac8e315d409/prek-0.3.8-py3-none-linux_armv6l.whl", hash = "sha256:6fb646ada60658fa6dd7771b2e0fb097f005151be222f869dada3eb26d79ed33", size = 5226646, upload-time = "2026-03-23T08:23:18.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/52/7308a033fa43b7e8e188797bd2b3b017c0f0adda70fa7af575b1f43ea888/prek-0.3.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3d7fdadb15efc19c09953c7a33cf2061a70f367d1e1957358d3ad5cc49d0616", size = 5620104, upload-time = "2026-03-23T08:23:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b1/f106ac000a91511a9cd80169868daf2f5b693480ef5232cec5517a38a512/prek-0.3.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:72728c3295e79ca443f8c1ec037d2a5b914ec73a358f69cf1bc1964511876bf8", size = 5199867, upload-time = "2026-03-23T08:23:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e9/970713f4b019f69de9844e1bab37b8ddb67558e410916f4eb5869a696165/prek-0.3.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:48efc28f2f53b5b8087efca9daaed91572d62df97d5f24a1c7a087fecb5017de", size = 5441801, upload-time = "2026-03-23T08:23:32.617Z" }, + { url = "https://files.pythonhosted.org/packages/12/a4/7ef44032b181753e19452ec3b09abb3a32607cf6b0a0508f0604becaaf2b/prek-0.3.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6ca9d63bacbc448a5c18e955c78d3ac5176c3a17c3baacdd949b1a623e08a36", size = 5155107, upload-time = "2026-03-23T08:23:31.021Z" }, + { url = "https://files.pythonhosted.org/packages/bd/77/4d9c8985dbba84149760785dfe07093ea1e29d710257dfb7c89615e2234c/prek-0.3.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1000f7029696b4fe712fb1fefd4c55b9c4de72b65509c8e50296370a06f9dc3f", size = 5566541, upload-time = "2026-03-23T08:23:45.694Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1a/81e6769ac1f7f8346d09ce2ab0b47cf06466acd9ff72e87e5d1f0d98cd32/prek-0.3.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ff0bed0e2c1286522987d982168a86cbbd0d069d840506a46c9fda983515517", size = 6552991, upload-time = "2026-03-23T08:23:21.958Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fa/ce2df0dd2dc75a9437a52463239d0782998943d7b04e191fb89b83016c34/prek-0.3.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb087ac0ffda3ac65bbbae9a38326a7fd27ee007bb4a94323ce1eb539d8bbec", size = 5832972, upload-time = "2026-03-23T08:23:20.258Z" }, + { url = "https://files.pythonhosted.org/packages/18/6b/9d4269df9073216d296244595a21c253b6475dfc9076c0bd2906be7a436c/prek-0.3.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2e1e5e206ff7b31bd079cce525daddc96cd6bc544d20dc128921ad92f7a4c85d", size = 5448371, upload-time = "2026-03-23T08:23:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/60/1d/1e4d8a78abefa5b9d086e5a9f1638a74b5e540eec8a648d9946707701f29/prek-0.3.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dcea3fe23832a4481bccb7c45f55650cb233be7c805602e788bb7dba60f2d861", size = 5270546, upload-time = "2026-03-23T08:23:24.231Z" }, + { url = "https://files.pythonhosted.org/packages/77/07/34f36551a6319ae36e272bea63a42f59d41d2d47ab0d5fb00eb7b4e88e87/prek-0.3.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:4d25e647e9682f6818ab5c31e7a4b842993c14782a6ffcd128d22b784e0d677f", size = 5124032, upload-time = "2026-03-23T08:23:26.368Z" }, + { url = "https://files.pythonhosted.org/packages/e3/01/6d544009bb655e709993411796af77339f439526db4f3b3509c583ad8eb9/prek-0.3.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:de528b82935e33074815acff3c7c86026754d1212136295bc88fe9c43b4231d5", size = 5432245, upload-time = "2026-03-23T08:23:47.877Z" }, + { url = "https://files.pythonhosted.org/packages/54/96/1237ee269e9bfa283ffadbcba1f401f48a47aed2b2563eb1002740d6079d/prek-0.3.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6d660f1c25a126e6d9f682fe61449441226514f412a4469f5d71f8f8cad56db2", size = 5950550, upload-time = "2026-03-23T08:23:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6b/a574411459049bc691047c9912f375deda10c44a707b6ce98df2b658f0b3/prek-0.3.8-py3-none-win32.whl", hash = "sha256:b0c291c577615d9f8450421dff0b32bfd77a6b0d223ee4115a1f820cb636fdf1", size = 4949501, upload-time = "2026-03-23T08:23:16.338Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b4/46b59fe49f635acd9f6530778ce577f9d8b49452835726a5311ffc902c67/prek-0.3.8-py3-none-win_amd64.whl", hash = "sha256:bc147fdbdd4ec33fc7a987b893ecb69b1413ac100d95c9889a70f3fd58c73d06", size = 5346551, upload-time = "2026-03-23T08:23:34.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/05/9cca1708bb8c65264124eb4b04251e0f65ce5bfc707080bb6b492d5a0df7/prek-0.3.8-py3-none-win_arm64.whl", hash = "sha256:a2614647aeafa817a5802ccb9561e92eedc20dcf840639a1b00826e2c2442515", size = 5190872, upload-time = "2026-03-23T08:23:29.463Z" }, ] [[package]] @@ -700,16 +722,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -752,16 +774,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -775,11 +797,11 @@ wheels = [ [[package]] name = "python-json-logger" -version = "4.0.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, ] [[package]] @@ -855,30 +877,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + [[package]] name = "ruff" -version = "0.14.11" +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, - { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, - { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, - { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, - { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, - { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, - { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, - { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, - { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, - { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] @@ -892,48 +935,59 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.49" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, ] [[package]] name = "sqlmap" -version = "1.10" +version = "1.10.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/f2/2c4bc0919edd7ef60f507a998a19c25c7c14a7a36912c15e7461949fc5f0/sqlmap-1.10.tar.gz", hash = "sha256:79dde3d2050abe9cb902d508083e47f852c26192f33dea62075bc5e9510bec4d", size = 7224585, upload-time = "2026-01-02T00:29:45.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/fa/2490937c75ef0bd2406ab4202739cd338db7f9c4303fb1433fce7c73d450/sqlmap-1.10.3.tar.gz", hash = "sha256:3d166f0e2772bf08c9ee85d4f4cbba78e6c79b4f305828e31f1e813e33ff0e58", size = 7227808, upload-time = "2026-03-10T13:43:04.772Z" } [[package]] name = "sslyze" -version = "6.3.0" +version = "6.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -941,7 +995,7 @@ dependencies = [ { name = "pydantic" }, { name = "tls-parser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/92/ab3479082efc19ab8df3df1773b27eb01faf69a35f825c66c48aa83d4b08/sslyze-6.3.0.tar.gz", hash = "sha256:ce3ac4231de96e4ac02f1326fda856817dc39e6eac5d81efa2c4d3348da5ddb7", size = 1037097, upload-time = "2025-12-30T10:34:02.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/07/8f5c92c149bfe9cb31619eb46d8ab055be670c6c16c740122afd6b496150/sslyze-6.3.1.tar.gz", hash = "sha256:2cd4062f82a3fa605dbd34e3b71f99961e4e7cf438879da1c52f619855ffbc1d", size = 1065768, upload-time = "2026-03-29T09:58:58.034Z" } [[package]] name = "starlette" @@ -976,27 +1030,41 @@ wheels = [ [[package]] name = "ty" -version = "0.0.11" +version = "0.0.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/45/5ae578480168d4b3c08cf8e5eac3caf8eb7acdb1a06a9bed7519564bd9b4/ty-0.0.11.tar.gz", hash = "sha256:ebcbc7d646847cb6610de1da4ffc849d8b800e29fd1e9ebb81ba8f3fbac88c25", size = 4920340, upload-time = "2026-01-09T21:06:01.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/c2/a60543fb172ac7adaa3ae43b8db1d0dcd70aa67df254b70bf42f852a24f6/ty-0.0.28.tar.gz", hash = "sha256:1fbde7bc5d154d6f047b570d95665954fa83b75a0dce50d88cf081b40a27ea32", size = 5447781, upload-time = "2026-04-02T21:34:33.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/15/c2aa3d4633e6153a2e300d7dd0ebdedf904a60241d1922566f31c5f7f211/ty-0.0.28-py3-none-linux_armv6l.whl", hash = "sha256:6dbfb27524195ab1715163d7be065cc45037509fe529d9763aff6732c919f0d8", size = 10556282, upload-time = "2026-04-02T21:35:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/f6183838df89e9692235a71a69a9d4e0f12481bbdf1883f47010075793b0/ty-0.0.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c72a899ba94f7438bd07e897a84b36526b385aaf01d6f3eb6504e869232b3a6", size = 10425770, upload-time = "2026-04-02T21:34:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/68/82/e9208383412f8a320537ef4c44a768d2cb6c1330d9ab33087f0b932ccd1b/ty-0.0.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eef67f9cdfd31677bde801b611741dde779271ec6f471f818c7c6eccf515237f", size = 9899999, upload-time = "2026-04-02T21:34:40.297Z" }, + { url = "https://files.pythonhosted.org/packages/4d/26/0442f49589ba393fbd3b50751f8bb82137b036bc509762884f7b21c511d1/ty-0.0.28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70e7b98a91d8245641be1e4b55af8bc9b1ae82ec189794d35e14e546f1e15e66", size = 10400725, upload-time = "2026-04-02T21:34:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/57/d9/64128f1a7ceba72e49f35dd562533f44d4c56d0cf62efb21692377819dbc/ty-0.0.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd83d4ad9f99078b830aabb47792fac6dc39368bb0f72f3cc14607173ed6e25", size = 10387410, upload-time = "2026-04-02T21:34:46.889Z" }, + { url = "https://files.pythonhosted.org/packages/cc/52/498b6bdd1d0a985fd14ce83c31186f3b838ad79efdf68ce928f441a6962b/ty-0.0.28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0172984fc2fcd3e47ccd5da69f36f632cddc410f9a093144a05ad07d67cf06ed", size = 10880982, upload-time = "2026-04-02T21:34:53.687Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c8/fefd616f38a250b28f62ba73728cb6061715f03df0a610dce558a0fdfc0a/ty-0.0.28-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0bbf47d2bea82a09cab2ca4f48922d6c16a36608447acdc64163cd19beb28d3", size = 11459056, upload-time = "2026-04-02T21:34:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/16/15/9e18d763a5ef9c6a69396876586589fd5e0fd0acba35fae8a9a169680f48/ty-0.0.28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1774c9a0fb071607e3bdfa0ce8365488ac46809fc04ad1706562a8709a023247", size = 11156341, upload-time = "2026-04-02T21:35:01.824Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/8ac0281fc44c3297f0e58699ebf993c13621e32a0fab1025439d3ea8a2f1/ty-0.0.28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2849d6d212af78175430e8cc51a962a53851458182eb44a981b0e3981163177", size = 11006089, upload-time = "2026-04-02T21:34:38.111Z" }, + { url = "https://files.pythonhosted.org/packages/dd/de/5b5fdbe3bdb5c6f4918b33f1c55cd975b3d606057089a822439d5151bf93/ty-0.0.28-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3c576c15b867b3913c4a1d9be30ade4682303e24a576d2cc99bfd8f25ae838e9", size = 10367739, upload-time = "2026-04-02T21:34:57.679Z" }, + { url = "https://files.pythonhosted.org/packages/80/82/abdfb27ab988e6bd09502a4573f64a7e72db3e83acd7886af54448703c97/ty-0.0.28-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e5f13d10b3436bee3ea35851e5af400123f6693bfae48294ddfbbf553fa51ef", size = 10399528, upload-time = "2026-04-02T21:34:51.398Z" }, + { url = "https://files.pythonhosted.org/packages/ba/74/3ccbe468e8480ba53f83a1e52481d3e11756415f0ca1297fb2da65e29612/ty-0.0.28-py3-none-musllinux_1_2_i686.whl", hash = "sha256:759db467e399faedc7d5f1ca4b383dd8ecc71d7d79b2ca6ea6db4ac8e643378a", size = 10586771, upload-time = "2026-04-02T21:34:35.912Z" }, + { url = "https://files.pythonhosted.org/packages/ee/79/545c76dcef0c3f89fb733ec46118aed2a700e79d4e22cb142e3b5a80286c/ty-0.0.28-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0cd44e3c857951cbf3f8647722ca87475614fac8ac0371eb1f200a942315a2c2", size = 11110550, upload-time = "2026-04-02T21:34:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e4/e3c6f71c95a2cbabd7d88fd698b00b8af48e39aa10e0b10b839410fc3c6d/ty-0.0.28-py3-none-win32.whl", hash = "sha256:88e2c784ec5e0e2fb01b137d92fd595cdc27b98a553f4bb34b8bf138bac1be1e", size = 9985411, upload-time = "2026-04-02T21:34:44.763Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/79dbab4856d3d15e5173314ff1846be65d58b31de6efe62ef1c25c663b32/ty-0.0.28-py3-none-win_amd64.whl", hash = "sha256:faaffbef127cb67560ad6dbc6a8f8845a4033b818bcc78ad7af923e02df199db", size = 10986548, upload-time = "2026-04-02T21:34:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/01/b2/cc987aaf5babacc55caf0aeb751c83401e86e05e22ce82dace5a7e7e5354/ty-0.0.28-py3-none-win_arm64.whl", hash = "sha256:34a18ea09ee09612fb6555deccf1eed810e6f770b61a41243b494bcb7f624a1c", size = 10388573, upload-time = "2026-04-02T21:34:29.219Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/34/b1d05cdcd01589a8d2e63011e0a1e24dcefdc2a09d024fee3e27755963f6/ty-0.0.11-py3-none-linux_armv6l.whl", hash = "sha256:68f0b8d07b0a2ea7ec63a08ba2624f853e4f9fa1a06fce47fb453fa279dead5a", size = 9521748, upload-time = "2026-01-09T21:06:13.221Z" }, - { url = "https://files.pythonhosted.org/packages/43/21/f52d93f4b3784b91bfbcabd01b84dc82128f3a9de178536bcf82968f3367/ty-0.0.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbf82d7ef0618e9ae3cc3c37c33abcfa302c9b3e3b8ff11d71076f98481cb1a8", size = 9454903, upload-time = "2026-01-09T21:06:42.363Z" }, - { url = "https://files.pythonhosted.org/packages/ad/01/3a563dba8b1255e474c35e1c3810b7589e81ae8c41df401b6a37c8e2cde9/ty-0.0.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:121987c906e02264c3b511b95cb9f8a3cdd66f3283b8bbab678ca3525652e304", size = 8823417, upload-time = "2026-01-09T21:06:26.315Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b1/99b87222c05d3a28fb7bbfb85df4efdde8cb6764a24c1b138f3a615283dd/ty-0.0.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:999390b6cc045fe5e1b3da1c2c9ae8e8c0def23b69455e7c9191ba9ffd747023", size = 9290785, upload-time = "2026-01-09T21:05:59.028Z" }, - { url = "https://files.pythonhosted.org/packages/3d/9f/598809a8fff2194f907ba6de07ac3d7b7788342592d8f8b98b1b50c2fb49/ty-0.0.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed504d78eb613c49be3c848f236b345b6c13dc6bcfc4b202790a60a97e1d8f35", size = 9359392, upload-time = "2026-01-09T21:06:37.459Z" }, - { url = "https://files.pythonhosted.org/packages/71/3e/aeea2a97b38f3dcd9f8224bf83609848efa4bc2f484085508165567daa7b/ty-0.0.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fedc8b43cc8a9991e0034dd205f957a8380dd29bfce36f2a35b5d321636dfd9", size = 9852973, upload-time = "2026-01-09T21:06:21.245Z" }, - { url = "https://files.pythonhosted.org/packages/72/40/86173116995e38f954811a86339ac4c00a2d8058cc245d3e4903bc4a132c/ty-0.0.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0808bdfb7efe09881bf70249b85b0498fb8b75fbb036ce251c496c20adb10075", size = 10796113, upload-time = "2026-01-09T21:06:16.034Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/97c92c401dacae9baa3696163ebe8371635ebf34ba9fda781110d0124857/ty-0.0.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07185b3e38b18c562056dfbc35fb51d866f872977ea1ebcd64ca24a001b5b4f1", size = 10432137, upload-time = "2026-01-09T21:06:07.498Z" }, - { url = "https://files.pythonhosted.org/packages/18/10/9ab43f3cfc5f7792f6bc97620f54d0a0a81ef700be84ea7f6be330936a99/ty-0.0.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5c72f1ada8eb5be984502a600f71d1a3099e12fb6f3c0607aaba2f86f0e9d80", size = 10240520, upload-time = "2026-01-09T21:06:34.823Z" }, - { url = "https://files.pythonhosted.org/packages/74/18/8dd4fe6df1fd66f3e83b4798eddb1d8482d9d9b105f25099b76703402ebb/ty-0.0.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25f88e8789072830348cb59b761d5ced70642ed5600673b4bf6a849af71eca8b", size = 9973340, upload-time = "2026-01-09T21:06:39.657Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0b/fb2301450cf8f2d7164944d6e1e659cac9ec7021556cc173d54947cf8ef4/ty-0.0.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f370e1047a62dcedcd06e2b27e1f0b16c7f8ea2361d9070fcbf0d0d69baaa192", size = 9262101, upload-time = "2026-01-09T21:06:28.989Z" }, - { url = "https://files.pythonhosted.org/packages/f7/8c/d6374af023541072dee1c8bcfe8242669363a670b7619e6fffcc7415a995/ty-0.0.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:52be34047ed6177bfcef9247459a767ec03d775714855e262bca1fb015895e8a", size = 9382756, upload-time = "2026-01-09T21:06:24.097Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/edd1e63ffa8d49d720c475c2c1c779084e5efe50493afdc261938705d10a/ty-0.0.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9e5762ccb3778779378020b8d78f936b3f52ea83f18785319cceba3ae85d8e6", size = 9553944, upload-time = "2026-01-09T21:06:18.426Z" }, - { url = "https://files.pythonhosted.org/packages/35/cd/4afdb0d182d23d07ff287740c4954cc6dde5c3aed150ec3f2a1d72b00f71/ty-0.0.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9334646ee3095e778e3dbc45fdb2bddfc16acc7804283830ad84991ece16dd7", size = 10060365, upload-time = "2026-01-09T21:06:45.083Z" }, - { url = "https://files.pythonhosted.org/packages/d1/94/a009ad9d8b359933cfea8721c689c0331189be28650d74dcc6add4d5bb09/ty-0.0.11-py3-none-win32.whl", hash = "sha256:44cfb7bb2d6784bd7ffe7b5d9ea90851d9c4723729c50b5f0732d4b9a2013cfc", size = 9040448, upload-time = "2026-01-09T21:06:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/df/04/5a5dfd0aec0ea99ead1e824ee6e347fb623c464da7886aa1e3660fb0f36c/ty-0.0.11-py3-none-win_amd64.whl", hash = "sha256:1bb205db92715d4a13343bfd5b0c59ce8c0ca0daa34fb220ec9120fc66ccbda7", size = 9780112, upload-time = "2026-01-09T21:06:04.69Z" }, - { url = "https://files.pythonhosted.org/packages/ad/07/47d4fccd7bcf5eea1c634d518d6cb233f535a85d0b63fcd66815759e2fa0/ty-0.0.11-py3-none-win_arm64.whl", hash = "sha256:4688bd87b2dc5c85da277bda78daba14af2e66f3dda4d98f3604e3de75519eba", size = 9194038, upload-time = "2026-01-09T21:06:10.152Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] [[package]] @@ -1031,15 +1099,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/f2/368268300fb8af33743508d738ef7bb4d56afdb46c6d9c0fa3dd515df171/uvicorn-0.43.0.tar.gz", hash = "sha256:ab1652d2fb23abf124f36ccc399828558880def222c3cb3d98d24021520dc6e8", size = 85686, upload-time = "2026-04-03T18:37:48.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/55/df/0cf5b0c451602748fdc7a702d4667f6e209bf96aa6e3160d754234445f2a/uvicorn-0.43.0-py3-none-any.whl", hash = "sha256:46fac64f487fd968cd999e5e49efbbe64bd231b5bd8b4a0b482a23ebce499620", size = 68591, upload-time = "2026-04-03T18:37:47.64Z" }, ] [package.optional-dependencies] @@ -1169,10 +1237,12 @@ dependencies = [ { name = "pydantic-settings" }, { name = "python-json-logger" }, { name = "python-owasp-zap-v2-4" }, + { name = "rich" }, { name = "sqlalchemy" }, { name = "sqlmap" }, { name = "sslyze" }, { name = "structlog" }, + { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1189,29 +1259,31 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.20.0" }, - { name = "alembic", specifier = ">=1.18.0" }, - { name = "fastapi", specifier = ">=0.115.13" }, + { name = "alembic", specifier = ">=1.18.4" }, + { name = "fastapi", specifier = ">=0.135.3" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "httpx-secure", specifier = ">=1.2.0" }, { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pydantic-settings", specifier = ">=2.7.1" }, - { name = "python-json-logger", specifier = ">=4.0.0" }, + { name = "pydantic-settings", specifier = ">=2.13.1" }, + { name = "python-json-logger", specifier = ">=4.1.0" }, { name = "python-owasp-zap-v2-4", specifier = ">=0.0.22" }, - { name = "sqlalchemy", specifier = ">=2.0.28,<2.1" }, - { name = "sqlmap", specifier = ">=1.8.11" }, - { name = "sslyze", specifier = ">=6.0.0" }, + { name = "rich", specifier = ">=13.7.0" }, + { name = "sqlalchemy", specifier = ">=2.0.49,<2.1" }, + { name = "sqlmap", specifier = ">=1.10.3" }, + { name = "sslyze", specifier = ">=6.3.1" }, { name = "structlog", specifier = ">=25.5.0" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, + { name = "typer", specifier = ">=0.12.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.43.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "prek", specifier = ">=0.2.23" }, + { name = "prek", specifier = ">=0.3.8" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "ruff", specifier = ">=0.8.5" }, - { name = "ty", specifier = ">=0.0.11" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.9" }, + { name = "ty", specifier = ">=0.0.28" }, ] [[package]] diff --git a/web/.dockerignore b/web/.dockerignore deleted file mode 100644 index af3f68a..0000000 --- a/web/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -dist -.env -*.log -.DS_Store diff --git a/web/.env.example b/web/.env.example deleted file mode 100644 index e3b915c..0000000 --- a/web/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -# Variables d'environnement pour le client web - -# API Backend URL -# - En développement local (bun dev): utilise http://localhost:8000 via le proxy Vite -# - En développement Docker (docker-compose up): utilise http://api:8000 via le réseau Docker -# - En production: configuré dans nginx.conf -VITE_API_URL=http://localhost:8000 diff --git a/web/.gitignore b/web/.gitignore deleted file mode 100644 index 7efecad..0000000 --- a/web/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules -dist -dist-ssr -*.local -.env -.env.local -.env.production -.DS_Store -package-lock.json -yarn.lock -pnpm-lock.yaml diff --git a/web/.oxfmtrc.json b/web/.oxfmtrc.json deleted file mode 100644 index e7702f8..0000000 --- a/web/.oxfmtrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/configuration_schema.json", - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf" -} diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json deleted file mode 100644 index 64dea1e..0000000 --- a/web/.oxlintrc.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://oxc.rs/schema.json", - "plugins": ["react", "typescript"], - "env": { - "browser": true, - "es2024": true - }, - "rules": { - "eqeqeq": "error", - "no-console": ["warn", { "allow": ["warn", "error"] }], - "no-debugger": "error", - "no-var": "error", - "prefer-const": "error", - "react/jsx-no-target-blank": "error", - "react/jsx-key": "error", - "react/jsx-no-useless-fragment": "error", - "react/self-closing-comp": ["error", { "html": false }], - "react/no-children-prop": "error", - "react/no-danger-with-children": "error", - "react/no-deprecated": "warn", - "react/no-find-dom-node": "error", - "react/no-is-mounted": "error", - "react/no-render-return-value": "error", - "react/no-string-refs": "error", - "react/no-unescaped-entities": "error", - "react/no-unknown-property": "error", - "react/require-render-return": "error", - "react/void-dom-elements-no-children": "error" - } -} diff --git a/web/Dockerfile b/web/Dockerfile deleted file mode 100644 index 962497f..0000000 --- a/web/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Dockerfile pour le client web React -FROM oven/bun:1.1-alpine AS builder - -WORKDIR /app - -# Copier les fichiers de dépendances -COPY package.json bun.lock* ./ -RUN bun install - -# Copier le code source -COPY . . - -# Build de production -RUN bun run build - -# Serveur de production avec Nginx -FROM nginx:alpine - -# Copier les fichiers buildés -COPY --from=builder /app/dist /usr/share/nginx/html - -# Configuration Nginx pour React Router et proxy API -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/web/bun.lock b/web/bun.lock deleted file mode 100644 index b1ae227..0000000 --- a/web/bun.lock +++ /dev/null @@ -1,564 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "web-check-ui", - "dependencies": { - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tooltip": "^1.1.8", - "@tanstack/react-query": "^5.17.19", - "axios": "^1.6.5", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^3.0.6", - "lucide-react": "^0.562.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwind-merge": "^3.4.0", - }, - "devDependencies": { - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.17", - "oxfmt": "^0.23.0", - "oxlint": "^0.15.5", - "postcss": "^8.4.33", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3", - "vite": "^5.0.12", - }, - }, - }, - "packages": { - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - - "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], - - "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], - - "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], - - "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - - "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - - "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], - - "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], - - "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], - - "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], - - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.23.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-shGng2EjBspvuqtFtcjcKf0WoZ9QCdL8iLYgdOoKSiSQ9pPyLJ4jQf62yhm4b2PpZNVcV/20gV6d8SyKzg6SZQ=="], - - "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.23.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DxQ7Hm7B+6JiIkiRU3CSJmM15nTJDDezyaAv+x9NN8BfU0C49O8JuZIFu1Lr9AKEPV+ECIYM2X4HU0xm6IdiMQ=="], - - "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7qTXPpENi45sEKsaYFit4VRywPVkX+ZJc5JVA17KW1coJ/SLUuRAdLjRipU+QTZsr1TF93HCmGFSlUjB7lmEVQ=="], - - "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qkFXbf+K01B++j69o9mLvvyfhmmL4+qX7hGPA2PRDkE5xxuUTWdqboQQc1FgGI0teUlIYYyxjamq9UztL2A7NA=="], - - "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-J7Q13Ujyn8IgjHD96urA377GOy8HerxC13OrEyYaM8iwH3gc/EoboK9AKu0bxp9qai4btPFDhnkRnpCwJE9pAw=="], - - "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3gb25Zk2/y4An8fi399KdpLkDYFTJEB5Nq/sSHmeXG0pZlR/jnKoXEFHsjU+9nqF2wsuZ+tmkoi/swcaGG8+Qg=="], - - "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.23.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-JKfRP2ENWwRZ73rMZFyChvRi/+oDEW+3obp1XIwecot8gvDHgGZ4nX3hTp4VPiBFL89JORMpWSKzJvjRDucJIw=="], - - "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.23.0", "", { "os": "win32", "cpu": "x64" }, "sha512-vgqtYK1X1n/KexCNQKWXao3hyOnmWuCzk2sQyCSpkLhjSNIDPm7dmnEkvOXhf1t0O5RjCwHpk2VB6Fuaq3GULg=="], - - "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.15.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7GOyGM6D36lUhsOvavAVpF72SycPVG0Enunx0bzv8g0+9TklzOSFN3FJlZjLst14VPdZWujZMLgkQC7tOp+Rwg=="], - - "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.15.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-pbrnYFwMn/fuX0z3IeQ05Nvo/b1zGxjmmWgkrQSDwYHxBxP6NT41hk1pmqkcA+v53xk9wvOa/6vBBI/U30F8Ow=="], - - "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.15.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-QWjG3YVsDlIvDTBUPmtPiyqP34ZQpFJqQh2JO94pBih11lFxQ0IGVMEXDhmW3WdiSFPZSJsZGzWynalM9eg+RA=="], - - "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.15.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-4W0YsmMSbNzzExOWhk+6zNfmJEmKFqSjFIn8CKLtYFvH8kF6KjoW4/0HNsDNYW5Fz+KOut/2JgkvxAiKH+r0zA=="], - - "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.15.15", "", { "os": "linux", "cpu": "x64" }, "sha512-agP3e+eQ6tE5tqN6VI4Uukx2yvjwYFjtrDMcB19J7PmGOaFRwuMuT0sNWK/9guvhuS9aCINNZTi3kEhMy9Qgng=="], - - "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.15.15", "", { "os": "linux", "cpu": "x64" }, "sha512-L2qE9NhhUafsJOO4pofLx/0hW5IB0sfJa6bS85q0j+ySaI0f3CxMaAadrZLFSuqHWB3oF18B5yvzaPWsc2ohbQ=="], - - "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.15.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-B7f4VAS/E78n8zy6XZlNeyYOtWTel4BJn/22Ap2yEAlNzO34ot8dGfpLk6MqTUWJrRnARwVBVmc3wRVrsOT5yg=="], - - "@oxlint/win32-x64": ["@oxlint/win32-x64@0.15.15", "", { "os": "win32", "cpu": "x64" }, "sha512-ZM9T3/OpaQ3qvrk/VuHO2EQmhNH4cOZdr/b/Ju9VKwBr+ahhqMn3W5srrplWQWxfsb0yd1yBj7iD0jdAps2iLg=="], - - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], - - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], - - "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], - - "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], - - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], - - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], - - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], - - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], - - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], - - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], - - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], - - "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], - - "@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="], - - "@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], - - "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], - - "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], - - "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], - - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], - - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], - - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], - - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - - "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - - "oxfmt": ["oxfmt@0.23.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.23.0", "@oxfmt/darwin-x64": "0.23.0", "@oxfmt/linux-arm64-gnu": "0.23.0", "@oxfmt/linux-arm64-musl": "0.23.0", "@oxfmt/linux-x64-gnu": "0.23.0", "@oxfmt/linux-x64-musl": "0.23.0", "@oxfmt/win32-arm64": "0.23.0", "@oxfmt/win32-x64": "0.23.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-dh4rlNBua93aVf2ZaDecbQxVLMnUUTvDi1K1fdvBdontQeEf6K22Z1KQg5QKl2D9aNFeFph+wOVwcjjYUIO6Mw=="], - - "oxlint": ["oxlint@0.15.15", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.15.15", "@oxlint/darwin-x64": "0.15.15", "@oxlint/linux-arm64-gnu": "0.15.15", "@oxlint/linux-arm64-musl": "0.15.15", "@oxlint/linux-x64-gnu": "0.15.15", "@oxlint/linux-x64-musl": "0.15.15", "@oxlint/win32-arm64": "0.15.15", "@oxlint/win32-x64": "0.15.15" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-oQNc1mAHrrbKiXyKJMGs9VCZfwGfLy7YiQKa4qupi71X/u4xyWqOh36YKXqWOXnmm2y7vfWFpGZlhJPAa9tMqA=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], - - "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], - - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], - - "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], - - "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], - - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - - "tailwindcss": ["tailwindcss@3.4.19", "", { "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.2", "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.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], - - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], - - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - } -} diff --git a/web/components.json b/web/components.json deleted file mode 100644 index 6d3998d..0000000 --- a/web/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/index.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 47f6d78..0000000 --- a/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Web-Check Security Scanner - - -
- - - diff --git a/web/nginx.conf b/web/nginx.conf deleted file mode 100644 index 0a7d08f..0000000 --- a/web/nginx.conf +++ /dev/null @@ -1,35 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Compression - gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - # React Router - location / { - try_files $uri $uri/ /index.html; - } - - # Proxy API vers le backend - location /api { - proxy_pass http://api:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Timeouts pour les scans longs - proxy_read_timeout 1800s; - proxy_connect_timeout 600s; - proxy_send_timeout 600s; - } - - # Cache des assets statiques - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } -} diff --git a/web/package.json b/web/package.json deleted file mode 100644 index dbccf19..0000000 --- a/web/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "web-check-ui", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "oxlint .", - "lint:fix": "oxlint --fix .", - "format": "oxfmt .", - "format:check": "oxfmt --check .", - "check": "bun run format:check && bun run lint && tsc --noEmit" - }, - "dependencies": { - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tooltip": "^1.1.8", - "@tanstack/react-query": "^5.17.19", - "axios": "^1.6.5", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^3.0.6", - "lucide-react": "^0.562.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwind-merge": "^3.4.0" - }, - "devDependencies": { - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.17", - "oxfmt": "^0.23.0", - "oxlint": "^0.15.5", - "postcss": "^8.4.33", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3", - "vite": "^5.0.12" - } -} diff --git a/web/postcss.config.js b/web/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/web/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/web/src/App.tsx b/web/src/App.tsx deleted file mode 100644 index 2ea5205..0000000 --- a/web/src/App.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Shield, Loader2 } from "lucide-react"; -import { scans } from "./services/api"; -import { ScanForm } from "./components/ScanForm"; -import { ScanResult } from "./components/ScanResult"; -import { ScanStats } from "./components/ScanStats"; -import { ScanLogStream } from "./components/ScanLogStream"; -import { Card, CardContent } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { cn } from "@/lib/utils"; -import type { CheckResult, ScanTool } from "./types/api"; - -function App() { - const [results, setResults] = useState([]); - const [activeScanId, setActiveScanId] = useState(null); - const [selectedScanId, setSelectedScanId] = useState(null); - const [isLoadingScan, setIsLoadingScan] = useState(false); - const queryClient = useQueryClient(); - - // Récupérer la liste des scans existants - const { - data: savedScans, - isLoading: isLoadingScans, - error: scansError, - } = useQuery({ - queryKey: ["scans"], - queryFn: async () => { - const result = await scans.list(); - return result; - }, - refetchInterval: 10000, // Refresh toutes les 10s - }); - - // Automatic selection of first scan - - // Mutation pour démarrer un scan complet avec logs streaming - const startFullScan = useMutation({ - mutationFn: async ({ - target, - tools, - timeout, - }: { - target: string; - tools: ScanTool[]; - timeout: number; - }) => { - return scans.start({ target, modules: tools, timeout }); - }, - onSuccess: (data) => { - setActiveScanId(data.scan_id); - queryClient.invalidateQueries({ queryKey: ["scans"] }); - }, - }); - - const handleScan = (target: string, tools: ScanTool[], timeout: number) => { - // Réinitialiser l'état lors d'un nouveau scan - setSelectedScanId(null); - setResults([]); - // Utiliser la nouvelle API avec streaming de logs - startFullScan.mutate({ target, tools, timeout }); - }; - - const handleScanComplete = () => { - // Rafraîchir les scans et récupérer les résultats - queryClient.invalidateQueries({ queryKey: ["scans"] }); - if (activeScanId) { - scans.get(activeScanId).then((scan) => { - setResults(scan.results); - setSelectedScanId(activeScanId); // Marquer ce scan comme sélectionné - setActiveScanId(null); - }); - } - }; - - const handleScanClick = async (scanId: string) => { - // Si on clique sur le même scan, ne rien faire - if (selectedScanId === scanId && results.length > 0) { - return; - } - - setIsLoadingScan(true); - setSelectedScanId(scanId); - setResults([]); // Réinitialiser les résultats avant de charger - - try { - const scan = await scans.get(scanId); - setResults(scan.results); - } catch (error) { - console.error("Failed to load scan:", error); - setResults([]); - } finally { - setIsLoadingScan(false); - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case "success": - return "text-green-400"; - case "error": - return "text-red-400"; - default: - return "text-yellow-400"; - } - }; - - return ( -
- {/* Header */} -
-
-
-
- -
-
-

Web-Check

-

Security Scanner Dashboard

-
-
-
-
- - {/* Main Content */} -
-
- {/* Sidebar - Formulaire */} -
- - - {/* Scans sauvegardés */} - {isLoadingScans && ( - - -

Loading scans...

-
-
- )} - - {scansError && ( - - -

- Error:{" "} - {scansError instanceof Error ? scansError.message : "Unable to load scans"} -

-
-
- )} - - {!isLoadingScans && !scansError && savedScans && savedScans.length === 0 && ( - - -

No scans available

-
-
- )} - - {savedScans && savedScans.length > 0 && ( - - -

Recent Scans ({savedScans.length})

-
- {savedScans.slice(0, 5).map((scan) => ( -
handleScanClick(scan.scan_id)} - className={cn( - "bg-slate-700/50 p-3 rounded-lg hover:bg-slate-700 transition-colors cursor-pointer border", - selectedScanId === scan.scan_id - ? "border-primary ring-2 ring-primary/50" - : "border-slate-600" - )} - > -

{scan.target}

-
- - {scan.scan_id} - - - {scan.status} - -
-
- ))} -
-
-
- )} -
- - {/* Main Content - Résultats */} -
- {/* Logs en temps réel */} - {activeScanId && ( -
- -
- )} - - {/* Statistiques */} - {results.length > 0 && } - - {startFullScan.isPending && !activeScanId && ( - - - -

Starting scan...

-

- Connecting to scanning services -

-
-
- )} - - {isLoadingScan && ( - - - -

Loading results...

-
-
- )} - - {startFullScan.isError && ( - - -

Scan Error

-

- {startFullScan.error instanceof Error - ? startFullScan.error.message - : "An error occurred"} -

-
-
- )} - - {/* Résultats des scans */} - {results.length > 0 && ( -
- {/* En-tête des résultats */} - {selectedScanId && ( -
-

Scan Results

- - {selectedScanId} - -
- )} - {results.map((result, idx) => ( - - ))} -
- )} - - {/* Message "Aucun résultat" uniquement si vraiment rien n'est en cours */} - {results.length === 0 && - !startFullScan.isPending && - !activeScanId && - !isLoadingScan && - !selectedScanId && ( - - - 🔍 -

No Results

-

- Start a scan to begin analyzing your target -

-
-
- )} - - {/* Message si scan sélectionné mais aucun résultat */} - {results.length === 0 && selectedScanId && !isLoadingScan && !activeScanId && ( - - - 📭 -

No Results for This Scan

-

- This scan generated no results or may still be running. -

-
-
- )} -
-
-
-
- ); -} - -export default App; diff --git a/web/src/components/ScanForm.tsx b/web/src/components/ScanForm.tsx deleted file mode 100644 index 33959dd..0000000 --- a/web/src/components/ScanForm.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState } from "react"; -import { Rocket } from "lucide-react"; -import type { ScanTool } from "../types/api"; -import { AVAILABLE_TOOLS, FULL_SCAN_CONFIG } from "../constants/tools"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ToolSelector } from "./ToolSelector"; - -interface ScanFormProps { - onSubmit: (target: string, tools: ScanTool[], timeout: number) => void; - isLoading: boolean; -} - -export function ScanForm({ onSubmit, isLoading }: ScanFormProps) { - const [target, setTarget] = useState(""); - const [selectedTools, setSelectedTools] = useState(["nuclei"]); - const [timeout, setTimeout] = useState(900); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (target && selectedTools.length > 0) { - onSubmit(target, selectedTools, timeout); - } - }; - - const handleFullScan = (e: React.MouseEvent) => { - e.preventDefault(); - if (target) { - setSelectedTools(FULL_SCAN_CONFIG.tools); - onSubmit(target, FULL_SCAN_CONFIG.tools, FULL_SCAN_CONFIG.timeout); - } - }; - - const toggleTool = (toolId: ScanTool) => { - setSelectedTools((prev) => - prev.includes(toolId) ? prev.filter((t) => t !== toolId) : [...prev, toolId] - ); - }; - - const selectAllTools = () => { - setSelectedTools(AVAILABLE_TOOLS.map((tool) => tool.id)); - }; - - const clearAllTools = () => { - setSelectedTools([]); - }; - - return ( - - - New Scan - Configure your security scan - - -
- {/* Target URL */} -
- - setTarget(e.target.value)} - placeholder="https://example.com" - required - className="bg-slate-700 border-slate-600" - /> -
- - {/* Tool Selection */} - - - {/* Timeout */} -
- - setTimeout(Number(e.target.value))} - min="30" - max="3600" - className="bg-slate-700 border-slate-600" - /> -
- - {/* Action Buttons */} -
- - -
- -
-
- ); -} diff --git a/web/src/components/ScanLogStream.tsx b/web/src/components/ScanLogStream.tsx deleted file mode 100644 index 7e93302..0000000 --- a/web/src/components/ScanLogStream.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { useEffect, useState, useRef, useMemo } from "react"; -import { - Plug, - Info, - CheckCircle2, - AlertTriangle, - XCircle, - Container, - PartyPopper, -} from "lucide-react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { cn } from "@/lib/utils"; -import { ScanTimeline } from "./ScanTimeline"; - -interface LogEntry { - timestamp: string; - scan_id?: string; - type: "connected" | "info" | "success" | "warning" | "error" | "docker" | "complete"; - message: string; - module?: string; - command?: string; - findings_count?: number; - status?: string; -} - -interface ScanLogStreamProps { - scanId: string; - onComplete?: () => void; -} - -const logTypeColors = { - connected: "text-blue-400", - info: "text-muted-foreground", - success: "text-green-400", - warning: "text-yellow-400", - error: "text-red-400", - docker: "text-purple-400", - complete: "text-green-500", -}; - -const LogIcon = ({ type }: { type: LogEntry["type"] }) => { - const iconClass = "w-4 h-4"; - switch (type) { - case "connected": - return ; - case "info": - return ; - case "success": - return ; - case "warning": - return ; - case "error": - return ; - case "docker": - return ; - case "complete": - return ; - } -}; - -export function ScanLogStream({ scanId, onComplete }: ScanLogStreamProps) { - const [logs, setLogs] = useState([]); - const [isConnected, setIsConnected] = useState(false); - const [error, setError] = useState(null); - const logsContainerRef = useRef(null); - - useEffect(() => { - const apiUrl = window.location.origin; - const eventSource = new EventSource(`${apiUrl}/api/scans/${scanId}/logs`); - - eventSource.onopen = () => { - setIsConnected(true); - setError(null); - }; - - eventSource.onmessage = (event) => { - try { - const data: LogEntry = JSON.parse(event.data); - setLogs((prev) => [...prev, data]); - - if (data.type === "complete") { - eventSource.close(); - setIsConnected(false); - onComplete?.(); - } - } catch (err) { - console.error("Failed to parse log entry:", err); - } - }; - - eventSource.onerror = (err) => { - console.error("EventSource error:", err); - setError("Connection lost with server"); - setIsConnected(false); - eventSource.close(); - }; - - return () => { - eventSource.close(); - setIsConnected(false); - }; - }, [scanId, onComplete]); - - // Auto-scroll to bottom when new logs arrive - useEffect(() => { - if (logsContainerRef.current) { - logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; - } - }, [logs]); - - // Extraire les informations de timeline depuis les logs - const timelineSteps = useMemo(() => { - const modulesMap = new Map< - string, - { - module: string; - status: "pending" | "running" | "success" | "error"; - startTime?: string; - endTime?: string; - findingsCount?: number; - } - >(); - - // Extraire les modules du message initial - const infoLog = logs.find( - (l) => l.type === "info" && l.message?.includes("Starting scan with modules") - ); - if (infoLog) { - const match = infoLog.message.match(/modules: (.+)/); - if (match) { - const moduleNames = match[1].split(",").map((m) => m.trim()); - moduleNames.forEach((module) => { - if (!modulesMap.has(module)) { - modulesMap.set(module, { - module, - status: "pending", - }); - } - }); - } - } - - logs.forEach((log) => { - if (!log.module) return; - - const existing = modulesMap.get(log.module); - - if (log.type === "docker") { - // Module démarré - modulesMap.set(log.module, { - ...existing, - module: log.module, - status: "running", - startTime: log.timestamp, - }); - } else if (log.type === "success") { - // Module terminé avec succès - modulesMap.set(log.module, { - ...existing, - module: log.module, - status: "success", - endTime: log.timestamp, - findingsCount: log.findings_count ?? 0, - }); - } else if (log.type === "error") { - // Module en erreur - modulesMap.set(log.module, { - ...existing, - module: log.module, - status: "error", - endTime: log.timestamp, - }); - } - }); - - return Array.from(modulesMap.values()); - }, [logs]); - - return ( -
- {/* Timeline */} - {timelineSteps.length > 0 && } - - {/* Logs */} - - -
-
- Real-Time Logs - Scan ID: {scanId} -
- {isConnected ? ( - - - - - - Connected - - ) : ( - Disconnected - )} -
-
- - - {/* Error */} - {error && ( -
-

{error}

-
- )} - - {/* Logs Container */} -
- {logs.length === 0 ? ( -

Waiting for logs...

- ) : ( - logs.map((log, idx) => ( -
- - {log.timestamp - ? new Date(log.timestamp).toLocaleTimeString("en-US") - : "--:--:--"} - - - - -
- - {log.module && ( - - {log.module} - - )} - {log.message} - - {log.command && ( -
- $ {log.command} -
- )} - {log.findings_count !== undefined && ( - - {log.findings_count} vulnerabilities - - )} -
-
- )) - )} -
- - {/* Stats */} -
- {logs.length} events -
-
-
-
- ); -} diff --git a/web/src/components/ScanResult.tsx b/web/src/components/ScanResult.tsx deleted file mode 100644 index 15fda5b..0000000 --- a/web/src/components/ScanResult.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { formatDistanceToNow } from "date-fns"; -import { enUS } from "date-fns/locale"; -import { ExternalLink, AlertCircle, ShieldAlert } from "lucide-react"; -import type { CheckResult } from "../types/api"; -import { SeverityBadge } from "./SeverityBadge"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -import { cn } from "@/lib/utils"; - -interface ScanResultProps { - result: CheckResult; -} - -const statusColors = { - success: "text-green-400", - error: "text-red-400", - timeout: "text-yellow-400", - running: "text-blue-400", -}; - -const severityBorderColors = { - critical: "border-l-red-600", - high: "border-l-orange-500", - medium: "border-l-yellow-500", - low: "border-l-blue-500", - info: "border-l-gray-500", -}; - -export function ScanResult({ result }: ScanResultProps) { - const criticalFindings = result.findings.filter((f) => f.severity === "critical"); - const highFindings = result.findings.filter((f) => f.severity === "high"); - const mediumFindings = result.findings.filter((f) => f.severity === "medium"); - const lowFindings = result.findings.filter((f) => f.severity === "low"); - const infoFindings = result.findings.filter((f) => f.severity === "info"); - - return ( - - -
-
- {result.module} - {result.target} -
-
- - {result.status.toUpperCase()} - -

- {formatDistanceToNow(new Date(result.timestamp), { addSuffix: true, locale: enUS })} -

-
-
-
- - - {/* Stats */} -
-
-

Duration

-

{(result.duration_ms / 1000).toFixed(1)}s

-
-
-

Vulnerabilities

-

{result.findings.length}

-
-
-

Category

-

{result.category}

-
-
- - {/* Error */} - {result.error && ( -
- -

{result.error}

-
- )} - - {/* Findings organized by severity */} - {result.findings.length > 0 && ( -
-
-

- - Detected Vulnerabilities -

-
- {criticalFindings.length > 0 && ( - {criticalFindings.length} Critical - )} - {highFindings.length > 0 && ( - {highFindings.length} High - )} - {mediumFindings.length > 0 && ( - {mediumFindings.length} Medium - )} - {lowFindings.length > 0 && ( - {lowFindings.length} Low - )} - {infoFindings.length > 0 && ( - {infoFindings.length} Info - )} -
-
- - - {/* Critical Findings */} - {criticalFindings.length > 0 && ( - - -
- CRITICAL - - Critical Issues ({criticalFindings.length}) - -
-
- -
- {criticalFindings.map((finding, idx) => ( - - ))} -
-
-
- )} - - {/* High Findings */} - {highFindings.length > 0 && ( - - -
- HIGH - High Issues ({highFindings.length}) -
-
- -
- {highFindings.map((finding, idx) => ( - - ))} -
-
-
- )} - - {/* Medium Findings */} - {mediumFindings.length > 0 && ( - - -
- MEDIUM - Medium Issues ({mediumFindings.length}) -
-
- -
- {mediumFindings.map((finding, idx) => ( - - ))} -
-
-
- )} - - {/* Low Findings */} - {lowFindings.length > 0 && ( - - -
- LOW - Low Issues ({lowFindings.length}) -
-
- -
- {lowFindings.map((finding, idx) => ( - - ))} -
-
-
- )} - - {/* Info Findings */} - {infoFindings.length > 0 && ( - - -
- INFO - Informational ({infoFindings.length}) -
-
- -
- {infoFindings.map((finding, idx) => ( - - ))} -
-
-
- )} -
-
- )} -
-
- ); -} - -// Helper component for finding cards -interface FindingCardProps { - finding: CheckResult["findings"][0]; -} - -function FindingCard({ finding }: FindingCardProps) { - return ( - - -
- {finding.title} -
-
- - {finding.description} -
- {finding.cve && ( - - {finding.cve} - - )} - {finding.cvss_score !== undefined && finding.cvss_score !== null && ( - CVSS: {finding.cvss_score.toFixed(1)} - )} - {finding.reference && ( - - Reference - - - )} -
-
-
- ); -} diff --git a/web/src/components/ScanStats.tsx b/web/src/components/ScanStats.tsx deleted file mode 100644 index 50d4965..0000000 --- a/web/src/components/ScanStats.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { BarChart3 } from "lucide-react"; -import type { CheckResult } from "../types/api"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -interface ScanStatsProps { - results: CheckResult[]; -} - -export function ScanStats({ results }: ScanStatsProps) { - if (results.length === 0) return null; - - const stats = { - total: results.length, - success: results.filter((r) => r.status === "success").length, - error: results.filter((r) => r.status === "error").length, - timeout: results.filter((r) => r.status === "timeout").length, - totalFindings: results.reduce((sum, r) => sum + r.findings.length, 0), - critical: results.reduce( - (sum, r) => sum + r.findings.filter((f) => f.severity === "critical").length, - 0 - ), - high: results.reduce( - (sum, r) => sum + r.findings.filter((f) => f.severity === "high").length, - 0 - ), - medium: results.reduce( - (sum, r) => sum + r.findings.filter((f) => f.severity === "medium").length, - 0 - ), - low: results.reduce((sum, r) => sum + r.findings.filter((f) => f.severity === "low").length, 0), - totalDuration: results.reduce((sum, r) => sum + r.duration_ms, 0), - }; - - return ( - - - - - Scan Statistics - - - -
-
-

Tools

-

{stats.total}

-

- {stats.success} successful - {stats.error > 0 && `, ${stats.error} errors`} - {stats.timeout > 0 && `, ${stats.timeout} timeout`} -

-
- -
-

Vulnerabilities

-

{stats.totalFindings}

-

Total

-
- -
-

Total Duration

-

{(stats.totalDuration / 1000).toFixed(1)}s

-

Cumulative

-
- -
-

Average Duration

-

- {(stats.totalDuration / stats.total / 1000).toFixed(1)}s -

-

Per Tool

-
-
- - {stats.totalFindings > 0 && ( -
-

Distribution by Severity

-
- - -

{stats.critical}

-

Critical

-
-
- - -

{stats.high}

-

High

-
-
- - -

{stats.medium}

-

Medium

-
-
- - -

{stats.low}

-

Low

-
-
-
-
- )} -
-
- ); -} diff --git a/web/src/components/ScanTimeline.tsx b/web/src/components/ScanTimeline.tsx deleted file mode 100644 index 129eca9..0000000 --- a/web/src/components/ScanTimeline.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { CheckCircle2, Clock, Loader2, AlertCircle } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { cn } from "@/lib/utils"; - -interface TimelineStep { - module: string; - status: "pending" | "running" | "success" | "error"; - startTime?: string; - endTime?: string; - findingsCount?: number; -} - -interface ScanTimelineProps { - steps: TimelineStep[]; -} - -const statusConfig = { - pending: { - icon: Clock, - color: "text-muted-foreground", - bgColor: "bg-slate-700", - label: "Pending", - }, - running: { - icon: Loader2, - color: "text-blue-400", - bgColor: "bg-blue-500/20", - label: "Running", - }, - success: { - icon: CheckCircle2, - color: "text-green-400", - bgColor: "bg-green-500/20", - label: "Completed", - }, - error: { - icon: AlertCircle, - color: "text-red-400", - bgColor: "bg-red-500/20", - label: "Error", - }, -}; - -export function ScanTimeline({ steps }: ScanTimelineProps) { - const completedSteps = steps.filter((s) => s.status === "success").length; - const totalSteps = steps.length; - const progressPercentage = totalSteps > 0 ? (completedSteps / totalSteps) * 100 : 0; - - return ( - - -
- Scan Progress - - {completedSteps}/{totalSteps} modules - -
- {/* Barre de progression */} -
-
-
- - - - {steps.map((step, idx) => { - const config = statusConfig[step.status]; - const Icon = config.icon; - const isLast = idx === steps.length - 1; - - return ( -
- {/* Ligne verticale */} - {!isLast && ( -
- )} - - {/* Icône */} -
- -
- - {/* Contenu */} -
-
-

{step.module}

- {config.label} -
- - {step.startTime && ( -

- Started at {new Date(step.startTime).toLocaleTimeString("en-US")} -

- )} - - {step.status === "success" && step.findingsCount !== undefined && ( -
- - - {step.findingsCount} vulnerabilit{step.findingsCount !== 1 ? "ies" : "y"}{" "} - detected - -
- )} - - {step.status === "error" && ( -
- - Scan failed -
- )} -
-
- ); - })} - - - ); -} diff --git a/web/src/components/SeverityBadge.tsx b/web/src/components/SeverityBadge.tsx deleted file mode 100644 index e31eae1..0000000 --- a/web/src/components/SeverityBadge.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import type { Severity } from "../types/api"; -import { cn } from "@/lib/utils"; - -interface BadgeProps { - severity: Severity; - children: React.ReactNode; -} - -const severityVariants: Record = { - critical: "bg-red-600 hover:bg-red-700 text-white", - high: "bg-orange-500 hover:bg-orange-600 text-white", - medium: "bg-yellow-500 hover:bg-yellow-600 text-black", - low: "bg-blue-500 hover:bg-blue-600 text-white", - info: "bg-gray-500 hover:bg-gray-600 text-white", -}; - -export function SeverityBadge({ severity, children }: BadgeProps) { - return ( - - {children} - - ); -} diff --git a/web/src/components/ToolSelector.tsx b/web/src/components/ToolSelector.tsx deleted file mode 100644 index ec3a158..0000000 --- a/web/src/components/ToolSelector.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import type { ScanTool } from "../types/api"; -import { AVAILABLE_TOOLS } from "../constants/tools"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Badge } from "@/components/ui/badge"; -import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; - -interface ToolSelectorProps { - selectedTools: ScanTool[]; - onToggleTool: (toolId: ScanTool) => void; - onSelectAll: () => void; - onClearAll: () => void; -} - -export function ToolSelector({ - selectedTools, - onToggleTool, - onSelectAll, - onClearAll, -}: ToolSelectorProps) { - return ( - -
- {/* Header */} -
- - Tools ({selectedTools.length}/{AVAILABLE_TOOLS.length}) - -
- - - -
-
- - {/* Tool Grid */} -
- {AVAILABLE_TOOLS.map((tool) => { - const isSelected = selectedTools.includes(tool.id); - - return ( - - - - - -
-
- {tool.icon} - {tool.name} - - {tool.category} - -
-

- {tool.description} -

-
- Timeout: {tool.defaultTimeout}s -
-
-
-
- ); - })} -
-
-
- ); -} diff --git a/web/src/components/ui/accordion.tsx b/web/src/components/ui/accordion.tsx deleted file mode 100644 index ba06ffa..0000000 --- a/web/src/components/ui/accordion.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDown } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Accordion = AccordionPrimitive.Root; - -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AccordionItem.displayName = "AccordionItem"; - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - -)); -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)); -AccordionContent.displayName = AccordionPrimitive.Content.displayName; - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx deleted file mode 100644 index e2367b4..0000000 --- a/web/src/components/ui/badge.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -export interface BadgeProps - extends React.HTMLAttributes, VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return
; -} - -export { Badge, badgeVariants }; diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx deleted file mode 100644 index 8eeb511..0000000 --- a/web/src/components/ui/button.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - ); - } -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx deleted file mode 100644 index ea956e7..0000000 --- a/web/src/components/ui/card.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Card = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardFooter.displayName = "CardFooter"; - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx deleted file mode 100644 index 968ed42..0000000 --- a/web/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; -import { Check } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)); -Checkbox.displayName = CheckboxPrimitive.Root.displayName; - -export { Checkbox }; diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx deleted file mode 100644 index 7bad21e..0000000 --- a/web/src/components/ui/input.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { - return ( - - ); - } -); -Input.displayName = "Input"; - -export { Input }; diff --git a/web/src/components/ui/label.tsx b/web/src/components/ui/label.tsx deleted file mode 100644 index dea5822..0000000 --- a/web/src/components/ui/label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -); - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & VariantProps ->(({ className, ...props }, ref) => ( - -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/web/src/components/ui/tooltip.tsx b/web/src/components/ui/tooltip.tsx deleted file mode 100644 index 6fdf76a..0000000 --- a/web/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "@/lib/utils"; - -const TooltipProvider = TooltipPrimitive.Provider; - -const Tooltip = TooltipPrimitive.Root; - -const TooltipTrigger = TooltipPrimitive.Trigger; - -const TooltipContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - -)); -TooltipContent.displayName = TooltipPrimitive.Content.displayName; - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/web/src/constants/tools.ts b/web/src/constants/tools.ts deleted file mode 100644 index 3fed865..0000000 --- a/web/src/constants/tools.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Tool definitions and configurations - */ - -import type { ScanTool, ToolInfo } from "../types/api"; - -export const TOOL_INFO: Record = { - nuclei: { - id: "nuclei", - name: "Nuclei", - description: "CVE and vulnerability scanning with community templates", - category: "quick", - defaultTimeout: 300, - icon: "🎯", - }, - nikto: { - id: "nikto", - name: "Nikto", - description: "Web server scanning for misconfigurations", - category: "quick", - defaultTimeout: 600, - icon: "🕷️", - }, - zap: { - id: "zap", - name: "OWASP ZAP", - description: "Comprehensive security scan (XSS, SQLi, etc.)", - category: "deep", - defaultTimeout: 900, - icon: "⚡", - }, - testssl: { - id: "testssl", - name: "SSLyze", - description: "SSL/TLS analysis and cryptographic configuration", - category: "deep", - defaultTimeout: 300, - icon: "🔒", - }, - ffuf: { - id: "ffuf", - name: "FFUF", - description: "Directory and hidden file fuzzing", - category: "security", - defaultTimeout: 600, - icon: "🔍", - }, - sqlmap: { - id: "sqlmap", - name: "SQLMap", - description: "Automated SQL injection testing", - category: "security", - defaultTimeout: 900, - icon: "💉", - }, - wapiti: { - id: "wapiti", - name: "Wapiti", - description: "Web vulnerability scanner (XSS, injection, etc.)", - category: "security", - defaultTimeout: 600, - icon: "🕸️", - }, - xsstrike: { - id: "xsstrike", - name: "XSStrike", - description: "Advanced XSS vulnerability detection", - category: "security", - defaultTimeout: 300, - icon: "⚔️", - }, -}; - -export const AVAILABLE_TOOLS: ToolInfo[] = Object.values(TOOL_INFO); - -export const TOOL_CATEGORIES = { - quick: { - name: "Quick Scan", - description: "Fast scans for initial assessment", - color: "green", - }, - deep: { - name: "Deep Analysis", - description: "Detailed analysis with in-depth testing", - color: "blue", - }, - security: { - name: "Advanced Security", - description: "Specialized security tests", - color: "purple", - }, -} as const; - -export const FULL_SCAN_CONFIG = { - timeout: 3600, // 1 hour - tools: Object.keys(TOOL_INFO) as ScanTool[], - name: "Full Scan", - description: "Executes all available scanning tools", -}; diff --git a/web/src/index.css b/web/src/index.css deleted file mode 100644 index f76ba0c..0000000 --- a/web/src/index.css +++ /dev/null @@ -1,51 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --radius: 0.5rem; - } -} - -@layer base { - * { - @apply border-border; - } - - body { - @apply bg-background text-foreground; - margin: 0; - min-height: 100vh; - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } -} - -#root { - min-height: 100vh; -} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts deleted file mode 100644 index 302216d..0000000 --- a/web/src/lib/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -/** - * Utility function for merging CSS class names. - * Combines clsx for conditional classes with tailwind-merge for deduplication. - */ -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/web/src/main.tsx b/web/src/main.tsx deleted file mode 100644 index f73be90..0000000 --- a/web/src/main.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import App from "./App.tsx"; -import "./index.css"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - retry: 1, - }, - }, -}); - -createRoot(document.getElementById("root")!).render( - - - - - -); diff --git a/web/src/services/api.ts b/web/src/services/api.ts deleted file mode 100644 index aaa4eca..0000000 --- a/web/src/services/api.ts +++ /dev/null @@ -1,129 +0,0 @@ -import axios from "axios"; -import type { CheckResult, ScanRequest, ScanResponse, ScanTool } from "../types/api"; - -const api = axios.create({ - baseURL: "/api", - headers: { - "Content-Type": "application/json", - }, -}); - -// Log des erreurs pour debug -api.interceptors.response.use( - (response) => response, - (error) => { - console.error("API Error:", { - url: error.config?.url, - method: error.config?.method, - status: error.response?.status, - data: error.response?.data, - message: error.message, - }); - return Promise.reject(error); - } -); - -// Quick scans -export const quickScans = { - nuclei: (url: string, timeout: number = 300): Promise => - api.get("/quick/nuclei", { params: { url, timeout } }).then((res) => res.data), - - nikto: (url: string, timeout: number = 600): Promise => - api.get("/quick/nikto", { params: { url, timeout } }).then((res) => res.data), - - dns: (url: string): Promise => - api.get("/quick/dns", { params: { url } }).then((res) => res.data), -}; - -// Deep scans -export const deepScans = { - zap: (url: string, timeout: number = 900): Promise => - api.get("/deep/zap", { params: { url, timeout } }).then((res) => res.data), - - testssl: (url: string, timeout: number = 300): Promise => - api.get("/deep/sslyze", { params: { url, timeout } }).then((res) => res.data), -}; - -// Security scans -export const securityScans = { - ffuf: ( - url: string, - timeout: number = 600, - wordlist: string = "common.txt" - ): Promise => - api.get("/security/ffuf", { params: { url, timeout, wordlist } }).then((res) => res.data), - - sqlmap: (url: string, timeout: number = 900): Promise => - api.get("/security/sqlmap", { params: { url, timeout } }).then((res) => res.data), -}; - -// Run multiple scans -export const runMultipleScans = async ( - url: string, - tools: ScanTool[], - timeout: number -): Promise => { - const scanPromises = tools.map((tool) => { - switch (tool) { - case "nuclei": - return quickScans.nuclei(url, timeout); - case "nikto": - return quickScans.nikto(url, timeout); - case "zap": - return deepScans.zap(url, timeout); - case "testssl": - return deepScans.testssl(url, timeout); - case "ffuf": - return securityScans.ffuf(url, timeout); - case "sqlmap": - return securityScans.sqlmap(url, timeout); - default: - throw new Error(`Unknown tool: ${tool}`); - } - }); - - return Promise.allSettled(scanPromises).then((results) => - results.map((result, index) => { - if (result.status === "fulfilled") { - return result.value; - } else { - return { - module: tools[index], - category: "quick" as const, - target: url, - timestamp: new Date().toISOString(), - duration_ms: 0, - status: "error" as const, - findings: [], - error: result.reason?.message || "Unknown error", - }; - } - }) - ); -}; - -// Scan management -export const scans = { - start: (request: ScanRequest): Promise => - api.post("/scans/start", request).then((res) => res.data), - - get: (scanId: string): Promise => - api.get(`/scans/${scanId}`).then((res) => res.data), - - list: (): Promise => api.get("/scans").then((res) => res.data), - - delete: (scanId: string): Promise => api.delete(`/scans/${scanId}`).then((res) => res.data), - - streamLogs: (scanId: string): string => { - const baseURL = api.defaults.baseURL || "/api"; - return `${baseURL}/scans/${scanId}/logs`; - }, -}; - -// Health check -export const health = { - check: (): Promise<{ status: string; timestamp: string }> => - api.get("/health").then((res) => res.data), -}; - -export default api; diff --git a/web/src/types/api.ts b/web/src/types/api.ts deleted file mode 100644 index c868002..0000000 --- a/web/src/types/api.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Types based on Pydantic API models -export type Severity = "critical" | "high" | "medium" | "low" | "info"; -export type ScanStatus = "success" | "error" | "timeout" | "running"; -export type ScanCategory = "quick" | "deep" | "security"; - -export type ScanTool = - | "nuclei" - | "nikto" - | "zap" - | "testssl" - | "ffuf" - | "sqlmap" - | "wapiti" - | "xsstrike"; - -export interface Finding { - severity: Severity; - title: string; - description: string; - reference?: string; - cve?: string; - cvss_score?: number; -} - -export interface CheckResult { - module: string; - category: ScanCategory; - target: string; - timestamp: string; - duration_ms: number; - status: ScanStatus; - data?: Record; - findings: Finding[]; - error?: string; -} - -export interface ScanRequest { - target: string; - modules: string[]; - timeout: number; -} - -export interface ScanResponse { - scan_id: string; - target: string; - status: ScanStatus; - started_at: string; - completed_at?: string; - results: CheckResult[]; -} - -export interface ToolInfo { - id: ScanTool; - name: string; - description: string; - category: ScanCategory; - defaultTimeout: number; - icon: string; -} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/web/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/web/tailwind.config.js b/web/tailwind.config.js deleted file mode 100644 index dc57383..0000000 --- a/web/tailwind.config.js +++ /dev/null @@ -1,79 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ["class"], - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { - height: "0", - }, - to: { - height: "var(--radix-accordion-content-height)", - }, - }, - "accordion-up": { - from: { - height: "var(--radix-accordion-content-height)", - }, - to: { - height: "0", - }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [], -}; diff --git a/web/tsconfig.json b/web/tsconfig.json deleted file mode 100644 index 33514fa..0000000 --- a/web/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - /* Path aliases */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json deleted file mode 100644 index 42872c5..0000000 --- a/web/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/web/vite.config.ts b/web/vite.config.ts deleted file mode 100644 index dbba8d2..0000000 --- a/web/vite.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from "path"; -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - server: { - host: true, - port: 3000, - proxy: { - "/api": { - target: process.env.VITE_API_URL || "http://api:8000", - changeOrigin: true, - rewrite: (path) => path, - }, - }, - }, -});