diff --git a/README.testing.md b/README.testing.md index 4fe9d2d..a6b49ce 100644 --- a/README.testing.md +++ b/README.testing.md @@ -62,10 +62,12 @@ Full WAF evaluation with real target apps plus the CRS Albedo backend: ```sh # Prerequisites -cp deploy/demo/.env.example deploy/demo/.env +cp deploy/docker/.env.example deploy/docker/.env cp benchmarks/lab/.env.example benchmarks/lab/.env git submodule update --init --recursive +# Ensure deploy/docker/.env has ADMIN_EMAIL and ADMIN_PASSWORD set. + # Bring up the lab make eval-up diff --git a/benchmarks/Makefile b/benchmarks/Makefile index 4e0c6dd..1f5d28d 100644 --- a/benchmarks/Makefile +++ b/benchmarks/Makefile @@ -1,9 +1,10 @@ REPO_ROOT := $(shell git rev-parse --show-toplevel 2>/dev/null || pwd) -DEMO_COMPOSE := $(REPO_ROOT)/deploy/demo/docker-compose.yml +CORE_COMPOSE := $(REPO_ROOT)/deploy/docker/docker-compose.yml LAB_COMPOSE := $(REPO_ROOT)/benchmarks/lab/docker-compose.targets.yml -DEMO_ENV := $(REPO_ROOT)/deploy/demo/.env +CORE_ENV := $(REPO_ROOT)/deploy/docker/.env LAB_ENV := $(REPO_ROOT)/benchmarks/lab/.env RUNNERS := $(REPO_ROOT)/benchmarks/lab/runners +COMPOSE := docker compose -f $(CORE_COMPOSE) -f $(LAB_COMPOSE) --env-file $(CORE_ENV) --env-file $(LAB_ENV) RUN_ID ?= $(shell date +%Y%m%d-%H%M%S) TARGET_VHOST ?= @@ -16,17 +17,13 @@ DIRECT_PORT ?= 3000 # ── Lab lifecycle ────────────────────────────────────────────────────────── -## Bring up the demo + all lab targets and register vhosts. +## Bring up the real stack + all lab targets and register vhosts. lab-up: - @echo "==> Starting guard-proxy demo + lab targets..." - docker compose \ - -f $(DEMO_COMPOSE) \ - -f $(LAB_COMPOSE) \ - --env-file $(DEMO_ENV) \ - --env-file $(LAB_ENV) \ - up -d --build + @echo "==> Starting guard-proxy stack + lab targets..." + $(COMPOSE) up -d --build + @echo "==> Seeding admin user..." + $(COMPOSE) exec -T backend /app/.venv/bin/python scripts/seed_admin.py @echo "==> Seeding vhosts..." - bash $(REPO_ROOT)/deploy/demo/setup-demo.sh bash $(REPO_ROOT)/benchmarks/lab/setup-lab.sh --skip-compose ## Stop the lab (preserve volumes). @@ -97,7 +94,7 @@ help: @echo "Guard Proxy Evaluation Lab" @echo "" @echo "Setup:" - @echo " cp deploy/demo/.env.example deploy/demo/.env" + @echo " cp deploy/docker/.env.example deploy/docker/.env" @echo " cp benchmarks/lab/.env.example benchmarks/lab/.env" @echo " git submodule update --init --recursive" @echo "" diff --git a/benchmarks/lab/docker-compose.targets.yml b/benchmarks/lab/docker-compose.targets.yml index 871a2d6..3a1a227 100644 --- a/benchmarks/lab/docker-compose.targets.yml +++ b/benchmarks/lab/docker-compose.targets.yml @@ -1,10 +1,10 @@ # Evaluation lab target applications. # -# This is an OVERLAY on top of the demo stack. Run as: +# This is an OVERLAY on top of the real Guard Proxy stack. Run as: # docker compose \ -# -f deploy/demo/docker-compose.yml \ +# -f deploy/docker/docker-compose.yml \ # -f benchmarks/lab/docker-compose.targets.yml \ -# --env-file deploy/demo/.env \ +# --env-file deploy/docker/.env \ # --env-file benchmarks/lab/.env \ # up -d --build # @@ -14,9 +14,12 @@ # Pinned image digests ensure reproducible test results across runs. # Update pins by running: docker pull && docker inspect --format '{{index .RepoDigests 0}}' -name: guard-proxy-demo +name: guard-proxy services: + backend: + ports: + - "${BACKEND_HTTP_PORT:-8000}:8000" # ── OWASP Juice Shop ────────────────────────────────────────────────────── # Intentionally vulnerable Node.js app designed for security testing. @@ -163,7 +166,7 @@ services: networks: gp_internal: external: true - name: guard-proxy-demo_gp_internal + name: guard-proxy_gp_internal volumes: dvwa_db_data: diff --git a/benchmarks/lab/runners/lib.sh b/benchmarks/lab/runners/lib.sh index a29e341..804d2fd 100755 --- a/benchmarks/lab/runners/lib.sh +++ b/benchmarks/lab/runners/lib.sh @@ -13,17 +13,17 @@ REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" LAB_DIR="${REPO_ROOT}/benchmarks/lab" RESULTS_BASE="${REPO_ROOT}/benchmarks/results" RUN_DIR="${RESULTS_BASE}/run-${RUN_ID}" -DEMO_ENV="${REPO_ROOT}/deploy/demo/.env" +CORE_ENV="${REPO_ROOT}/deploy/docker/.env" LAB_ENV="${LAB_DIR}/.env" -# Docker network shared by the demo stack and targets. -DOCKER_NETWORK="guard-proxy-demo_gp_internal" +# Docker network shared by the real stack and lab targets. +DOCKER_NETWORK="guard-proxy_gp_internal" # ── Environment helpers ──────────────────────────────────────────────────── env_value() { local name="$1"; local fallback="${2:-}"; local value - value="$(grep -E "^${name}=" "${LAB_ENV}" "${DEMO_ENV}" 2>/dev/null | tail -n 1 | cut -d= -f2- || true)" + value="$(grep -E "^${name}=" "${CORE_ENV}" "${LAB_ENV}" 2>/dev/null | tail -n 1 | cut -d= -f2- || true)" if [[ -z "${value}" ]]; then printf '%s' "${fallback}"; else printf '%s' "${value}"; fi } @@ -90,9 +90,9 @@ PY compose_container_id() { local service="$1" docker compose \ - -f "${REPO_ROOT}/deploy/demo/docker-compose.yml" \ + -f "${REPO_ROOT}/deploy/docker/docker-compose.yml" \ -f "${LAB_DIR}/docker-compose.targets.yml" \ - --env-file "${DEMO_ENV}" \ + --env-file "${CORE_ENV}" \ --env-file "${LAB_ENV}" \ ps -q "${service}" 2>/dev/null || true } diff --git a/benchmarks/lab/scenarios/crs-ftw/config.yaml b/benchmarks/lab/scenarios/crs-ftw/config.yaml index c1468c0..86bf886 100644 --- a/benchmarks/lab/scenarios/crs-ftw/config.yaml +++ b/benchmarks/lab/scenarios/crs-ftw/config.yaml @@ -6,7 +6,7 @@ # # Usage (from repo root): # docker run --rm \ -# --network guard-proxy-demo_gp_internal \ +# --network guard-proxy_gp_internal \ # -v "$(pwd)/configs/coraza/crs/tests/regression/tests:/tests:ro" \ # -v "$(pwd)/benchmarks/lab/scenarios/crs-ftw/config.yaml:/config.yaml:ro" \ # ghcr.io/coreruleset/go-ftw:latest \ diff --git a/benchmarks/lab/setup-lab.sh b/benchmarks/lab/setup-lab.sh index 95e6fb9..d93625c 100755 --- a/benchmarks/lab/setup-lab.sh +++ b/benchmarks/lab/setup-lab.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # setup-lab.sh — Bring up the evaluation lab and register all target vhosts. # -# Extends the demo stack with WordPress/Juice Shop/DVWA targets, seeds two +# Extends the real Guard Proxy stack with WordPress/Juice Shop/DVWA targets, seeds two # WAF policies (baseline PL1 and high-paranoia PL2), and wires each target # domain through HAProxy via the guard-proxy backend API. # # Prerequisites: -# - deploy/demo/.env (copy from deploy/demo/.env.example) +# - deploy/docker/.env (copy from deploy/docker/.env.example) # - benchmarks/lab/.env (copy from benchmarks/lab/.env.example) # - CRS submodule initialised: git submodule update --init --recursive # - Docker with Docker Compose v2 @@ -17,9 +17,9 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" -DEMO_COMPOSE="${REPO_ROOT}/deploy/demo/docker-compose.yml" +CORE_COMPOSE="${REPO_ROOT}/deploy/docker/docker-compose.yml" TARGETS_COMPOSE="${SCRIPT_DIR}/docker-compose.targets.yml" -DEMO_ENV="${REPO_ROOT}/deploy/demo/.env" +CORE_ENV="${REPO_ROOT}/deploy/docker/.env" LAB_ENV="${SCRIPT_DIR}/.env" TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-240}" SKIP_COMPOSE=false @@ -30,7 +30,7 @@ for arg in "$@"; do esac done -for f in "${DEMO_ENV}" "${LAB_ENV}"; do +for f in "${CORE_ENV}" "${LAB_ENV}"; do if [[ ! -f "${f}" ]]; then echo "Missing ${f}. Copy the matching .env.example first." >&2 exit 1 @@ -38,21 +38,21 @@ for f in "${DEMO_ENV}" "${LAB_ENV}"; do done if docker compose version >/dev/null 2>&1; then - COMPOSE=(docker compose -f "${DEMO_COMPOSE}" -f "${TARGETS_COMPOSE}" --env-file "${DEMO_ENV}" --env-file "${LAB_ENV}") + COMPOSE=(docker compose -f "${CORE_COMPOSE}" -f "${TARGETS_COMPOSE}" --env-file "${CORE_ENV}" --env-file "${LAB_ENV}") elif command -v docker-compose >/dev/null 2>&1; then - COMPOSE=(docker-compose -f "${DEMO_COMPOSE}" -f "${TARGETS_COMPOSE}" --env-file "${DEMO_ENV}" --env-file "${LAB_ENV}") + COMPOSE=(docker-compose -f "${CORE_COMPOSE}" -f "${TARGETS_COMPOSE}" --env-file "${CORE_ENV}" --env-file "${LAB_ENV}") else echo "Docker Compose is required." >&2 exit 1 fi -# ── Helpers (mirrored from deploy/demo/setup-demo.sh) ────────────────────── +# ── Helpers ──────────────────────────────────────────────────────────────── env_value() { local name="$1" local fallback="${2:-}" local value - value="$(grep -E "^${name}=" "${LAB_ENV}" "${DEMO_ENV}" 2>/dev/null | tail -n 1 | cut -d= -f2- || true)" + value="$(grep -E "^${name}=" "${CORE_ENV}" "${LAB_ENV}" 2>/dev/null | tail -n 1 | cut -d= -f2- || true)" if [[ -z "${value}" ]]; then printf '%s' "${fallback}"; else printf '%s' "${value}"; fi } @@ -154,14 +154,12 @@ PY ensure_crs_bundle if [[ "${SKIP_COMPOSE}" == false ]]; then - echo "Starting demo + lab target stack..." + echo "Starting Guard Proxy + lab target stack..." "${COMPOSE[@]}" up -d --build wait_for_healthy backend wait_for_healthy coraza wait_for_healthy haproxy - wait_for_healthy demo-app - wait_for_healthy demo-api wait_for_healthy juiceshop wait_for_healthy dvwa wait_for_healthy wordpress diff --git a/benchmarks/lab/teardown-lab.sh b/benchmarks/lab/teardown-lab.sh index 955b37a..9a66ca0 100755 --- a/benchmarks/lab/teardown-lab.sh +++ b/benchmarks/lab/teardown-lab.sh @@ -9,9 +9,9 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" -DEMO_COMPOSE="${REPO_ROOT}/deploy/demo/docker-compose.yml" +CORE_COMPOSE="${REPO_ROOT}/deploy/docker/docker-compose.yml" TARGETS_COMPOSE="${SCRIPT_DIR}/docker-compose.targets.yml" -DEMO_ENV="${REPO_ROOT}/deploy/demo/.env" +CORE_ENV="${REPO_ROOT}/deploy/docker/.env" LAB_ENV="${SCRIPT_DIR}/.env" CLEAN=false @@ -20,11 +20,11 @@ for arg in "$@"; do done if docker compose version >/dev/null 2>&1; then - COMPOSE=(docker compose -f "${DEMO_COMPOSE}" -f "${TARGETS_COMPOSE}") - [[ -f "${DEMO_ENV}" ]] && COMPOSE+=(--env-file "${DEMO_ENV}") + COMPOSE=(docker compose -f "${CORE_COMPOSE}" -f "${TARGETS_COMPOSE}") + [[ -f "${CORE_ENV}" ]] && COMPOSE+=(--env-file "${CORE_ENV}") [[ -f "${LAB_ENV}" ]] && COMPOSE+=(--env-file "${LAB_ENV}") else - COMPOSE=(docker-compose -f "${DEMO_COMPOSE}" -f "${TARGETS_COMPOSE}") + COMPOSE=(docker-compose -f "${CORE_COMPOSE}" -f "${TARGETS_COMPOSE}") fi if [[ "${CLEAN}" == true ]]; then diff --git a/deploy/demo/.env.example b/deploy/demo/.env.example deleted file mode 100644 index 3fd82d9..0000000 --- a/deploy/demo/.env.example +++ /dev/null @@ -1,25 +0,0 @@ -# Copy this file to .env before running the demo. -# These values are local-demo defaults only. - -POSTGRES_USER=guard_proxy -POSTGRES_PASSWORD=guard_proxy_demo_password -POSTGRES_DB=guard_proxy - -JWT_SECRET_KEY=guard-proxy-demo-jwt-secret-key-please-change -LOG_INGEST_SHARED_SECRET=guard-proxy-demo-log-ingest-shared-secret-please-change - -CORS_ORIGINS=["http://localhost:3000","http://127.0.0.1:3000"] -VITE_API_BASE_URL=http://localhost:8000 -HAPROXY_HTTP_PORT=8080 -HAPROXY_HTTPS_PORT=8443 -BACKEND_HTTP_PORT=8000 -FRONTEND_HTTP_PORT=3000 - -ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=GuardProxyDemo12345 -ADMIN_FULL_NAME=Demo Administrator - -DEMO_DOMAIN=app.local -DEMO_BACKEND_URL=http://demo-app:8080 -DEMO_SECOND_DOMAIN=api.local -DEMO_SECOND_BACKEND_URL=http://demo-api:8080 diff --git a/deploy/demo/README.md b/deploy/demo/README.md deleted file mode 100644 index 6a7ebe2..0000000 --- a/deploy/demo/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Guard Proxy demo - -This directory starts a self-contained local demo stack: - -- Guard Proxy backend, frontend, HAProxy, Coraza, log shipper, PostgreSQL -- two small HTTP echo applications behind HAProxy/Coraza -- a setup script that creates an admin, creates a demo WAF policy, creates - `app.local` and `api.local` vhosts, and applies runtime config - -## Run - -```bash -cp deploy/demo/.env.example deploy/demo/.env -git submodule update --init --recursive -chmod +x deploy/demo/setup-demo.sh -./deploy/demo/setup-demo.sh -``` - -Open the admin panel: - -```text -http://localhost:3000 -``` - -The demo exposes the backend directly on `http://localhost:8000` for the admin -panel and setup script. HAProxy listens on `http://localhost:8080` for HTTP WAF -traffic and on `https://localhost:8443` for a temporary self-signed TLS demo. - -The TLS bind is intentionally a demo-only patch applied after `/config/apply`. -The next generated config apply can overwrite it. - -Default local credentials from `.env.example`: - -```text -admin@example.com -GuardProxyDemo12345 -``` - -## Try routing through the WAF - -First app: - -```bash -curl -i -H 'Host: app.local' 'http://127.0.0.1:8080/hello?name=demo' -``` - -Expected result: HTTP 200 from `demo-frontend-app`. - -Second app: - -```bash -curl -i -H 'Host: api.local' 'http://127.0.0.1:8080/v1/status' -``` - -Expected result: HTTP 200 from `demo-api-service`. - -## Trigger a WAF block - -This requires the OWASP CRS submodule to be initialized before building the demo. -The setup script checks this and stops early if `configs/coraza/crs/rules` is -missing. - -HTTP: - -```bash -curl -i -H 'Host: app.local' "http://127.0.0.1:8080/?id=1%27%20OR%20%271%27%3D%271" -``` - -HTTPS: - -```bash -curl -k -i -H 'Host: api.local' "https://127.0.0.1:8443/?id=1%27%20OR%20%271%27%3D%271" -``` - -Expected result: HTTP 403 from the WAF. - -TLS demo: - -```bash -curl -k -i -H 'Host: app.local' 'https://127.0.0.1:8443/hello?tls=1' -curl -k -i -H 'Host: api.local' 'https://127.0.0.1:8443/v1/status' -``` - -## Stop - -```bash -docker compose --env-file deploy/demo/.env -f deploy/demo/docker-compose.yml down -``` - -Remove demo volumes too: - -```bash -docker compose --env-file deploy/demo/.env -f deploy/demo/docker-compose.yml down -v -``` diff --git a/deploy/demo/app/Dockerfile b/deploy/demo/app/Dockerfile deleted file mode 100644 index 5b70770..0000000 --- a/deploy/demo/app/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM python:3.13-alpine - -WORKDIR /app -COPY server.py /app/server.py - -EXPOSE 8080 - -CMD ["python", "/app/server.py"] diff --git a/deploy/demo/app/server.py b/deploy/demo/app/server.py deleted file mode 100644 index e18c79c..0000000 --- a/deploy/demo/app/server.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import json -import os -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer - - -class EchoHandler(BaseHTTPRequestHandler): - server_version = "guard-proxy-demo-echo/0.1" - - def do_GET(self) -> None: - self._send_echo() - - def do_POST(self) -> None: - self._send_echo() - - def do_PUT(self) -> None: - self._send_echo() - - def do_DELETE(self) -> None: - self._send_echo() - - def _send_echo(self) -> None: - body = { - "service": os.getenv("DEMO_APP_NAME", "guard-proxy-demo-echo"), - "method": self.command, - "path": self.path, - "client": self.client_address[0], - "headers": { - key: value - for key, value in self.headers.items() - if key.lower() - in { - "host", - "user-agent", - "x-forwarded-for", - "x-request-id", - "x-waf-score", - } - }, - } - payload = json.dumps(body, indent=2, sort_keys=True).encode("utf-8") - - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", str(len(payload))) - self.end_headers() - self.wfile.write(payload) - - def log_message(self, format: str, *args: object) -> None: - print(f"{self.address_string()} - {format % args}") - - -def main() -> None: - app_name = os.getenv("DEMO_APP_NAME", "guard-proxy-demo-echo") - server = ThreadingHTTPServer(("0.0.0.0", 8080), EchoHandler) - print(f"{app_name} listening on :8080") - server.serve_forever() - - -if __name__ == "__main__": - main() diff --git a/deploy/demo/certs/.gitignore b/deploy/demo/certs/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/deploy/demo/certs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/deploy/demo/docker-compose.yml b/deploy/demo/docker-compose.yml deleted file mode 100644 index 56c3738..0000000 --- a/deploy/demo/docker-compose.yml +++ /dev/null @@ -1,207 +0,0 @@ -name: guard-proxy-demo - -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] - interval: 5s - timeout: 5s - retries: 12 - networks: - - gp_internal - - backend: - build: - context: ../../src/backend - dockerfile: Dockerfile - restart: unless-stopped - env_file: - - .env - environment: - DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}" - GUARD_PROXY_RUNTIME_DIR: /var/lib/guard-proxy/generated - depends_on: - postgres: - condition: service_healthy - ports: - - "${BACKEND_HTTP_PORT:-8000}:8000" - healthcheck: - test: - [ - "CMD-SHELL", - "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)\"", - ] - interval: 10s - timeout: 5s - retries: 10 - volumes: - - backend_logs:/var/log/guard-proxy - - ../../configs/haproxy/coraza.cfg:/usr/local/etc/haproxy/coraza.cfg:ro - - generated_config:/var/lib/guard-proxy/generated - - haproxy_runtime:/var/run/haproxy - networks: - - gp_internal - - frontend: - build: - context: ../../src/frontend - dockerfile: Dockerfile - restart: unless-stopped - environment: - VITE_API_BASE_URL: ${VITE_API_BASE_URL} - depends_on: - backend: - condition: service_healthy - ports: - - "${FRONTEND_HTTP_PORT:-3000}:5173" - healthcheck: - test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:5173 >/dev/null"] - interval: 10s - timeout: 5s - retries: 10 - networks: - - gp_edge - - demo-app: - build: - context: ./app - dockerfile: Dockerfile - environment: - DEMO_APP_NAME: demo-frontend-app - restart: unless-stopped - healthcheck: - test: - [ - "CMD-SHELL", - "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health', timeout=2)\"", - ] - interval: 10s - timeout: 5s - retries: 10 - networks: - - gp_internal - - demo-api: - build: - context: ./app - dockerfile: Dockerfile - environment: - DEMO_APP_NAME: demo-api-service - restart: unless-stopped - healthcheck: - test: - [ - "CMD-SHELL", - "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health', timeout=2)\"", - ] - interval: 10s - timeout: 5s - retries: 10 - networks: - - gp_internal - - coraza: - build: - context: ../.. - dockerfile: deploy/docker/coraza.Dockerfile - restart: unless-stopped - depends_on: - backend: - condition: service_healthy - volumes: - - ../../configs/coraza/coraza-spoa.yaml:/etc/coraza-spoa/coraza-spoa.yaml:ro - - ../../configs/coraza/coraza.conf:/etc/coraza/coraza.conf:ro - - ../../configs/coraza/crs-setup.conf:/etc/coraza/crs-setup.conf:ro - - ../../configs/coraza/crs:/etc/coraza/crs:ro - - generated_config:/runtime:ro - - coraza_audit:/var/log/coraza - healthcheck: - test: ["CMD", "nc", "-z", "127.0.0.1", "9000"] - interval: 10s - timeout: 5s - retries: 10 - networks: - - gp_internal - - log-shipper: - build: - context: ../../src/log-shipper - dockerfile: Dockerfile - restart: unless-stopped - env_file: - - .env - environment: - INGEST_URL: "http://backend:8000/logs/ingest" - CORAZA_AUDIT_LOG: "/var/log/coraza/audit.log" - SHIPPER_STATE_FILE: "/var/lib/log-shipper/offset" - depends_on: - backend: - condition: service_healthy - coraza: - condition: service_started - volumes: - - coraza_audit:/var/log/coraza:ro - - shipper_state:/var/lib/log-shipper - networks: - - gp_internal - - haproxy: - image: haproxy:3.0-alpine - user: "0:0" - command: - [ - "sh", - "-c", - "set -eu; if [ ! -f /etc/haproxy/generated/current/haproxy.cfg ]; then mkdir -p /etc/haproxy/generated/releases/seed; cp /usr/local/etc/haproxy/haproxy.cfg /etc/haproxy/generated/releases/seed/haproxy.cfg; ln -sfn releases/seed /etc/haproxy/generated/current; fi; exec haproxy -W -S /var/run/haproxy/master.sock,mode,666,level,operator -f /etc/haproxy/generated/current/haproxy.cfg", - ] - restart: unless-stopped - depends_on: - backend: - condition: service_healthy - coraza: - condition: service_started - demo-app: - condition: service_healthy - demo-api: - condition: service_healthy - ports: - - "${HAPROXY_HTTP_PORT:-8080}:80" - - "${HAPROXY_HTTPS_PORT:-8443}:443" - volumes: - - ../../configs/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro - - ../../configs/haproxy/coraza.cfg:/usr/local/etc/haproxy/coraza.cfg:ro - - ./certs/demo.pem:/etc/haproxy/certs/demo.pem:ro - - haproxy_logs:/var/log/haproxy - - generated_config:/etc/haproxy/generated - - haproxy_runtime:/var/run/haproxy - healthcheck: - test: ["CMD-SHELL", "haproxy -c -f /etc/haproxy/generated/current/haproxy.cfg"] - interval: 10s - timeout: 5s - retries: 10 - networks: - - gp_edge - - gp_internal - -networks: - gp_edge: - driver: bridge - gp_internal: - driver: bridge - -volumes: - pgdata: - haproxy_logs: - haproxy_runtime: - backend_logs: - generated_config: - coraza_audit: - shipper_state: diff --git a/deploy/demo/setup-demo.sh b/deploy/demo/setup-demo.sh deleted file mode 100755 index 9d199ef..0000000 --- a/deploy/demo/setup-demo.sh +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" -COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml" -ENV_FILE="${SCRIPT_DIR}/.env" -TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-180}" - -if [[ ! -f "${ENV_FILE}" ]]; then - echo "Missing ${ENV_FILE}. Copy deploy/demo/.env.example to deploy/demo/.env first." >&2 - exit 1 -fi - -if docker compose version >/dev/null 2>&1; then - COMPOSE=(docker compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}") -elif command -v docker-compose >/dev/null 2>&1; then - COMPOSE=(docker-compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}") -else - echo "Docker Compose is required." >&2 - exit 1 -fi - -env_value() { - local name="$1" - local fallback="${2:-}" - local value - - value="$(grep -E "^${name}=" "${ENV_FILE}" | tail -n 1 | cut -d= -f2- || true)" - if [[ -z "${value}" ]]; then - printf '%s' "${fallback}" - else - printf '%s' "${value}" - fi -} - -json_string() { - python3 -c 'import json, sys; print(json.dumps(sys.argv[1]))' "$1" -} - -api_json() { - local method="$1" - local path="$2" - local token="${3:-}" - local body="${4:-}" - local response_file - - response_file="$(mktemp)" - if [[ -n "${body}" ]]; then - http_code="$( - curl \ - --silent \ - --show-error \ - --output "${response_file}" \ - --write-out '%{http_code}' \ - --request "${method}" \ - --header "Content-Type: application/json" \ - ${token:+--header "Authorization: Bearer ${token}"} \ - --data "${body}" \ - "${API_BASE_URL}${path}" - )" - else - http_code="$( - curl \ - --silent \ - --show-error \ - --output "${response_file}" \ - --write-out '%{http_code}' \ - --request "${method}" \ - ${token:+--header "Authorization: Bearer ${token}"} \ - "${API_BASE_URL}${path}" - )" - fi - - if [[ "${http_code}" -lt 200 || "${http_code}" -ge 300 ]]; then - echo "API ${method} ${path} failed with HTTP ${http_code}:" >&2 - cat "${response_file}" >&2 - rm -f "${response_file}" - return 1 - fi - - cat "${response_file}" - rm -f "${response_file}" -} - -container_id() { - local service="$1" - "${COMPOSE[@]}" ps -q "${service}" -} - -health_status() { - local service="$1" - local id - - id="$(container_id "${service}")" - if [[ -z "${id}" ]]; then - echo "missing" - return - fi - - docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${id}" -} - -wait_for_healthy() { - local service="$1" - local deadline=$((SECONDS + TIMEOUT_SECONDS)) - local status - - echo "Waiting for ${service}..." - while (( SECONDS < deadline )); do - status="$(health_status "${service}")" - case "${status}" in - healthy) - echo "${service} is healthy." - return 0 - ;; - exited|dead) - echo "${service} is ${status}." >&2 - return 1 - ;; - esac - sleep 2 - done - - echo "Timed out waiting for ${service}; last status: ${status:-unknown}." >&2 - return 1 -} - -ensure_crs_bundle() { - if compgen -G "${REPO_ROOT}/configs/coraza/crs/rules/*.conf" >/dev/null; then - return - fi - - echo "Missing OWASP CRS rules in configs/coraza/crs." >&2 - echo "Run: git submodule update --init --recursive" >&2 - echo "Then rerun: ./deploy/demo/setup-demo.sh" >&2 - exit 1 -} - -ADMIN_EMAIL="$(env_value ADMIN_EMAIL admin@example.com)" -ADMIN_PASSWORD="$(env_value ADMIN_PASSWORD GuardProxyDemo12345)" -ADMIN_FULL_NAME="$(env_value ADMIN_FULL_NAME 'Demo Administrator')" -DEMO_DOMAIN="$(env_value DEMO_DOMAIN app.local)" -DEMO_BACKEND_URL="$(env_value DEMO_BACKEND_URL http://demo-app:8080)" -DEMO_SECOND_DOMAIN="$(env_value DEMO_SECOND_DOMAIN api.local)" -DEMO_SECOND_BACKEND_URL="$(env_value DEMO_SECOND_BACKEND_URL http://demo-api:8080)" -HAPROXY_HTTP_PORT="$(env_value HAPROXY_HTTP_PORT 8080)" -HAPROXY_HTTPS_PORT="$(env_value HAPROXY_HTTPS_PORT 8443)" -BACKEND_HTTP_PORT="$(env_value BACKEND_HTTP_PORT 8000)" -FRONTEND_HTTP_PORT="$(env_value FRONTEND_HTTP_PORT 3000)" -API_BASE_URL="http://127.0.0.1:${BACKEND_HTTP_PORT}" -WAF_BASE_URL="http://127.0.0.1:${HAPROXY_HTTP_PORT}" -WAF_TLS_BASE_URL="https://127.0.0.1:${HAPROXY_HTTPS_PORT}" -FRONTEND_BASE_URL="http://localhost:${FRONTEND_HTTP_PORT}" - -cd "${REPO_ROOT}" - -ensure_crs_bundle - -ensure_demo_certificate() { - local cert_dir="${SCRIPT_DIR}/certs" - local cert_file="${cert_dir}/demo.pem" - local crt_file="${cert_dir}/demo.crt" - local key_file="${cert_dir}/demo.key" - - if [[ -f "${cert_file}" ]]; then - return - fi - - if ! command -v openssl >/dev/null 2>&1; then - echo "OpenSSL is required to generate the demo TLS certificate." >&2 - exit 1 - fi - - echo "Generating self-signed demo TLS certificate for ${DEMO_DOMAIN} and ${DEMO_SECOND_DOMAIN}..." - mkdir -p "${cert_dir}" - openssl req \ - -x509 \ - -nodes \ - -newkey rsa:2048 \ - -days 30 \ - -subj "/CN=${DEMO_DOMAIN}" \ - -addext "subjectAltName=DNS:${DEMO_DOMAIN},DNS:${DEMO_SECOND_DOMAIN}" \ - -keyout "${key_file}" \ - -out "${crt_file}" \ - >/dev/null 2>&1 - cat "${crt_file}" "${key_file}" > "${cert_file}" -} - -enable_demo_https() { - echo "Applying temporary demo HTTPS bind to generated HAProxy config..." - "${COMPOSE[@]}" exec -T haproxy sh -c \ - "grep -q 'bind \\*:443 ssl crt /etc/haproxy/certs/demo.pem' /etc/haproxy/generated/current/haproxy.cfg || sed -i '/^[[:space:]]*bind \\*:80$/a\\ bind *:443 ssl crt /etc/haproxy/certs/demo.pem' /etc/haproxy/generated/current/haproxy.cfg" - "${COMPOSE[@]}" exec -T haproxy haproxy -c -f /etc/haproxy/generated/current/haproxy.cfg >/dev/null - "${COMPOSE[@]}" restart haproxy >/dev/null - wait_for_healthy haproxy -} - -ensure_demo_certificate -"${COMPOSE[@]}" up -d --build - -wait_for_healthy backend -wait_for_healthy demo-app -wait_for_healthy demo-api -wait_for_healthy coraza -wait_for_healthy haproxy - -echo "Seeding admin user..." -"${COMPOSE[@]}" exec -T backend /app/.venv/bin/python scripts/seed_admin.py \ - --email "${ADMIN_EMAIL}" \ - --password "${ADMIN_PASSWORD}" \ - --full-name "${ADMIN_FULL_NAME}" - -echo "Logging in..." -login_body="$( - printf '{"email":%s,"password":%s}' \ - "$(json_string "${ADMIN_EMAIL}")" \ - "$(json_string "${ADMIN_PASSWORD}")" -)" -token="$( - api_json POST /auth/login "" "${login_body}" \ - | python3 -c 'import json, sys; print(json.load(sys.stdin)["access_token"])' -)" - -echo "Ensuring demo WAF policy exists..." -policy_body='{"name":"Demo CRS","description":"Demo policy for app.local and api.local","paranoia_level":1,"inbound_anomaly_threshold":5,"outbound_anomaly_threshold":4,"enforcement_mode":"block"}' -policy_response="$(api_json POST /policies "${token}" "${policy_body}" || true)" -if [[ -z "${policy_response}" ]]; then - policy_response="$(api_json GET /policies "${token}")" -fi -policy_id="$( - POLICY_RESPONSE="${policy_response}" python3 - <<'PY' -import json -import os - -data = json.loads(os.environ["POLICY_RESPONSE"]) -if isinstance(data, list): - for item in data: - if item["name"] == "Demo CRS": - print(item["id"]) - break - else: - raise SystemExit("Demo CRS policy not found") -else: - print(data["id"]) -PY -)" - -ensure_vhost() { - local domain="$1" - local backend_url="$2" - local description="$3" - local vhost_body - local vhost_response - local vhosts_response - local vhost_id - - echo "Ensuring vhost ${domain} -> ${backend_url} exists..." - vhost_body="$( - printf '{"domain":%s,"backend_url":%s,"description":%s,"ssl_enabled":false,"is_active":true,"policy_id":%s}' \ - "$(json_string "${domain}")" \ - "$(json_string "${backend_url}")" \ - "$(json_string "${description}")" \ - "${policy_id}" - )" - vhost_response="$(api_json POST /vhosts "${token}" "${vhost_body}" || true)" - if [[ -n "${vhost_response}" ]]; then - return - fi - - vhosts_response="$(api_json GET /vhosts "${token}")" - vhost_id="$( - VHOSTS_RESPONSE="${vhosts_response}" DEMO_DOMAIN="${domain}" python3 - <<'PY' -import json -import os - -data = json.loads(os.environ["VHOSTS_RESPONSE"]) -domain = os.environ["DEMO_DOMAIN"] -for item in data: - if item["domain"] == domain: - print(item["id"]) - break -else: - raise SystemExit(f"vhost {domain!r} not found") -PY - )" - vhost_response="$(api_json PATCH "/vhosts/${vhost_id}" "${token}" "${vhost_body}")" -} - -ensure_vhost "${DEMO_DOMAIN}" "${DEMO_BACKEND_URL}" "Demo frontend application" -ensure_vhost "${DEMO_SECOND_DOMAIN}" "${DEMO_SECOND_BACKEND_URL}" "Demo API application" - -echo "Applying generated HAProxy/Coraza config..." -api_json POST /config/apply "${token}" >/dev/null -enable_demo_https - -echo -echo "Demo is ready." -echo "Admin panel: ${FRONTEND_BASE_URL}" -echo "Backend API: ${API_BASE_URL}" -echo "WAF proxy: ${WAF_BASE_URL}" -echo "WAF TLS: ${WAF_TLS_BASE_URL}" -echo -echo "Try:" -echo " curl -i -H 'Host: ${DEMO_DOMAIN}' '${WAF_BASE_URL}/hello?name=demo'" -echo " curl -i -H 'Host: ${DEMO_SECOND_DOMAIN}' '${WAF_BASE_URL}/v1/status'" -echo " curl -k -i -H 'Host: ${DEMO_DOMAIN}' '${WAF_TLS_BASE_URL}/hello?tls=1'" -echo " curl -k -i -H 'Host: ${DEMO_SECOND_DOMAIN}' '${WAF_TLS_BASE_URL}/v1/status'" diff --git a/deploy/docker/.env.example b/deploy/docker/.env.example index 49c1b06..70d95a8 100644 --- a/deploy/docker/.env.example +++ b/deploy/docker/.env.example @@ -20,7 +20,7 @@ VITE_API_BASE_URL=http://localhost:8080 # SHIPPER_BACKOFF_MAX=30 # SHIPPER_REQUEST_TIMEOUT=5 -# Optional: set before running `make seed` -# ADMIN_EMAIL=admin@example.com -# ADMIN_PASSWORD= -# ADMIN_FULL_NAME=Administrator +# Local admin seed used by `make seed` and the benchmark lab. +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=GuardProxyDemo12345 +ADMIN_FULL_NAME=Administrator diff --git a/docs/course-project-report.md b/docs/course-project-report.md index 20bbfb6..617a5e8 100644 --- a/docs/course-project-report.md +++ b/docs/course-project-report.md @@ -136,9 +136,9 @@ Authorization: Bearer jwt-token Content-Type: application/json { - "domain": "app.local", - "backend_url": "http://demo-app:8080", - "description": "Aplikacja demonstracyjna", + "domain": "juice.local", + "backend_url": "http://juiceshop:3000", + "description": "OWASP Juice Shop", "ssl_enabled": false, "is_active": true, "policy_id": 1 @@ -284,15 +284,16 @@ Najwazniejsze adresy: | Backend lokalny bez proxy | `http://127.0.0.1:8000` | | Swagger UI | `http://127.0.0.1:8000/docs` | -## 11. Demo +## 11. Demo i laboratorium ewaluacyjne -Projekt zawiera osobny katalog demo w `deploy/demo`. Demo uruchamia Guard Proxy -razem z dwiema prostymi aplikacjami HTTP echo za WAF-em. Pozwala to pokazac: +Projekt uruchamia Guard Proxy przez glowny plik `deploy/docker/docker-compose.yml`. +Laboratorium w `benchmarks/lab` dodaje aplikacje testowe za WAF-em. Pozwala to +pokazac: - logowanie do panelu, - utworzenie polityki WAF, -- utworzenie dwoch vhostow kierujacych na dwie rozne aplikacje demo, -- routing po naglowku `Host` (`app.local` oraz `api.local`), +- utworzenie vhostow kierujacych na aplikacje Juice Shop, DVWA i WordPress, +- routing po naglowku `Host` (`juice.local`, `dvwa.local`, `wp.local`), - zastosowanie konfiguracji, - poprawne przepuszczenie zwyklego ruchu HTTP, - dzialanie HAProxy, Coraza i backendu jako jednego systemu. diff --git a/docs/evaluation-plan.md b/docs/evaluation-plan.md index 47cb51d..c2d256c 100644 --- a/docs/evaluation-plan.md +++ b/docs/evaluation-plan.md @@ -100,13 +100,12 @@ Compose overlay: `benchmarks/lab/docker-compose.targets.yml` ## 4. Test Targets -| App | Purpose | Vhost | -| ---------------------------------- | ------------------------------------------------------------------- | ------------- | -| **OWASP Juice Shop** v17 | Intentionally vulnerable Node.js app — scanner target | `juice.local` | -| **DVWA** (Damn Vulnerable Web App) | Classic PHP vulnerable app — SQLi/XSS/LFI scenarios | `dvwa.local` | -| **WordPress** 6.x (php8.3) | Real-world CMS — scanner-assisted coverage and benign corpus target | `wp.local` | -| **Albedo** | CRS go-ftw regression backend compatible with CRS test assumptions | `ftw.local` | -| **demo-app** (echo server) | Existing minimal target — smoke check | `app.local` | +| App | Purpose | Vhost | +|---|---|---| +| **OWASP Juice Shop** v17 | Intentionally vulnerable Node.js app — scanner target | `juice.local` | +| **DVWA** (Damn Vulnerable Web App) | Classic PHP vulnerable app — SQLi/XSS/LFI scenarios | `dvwa.local` | +| **WordPress** 6.x (php8.3) | Real-world CMS — scanner-assisted coverage and benign corpus target | `wp.local` | +| **Albedo** | CRS go-ftw regression backend compatible with CRS test assumptions | `ftw.local` | WordPress is run **without** CRS application exclusion plugins. This is intentional: any false-positive result is reported as an **untuned CRS+WordPress baseline** for the documented policy, not as a universal property of Guard Proxy. @@ -235,9 +234,10 @@ cd /opt/guard-proxy git submodule update --init --recursive # 4. Copy env files -cp deploy/demo/.env.example deploy/demo/.env +cp deploy/docker/.env.example deploy/docker/.env cp benchmarks/lab/.env.example benchmarks/lab/.env # Edit both .env files if needed (passwords, ports) +# deploy/docker/.env must include ADMIN_EMAIL and ADMIN_PASSWORD for lab seeding. # 5. Bring up the lab make eval-up