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" }, ]