From ff3e6b256e2705b83c25a247606445410ba89139 Mon Sep 17 00:00:00 2001 From: KevinDeBenedetti Date: Mon, 16 Mar 2026 23:06:54 +0100 Subject: [PATCH 1/3] feat: add UI components (Badge, Button, Card, Checkbox, Input, Label, Tooltip) and utility functions - Implemented Badge component for displaying status indicators. - Created Button component with various styles and sizes. - Developed Card component with subcomponents (Header, Title, Description, Content, Footer) for structured content display. - Added Checkbox component for user selections. - Introduced Input component for form fields. - Created Label component for form field labeling. - Implemented Tooltip component for additional context on hover. - Added utility function for merging CSS class names. feat: define tool configurations and API services - Added TOOL_INFO constant for tool definitions and configurations. - Implemented API service for handling scan requests and responses. - Created functions for quick, deep, and security scans. - Added scan management functions for starting, retrieving, and deleting scans. chore: set up project structure and configuration - Initialized Tailwind CSS for styling. - Configured Vite for development and build processes. - Set up TypeScript configuration for type safety. - Updated dependencies in pyproject.toml for enhanced functionality. --- CLI.md | 254 ++++++++++++++++++ Makefile | 24 +- TODO.yml | 16 ++ Dockerfile => apps/Dockerfile | 8 +- apps/Makefile | 203 ++++++++++++++ apps/__init__.py | 1 + alembic.ini => apps/alembic.ini | 0 {alembic => apps/alembic}/README | 0 {alembic => apps/alembic}/env.py | 0 {alembic => apps/alembic}/script.py.mako | 0 .../1dd0c571c561_initial_database_schema.py | 0 {api => apps/api}/__init__.py | 0 {api => apps/api}/database.py | 0 {api => apps/api}/main.py | 0 {api => apps/api}/models/__init__.py | 0 {api => apps/api}/models/db_models.py | 0 {api => apps/api}/models/findings.py | 0 {api => apps/api}/models/results.py | 0 {api => apps/api}/routers/__init__.py | 0 {api => apps/api}/routers/advanced.py | 0 {api => apps/api}/routers/deep.py | 0 {api => apps/api}/routers/health.py | 0 {api => apps/api}/routers/quick.py | 0 {api => apps/api}/routers/scans.py | 0 {api => apps/api}/routers/security.py | 0 {api => apps/api}/services/__init__.py | 0 {api => apps/api}/services/db_service.py | 0 {api => apps/api}/services/docker_runner.py | 0 {api => apps/api}/services/log_streamer.py | 0 {api => apps/api}/services/nikto.py | 0 {api => apps/api}/services/nuclei.py | 0 {api => apps/api}/services/sqlmap_scanner.py | 0 {api => apps/api}/services/sslyze_scanner.py | 0 {api => apps/api}/services/wapiti_scanner.py | 0 .../api}/services/xsstrike_scanner.py | 0 {api => apps/api}/services/zap_native.py | 0 {api => apps/api}/tests/__init__.py | 0 {api => apps/api}/tests/conftest.py | 0 {api => apps/api}/tests/test_api.py | 0 {api => apps/api}/tests/test_models.py | 0 {api => apps/api}/tests/test_scanners.py | 0 {api => apps/api}/utils/__init__.py | 0 {api => apps/api}/utils/config.py | 0 apps/cli/__init__.py | 3 + apps/cli/commands/__init__.py | 7 + apps/cli/commands/config.py | 60 +++++ apps/cli/commands/results.py | 138 ++++++++++ apps/cli/commands/scan.py | 171 ++++++++++++ apps/cli/main.py | 76 ++++++ apps/cli/utils/__init__.py | 14 + apps/cli/utils/config.py | 25 ++ apps/cli/utils/http_client.py | 150 +++++++++++ {config => apps/config}/settings.conf | 0 {config => apps/config}/wordlists/common.txt | 0 docker-compose.yml => apps/docker-compose.yml | 8 +- {web => apps/web}/.dockerignore | 0 {web => apps/web}/.env.example | 0 {web => apps/web}/.gitignore | 0 {web => apps/web}/.oxfmtrc.json | 0 {web => apps/web}/.oxlintrc.json | 0 {web => apps/web}/Dockerfile | 0 {web => apps/web}/bun.lock | 0 {web => apps/web}/components.json | 0 {web => apps/web}/index.html | 0 {web => apps/web}/nginx.conf | 0 {web => apps/web}/package.json | 0 {web => apps/web}/postcss.config.js | 0 {web => apps/web}/src/App.tsx | 0 {web => apps/web}/src/components/ScanForm.tsx | 0 .../web}/src/components/ScanLogStream.tsx | 0 .../web}/src/components/ScanResult.tsx | 0 .../web}/src/components/ScanStats.tsx | 0 .../web}/src/components/ScanTimeline.tsx | 0 .../web}/src/components/SeverityBadge.tsx | 0 .../web}/src/components/ToolSelector.tsx | 0 .../web}/src/components/ui/accordion.tsx | 0 {web => apps/web}/src/components/ui/badge.tsx | 0 .../web}/src/components/ui/button.tsx | 0 {web => apps/web}/src/components/ui/card.tsx | 0 .../web}/src/components/ui/checkbox.tsx | 0 {web => apps/web}/src/components/ui/input.tsx | 0 {web => apps/web}/src/components/ui/label.tsx | 0 .../web}/src/components/ui/tooltip.tsx | 0 {web => apps/web}/src/constants/tools.ts | 0 {web => apps/web}/src/index.css | 0 {web => apps/web}/src/lib/utils.ts | 0 {web => apps/web}/src/main.tsx | 0 {web => apps/web}/src/services/api.ts | 0 {web => apps/web}/src/types/api.ts | 0 {web => apps/web}/src/vite-env.d.ts | 0 {web => apps/web}/tailwind.config.js | 0 {web => apps/web}/tsconfig.json | 0 {web => apps/web}/tsconfig.node.json | 0 {web => apps/web}/vite.config.ts | 0 pyproject.toml | 13 +- uv.lock | 62 +++++ 96 files changed, 1210 insertions(+), 23 deletions(-) create mode 100644 CLI.md create mode 100644 TODO.yml rename Dockerfile => apps/Dockerfile (92%) create mode 100644 apps/Makefile create mode 100644 apps/__init__.py rename alembic.ini => apps/alembic.ini (100%) rename {alembic => apps/alembic}/README (100%) rename {alembic => apps/alembic}/env.py (100%) rename {alembic => apps/alembic}/script.py.mako (100%) rename {alembic => apps/alembic}/versions/1dd0c571c561_initial_database_schema.py (100%) rename {api => apps/api}/__init__.py (100%) rename {api => apps/api}/database.py (100%) rename {api => apps/api}/main.py (100%) rename {api => apps/api}/models/__init__.py (100%) rename {api => apps/api}/models/db_models.py (100%) rename {api => apps/api}/models/findings.py (100%) rename {api => apps/api}/models/results.py (100%) rename {api => apps/api}/routers/__init__.py (100%) rename {api => apps/api}/routers/advanced.py (100%) rename {api => apps/api}/routers/deep.py (100%) rename {api => apps/api}/routers/health.py (100%) rename {api => apps/api}/routers/quick.py (100%) rename {api => apps/api}/routers/scans.py (100%) rename {api => apps/api}/routers/security.py (100%) rename {api => apps/api}/services/__init__.py (100%) rename {api => apps/api}/services/db_service.py (100%) rename {api => apps/api}/services/docker_runner.py (100%) rename {api => apps/api}/services/log_streamer.py (100%) rename {api => apps/api}/services/nikto.py (100%) rename {api => apps/api}/services/nuclei.py (100%) rename {api => apps/api}/services/sqlmap_scanner.py (100%) rename {api => apps/api}/services/sslyze_scanner.py (100%) rename {api => apps/api}/services/wapiti_scanner.py (100%) rename {api => apps/api}/services/xsstrike_scanner.py (100%) rename {api => apps/api}/services/zap_native.py (100%) rename {api => apps/api}/tests/__init__.py (100%) rename {api => apps/api}/tests/conftest.py (100%) rename {api => apps/api}/tests/test_api.py (100%) rename {api => apps/api}/tests/test_models.py (100%) rename {api => apps/api}/tests/test_scanners.py (100%) rename {api => apps/api}/utils/__init__.py (100%) rename {api => apps/api}/utils/config.py (100%) create mode 100644 apps/cli/__init__.py create mode 100644 apps/cli/commands/__init__.py create mode 100644 apps/cli/commands/config.py create mode 100644 apps/cli/commands/results.py create mode 100644 apps/cli/commands/scan.py create mode 100644 apps/cli/main.py create mode 100644 apps/cli/utils/__init__.py create mode 100644 apps/cli/utils/config.py create mode 100644 apps/cli/utils/http_client.py rename {config => apps/config}/settings.conf (100%) rename {config => apps/config}/wordlists/common.txt (100%) rename docker-compose.yml => apps/docker-compose.yml (97%) rename {web => apps/web}/.dockerignore (100%) rename {web => apps/web}/.env.example (100%) rename {web => apps/web}/.gitignore (100%) rename {web => apps/web}/.oxfmtrc.json (100%) rename {web => apps/web}/.oxlintrc.json (100%) rename {web => apps/web}/Dockerfile (100%) rename {web => apps/web}/bun.lock (100%) rename {web => apps/web}/components.json (100%) rename {web => apps/web}/index.html (100%) rename {web => apps/web}/nginx.conf (100%) rename {web => apps/web}/package.json (100%) rename {web => apps/web}/postcss.config.js (100%) rename {web => apps/web}/src/App.tsx (100%) rename {web => apps/web}/src/components/ScanForm.tsx (100%) rename {web => apps/web}/src/components/ScanLogStream.tsx (100%) rename {web => apps/web}/src/components/ScanResult.tsx (100%) rename {web => apps/web}/src/components/ScanStats.tsx (100%) rename {web => apps/web}/src/components/ScanTimeline.tsx (100%) rename {web => apps/web}/src/components/SeverityBadge.tsx (100%) rename {web => apps/web}/src/components/ToolSelector.tsx (100%) rename {web => apps/web}/src/components/ui/accordion.tsx (100%) rename {web => apps/web}/src/components/ui/badge.tsx (100%) rename {web => apps/web}/src/components/ui/button.tsx (100%) rename {web => apps/web}/src/components/ui/card.tsx (100%) rename {web => apps/web}/src/components/ui/checkbox.tsx (100%) rename {web => apps/web}/src/components/ui/input.tsx (100%) rename {web => apps/web}/src/components/ui/label.tsx (100%) rename {web => apps/web}/src/components/ui/tooltip.tsx (100%) rename {web => apps/web}/src/constants/tools.ts (100%) rename {web => apps/web}/src/index.css (100%) rename {web => apps/web}/src/lib/utils.ts (100%) rename {web => apps/web}/src/main.tsx (100%) rename {web => apps/web}/src/services/api.ts (100%) rename {web => apps/web}/src/types/api.ts (100%) rename {web => apps/web}/src/vite-env.d.ts (100%) rename {web => apps/web}/tailwind.config.js (100%) rename {web => apps/web}/tsconfig.json (100%) rename {web => apps/web}/tsconfig.node.json (100%) rename {web => apps/web}/vite.config.ts (100%) 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/Makefile b/Makefile index f16537f..a57bfd6 100644 --- a/Makefile +++ b/Makefile @@ -101,25 +101,25 @@ install: ## Install/setup development environment run: ## Run API locally (outside Docker) @echo "$(GREEN)๐Ÿš€ Starting API locally...$(NC)" - @uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload + @uv run uvicorn apps.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 + @uv run pytest apps/api/tests/ -v lint: ## Lint code @echo "$(GREEN)๐Ÿ” Linting...$(NC)" - @uv run ruff check api/ + @uv run ruff check apps/ format: ## Format code @echo "$(GREEN)โœจ Formatting code...$(NC)" - @uv run ruff format api/ + @uv run ruff format apps/ 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/ + @uv run ruff format --check apps/ + @uv run ruff check apps/ + @uv run ty check apps/api @echo "$(GREEN)โœ… All checks passed!$(NC)" ci: ## Test all CI workflow steps locally @@ -132,23 +132,23 @@ ci: ## Test all CI workflow steps locally @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/ + @uv run ruff check --output-format=github --target-version=py312 apps/ @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/ + @uv run ruff format --check --target-version=py312 apps/ @echo "$(GREEN)โœ… Python format check passed$(NC)" @echo "" @echo "$(YELLOW)๐Ÿ“‹ Step 4/11: Python Type Check (ty)$(NC)" - @uv run ty check api/ + @uv run ty check apps/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 + @uv run pytest apps/api/tests/ -m "not slow" --cov=apps.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 + @docker buildx build -t web-check:test -f apps/Dockerfile . --load @echo "$(GREEN)โœ… Python Docker build passed$(NC)" @echo "" @echo "$(YELLOW)๐Ÿ“‹ Step 7/11: React Lint (oxlint)$(NC)" diff --git a/TODO.yml b/TODO.yml new file mode 100644 index 0000000..b661526 --- /dev/null +++ b/TODO.yml @@ -0,0 +1,16 @@ +issues: + - github_id: ~ + type: feat + title: Add a CLI all in one tool + status: in-progress + priority: medium + assignees: + - KevinDeBenedetti + body: | + ## Goal + Create a CLI tool that can be used to purge old deployments and workflow runs in GitHub Actions. This tool should be reusable and configurable, allowing users to specify how many recent deployments and workflow runs to keep. + + ## Acceptance criteria + - [ย ] The CLI tool should be able to connect to the GitHub API and authenticate using a personal access token. + - [ย ] The tool should allow users to specify the repository and the number of recent deployments and workflow runs to keep. + - [ย ] The tool should delete old deployments and workflow runs that exceed the specified number diff --git a/Dockerfile b/apps/Dockerfile similarity index 92% rename from Dockerfile rename to apps/Dockerfile index c7f13b5..271a5a6 100644 --- a/Dockerfile +++ b/apps/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/apps/Makefile b/apps/Makefile new file mode 100644 index 0000000..a57bfd6 --- /dev/null +++ b/apps/Makefile @@ -0,0 +1,203 @@ +.PHONY: help install dev run test lint format check start stop restart logs \ + clean clean-all + +# ============================================================================== +# Variables +# ============================================================================== +PYTHON_VERSION ?= 3.12 + +# Colors for display +RED = \033[0;31m +GREEN = \033[0;32m +YELLOW = \033[1;33m +BLUE = \033[0;34m +CYAN = \033[0;36m +NC = \033[0m + +# ============================================================================== +##@ Help +# ============================================================================== + +help: ## Display this help + @echo "" + @echo "$(BLUE)โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—$(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 "" + +# ============================================================================== +##@ Docker - Quick Start +# ============================================================================== + +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)" + +stop: ## Stop all containers + @echo "$(YELLOW)๐Ÿ›‘ Stopping Web-Check...$(NC)" + @docker compose --profile prod --profile dev down + @echo "$(GREEN)โœ… Stopped$(NC)" + +restart: stop start ## Restart production environment + +logs: ## View logs (all containers) + @docker compose logs -f + +logs-api: ## View API logs only + @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 +# ============================================================================== + +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; } + @uv python install $(PYTHON_VERSION) + @uv sync --all-extras --dev + @cd web && bun install + @echo "$(GREEN)โœ… Development environment ready!$(NC)" + +run: ## Run API locally (outside Docker) + @echo "$(GREEN)๐Ÿš€ Starting API locally...$(NC)" + @uv run uvicorn apps.api.main:app --host 0.0.0.0 --port 8000 --reload + +test: ## Run tests + @echo "$(GREEN)๐Ÿงช Running tests...$(NC)" + @uv run pytest apps/api/tests/ -v + +lint: ## Lint code + @echo "$(GREEN)๐Ÿ” Linting...$(NC)" + @uv run ruff check apps/ + +format: ## Format code + @echo "$(GREEN)โœจ Formatting code...$(NC)" + @uv run ruff format apps/ + +check: ## Run all code quality checks + @echo "$(GREEN)โœ… Running all checks...$(NC)" + @uv run ruff format --check apps/ + @uv run ruff check apps/ + @uv run ty check apps/api + @echo "$(GREEN)โœ… All checks passed!$(NC)" + +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 apps/ + @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 apps/ + @echo "$(GREEN)โœ… Python format check passed$(NC)" + @echo "" + @echo "$(YELLOW)๐Ÿ“‹ Step 4/11: Python Type Check (ty)$(NC)" + @uv run ty check apps/api + @echo "$(GREEN)โœ… Python type check passed$(NC)" + @echo "" + @echo "$(YELLOW)๐Ÿ“‹ Step 5/11: Python Tests (Pytest)$(NC)" + @uv run pytest apps/api/tests/ -m "not slow" --cov=apps.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 apps/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)" + @echo "" + @echo "$(BLUE)โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—$(NC)" + @echo "$(BLUE)โ•‘ $(GREEN)โœ… All CI Checks Passed Successfully!$(BLUE) โ•‘$(NC)" + @echo "$(BLUE)โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•$(NC)" + @echo "" + +# ============================================================================== +##@ Cleanup +# ============================================================================== + +clean: ## Clean output files + @echo "$(YELLOW)๐Ÿงน Cleaning outputs...$(NC)" + @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; \ + if [[ $$REPLY =~ ^[Yy]$$ ]]; then \ + echo "$(YELLOW)๐Ÿงน Cleaning everything...$(NC)"; \ + docker compose --profile prod --profile dev down -v; \ + docker system prune -f; \ + rm -rf outputs/*; \ + rm -rf web/dist web/node_modules; \ + echo "$(GREEN)โœ… Complete cleanup done$(NC)"; \ + fi + +.DEFAULT_GOAL := help 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 100% rename from alembic/env.py rename to apps/alembic/env.py 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/alembic/versions/1dd0c571c561_initial_database_schema.py b/apps/alembic/versions/1dd0c571c561_initial_database_schema.py similarity index 100% rename from alembic/versions/1dd0c571c561_initial_database_schema.py rename to apps/alembic/versions/1dd0c571c561_initial_database_schema.py 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 100% rename from api/database.py rename to apps/api/database.py diff --git a/api/main.py b/apps/api/main.py similarity index 100% rename from api/main.py rename to apps/api/main.py 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 100% rename from api/models/db_models.py rename to apps/api/models/db_models.py 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 100% rename from api/models/results.py rename to apps/api/models/results.py 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 100% rename from api/routers/advanced.py rename to apps/api/routers/advanced.py diff --git a/api/routers/deep.py b/apps/api/routers/deep.py similarity index 100% rename from api/routers/deep.py rename to apps/api/routers/deep.py 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 100% rename from api/routers/quick.py rename to apps/api/routers/quick.py diff --git a/api/routers/scans.py b/apps/api/routers/scans.py similarity index 100% rename from api/routers/scans.py rename to apps/api/routers/scans.py diff --git a/api/routers/security.py b/apps/api/routers/security.py similarity index 100% rename from api/routers/security.py rename to apps/api/routers/security.py 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 100% rename from api/services/db_service.py rename to apps/api/services/db_service.py 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 100% rename from api/services/nikto.py rename to apps/api/services/nikto.py diff --git a/api/services/nuclei.py b/apps/api/services/nuclei.py similarity index 100% rename from api/services/nuclei.py rename to apps/api/services/nuclei.py diff --git a/api/services/sqlmap_scanner.py b/apps/api/services/sqlmap_scanner.py similarity index 100% rename from api/services/sqlmap_scanner.py rename to apps/api/services/sqlmap_scanner.py diff --git a/api/services/sslyze_scanner.py b/apps/api/services/sslyze_scanner.py similarity index 100% rename from api/services/sslyze_scanner.py rename to apps/api/services/sslyze_scanner.py diff --git a/api/services/wapiti_scanner.py b/apps/api/services/wapiti_scanner.py similarity index 100% rename from api/services/wapiti_scanner.py rename to apps/api/services/wapiti_scanner.py diff --git a/api/services/xsstrike_scanner.py b/apps/api/services/xsstrike_scanner.py similarity index 100% rename from api/services/xsstrike_scanner.py rename to apps/api/services/xsstrike_scanner.py diff --git a/api/services/zap_native.py b/apps/api/services/zap_native.py similarity index 100% rename from api/services/zap_native.py rename to apps/api/services/zap_native.py 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 100% rename from api/tests/test_api.py rename to apps/api/tests/test_api.py diff --git a/api/tests/test_models.py b/apps/api/tests/test_models.py similarity index 100% rename from api/tests/test_models.py rename to apps/api/tests/test_models.py diff --git a/api/tests/test_scanners.py b/apps/api/tests/test_scanners.py similarity index 100% rename from api/tests/test_scanners.py rename to apps/api/tests/test_scanners.py 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 100% rename from api/utils/config.py rename to apps/api/utils/config.py 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..7bf1500 --- /dev/null +++ b/apps/cli/commands/config.py @@ -0,0 +1,60 @@ +"""Configuration command implementation.""" + +import structlog +import typer +from rich.console import Console +from rich.table import Table + +from cli.utils import CLISettings + +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) diff --git a/apps/cli/commands/results.py b/apps/cli/commands/results.py new file mode 100644 index 0000000..768ac94 --- /dev/null +++ b/apps/cli/commands/results.py @@ -0,0 +1,138 @@ +"""Results command implementation.""" + +import structlog +import typer +from rich.console import Console + +from cli.utils import CLISettings, APIClient, format_table, format_json + +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: + params = {"limit": limit} + if status: + params["status"] = status + + with console.status("[bold green]Fetching results..."): + response = client.get("/api/scans", **params) + + results = response.get("data", []) + + if not results: + console.print("[yellow]No scan results found[/yellow]") + return + + if output_format == "json": + format_json(results) + else: + # Format for table display + display_data = [] + for result in results: + display_data.append({ + "ID": result.get("id", "N/A")[:8], + "Module": result.get("module", "N/A"), + "Target": result.get("target", "N/A"), + "Status": result.get("status", "N/A"), + "Findings": len(result.get("findings", [])), + "Duration (ms)": result.get("duration_ms", 0), + }) + + 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) + 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..."): + response = client.get(f"/api/scans/{scan_id}") + + result = response.get("data") + + if not result: + console.print("[yellow]Scan result not found[/yellow]") + raise typer.Exit(1) + + if output_format == "json": + format_json(result) + else: + # Display detailed result + console.print(f"\n[bold cyan]Scan Details[/bold cyan]") + console.print(f"ID: {result.get('id', 'N/A')}") + console.print(f"Module: {result.get('module', 'N/A')}") + console.print(f"Target: {result.get('target', 'N/A')}") + console.print(f"Status: {result.get('status', 'N/A')}") + console.print(f"Duration: {result.get('duration_ms', 0)}ms") + console.print(f"Timestamp: {result.get('timestamp', 'N/A')}") + + if result.get("error"): + console.print(f"\n[red]Error: {result['error']}[/red]") + + if result.get("findings"): + console.print(f"\n[bold]Findings ({len(result['findings'])})[/bold]") + for i, finding in enumerate(result["findings"], 1): + severity = finding.get("severity", "unknown").upper() + console.print(f" [{i}] {finding.get('title', 'N/A')} ({severity})") + + 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) + finally: + client.close() + + +@results_app.command() +def clear( + confirm: bool = typer.Option( + False, "--confirm", help="Confirm deletion without prompt" + ), +) -> None: + """Clear all scan results.""" + if not confirm: + result = typer.confirm("Are you sure you want to delete all results?") + if not result: + console.print("[yellow]Operation cancelled[/yellow]") + return + + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Clearing results..."): + response = client.post("/api/scans/clear") + + console.print("[green]โœ“ All results cleared[/green]") + except Exception as e: + logger.error("clear_results_failed", error=str(e)) + console.print(f"[red]โœ— Failed to clear results: {e}[/red]") + raise typer.Exit(1) + finally: + client.close() diff --git a/apps/cli/commands/scan.py b/apps/cli/commands/scan.py new file mode 100644 index 0000000..741e3e8 --- /dev/null +++ b/apps/cli/commands/scan.py @@ -0,0 +1,171 @@ +"""Scan command implementation.""" + +from typing import Optional + +import structlog +import typer +from rich.console import Console +from rich.spinner import Spinner + +from cli.utils import CLISettings, APIClient, format_findings, format_json, format_table + +logger = structlog.get_logger() +console = Console() + +scan_app = typer.Typer(help="Scan operations") + + +@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.post( + "/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) + 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.post( + "/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) + finally: + client.close() + + +@scan_app.command() +def quick( + 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 quick security scan. + + Runs fast scanning modules (Nuclei + DNS checks). + """ + settings = CLISettings() + client = APIClient(settings.api_url, settings.api_timeout) + + try: + with console.status("[bold green]Running quick scan..."): + result = client.post( + "/api/quick/scan", + url=url, + timeout=timeout, + ) + + _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) + 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.post( + "/api/deep/ssl", + 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) + finally: + client.close() + + +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..eb61d5a --- /dev/null +++ b/apps/cli/main.py @@ -0,0 +1,76 @@ +"""Web-Check CLI main application.""" + +import sys + +import structlog +import typer +from rich.console import Console + +from cli import __version__ +from cli.commands import config_app, results_app, scan_app +from cli.utils import CLISettings + +logger = structlog.get_logger() +console = Console() + +app = typer.Typer( + help="Web-Check Security Scanner CLI", + pretty_exceptions_enable=False, +) + +# 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( + 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(), + ] + ) + + +def _show_version(value: bool) -> None: + """Display version and exit.""" + if value: + console.print(f"Web-Check CLI v{__version__}") + raise typer.Exit() + + +@app.command() +def health() -> None: + """Check API health status.""" + 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 = response.json() + + status = health.get("status", "unknown") + status_color = "green" if status == "healthy" else "yellow" + console.print(f"[{status_color}]API Status: {status}[/{status_color}]") + + except Exception as e: + console.print(f"[red]โœ— API unreachable: {e}[/red]") + raise typer.Exit(1) + + +if __name__ == "__main__": + app() diff --git a/apps/cli/utils/__init__.py b/apps/cli/utils/__init__.py new file mode 100644 index 0000000..0174076 --- /dev/null +++ b/apps/cli/utils/__init__.py @@ -0,0 +1,14 @@ +"""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..f133779 --- /dev/null +++ b/apps/cli/utils/config.py @@ -0,0 +1,25 @@ +"""CLI configuration and settings.""" + +from pathlib import Path +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, + ) + + +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..640602d --- /dev/null +++ b/apps/cli/utils/http_client.py @@ -0,0 +1,150 @@ +"""HTTP client utilities for API communication.""" + +import json +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, **params: Any) -> dict[str, Any]: + """Make POST request to API. + + Args: + endpoint: API endpoint path + **params: Query or body parameters + + Returns: + Response JSON + """ + url = f"{self.base_url}/{endpoint.lstrip('/')}" + try: + response = self.client.post(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 get(self, endpoint: str, **params: Any) -> dict[str, Any]: + """Make GET request to API. + + Args: + endpoint: API endpoint path + **params: Query parameters + + Returns: + Response JSON + """ + 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/apps/docker-compose.yml similarity index 97% rename from docker-compose.yml rename to apps/docker-compose.yml index 0d2b614..6cd5209 100644 --- a/docker-compose.yml +++ b/apps/docker-compose.yml @@ -13,7 +13,7 @@ services: # ========================================================================== web: build: - context: ./web + context: ../web dockerfile: Dockerfile container_name: web-check-web ports: @@ -38,7 +38,7 @@ services: ports: - "${WEB_PORT:-3000}:3000" volumes: - - ./web:/app + - ../web:/app - /app/node_modules command: sh -c "bun install && bun run dev -- --host" environment: @@ -54,7 +54,9 @@ services: # API Server # ========================================================================== api: - build: . + build: + context: .. + dockerfile: apps/Dockerfile container_name: web-check-api ports: - "${API_PORT:-8000}:8000" diff --git a/web/.dockerignore b/apps/web/.dockerignore similarity index 100% rename from web/.dockerignore rename to apps/web/.dockerignore diff --git a/web/.env.example b/apps/web/.env.example similarity index 100% rename from web/.env.example rename to apps/web/.env.example diff --git a/web/.gitignore b/apps/web/.gitignore similarity index 100% rename from web/.gitignore rename to apps/web/.gitignore diff --git a/web/.oxfmtrc.json b/apps/web/.oxfmtrc.json similarity index 100% rename from web/.oxfmtrc.json rename to apps/web/.oxfmtrc.json diff --git a/web/.oxlintrc.json b/apps/web/.oxlintrc.json similarity index 100% rename from web/.oxlintrc.json rename to apps/web/.oxlintrc.json diff --git a/web/Dockerfile b/apps/web/Dockerfile similarity index 100% rename from web/Dockerfile rename to apps/web/Dockerfile diff --git a/web/bun.lock b/apps/web/bun.lock similarity index 100% rename from web/bun.lock rename to apps/web/bun.lock diff --git a/web/components.json b/apps/web/components.json similarity index 100% rename from web/components.json rename to apps/web/components.json diff --git a/web/index.html b/apps/web/index.html similarity index 100% rename from web/index.html rename to apps/web/index.html diff --git a/web/nginx.conf b/apps/web/nginx.conf similarity index 100% rename from web/nginx.conf rename to apps/web/nginx.conf diff --git a/web/package.json b/apps/web/package.json similarity index 100% rename from web/package.json rename to apps/web/package.json diff --git a/web/postcss.config.js b/apps/web/postcss.config.js similarity index 100% rename from web/postcss.config.js rename to apps/web/postcss.config.js diff --git a/web/src/App.tsx b/apps/web/src/App.tsx similarity index 100% rename from web/src/App.tsx rename to apps/web/src/App.tsx diff --git a/web/src/components/ScanForm.tsx b/apps/web/src/components/ScanForm.tsx similarity index 100% rename from web/src/components/ScanForm.tsx rename to apps/web/src/components/ScanForm.tsx diff --git a/web/src/components/ScanLogStream.tsx b/apps/web/src/components/ScanLogStream.tsx similarity index 100% rename from web/src/components/ScanLogStream.tsx rename to apps/web/src/components/ScanLogStream.tsx diff --git a/web/src/components/ScanResult.tsx b/apps/web/src/components/ScanResult.tsx similarity index 100% rename from web/src/components/ScanResult.tsx rename to apps/web/src/components/ScanResult.tsx diff --git a/web/src/components/ScanStats.tsx b/apps/web/src/components/ScanStats.tsx similarity index 100% rename from web/src/components/ScanStats.tsx rename to apps/web/src/components/ScanStats.tsx diff --git a/web/src/components/ScanTimeline.tsx b/apps/web/src/components/ScanTimeline.tsx similarity index 100% rename from web/src/components/ScanTimeline.tsx rename to apps/web/src/components/ScanTimeline.tsx diff --git a/web/src/components/SeverityBadge.tsx b/apps/web/src/components/SeverityBadge.tsx similarity index 100% rename from web/src/components/SeverityBadge.tsx rename to apps/web/src/components/SeverityBadge.tsx diff --git a/web/src/components/ToolSelector.tsx b/apps/web/src/components/ToolSelector.tsx similarity index 100% rename from web/src/components/ToolSelector.tsx rename to apps/web/src/components/ToolSelector.tsx diff --git a/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx similarity index 100% rename from web/src/components/ui/accordion.tsx rename to apps/web/src/components/ui/accordion.tsx diff --git a/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx similarity index 100% rename from web/src/components/ui/badge.tsx rename to apps/web/src/components/ui/badge.tsx diff --git a/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx similarity index 100% rename from web/src/components/ui/button.tsx rename to apps/web/src/components/ui/button.tsx diff --git a/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx similarity index 100% rename from web/src/components/ui/card.tsx rename to apps/web/src/components/ui/card.tsx diff --git a/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx similarity index 100% rename from web/src/components/ui/checkbox.tsx rename to apps/web/src/components/ui/checkbox.tsx diff --git a/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx similarity index 100% rename from web/src/components/ui/input.tsx rename to apps/web/src/components/ui/input.tsx diff --git a/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx similarity index 100% rename from web/src/components/ui/label.tsx rename to apps/web/src/components/ui/label.tsx diff --git a/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx similarity index 100% rename from web/src/components/ui/tooltip.tsx rename to apps/web/src/components/ui/tooltip.tsx diff --git a/web/src/constants/tools.ts b/apps/web/src/constants/tools.ts similarity index 100% rename from web/src/constants/tools.ts rename to apps/web/src/constants/tools.ts diff --git a/web/src/index.css b/apps/web/src/index.css similarity index 100% rename from web/src/index.css rename to apps/web/src/index.css diff --git a/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts similarity index 100% rename from web/src/lib/utils.ts rename to apps/web/src/lib/utils.ts diff --git a/web/src/main.tsx b/apps/web/src/main.tsx similarity index 100% rename from web/src/main.tsx rename to apps/web/src/main.tsx diff --git a/web/src/services/api.ts b/apps/web/src/services/api.ts similarity index 100% rename from web/src/services/api.ts rename to apps/web/src/services/api.ts diff --git a/web/src/types/api.ts b/apps/web/src/types/api.ts similarity index 100% rename from web/src/types/api.ts rename to apps/web/src/types/api.ts diff --git a/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts similarity index 100% rename from web/src/vite-env.d.ts rename to apps/web/src/vite-env.d.ts diff --git a/web/tailwind.config.js b/apps/web/tailwind.config.js similarity index 100% rename from web/tailwind.config.js rename to apps/web/tailwind.config.js diff --git a/web/tsconfig.json b/apps/web/tsconfig.json similarity index 100% rename from web/tsconfig.json rename to apps/web/tsconfig.json diff --git a/web/tsconfig.node.json b/apps/web/tsconfig.node.json similarity index 100% rename from web/tsconfig.node.json rename to apps/web/tsconfig.node.json diff --git a/web/vite.config.ts b/apps/web/vite.config.ts similarity index 100% rename from web/vite.config.ts rename to apps/web/vite.config.ts diff --git a/pyproject.toml b/pyproject.toml index c64c25f..2a5d156 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,8 +22,13 @@ dependencies = [ "python-owasp-zap-v2.4>=0.0.22", "sslyze>=6.0.0", "sqlmap>=1.8.11", + "typer[all]>=0.12.0", + "rich>=13.7.0", ] +[project.scripts] +web-check = "cli.main:app" + [dependency-groups] dev = [ "pytest>=9.0.2", @@ -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..1a4bdd5 100644 --- a/uv.lock +++ b/uv.lock @@ -473,6 +473,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 +548,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" @@ -855,6 +876,19 @@ 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" @@ -881,6 +915,15 @@ wheels = [ { 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" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +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/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]] name = "six" version = "1.17.0" @@ -999,6 +1042,21 @@ wheels = [ { 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" }, ] +[[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/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]] name = "typing-extensions" version = "4.15.0" @@ -1169,10 +1227,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"] }, ] @@ -1197,10 +1257,12 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.7.1" }, { name = "python-json-logger", specifier = ">=4.0.0" }, { name = "python-owasp-zap-v2-4", specifier = ">=0.0.22" }, + { name = "rich", specifier = ">=13.7.0" }, { name = "sqlalchemy", specifier = ">=2.0.28,<2.1" }, { name = "sqlmap", specifier = ">=1.8.11" }, { name = "sslyze", specifier = ">=6.0.0" }, { name = "structlog", specifier = ">=25.5.0" }, + { name = "typer", extras = ["all"], specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, ] From 5fe629a8312d2364c8dce2613ed2e6aae6a92322 Mon Sep 17 00:00:00 2001 From: KevinDeBenedetti Date: Sat, 4 Apr 2026 14:28:44 +0200 Subject: [PATCH 2/3] refactor: restructure CI/CD workflows and enhance Docker setup - Updated GitHub Actions workflows for continuous deployment (cd.yml): - Simplified release process by using a dedicated release workflow. - Introduced separate jobs for deploying API and Web images. - Added documentation sync job and bot commit check for pull requests. - Refined continuous integration (ci.yml): - Consolidated Python and React CI jobs into reusable workflows. - Implemented security checks including secret scanning and audits. - Added bot commit guard to ensure only allowed bots can commit. - Created Dockerfile for the Web-Check API with optimized dependency management and setup. - Added docker-compose.yml for orchestrating services including API, web, and security scanners. - Introduced architecture and configuration documentation to enhance project clarity. - Removed outdated Makefile and TODO.yml, streamlining project structure. - Updated package.json for React to include type checking script. - Added comprehensive development documentation covering setup, testing, and code quality checks. --- .github/copilot-instructions.md | 381 ------------------ .github/workflows/cd.yml | 116 +++--- .github/workflows/ci.yml | 260 +++--------- apps/Dockerfile => Dockerfile | 0 TODO.md | 4 + TODO.yml | 16 - apps/Makefile | 203 ---------- apps/web/package.json | 1 + apps/docker-compose.yml => docker-compose.yml | 16 +- docs/architecture.md | 55 +++ docs/configuration.md | 64 +++ docs/development.md | 105 +++++ docs/index.md | 25 ++ 13 files changed, 368 insertions(+), 878 deletions(-) delete mode 100644 .github/copilot-instructions.md rename apps/Dockerfile => Dockerfile (100%) create mode 100644 TODO.md delete mode 100644 TODO.yml delete mode 100644 apps/Makefile rename apps/docker-compose.yml => docker-compose.yml (94%) create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/development.md create mode 100644 docs/index.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index c994576..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,381 +0,0 @@ -# GitHub Copilot Instructions - -This repository is **Web-Check**, a Docker-based security scanning toolkit. - -## Context - -- **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) - -## Code Style - -- 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 diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9565c3d..04f6f91 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,69 +1,63 @@ -name: CD - Release Please +name: CD on: push: branches: - main + pull_request_target: + types: [opened, synchronize] -permissions: - contents: write - issues: write - pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true 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 + # 1. 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 + + # 2. 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 + + # 3. Build & push Web Docker image (on release) + deploy-web: + name: Deploy Web + needs: release + if: needs.release.outputs.released == 'true' + uses: KevinDeBenedetti/github-workflows/.github/workflows/cd-docker.yml@main + with: + image-name: web-check-web + context: "./apps/web" + dockerfile: Dockerfile + tag-latest: true + version: ${{ needs.release.outputs.tag }} + secrets: inherit + + # 4. 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 + + # 5. Bot commit guard on PRs + bot-check: + name: Check bot commits + if: github.event_name == 'pull_request_target' + uses: KevinDeBenedetti/github-workflows/.github/workflows/check-bot-commits.yml@main + with: + allowed-bots: '["dependabot[bot]"]' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beeccb9..95ec25e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,6 @@ name: CI -permissions: - contents: read - on: pull_request: types: @@ -16,220 +13,65 @@ on: - "!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() }} + # 1. Python CI โ€” lint, format, typecheck, test + python: + name: Python CI + 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 - 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 + # 2. React CI โ€” lint, typecheck, build + react: + name: React CI + uses: KevinDeBenedetti/github-workflows/.github/workflows/ci-node.yml@main + with: + working-directory: "./apps/web" + package-manager: bun + run-lint: true + run-typecheck: true + run-test: false + run-build: true - 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 + # 3. Security โ€” secret scan + audits + security: + name: Security + uses: KevinDeBenedetti/github-workflows/.github/workflows/security.yml@main + with: + run-node-audit: true + node-working-directory: "./apps/web" + 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 - 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. Bot commit guard + bot-check: + name: Bot check + uses: KevinDeBenedetti/github-workflows/.github/workflows/check-bot-commits.yml@main + with: + allowed-bots: '["dependabot[bot]"]' + permissions: + contents: read + pull-requests: read - # 4. Final check - all jobs passed + # 5. Gate โ€” all required checks 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 + - python + - react steps: - name: All checks passed run: echo "โœ… All CI checks passed successfully!" diff --git a/apps/Dockerfile b/Dockerfile similarity index 100% rename from apps/Dockerfile rename to Dockerfile diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6382be5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +# TODO + +- FEAT: add documentation for web-check +- FEAT: add a chat bot using copilot sdk with UI \ No newline at end of file diff --git a/TODO.yml b/TODO.yml deleted file mode 100644 index b661526..0000000 --- a/TODO.yml +++ /dev/null @@ -1,16 +0,0 @@ -issues: - - github_id: ~ - type: feat - title: Add a CLI all in one tool - status: in-progress - priority: medium - assignees: - - KevinDeBenedetti - body: | - ## Goal - Create a CLI tool that can be used to purge old deployments and workflow runs in GitHub Actions. This tool should be reusable and configurable, allowing users to specify how many recent deployments and workflow runs to keep. - - ## Acceptance criteria - - [ย ] The CLI tool should be able to connect to the GitHub API and authenticate using a personal access token. - - [ย ] The tool should allow users to specify the repository and the number of recent deployments and workflow runs to keep. - - [ย ] The tool should delete old deployments and workflow runs that exceed the specified number diff --git a/apps/Makefile b/apps/Makefile deleted file mode 100644 index a57bfd6..0000000 --- a/apps/Makefile +++ /dev/null @@ -1,203 +0,0 @@ -.PHONY: help install dev run test lint format check start stop restart logs \ - clean clean-all - -# ============================================================================== -# Variables -# ============================================================================== -PYTHON_VERSION ?= 3.12 - -# Colors for display -RED = \033[0;31m -GREEN = \033[0;32m -YELLOW = \033[1;33m -BLUE = \033[0;34m -CYAN = \033[0;36m -NC = \033[0m - -# ============================================================================== -##@ Help -# ============================================================================== - -help: ## Display this help - @echo "" - @echo "$(BLUE)โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—$(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 "" - -# ============================================================================== -##@ Docker - Quick Start -# ============================================================================== - -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)" - -stop: ## Stop all containers - @echo "$(YELLOW)๐Ÿ›‘ Stopping Web-Check...$(NC)" - @docker compose --profile prod --profile dev down - @echo "$(GREEN)โœ… Stopped$(NC)" - -restart: stop start ## Restart production environment - -logs: ## View logs (all containers) - @docker compose logs -f - -logs-api: ## View API logs only - @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 -# ============================================================================== - -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; } - @uv python install $(PYTHON_VERSION) - @uv sync --all-extras --dev - @cd web && bun install - @echo "$(GREEN)โœ… Development environment ready!$(NC)" - -run: ## Run API locally (outside Docker) - @echo "$(GREEN)๐Ÿš€ Starting API locally...$(NC)" - @uv run uvicorn apps.api.main:app --host 0.0.0.0 --port 8000 --reload - -test: ## Run tests - @echo "$(GREEN)๐Ÿงช Running tests...$(NC)" - @uv run pytest apps/api/tests/ -v - -lint: ## Lint code - @echo "$(GREEN)๐Ÿ” Linting...$(NC)" - @uv run ruff check apps/ - -format: ## Format code - @echo "$(GREEN)โœจ Formatting code...$(NC)" - @uv run ruff format apps/ - -check: ## Run all code quality checks - @echo "$(GREEN)โœ… Running all checks...$(NC)" - @uv run ruff format --check apps/ - @uv run ruff check apps/ - @uv run ty check apps/api - @echo "$(GREEN)โœ… All checks passed!$(NC)" - -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 apps/ - @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 apps/ - @echo "$(GREEN)โœ… Python format check passed$(NC)" - @echo "" - @echo "$(YELLOW)๐Ÿ“‹ Step 4/11: Python Type Check (ty)$(NC)" - @uv run ty check apps/api - @echo "$(GREEN)โœ… Python type check passed$(NC)" - @echo "" - @echo "$(YELLOW)๐Ÿ“‹ Step 5/11: Python Tests (Pytest)$(NC)" - @uv run pytest apps/api/tests/ -m "not slow" --cov=apps.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 apps/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)" - @echo "" - @echo "$(BLUE)โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—$(NC)" - @echo "$(BLUE)โ•‘ $(GREEN)โœ… All CI Checks Passed Successfully!$(BLUE) โ•‘$(NC)" - @echo "$(BLUE)โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•$(NC)" - @echo "" - -# ============================================================================== -##@ Cleanup -# ============================================================================== - -clean: ## Clean output files - @echo "$(YELLOW)๐Ÿงน Cleaning outputs...$(NC)" - @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; \ - if [[ $$REPLY =~ ^[Yy]$$ ]]; then \ - echo "$(YELLOW)๐Ÿงน Cleaning everything...$(NC)"; \ - docker compose --profile prod --profile dev down -v; \ - docker system prune -f; \ - rm -rf outputs/*; \ - rm -rf web/dist web/node_modules; \ - echo "$(GREEN)โœ… Complete cleanup done$(NC)"; \ - fi - -.DEFAULT_GOAL := help diff --git a/apps/web/package.json b/apps/web/package.json index dbccf19..547e75e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ "lint:fix": "oxlint --fix .", "format": "oxfmt .", "format:check": "oxfmt --check .", + "typecheck": "tsc --noEmit", "check": "bun run format:check && bun run lint && tsc --noEmit" }, "dependencies": { diff --git a/apps/docker-compose.yml b/docker-compose.yml similarity index 94% rename from apps/docker-compose.yml rename to docker-compose.yml index 6cd5209..9c63d90 100644 --- a/apps/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: # ========================================================================== web: build: - context: ../web + context: ./apps/web dockerfile: Dockerfile container_name: web-check-web ports: @@ -38,7 +38,7 @@ services: ports: - "${WEB_PORT:-3000}:3000" volumes: - - ../web:/app + - ./apps/web:/app - /app/node_modules command: sh -c "bun install && bun run dev -- --host" environment: @@ -55,18 +55,18 @@ services: # ========================================================================== api: build: - context: .. - dockerfile: apps/Dockerfile + 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 + - ./apps/config:/app/config:ro - ./web-check.db:/app/web-check.db:rw - - ./alembic:/app/alembic:ro - - ./alembic.ini:/app/alembic.ini:ro + - ./apps/alembic:/app/alembic:ro + - ./apps/alembic.ini:/app/alembic.ini:ro environment: - DEBUG=${DEBUG:-false} - LOG_LEVEL=${LOG_LEVEL:-INFO} 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) From 943029700dc8f759a84217c6dc7cd908efc38cc9 Mon Sep 17 00:00:00 2001 From: KevinDeBenedetti Date: Sun, 5 Apr 2026 00:11:46 +0200 Subject: [PATCH 3/3] feat: code structure for improved readability and maintainability; remove redundant code blocks and optimize existing functions. --- .env.example | 8 + .github/copilot-instructions.md | 21 + .github/workflows/cd.yml | 63 -- .github/workflows/ci-cd.yml | 95 +++ .github/workflows/ci.yml | 77 --- Makefile | 220 +++---- TODO.md | 3 +- apps/alembic/env.py | 9 +- .../1dd0c571c561_initial_database_schema.py | 109 ++-- apps/api/database.py | 3 +- apps/api/main.py | 7 +- apps/api/models/db_models.py | 3 +- apps/api/models/results.py | 3 +- apps/api/routers/advanced.py | 3 +- apps/api/routers/deep.py | 3 +- apps/api/routers/quick.py | 8 +- apps/api/routers/scans.py | 7 +- apps/api/routers/security.py | 3 +- apps/api/services/db_service.py | 5 +- apps/api/services/nikto.py | 1 - apps/api/services/nuclei.py | 9 +- apps/api/services/sqlmap_scanner.py | 1 - apps/api/services/sslyze_scanner.py | 3 +- apps/api/services/wapiti_scanner.py | 7 +- apps/api/services/xsstrike_scanner.py | 1 - apps/api/services/zap_native.py | 3 +- apps/api/tests/test_api.py | 3 +- apps/api/tests/test_models.py | 5 +- apps/api/tests/test_scanners.py | 3 +- apps/api/utils/config.py | 12 + apps/cli/commands/config.py | 5 +- apps/cli/commands/results.py | 128 ++-- apps/cli/commands/scan.py | 183 +++++- apps/cli/main.py | 167 +++++- apps/cli/utils/__init__.py | 1 - apps/cli/utils/config.py | 2 +- apps/cli/utils/http_client.py | 12 +- apps/web/.dockerignore | 5 - apps/web/.env.example | 7 - apps/web/.gitignore | 11 - apps/web/.oxfmtrc.json | 12 - apps/web/.oxlintrc.json | 30 - apps/web/Dockerfile | 27 - apps/web/bun.lock | 564 ------------------ apps/web/components.json | 21 - apps/web/index.html | 13 - apps/web/nginx.conf | 35 -- apps/web/package.json | 45 -- apps/web/postcss.config.js | 6 - apps/web/src/App.tsx | 296 --------- apps/web/src/components/ScanForm.tsx | 118 ---- apps/web/src/components/ScanLogStream.tsx | 269 --------- apps/web/src/components/ScanResult.tsx | 257 -------- apps/web/src/components/ScanStats.tsx | 109 ---- apps/web/src/components/ScanTimeline.tsx | 138 ----- apps/web/src/components/SeverityBadge.tsx | 24 - apps/web/src/components/ToolSelector.tsx | 113 ---- apps/web/src/components/ui/accordion.tsx | 51 -- apps/web/src/components/ui/badge.tsx | 32 - apps/web/src/components/ui/button.tsx | 49 -- apps/web/src/components/ui/card.tsx | 55 -- apps/web/src/components/ui/checkbox.tsx | 28 - apps/web/src/components/ui/input.tsx | 22 - apps/web/src/components/ui/label.tsx | 19 - apps/web/src/components/ui/tooltip.tsx | 30 - apps/web/src/constants/tools.ts | 99 --- apps/web/src/index.css | 51 -- apps/web/src/lib/utils.ts | 10 - apps/web/src/main.tsx | 22 - apps/web/src/services/api.ts | 129 ---- apps/web/src/types/api.ts | 59 -- apps/web/src/vite-env.d.ts | 1 - apps/web/tailwind.config.js | 79 --- apps/web/tsconfig.json | 31 - apps/web/tsconfig.node.json | 10 - apps/web/vite.config.ts | 24 - docker-compose.yml | 52 +- pyproject.toml | 26 +- uv.lock | 244 ++++---- 79 files changed, 835 insertions(+), 3584 deletions(-) create mode 100644 .github/copilot-instructions.md delete mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci-cd.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 apps/web/.dockerignore delete mode 100644 apps/web/.env.example delete mode 100644 apps/web/.gitignore delete mode 100644 apps/web/.oxfmtrc.json delete mode 100644 apps/web/.oxlintrc.json delete mode 100644 apps/web/Dockerfile delete mode 100644 apps/web/bun.lock delete mode 100644 apps/web/components.json delete mode 100644 apps/web/index.html delete mode 100644 apps/web/nginx.conf delete mode 100644 apps/web/package.json delete mode 100644 apps/web/postcss.config.js delete mode 100644 apps/web/src/App.tsx delete mode 100644 apps/web/src/components/ScanForm.tsx delete mode 100644 apps/web/src/components/ScanLogStream.tsx delete mode 100644 apps/web/src/components/ScanResult.tsx delete mode 100644 apps/web/src/components/ScanStats.tsx delete mode 100644 apps/web/src/components/ScanTimeline.tsx delete mode 100644 apps/web/src/components/SeverityBadge.tsx delete mode 100644 apps/web/src/components/ToolSelector.tsx delete mode 100644 apps/web/src/components/ui/accordion.tsx delete mode 100644 apps/web/src/components/ui/badge.tsx delete mode 100644 apps/web/src/components/ui/button.tsx delete mode 100644 apps/web/src/components/ui/card.tsx delete mode 100644 apps/web/src/components/ui/checkbox.tsx delete mode 100644 apps/web/src/components/ui/input.tsx delete mode 100644 apps/web/src/components/ui/label.tsx delete mode 100644 apps/web/src/components/ui/tooltip.tsx delete mode 100644 apps/web/src/constants/tools.ts delete mode 100644 apps/web/src/index.css delete mode 100644 apps/web/src/lib/utils.ts delete mode 100644 apps/web/src/main.tsx delete mode 100644 apps/web/src/services/api.ts delete mode 100644 apps/web/src/types/api.ts delete mode 100644 apps/web/src/vite-env.d.ts delete mode 100644 apps/web/tailwind.config.js delete mode 100644 apps/web/tsconfig.json delete mode 100644 apps/web/tsconfig.node.json delete mode 100644 apps/web/vite.config.ts 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 new file mode 100644 index 0000000..60d858b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,21 @@ +# GitHub Copilot Instructions - Web Check + +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. + +## Agent Profile + +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. + +## Best practises + +- 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 04f6f91..0000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: CD - -on: - push: - branches: - - main - pull_request_target: - types: [opened, synchronize] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - # 1. 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 - - # 2. 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 - - # 3. Build & push Web Docker image (on release) - deploy-web: - name: Deploy Web - needs: release - if: needs.release.outputs.released == 'true' - uses: KevinDeBenedetti/github-workflows/.github/workflows/cd-docker.yml@main - with: - image-name: web-check-web - context: "./apps/web" - dockerfile: Dockerfile - tag-latest: true - version: ${{ needs.release.outputs.tag }} - secrets: inherit - - # 4. 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 - - # 5. Bot commit guard on PRs - bot-check: - name: Check bot commits - if: github.event_name == 'pull_request_target' - uses: KevinDeBenedetti/github-workflows/.github/workflows/check-bot-commits.yml@main - with: - allowed-bots: '["dependabot[bot]"]' 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 95ec25e..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,77 +0,0 @@ - -name: CI - -on: - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - branches: - - "**" - - "!release/**" - -jobs: - # 1. Python CI โ€” lint, format, typecheck, test - python: - name: Python CI - 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 - - # 2. React CI โ€” lint, typecheck, build - react: - name: React CI - uses: KevinDeBenedetti/github-workflows/.github/workflows/ci-node.yml@main - with: - working-directory: "./apps/web" - package-manager: bun - run-lint: true - run-typecheck: true - run-test: false - run-build: true - - # 3. Security โ€” secret scan + audits - security: - name: Security - uses: KevinDeBenedetti/github-workflows/.github/workflows/security.yml@main - with: - run-node-audit: true - node-working-directory: "./apps/web" - 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 - - # 4. Bot commit guard - bot-check: - name: Bot check - uses: KevinDeBenedetti/github-workflows/.github/workflows/check-bot-commits.yml@main - with: - allowed-bots: '["dependabot[bot]"]' - permissions: - contents: read - pull-requests: read - - # 5. Gate โ€” all required checks passed - ci-passed: - name: CI Passed - runs-on: ubuntu-latest - needs: - - python - - react - steps: - - name: All checks passed - run: echo "โœ… All CI checks passed successfully!" diff --git a/Makefile b/Makefile index a57bfd6..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)" - -run: ## Run API locally (outside Docker) - @echo "$(GREEN)๐Ÿš€ Starting API locally...$(NC)" - @uv run uvicorn apps.api.main:app --host 0.0.0.0 --port 8000 --reload - -test: ## Run tests - @echo "$(GREEN)๐Ÿงช Running tests...$(NC)" - @uv run pytest apps/api/tests/ -v - -lint: ## Lint code - @echo "$(GREEN)๐Ÿ” Linting...$(NC)" - @uv run ruff check apps/ - -format: ## Format code - @echo "$(GREEN)โœจ Formatting code...$(NC)" - @uv run ruff format apps/ - -check: ## Run all code quality checks - @echo "$(GREEN)โœ… Running all checks...$(NC)" - @uv run ruff format --check apps/ - @uv run ruff check apps/ - @uv run ty check apps/api - @echo "$(GREEN)โœ… All checks passed!$(NC)" - -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 apps/ - @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 apps/ - @echo "$(GREEN)โœ… Python format check passed$(NC)" - @echo "" - @echo "$(YELLOW)๐Ÿ“‹ Step 4/11: Python Type Check (ty)$(NC)" - @uv run ty check apps/api - @echo "$(GREEN)โœ… Python type check passed$(NC)" - @echo "" - @echo "$(YELLOW)๐Ÿ“‹ Step 5/11: Python Tests (Pytest)$(NC)" - @uv run pytest apps/api/tests/ -m "not slow" --cov=apps.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 apps/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)" + @uv sync --all-groups + @echo "$(GREEN)โœ… Development environment ready$(NC)" + +run: ## Run API locally (without Docker) + @uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload + +ARGS ?= guide +cli: ## Run CLI wizard (or: make cli ARGS="scan quick https://example.com") + @uv run python -m cli.main $(ARGS) + +test: ## Run Python tests + @uv run pytest -q + +lint: ## Lint all code (ruff) + @uv run ruff check . + +format: ## Format all code (ruff) + @uv run ruff format . + +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 index 6382be5..5218a92 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,5 @@ # TODO - FEAT: add documentation for web-check -- FEAT: add a chat bot using copilot sdk with UI \ No newline at end of file +- 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/apps/alembic/env.py b/apps/alembic/env.py index e5fa623..073f020 100644 --- a/apps/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/apps/alembic/versions/1dd0c571c561_initial_database_schema.py b/apps/alembic/versions/1dd0c571c561_initial_database_schema.py index 6320add..a4bd512 100644 --- a/apps/alembic/versions/1dd0c571c561_initial_database_schema.py +++ b/apps/alembic/versions/1dd0c571c561_initial_database_schema.py @@ -1,77 +1,82 @@ """Initial database schema Revision ID: 1dd0c571c561 -Revises: +Revises: Create Date: 2026-01-09 18:34:28.830369 """ -from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa +from collections.abc import Sequence +import sqlalchemy as sa +from alembic import op # 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 +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_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_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_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_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) + 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') + 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/api/database.py b/apps/api/database.py index 9c8cd7f..f5aeb19 100644 --- a/apps/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/apps/api/main.py b/apps/api/main.py index b253c23..f171978 100644 --- a/apps/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/apps/api/models/db_models.py b/apps/api/models/db_models.py index 59a1311..7267a8a 100644 --- a/apps/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/apps/api/models/results.py b/apps/api/models/results.py index 5ecaf88..bfa4eb4 100644 --- a/apps/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/apps/api/routers/advanced.py b/apps/api/routers/advanced.py index 81fdb48..bd8e1a1 100644 --- a/apps/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/apps/api/routers/deep.py b/apps/api/routers/deep.py index cf973c9..b35986d 100644 --- a/apps/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/apps/api/routers/quick.py b/apps/api/routers/quick.py index df9aca6..8a98520 100644 --- a/apps/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/apps/api/routers/scans.py b/apps/api/routers/scans.py index c9fe849..6815e3b 100644 --- a/apps/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/apps/api/routers/security.py b/apps/api/routers/security.py index 0d06390..cef77e4 100644 --- a/apps/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/apps/api/services/db_service.py b/apps/api/services/db_service.py index a858680..b83b8de 100644 --- a/apps/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/apps/api/services/nikto.py b/apps/api/services/nikto.py index 23314e2..c97df5f 100644 --- a/apps/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/apps/api/services/nuclei.py b/apps/api/services/nuclei.py index 0ae9fe8..a627151 100644 --- a/apps/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/apps/api/services/sqlmap_scanner.py b/apps/api/services/sqlmap_scanner.py index 761f134..535b9a2 100644 --- a/apps/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/apps/api/services/sslyze_scanner.py b/apps/api/services/sslyze_scanner.py index b8dbd82..e5a9bc8 100644 --- a/apps/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/apps/api/services/wapiti_scanner.py b/apps/api/services/wapiti_scanner.py index ec8810c..312aab9 100644 --- a/apps/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/apps/api/services/xsstrike_scanner.py b/apps/api/services/xsstrike_scanner.py index d670abf..dfc8834 100644 --- a/apps/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/apps/api/services/zap_native.py b/apps/api/services/zap_native.py index 660fee6..8eb35b9 100644 --- a/apps/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/apps/api/tests/test_api.py b/apps/api/tests/test_api.py index 6b12167..580370c 100644 --- a/apps/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/apps/api/tests/test_models.py b/apps/api/tests/test_models.py index 8649648..a05878e 100644 --- a/apps/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/apps/api/tests/test_scanners.py b/apps/api/tests/test_scanners.py index 0b57ec9..e9a11f5 100644 --- a/apps/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/apps/api/utils/config.py b/apps/api/utils/config.py index 4f5e2ba..1b3ed40 100644 --- a/apps/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/commands/config.py b/apps/cli/commands/config.py index 7bf1500..c86abc2 100644 --- a/apps/cli/commands/config.py +++ b/apps/cli/commands/config.py @@ -2,11 +2,10 @@ import structlog import typer +from cli.utils import CLISettings from rich.console import Console from rich.table import Table -from cli.utils import CLISettings - logger = structlog.get_logger() console = Console() @@ -57,4 +56,4 @@ def validate() -> None: 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) + raise typer.Exit(1) from None diff --git a/apps/cli/commands/results.py b/apps/cli/commands/results.py index 768ac94..334ceec 100644 --- a/apps/cli/commands/results.py +++ b/apps/cli/commands/results.py @@ -1,11 +1,12 @@ """Results command implementation.""" +import builtins + import structlog import typer +from cli.utils import APIClient, CLISettings, format_json, format_table from rich.console import Console -from cli.utils import CLISettings, APIClient, format_table, format_json - logger = structlog.get_logger() console = Console() @@ -23,40 +24,38 @@ def list( client = APIClient(settings.api_url, settings.api_timeout) try: - params = {"limit": limit} - if status: - params["status"] = status - with console.status("[bold green]Fetching results..."): - response = client.get("/api/scans", **params) + response = client.get("/api/scans") - results = response.get("data", []) + # 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 results: + if not scans: console.print("[yellow]No scan results found[/yellow]") return if output_format == "json": - format_json(results) + format_json(scans) else: - # Format for table display - display_data = [] - for result in results: - display_data.append({ - "ID": result.get("id", "N/A")[:8], - "Module": result.get("module", "N/A"), - "Target": result.get("target", "N/A"), - "Status": result.get("status", "N/A"), - "Findings": len(result.get("findings", [])), - "Duration (ms)": result.get("duration_ms", 0), - }) - + 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) + raise typer.Exit(1) from None finally: client.close() @@ -72,67 +71,74 @@ def show( try: with console.status("[bold green]Fetching scan result..."): - response = client.get(f"/api/scans/{scan_id}") - - result = response.get("data") + result = client.get(f"/api/scans/{scan_id}") if not result: console.print("[yellow]Scan result not found[/yellow]") - raise typer.Exit(1) + raise typer.Exit(1) from None if output_format == "json": format_json(result) else: - # Display detailed result - console.print(f"\n[bold cyan]Scan Details[/bold cyan]") - console.print(f"ID: {result.get('id', 'N/A')}") - console.print(f"Module: {result.get('module', 'N/A')}") - console.print(f"Target: {result.get('target', 'N/A')}") - console.print(f"Status: {result.get('status', 'N/A')}") - console.print(f"Duration: {result.get('duration_ms', 0)}ms") - console.print(f"Timestamp: {result.get('timestamp', 'N/A')}") - - if result.get("error"): - console.print(f"\n[red]Error: {result['error']}[/red]") - - if result.get("findings"): - console.print(f"\n[bold]Findings ({len(result['findings'])})[/bold]") - for i, finding in enumerate(result["findings"], 1): - severity = finding.get("severity", "unknown").upper() - console.print(f" [{i}] {finding.get('title', 'N/A')} ({severity})") + 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) + 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" - ), + confirm: bool = typer.Option(False, "--confirm", help="Confirm deletion without prompt"), ) -> None: - """Clear all scan results.""" + """Clear all scan results from the local database.""" if not confirm: - result = typer.confirm("Are you sure you want to delete all results?") - if not result: + if not typer.confirm("Are you sure you want to delete all results?"): console.print("[yellow]Operation cancelled[/yellow]") return - settings = CLISettings() - client = APIClient(settings.api_url, settings.api_timeout) + import subprocess try: - with console.status("[bold green]Clearing results..."): - response = client.post("/api/scans/clear") - - console.print("[green]โœ“ All results cleared[/green]") + 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: - logger.error("clear_results_failed", error=str(e)) console.print(f"[red]โœ— Failed to clear results: {e}[/red]") - raise typer.Exit(1) - finally: - client.close() + raise typer.Exit(1) from None diff --git a/apps/cli/commands/scan.py b/apps/cli/commands/scan.py index 741e3e8..4524a8b 100644 --- a/apps/cli/commands/scan.py +++ b/apps/cli/commands/scan.py @@ -1,19 +1,22 @@ """Scan command implementation.""" -from typing import Optional +import time import structlog import typer +from cli.utils import APIClient, CLISettings, format_findings, format_json from rich.console import Console -from rich.spinner import Spinner - -from cli.utils import CLISettings, APIClient, format_findings, format_json, format_table +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( @@ -30,7 +33,7 @@ def nuclei( try: with console.status("[bold green]Running Nuclei scan..."): - result = client.post( + result = client.get( "/api/quick/nuclei", url=url, timeout=timeout, @@ -40,7 +43,7 @@ def nuclei( 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) + raise typer.Exit(1) from None finally: client.close() @@ -60,7 +63,7 @@ def nikto( try: with console.status("[bold green]Running Nikto scan..."): - result = client.post( + result = client.get( "/api/quick/nikto", url=url, timeout=timeout, @@ -70,7 +73,7 @@ def nikto( 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) + raise typer.Exit(1) from None finally: client.close() @@ -78,29 +81,21 @@ def nikto( @scan_app.command() def quick( 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 quick security scan. - - Runs fast scanning modules (Nuclei + DNS checks). - """ + """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.post( - "/api/quick/scan", - url=url, - timeout=timeout, - ) + 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) + raise typer.Exit(1) from None finally: client.close() @@ -120,8 +115,8 @@ def ssl( try: with console.status("[bold green]Running SSL scan..."): - result = client.post( - "/api/deep/ssl", + result = client.get( + "/api/deep/sslyze", url=url, timeout=timeout, ) @@ -130,11 +125,151 @@ def ssl( 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) + 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. @@ -151,9 +286,7 @@ def _display_result(result: dict, output_format: str) -> None: else: # Display summary status_icon = "โœ“" if status == "success" else "โœ—" - console.print( - f"\n[bold]{status_icon} Scan Result[/bold] ({module} - {duration}ms)\n" - ) + 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]") diff --git a/apps/cli/main.py b/apps/cli/main.py index eb61d5a..456d845 100644 --- a/apps/cli/main.py +++ b/apps/cli/main.py @@ -1,14 +1,14 @@ """Web-Check CLI main application.""" -import sys - import structlog import typer -from rich.console import Console - from cli import __version__ from cli.commands import config_app, results_app, scan_app -from cli.utils import CLISettings +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() @@ -16,6 +16,7 @@ app = typer.Typer( help="Web-Check Security Scanner CLI", pretty_exceptions_enable=False, + invoke_without_command=True, ) # Register subcommands @@ -26,6 +27,7 @@ @app.callback() def main( + ctx: typer.Context, version: bool = typer.Option( None, "--version", @@ -42,6 +44,9 @@ def main( structlog.processors.JSONRenderer(), ] ) + # No subcommand given โ†’ launch interactive guide + if ctx.invoked_subcommand is None: + guide() def _show_version(value: bool) -> None: @@ -51,9 +56,140 @@ def _show_version(value: bool) -> None: 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 health() -> None: - """Check API health status.""" +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 @@ -61,15 +197,20 @@ def health() -> None: with httpx.Client(timeout=5) as client: response = client.get(f"{settings.api_url}/api/health") response.raise_for_status() - health = response.json() - - status = health.get("status", "unknown") - status_color = "green" if status == "healthy" else "yellow" - console.print(f"[{status_color}]API Status: {status}[/{status_color}]") + 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) + raise typer.Exit(1) from None + + +@app.command() +def health() -> None: + """Check API health status.""" + _run_health() if __name__ == "__main__": diff --git a/apps/cli/utils/__init__.py b/apps/cli/utils/__init__.py index 0174076..bb6a4d8 100644 --- a/apps/cli/utils/__init__.py +++ b/apps/cli/utils/__init__.py @@ -11,4 +11,3 @@ "format_json", "format_table", ] - diff --git a/apps/cli/utils/config.py b/apps/cli/utils/config.py index f133779..e723865 100644 --- a/apps/cli/utils/config.py +++ b/apps/cli/utils/config.py @@ -1,6 +1,5 @@ """CLI configuration and settings.""" -from pathlib import Path from pydantic_settings import BaseSettings, SettingsConfigDict @@ -17,6 +16,7 @@ class CLISettings(BaseSettings): env_file=".env", env_prefix="WEB_CHECK_CLI_", case_sensitive=False, + extra="ignore", ) diff --git a/apps/cli/utils/http_client.py b/apps/cli/utils/http_client.py index 640602d..a7cd821 100644 --- a/apps/cli/utils/http_client.py +++ b/apps/cli/utils/http_client.py @@ -1,6 +1,5 @@ """HTTP client utilities for API communication.""" -import json from typing import Any import httpx @@ -26,19 +25,20 @@ def __init__(self, base_url: str, timeout: int = 600): self.timeout = timeout self.client = httpx.Client(timeout=timeout, follow_redirects=True) - def post(self, endpoint: str, **params: Any) -> dict[str, Any]: + def post(self, endpoint: str, json: dict | None = None, **params: Any) -> dict[str, Any]: """Make POST request to API. Args: endpoint: API endpoint path - **params: Query or body parameters + json: Optional JSON body + **params: Query parameters Returns: Response JSON """ url = f"{self.base_url}/{endpoint.lstrip('/')}" try: - response = self.client.post(url, params=params) + 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: @@ -46,7 +46,7 @@ def post(self, endpoint: str, **params: Any) -> dict[str, Any]: console.print(f"[red]Error: {e}[/red]") raise - def get(self, endpoint: str, **params: Any) -> dict[str, Any]: + def get(self, endpoint: str, **params: Any) -> Any: """Make GET request to API. Args: @@ -54,7 +54,7 @@ def get(self, endpoint: str, **params: Any) -> dict[str, Any]: **params: Query parameters Returns: - Response JSON + Response JSON (dict or list depending on endpoint) """ url = f"{self.base_url}/{endpoint.lstrip('/')}" try: diff --git a/apps/web/.dockerignore b/apps/web/.dockerignore deleted file mode 100644 index af3f68a..0000000 --- a/apps/web/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -dist -.env -*.log -.DS_Store diff --git a/apps/web/.env.example b/apps/web/.env.example deleted file mode 100644 index e3b915c..0000000 --- a/apps/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/apps/web/.gitignore b/apps/web/.gitignore deleted file mode 100644 index 7efecad..0000000 --- a/apps/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/apps/web/.oxfmtrc.json b/apps/web/.oxfmtrc.json deleted file mode 100644 index e7702f8..0000000 --- a/apps/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/apps/web/.oxlintrc.json b/apps/web/.oxlintrc.json deleted file mode 100644 index 64dea1e..0000000 --- a/apps/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/apps/web/Dockerfile b/apps/web/Dockerfile deleted file mode 100644 index 962497f..0000000 --- a/apps/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/apps/web/bun.lock b/apps/web/bun.lock deleted file mode 100644 index b1ae227..0000000 --- a/apps/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/apps/web/components.json b/apps/web/components.json deleted file mode 100644 index 6d3998d..0000000 --- a/apps/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/apps/web/index.html b/apps/web/index.html deleted file mode 100644 index 47f6d78..0000000 --- a/apps/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Web-Check Security Scanner - - -
- - - diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf deleted file mode 100644 index 0a7d08f..0000000 --- a/apps/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/apps/web/package.json b/apps/web/package.json deleted file mode 100644 index 547e75e..0000000 --- a/apps/web/package.json +++ /dev/null @@ -1,45 +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 .", - "typecheck": "tsc --noEmit", - "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/apps/web/postcss.config.js b/apps/web/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/apps/web/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx deleted file mode 100644 index 2ea5205..0000000 --- a/apps/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/apps/web/src/components/ScanForm.tsx b/apps/web/src/components/ScanForm.tsx deleted file mode 100644 index 33959dd..0000000 --- a/apps/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/apps/web/src/components/ScanLogStream.tsx b/apps/web/src/components/ScanLogStream.tsx deleted file mode 100644 index 7e93302..0000000 --- a/apps/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/apps/web/src/components/ScanResult.tsx b/apps/web/src/components/ScanResult.tsx deleted file mode 100644 index 15fda5b..0000000 --- a/apps/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/apps/web/src/components/ScanStats.tsx b/apps/web/src/components/ScanStats.tsx deleted file mode 100644 index 50d4965..0000000 --- a/apps/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/apps/web/src/components/ScanTimeline.tsx b/apps/web/src/components/ScanTimeline.tsx deleted file mode 100644 index 129eca9..0000000 --- a/apps/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/apps/web/src/components/SeverityBadge.tsx b/apps/web/src/components/SeverityBadge.tsx deleted file mode 100644 index e31eae1..0000000 --- a/apps/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/apps/web/src/components/ToolSelector.tsx b/apps/web/src/components/ToolSelector.tsx deleted file mode 100644 index ec3a158..0000000 --- a/apps/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/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx deleted file mode 100644 index ba06ffa..0000000 --- a/apps/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/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx deleted file mode 100644 index e2367b4..0000000 --- a/apps/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/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx deleted file mode 100644 index 8eeb511..0000000 --- a/apps/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/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx deleted file mode 100644 index ea956e7..0000000 --- a/apps/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/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx deleted file mode 100644 index 968ed42..0000000 --- a/apps/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/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx deleted file mode 100644 index 7bad21e..0000000 --- a/apps/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/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx deleted file mode 100644 index dea5822..0000000 --- a/apps/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/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx deleted file mode 100644 index 6fdf76a..0000000 --- a/apps/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/apps/web/src/constants/tools.ts b/apps/web/src/constants/tools.ts deleted file mode 100644 index 3fed865..0000000 --- a/apps/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/apps/web/src/index.css b/apps/web/src/index.css deleted file mode 100644 index f76ba0c..0000000 --- a/apps/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/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts deleted file mode 100644 index 302216d..0000000 --- a/apps/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/apps/web/src/main.tsx b/apps/web/src/main.tsx deleted file mode 100644 index f73be90..0000000 --- a/apps/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/apps/web/src/services/api.ts b/apps/web/src/services/api.ts deleted file mode 100644 index aaa4eca..0000000 --- a/apps/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/apps/web/src/types/api.ts b/apps/web/src/types/api.ts deleted file mode 100644 index c868002..0000000 --- a/apps/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/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/apps/web/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js deleted file mode 100644 index dc57383..0000000 --- a/apps/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/apps/web/tsconfig.json b/apps/web/tsconfig.json deleted file mode 100644 index 33514fa..0000000 --- a/apps/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/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json deleted file mode 100644 index 42872c5..0000000 --- a/apps/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/apps/web/vite.config.ts b/apps/web/vite.config.ts deleted file mode 100644 index dbba8d2..0000000 --- a/apps/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, - }, - }, - }, -}); diff --git a/docker-compose.yml b/docker-compose.yml index 9c63d90..7bbd694 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,54 +2,12 @@ # 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: ./apps/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: - - ./apps/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 # ========================================================================== @@ -64,13 +22,14 @@ services: - ./apps/api:/app/api:ro - ./outputs:/app/outputs:rw - ./apps/config:/app/config:ro - - ./web-check.db:/app/web-check.db:rw + - ./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 @@ -120,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/pyproject.toml b/pyproject.toml index 2a5d156..2cc3722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,21 +8,21 @@ 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", - "typer[all]>=0.12.0", + "sslyze>=6.3.1", + "sqlmap>=1.10.3", + "typer>=0.12.0", "rich>=13.7.0", ] @@ -33,10 +33,10 @@ web-check = "cli.main:app" 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] diff --git a/uv.lock b/uv.lock index 1a4bdd5..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]] @@ -602,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]] @@ -721,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]] @@ -773,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]] @@ -796,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]] @@ -891,28 +892,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.11" +version = "0.15.9" 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/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/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/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]] @@ -935,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" }, @@ -984,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" @@ -1019,27 +1030,26 @@ 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/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/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]] @@ -1089,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] @@ -1249,31 +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 = "rich", specifier = ">=13.7.0" }, - { name = "sqlalchemy", specifier = ">=2.0.28,<2.1" }, - { name = "sqlmap", specifier = ">=1.8.11" }, - { name = "sslyze", specifier = ">=6.0.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 = "typer", extras = ["all"], specifier = ">=0.12.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]]