commit 4d60fdd1396a7f584f28684af46f44db02516051 Author: matt_ed Date: Wed Jul 29 11:08:56 2026 -0700 Initial commit: Imperva WAF demo lab Flight-booking app scaffolded from the source running on the GCP host, plus the four-screen frontend and three OWASP attack endpoints (SQLi, reflected XSS, path traversal) built and locally verified against a live uvicorn/vite dev pair. Includes deploy.sh (local build -> ship over ssh -> swap on host) and docs/ carried over from the handoff. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3de1940 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +imperva-proxy/certs/*.key +node_modules/ +dist/ +__pycache__/ +*.pyc +*.db +.venv/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..463eff2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,158 @@ +# CLAUDE.md + +Context and working guide for the Imperva WAF demo lab. Read this first, then the +files under `docs/` for depth. This file is meant to orient any AI agent (or human) +picking up the project so they don't re-derive decisions that were already made and +deliberately reversed once. + +--- + +## What this project is + +A deliberately-vulnerable flight-booking web app sitting behind the **Imperva Cloud +WAF**, used to demonstrate that the WAF detects and blocks common OWASP attacks and +surfaces them in the Imperva portal. The app itself is not the point — it exists to +give the WAF something realistic to protect and to produce clean, distinct blocked +events on screen during a live demo. + +The stack runs as **rootless Podman containers** on a single small GCP instance. All +public traffic is forced through Imperva; the origin only accepts connections from +Imperva's IP ranges. + +## Current state (as of this handoff) + +- Backend (`backend-api`), frontend (`frontend-vue`), and Caddy (`caddy-proxy`) are + deployed and working on the GCP host. +- **Edge/origin TLS is done and verified.** Imperva terminates the visitor's TLS at + the edge and **re-encrypts to the origin over HTTPS** using a **self-signed cert** + that Caddy serves on `:443`. Confirmed working end to end (`https://flytoouter.space/` + loads with Imperva's trusted edge cert; origin serves self-signed to Imperva only). +- A **local-build → ship-over-SSH → swap-on-host** deploy pipeline is defined + (`deploy.sh`). See `docs/DEPLOY.md`. +- **Repo scaffolded locally** by pulling the exact source that's live on the host + (`~/imperva-backend`, `~/imperva-frontend`, `~/imperva-proxy`) via `scp`, so the repo + matches what's actually running, not just what was intended. +- **The frontend on the GCP host is still a near-empty stub** (just whitespace and a + page title) — the real four-screen Vue SPA now exists in this repo's `frontend/` but + has not been shipped yet. See "What's next". + +## What's next + +The four-screen frontend and three attack endpoints described below are **built and +locally verified**, but **not yet deployed to the GCP host** — the host still runs the +old stub frontend and two-endpoint backend. Next step is running `./deploy.sh` from a +real host terminal (see the Podman/sandbox gotcha below), then walking the +`docs/DEMO_RUNBOOK.md` pre-demo checklist against `https://flytoouter.space/`. + +Three OWASP attacks, each blocked by an Imperva **default signature** and shown as a +*distinct* event type in the portal: + +1. SQL Injection — `/api/v1/users?q=` (Passenger / Admin Lookup screen) +2. Reflected XSS — `/api/v1/search?term=` (Flight Search screen) +3. Path Traversal — `/api/v1/download?file=` (Boarding Pass Download screen) + +Plus a Login screen (`/api/v1/login`) kept as the credential-stuffing bonus (not in the +core demo — needs a custom rate policy, see DECISIONS.md D7). + +See `docs/DEMO_RUNBOOK.md` for payloads and expected portal classifications. + +**Resolved:** confirmed via `podman inspect` and the host's `.bash_history` that +`backend-api` and `frontend-vue` have **no bind mounts** — `demo.db` is created inside +the image by `init_db()` on container start, not mounted from the host. The real run +commands were: + +``` +podman run -d --name backend-api --restart always -p 8000:8000 backend-api +podman run -d --name frontend-vue --restart always -p 8080:8080 frontend-vue +``` + +`deploy.sh`'s `swap()` reproduces exactly this — `--name` and `-p` only, nothing else +needed. + +--- + +## Key facts an agent needs (these override the original handoff doc) + +- **Host:** GCP `e2-micro`, Ubuntu, static IP `35.203.147.44`, domain `flytoouter.space`. +- **Host user:** `matt`. **Rootless Podman socket is under UID `1001`, not 1000** — + the original architecture doc says 1000; that was wrong for this host. Prefer the + connection alias so the UID is irrelevant. +- **Podman connection alias (local → host):** `gcp-lab`, already configured and set as + default: + `ssh://matt@35.203.147.44:22/run/user/1001/podman/podman.sock` + key: `~/.ssh/google_compute_engine`. +- **SSH ingress** is locked to the operator's static residential IP. +- **HTTP/HTTPS ingress** (`allow-imperva-waf`) is restricted to Imperva's published + IPv4 CIDRs only; all `0.0.0.0/0` rules were purged. The vulnerable app is therefore + never directly reachable — attacks must traverse the WAF, which is what makes them + appear in the portal. +- **e2-micro cannot build the Vue multi-stage image** (npm build is memory-hungry even + with the swapfile). Always build locally and ship the finished image. Never build on + the host. + +## Repo layout (intended) + +``` +. +├── CLAUDE.md # this file +├── deploy.sh # local build → ship over ssh → swap on host +├── backend/ # FastAPI app + Containerfile +├── frontend/ # Vue SPA + multi-stage Containerfile +├── imperva-proxy/ +│ ├── Caddyfile # origin reverse proxy (mirrors host ~/imperva-proxy/) +│ └── certs/ # origin.crt / origin.key (self-signed; do NOT commit key) +└── docs/ + ├── ARCHITECTURE.md # topology, TLS legs, request flow, network + ├── DECISIONS.md # every design choice + why + rejected alternatives + ├── DEPLOY.md # the ship-over-ssh workflow in detail + └── DEMO_RUNBOOK.md # attacks, payloads, expected portal events +``` + +> Note: On the **host**, the Caddyfile and certs live at `~/imperva-proxy/`. Keep the +> repo copy in sync with what's mounted into `caddy-proxy`. + +## Conventions / gotchas (learned the hard way — see DECISIONS.md) + +- **One catch-all `:80`/`:443` site block in the Caddyfile.** With `auto_https off`, a + hostname block (e.g. `flytoouter.space { }`) collapses onto the same port as a + `:443`/`:80` block and Caddy fails with `ambiguous site definition`. Host-based + routing, if ever needed, goes *inside* the single block as a matcher — never as a + second site block. +- **Use `handle`, not `handle_path`,** for `/api/*` so the `/api` prefix is preserved + for the backend. +- **Mount paths in `podman run` must be absolute** (`/home/matt/...`), not `~`. A + tilde that doesn't expand creates an empty anonymous mount and Caddy dies with + `open /etc/caddy/certs/origin.crt: no such file or directory`. +- **Origin cert is self-signed on purpose.** Only Imperva sees it, so no browser + warnings. Never put the self-signed cert on the *edge* (visitor-facing) side — that + stays Imperva's managed cert. +- **`https://35.203.147.44/` resetting is expected** when the origin serves only the + intended port and/or your source IP isn't an Imperva CIDR. Test the origin from the + host with `curl -vk https://localhost/`, and test the real path via + `https://flytoouter.space/`. +- **Do not commit `origin.key`.** Add `imperva-proxy/certs/*.key` to `.gitignore`. +- **`podman` builds must run on the real host terminal, not inside a sandboxed + coding-agent shell.** If you're an AI agent editing this repo from within a sandbox, + check whether `podman`/`docker` resolves before assuming you can run `deploy.sh` + yourself. On this project's dev machine, the agent sandbox is a *nested* container + (different userland/libc than the real host) sitting on the same physical disk as + `~/workspace` — so files edited in the sandbox are real and immediately usable, but a + `podman` binary installed *inside* the sandbox will collide with the host's existing + rootless container storage (`~/.local/share/containers`, shared because it's the same + disk) and fail with a `database configuration mismatch` error. Don't try to work + around this by resetting or pointing storage elsewhere — it risks corrupting the + host's real container state. Edit code from the sandbox; run `./deploy.sh` from a + normal terminal on the actual workstation. + +## How to deploy (short version) + +From the repo root, with the `gcp-lab` connection present: + +```bash +./deploy.sh # build both images, ship over ssh, swap both containers +VERSION=v2 ./deploy.sh # same, but tag images v2 for a named rollback point +./deploy.sh --restart-caddy # after editing the Caddyfile (new routes) +``` + +Full detail, including per-step flags and the layer-shipping trade-off, in +`docs/DEPLOY.md`. diff --git a/backend/Containerfile b/backend/Containerfile new file mode 100644 index 0000000..bc126d7 --- /dev/null +++ b/backend/Containerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . +COPY boarding_passes/ ./boarding_passes/ + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/boarding_passes/BP-1001.txt b/backend/boarding_passes/BP-1001.txt new file mode 100644 index 0000000..c0c83fc --- /dev/null +++ b/backend/boarding_passes/BP-1001.txt @@ -0,0 +1,11 @@ +==================================== + OUTER SPACE FLIGHTS - BOARDING PASS +==================================== +Passenger: J. DOE +Flight: OUT101 +Route: Earth (JFK) -> Lunar Gateway +Departs: 2026-08-14 09:00 +Seat: 14C +Gate: G7 +Status: CONFIRMED +==================================== diff --git a/backend/boarding_passes/BP-1002.txt b/backend/boarding_passes/BP-1002.txt new file mode 100644 index 0000000..7594a02 --- /dev/null +++ b/backend/boarding_passes/BP-1002.txt @@ -0,0 +1,11 @@ +==================================== + OUTER SPACE FLIGHTS - BOARDING PASS +==================================== +Passenger: A. ALICE +Flight: OUT202 +Route: Earth (LAX) -> Mars Colony Alpha +Departs: 2026-09-02 03:30 +Seat: 02A +Gate: G14 +Status: CONFIRMED +==================================== diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..efa4345 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,131 @@ +import os +import sqlite3 +from fastapi import FastAPI, Query, HTTPException +from fastapi.responses import PlainTextResponse +from pydantic import BaseModel + +app = FastAPI(title="Imperva Demo Leaky API", version="1.0.0") + +DB_FILE = "demo.db" +BOARDING_PASS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "boarding_passes") + +FLIGHTS = [ + {"id": "OUT101", "origin": "Earth (JFK)", "destination": "Lunar Gateway", "depart": "2026-08-14 09:00", "price": 128000}, + {"id": "OUT202", "origin": "Earth (LAX)", "destination": "Mars Colony Alpha", "depart": "2026-09-02 03:30", "price": 890000}, + {"id": "OUT303", "origin": "Earth (LHR)", "destination": "Europa Research Station", "depart": "2026-10-21 14:15", "price": 2450000}, + {"id": "OUT404", "origin": "Lunar Gateway", "destination": "Mars Colony Alpha", "depart": "2026-08-30 22:00", "price": 610000}, + {"id": "OUT505", "origin": "Earth (NRT)", "destination": "Titan Outpost", "depart": "2026-11-11 06:45", "price": 3980000}, +] + +def init_db(): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT, + email TEXT, + role TEXT + ) + """) + cursor.execute("SELECT COUNT(*) FROM users") + if cursor.fetchone()[0] == 0: + cursor.executemany(""" + INSERT INTO users (username, email, role) VALUES (?, ?, ?) + """, [ + ("admin", "admin@flytoouter.space", "administrator"), + ("jdoe", "jdoe@flytoouter.space", "engineer"), + ("alice", "alice@flytoouter.space", "analyst") + ]) + conn.commit() + conn.close() + +init_db() + +class LoginRequest(BaseModel): + username: str + password: str + +@app.get("/api/v1/health") +def health_check(): + """Endpoint for uptime and monitoring checks.""" + return {"status": "ok", "service": "imperva-demo-backend"} + +@app.post("/api/v1/login") +def login(credentials: LoginRequest): + """ + Intentionally lacks rate limiting or lockout protection. + Demo Use: Showcases Imperva Advanced Bot Protection / Credential Stuffing mitigation. + """ + if credentials.username == "admin" and credentials.password == "Secret123!": + return {"status": "authenticated", "token": "demo-jwt-token-xyz789"} + return {"status": "failed", "message": "Invalid credentials"} + +@app.get("/api/v1/users") +def search_users(q: str = Query("", description="Search username")): + """ + INTENTIONAL VULNERABILITY: Raw string concatenation for SQL query. + Demo Use: Triggers Imperva default SQL Injection (SQLi) detection rules when passing payloads like `' OR '1'='1`. + """ + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + query = f"SELECT id, username, email, role FROM users WHERE username LIKE '%{q}%'" + try: + cursor.execute(query) + rows = cursor.fetchall() + conn.close() + return [{"id": r[0], "username": r[1], "email": r[2], "role": r[3]} for r in rows] + except Exception as e: + conn.close() + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") + +@app.get("/api/v1/search") +def search_flights(term: str = Query("", description="Destination search term")): + """ + INTENTIONAL VULNERABILITY: the search term is echoed back verbatim in + `query_echo` and the frontend renders it unescaped (v-html). + Demo Use: Triggers Imperva default Cross-Site Scripting (XSS) detection rules + when passing payloads like ``. + """ + term_lower = term.lower() + results = [ + f for f in FLIGHTS + if term_lower in f["origin"].lower() or term_lower in f["destination"].lower() + ] if term else FLIGHTS + + return { + "query_echo": f"Showing results for: {term}" if term else "Showing all flights", + "results": results, + } + + +@app.get("/api/v1/download") +def download_boarding_pass(file: str = Query(..., description="Boarding pass filename")): + """ + INTENTIONAL VULNERABILITY: raw path concatenation, no traversal sanitization. + Demo Use: Triggers Imperva default Path Traversal / Illegal Resource Access + detection rules when passing payloads like `../../../../etc/passwd`. + """ + path = os.path.join(BOARDING_PASS_DIR, file) + try: + with open(path, "r") as f: + content = f.read() + except FileNotFoundError: + raise HTTPException(status_code=404, detail="Boarding pass not found") + except IsADirectoryError: + raise HTTPException(status_code=400, detail="Not a file") + return PlainTextResponse(content) + + +@app.get("/api/v1/hidden-config") +def hidden_config(): + """ + Unauthenticated endpoint exposing internal application state. + Demo Use: API Discovery & custom URI blocking rules. + """ + return { + "env": "production-demo", + "db_connection": "sqlite:///demo.db", + "internal_api_key": "sk_live_demo_998877665544332211", + "debug_mode": True + } diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..fa9f6d8 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.111.0 +uvicorn==0.30.1 +pydantic==2.7.4 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..158bfdc --- /dev/null +++ b/deploy.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Local build -> ship over ssh -> swap on host. No container registry (see docs/DECISIONS.md D8). +# +# Run this from a real terminal on your workstation (NOT inside a sandboxed +# coding-agent shell) with the `gcp-lab` podman connection already configured: +# +# podman system connection add gcp-lab \ +# --identity ~/.ssh/google_compute_engine \ +# ssh://matt@35.203.147.44:22/run/user/1001/podman/podman.sock +# +# Usage: +# ./deploy.sh # build both, ship both, swap both containers +# VERSION=v2 ./deploy.sh # tag images v2 instead of a timestamp +# ./deploy.sh --build --ship --swap # explicit equivalent of the default +# ./deploy.sh --restart-caddy # after editing imperva-proxy/Caddyfile + +set -euo pipefail + +# --- config ------------------------------------------------------------- +HOST="35.203.147.44" +HOST_USER="matt" +SSH_KEY="$HOME/.ssh/google_compute_engine" +CONNECTION="gcp-lab" +VERSION="${VERSION:-$(date +%Y%m%d%H%M%S)}" +REMOTE_PROXY_DIR="/home/matt/imperva-proxy" + +BACKEND_IMAGE="backend-api" +FRONTEND_IMAGE="frontend-vue" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# --- steps ---------------------------------------------------------------- + +do_build() { + echo "==> Building ${BACKEND_IMAGE}:${VERSION}" + podman build -t "${BACKEND_IMAGE}:${VERSION}" -t "${BACKEND_IMAGE}:latest" ./backend + + echo "==> Building ${FRONTEND_IMAGE}:${VERSION}" + podman build -t "${FRONTEND_IMAGE}:${VERSION}" -t "${FRONTEND_IMAGE}:latest" ./frontend +} + +do_ship() { + echo "==> Shipping ${BACKEND_IMAGE}:${VERSION} over ssh" + podman save "${BACKEND_IMAGE}:${VERSION}" | ssh -i "$SSH_KEY" "${HOST_USER}@${HOST}" podman load + + echo "==> Shipping ${FRONTEND_IMAGE}:${VERSION} over ssh" + podman save "${FRONTEND_IMAGE}:${VERSION}" | ssh -i "$SSH_KEY" "${HOST_USER}@${HOST}" podman load +} + +do_swap() { + echo "==> Swapping backend-api container" + podman --connection "$CONNECTION" rm -f backend-api >/dev/null 2>&1 || true + podman --connection "$CONNECTION" run -d \ + --name backend-api \ + --restart always \ + -p 8000:8000 \ + "${BACKEND_IMAGE}:${VERSION}" + + echo "==> Swapping frontend-vue container" + podman --connection "$CONNECTION" rm -f frontend-vue >/dev/null 2>&1 || true + podman --connection "$CONNECTION" run -d \ + --name frontend-vue \ + --restart always \ + -p 8080:8080 \ + "${FRONTEND_IMAGE}:${VERSION}" +} + +do_restart_caddy() { + echo "==> Copying Caddyfile to host" + scp -i "$SSH_KEY" ./imperva-proxy/Caddyfile "${HOST_USER}@${HOST}:${REMOTE_PROXY_DIR}/Caddyfile" + + echo "==> Validating Caddyfile on host" + podman --connection "$CONNECTION" exec caddy-proxy caddy validate --config /etc/caddy/Caddyfile + + echo "==> Restarting caddy-proxy" + podman --connection "$CONNECTION" restart caddy-proxy +} + +check_connection() { + if ! podman system connection list --format '{{.Name}}' 2>/dev/null | grep -qx "$CONNECTION"; then + echo "error: podman connection '${CONNECTION}' is not configured." >&2 + echo " podman system connection add ${CONNECTION} --identity ${SSH_KEY} ssh://${HOST_USER}@${HOST}:22/run/user/1001/podman/podman.sock" >&2 + exit 1 + fi +} + +# --- arg parsing ------------------------------------------------------ + +RUN_BUILD=false +RUN_SHIP=false +RUN_SWAP=false +RUN_RESTART_CADDY=false + +if [ "$#" -eq 0 ]; then + RUN_BUILD=true + RUN_SHIP=true + RUN_SWAP=true +else + for arg in "$@"; do + case "$arg" in + --build) RUN_BUILD=true ;; + --ship) RUN_SHIP=true ;; + --swap) RUN_SWAP=true ;; + --restart-caddy) RUN_RESTART_CADDY=true ;; + *) + echo "unknown flag: $arg" >&2 + exit 1 + ;; + esac + done +fi + +if [ "$RUN_SHIP" = true ] || [ "$RUN_SWAP" = true ] || [ "$RUN_RESTART_CADDY" = true ]; then + check_connection +fi + +[ "$RUN_BUILD" = true ] && do_build +[ "$RUN_SHIP" = true ] && do_ship +[ "$RUN_SWAP" = true ] && do_swap +[ "$RUN_RESTART_CADDY" = true ] && do_restart_caddy + +echo "==> Done (version: ${VERSION})" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f6327bb --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,87 @@ +# Architecture + +## Overview + +Single GCP `e2-micro` running three rootless Podman containers under user `matt`. +Imperva Cloud WAF sits in front of everything. The origin is network-isolated so that +the only things that can reach the app are Imperva's edge nodes. + +``` +Visitor ──TLS──▶ Imperva Cloud WAF ──TLS (re-encrypt)──▶ Caddy :443 ──HTTP──▶ backend/frontend + (terminates, (self-signed cert) + inspects, + re-encrypts) +``` + +## The two TLS legs (this is the crux of the design) + +There are two independent TLS connections, and they use different certificates. Mixing +them up is the single easiest way to break this setup. + +**Leg 1 — Visitor → Imperva (edge).** +Imperva presents a publicly-trusted certificate for `flytoouter.space` to browsers. +This is Imperva's *managed* cert (auto-provisioned by Imperva). It must be publicly +trusted or every visitor gets a browser warning. We do **not** touch this with our +self-signed cert. + +**Leg 2 — Imperva → origin (re-encryption).** +Imperva opens a *second* TLS connection back to the GCP origin and inspects/re-encrypts +before forwarding. Caddy serves this leg on `:443` with a **self-signed certificate**. +Imperva is the only client that ever sees this cert, so self-signed is fine and +produces no visitor-facing warnings. + +DNS for `flytoouter.space` must point at **Imperva**, not at `35.203.147.44`. If it +resolves straight to the origin, browsers try to negotiate TLS with the self-signed +origin cert and fail. + +## Request flow through the origin + +Caddy (`caddy-proxy`) is the only container bound to a public port. It terminates the +re-encryption TLS and reverse-proxies over plain HTTP to the app containers via the +`host.containers.internal` alias on the default rootless bridge network: + +- `handle /api/*` → `host.containers.internal:8000` (FastAPI backend) +- `handle /*` → `host.containers.internal:8080` (Vue frontend) + +`handle` (not `handle_path`) is used so the `/api` prefix is preserved for the backend +routes (`/api/v1/users`, etc.). + +## Container stack + +| Container | Image | Port | Role | +|----------------|------------------------------------|-------------|--------------------------------------------------| +| `caddy-proxy` | `docker.io/library/caddy:2-alpine` | `443:443` | TLS termination (re-encryption leg) + reverse proxy | +| `backend-api` | custom Python 3.11-slim (FastAPI) | `8000:8000` | REST API + local SQLite `demo.db` | +| `frontend-vue` | custom multi-stage (Node build → Nginx) | `8080:8080` | Vue SPA (flight booking UI) | + +Caddy currently serves only `:443`. There is intentionally no `:80` site block (see +DECISIONS.md — a stray `:80` block under `auto_https off` caused the `ambiguous site +definition` failure). If Imperva needs an HTTP health check, an `:80` block can be +added back as a separate, non-conflicting listener. + +## Network / edge security + +- **Origin target tag:** `imperva-origin`. +- **`allow-imperva-waf`:** HTTP/HTTPS ingress restricted to Imperva's published IPv4 + CIDR blocks (e.g. `199.83.128.0/21`, `198.143.32.0/19`, …). Confirm `tcp:443` is in + the allowed ports since the origin path is now 443-only. +- **`allow-home-ssh`:** SSH (22) locked to the operator's static residential IP. +- All default open (`0.0.0.0/0`) rules purged. + +Because the origin only answers Imperva CIDRs, the deliberately-vulnerable endpoints +are unreachable directly. Every attack must pass through the WAF — which is exactly why +they register as blocked events in the portal. + +## Host hardening (from original build) + +- `/swapfile` mounted via `/etc/fstab` to mitigate OOM kills on the burstable e2-micro. +- `loginctl enable-linger matt` so rootless containers persist across reboots. +- `sysctl net.ipv4.ip_unprivileged_port_start=80` to let rootless Caddy bind low ports. + +## Cert details (origin, Leg 2) + +- Self-signed, RSA 2048, 3650-day validity. +- **SAN covers both** `DNS:flytoouter.space` and `IP:35.203.147.44`, so whichever way + Imperva addresses the backend, the cert matches. +- Files live on the host at `~/imperva-proxy/certs/{origin.crt,origin.key}` and are + bind-mounted read-only into Caddy at `/etc/caddy/certs/`. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..fa96f77 --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,169 @@ +# Design Decisions & Rationale + +An ADR-style log of the choices made in this project, *why* they were made, and what was +rejected. Several of these were reversed mid-stream — the reversals are kept here on +purpose so nobody re-walks a dead end. + +--- + +## D1 — WAF terminates client TLS; origin does not do public HTTPS + +**Decision:** Imperva handles all public-facing TLS. The origin never runs ACME / +Let's Encrypt and never serves a publicly-trusted cert. + +**Why:** The whole point of the lab is that Imperva is the edge. Having the origin also +try to obtain public certs is redundant, and on this locked-down host ACME would fail +anyway (egress is restricted and ingress is Imperva-only). Keeping cert management at +the edge is simpler and matches the intended topology. + +--- + +## D2 — First attempt: plain-HTTP origin (REVERSED) + +**Decision (later reversed):** Have Caddy serve only plain HTTP on `:80` and let +Imperva connect to the origin over HTTP; `auto_https off`. + +**Why it was chosen:** The user's initial goal was "forward https/http traffic from the +WAF without Caddy doing SSL." The cleanest literal reading is: Imperva terminates TLS, +origin speaks plain HTTP, Caddy needs no cert at all. + +**Why it was reversed:** The user then wanted a certificate they could upload to / +associate with Imperva (origin re-encryption). Plain HTTP has no cert to produce, so we +moved to a self-signed HTTPS origin (D5). + +**Artifacts from this phase worth remembering:** +- `https://35.203.147.44/` returning `PR_CONNECT_RESET_ERROR` was *expected* here — + nothing was listening on 443. That reset is not a bug; it's the correct result of + hitting a port with no listener. Test HTTP origins with `curl -v http://localhost/` + from the host, and the real path via the hostname. + +--- + +## D3 — Single catch-all site block in the Caddyfile + +**Decision:** Exactly one site block (`:443`, or `:80` in the HTTP phase). No separate +hostname block. + +**Why:** With `auto_https off`, a hostname block like `flytoouter.space { }` no longer +routes to 443 automatically — it binds the same plain port as an explicit `:80`/`:443` +block. Two blocks fighting over one port produces: + +``` +Error: adapting config using caddyfile: ambiguous site definition: :80 +``` + +This actually happened (a leftover domain block from the pre-WAF config remained in the +file). The container crash-looped, spamming the error hundreds of times — that volume is +just the restart policy retrying, not hundreds of distinct problems. + +**Rejected:** keeping host-specific blocks. If host-based routing is ever wanted (e.g. +rejecting requests whose Host isn't `flytoouter.space` as origin hardening), it goes +*inside* the single block as a `@host` matcher, never as a second site block. + +--- + +## D4 — `handle` not `handle_path` for `/api/*` + +**Decision:** Route the API with `handle /api/* { reverse_proxy … }`. + +**Why:** `handle_path` strips the matched prefix; `handle` preserves it. The backend's +routes are defined under `/api/v1/...`, so the prefix must survive the proxy hop. + +--- + +## D5 — Self-signed cert on the origin, generated with OpenSSL (not Caddy internal CA) + +**Decision:** Generate the origin cert with `openssl` (self-signed, RSA 2048, 3650 +days, SAN = DNS + IP) and hand the files to Caddy via `tls `. + +**Why OpenSSL over Caddy's `tls internal`:** `tls internal` mints a cert via Caddy's +internal CA and buries it in `/data/caddy/pki/...`, which is awkward to export. The lab +needs the cert *as a file on disk* — to upload to / associate with Imperva and to have +explicit control over the SAN. OpenSSL gives that in one command. + +**Why self-signed is acceptable here:** Only Imperva connects to the origin. Browsers +only ever see Imperva's edge cert (D1). So the self-signed cert causes zero +visitor-facing warnings. The critical rule: **the self-signed cert belongs only on the +re-encryption leg, never on the edge/visitor leg.** + +**SAN includes both DNS and IP** so Imperva matches regardless of whether it addresses +the origin by hostname or by `35.203.147.44`. + +--- + +## D6 — Debugging the "cert not found" start failure + +**Symptom:** `caddy validate` reported +`open /etc/caddy/certs/origin.crt: no such file or directory`, and curl to `:443` reset. + +**Root cause & rule:** The config was fine; the cert file wasn't visible *inside the +container*. Two common causes on rootless Podman: +1. **Tilde didn't expand** in `-v ~/imperva-proxy/certs:...`, creating an empty + anonymous mount. Fix: use absolute paths (`/home/matt/...`). +2. Stale `:Z` SELinux relabel state from an earlier container. Fix: recreate the + container. + +The TLS reset was a *symptom* of Caddy refusing to start the HTTPS server, not an +independent TLS problem. Always read the `validate` / `podman logs` output first. + +--- + +## D7 — Three OWASP attacks for the demo: SQLi, Reflected XSS, Path Traversal + +**Decision:** Demo exactly these three. + +**Why these three:** Imperva blocks all three with **default, out-of-the-box +signatures** (no custom rules needed), and each surfaces as a **different, clearly +labeled event** in the portal (SQL Injection / Cross-Site Scripting / Illegal Resource +Access). Three distinct classifications reads as comprehensive coverage rather than +three flavors of one thing. + +**Why credential stuffing / rate-limiting was left OUT of the demo set:** It needs a +**custom rate policy** configured in Imperva, not a signature match. It doesn't produce +the clean one-payload-one-block moment the other three do. Kept as an optional "bonus" +for later if we want to showcase custom rules. + +--- + +## D8 — Deploy via `podman save | ssh podman load`, not a container registry + +**Decision:** Build locally, pipe the image over the existing SSH connection into the +host's Podman, then swap the container via the `gcp-lab` connection alias. + +**Why:** Zero new infrastructure. All the pieces already exist (SSH key, locked +ingress, remote Podman socket, connection alias). A registry (GCP Artifact Registry or +GHCR) would add credential management on both ends and, for Artifact Registry, IAM +wiring (service account + `artifactregistry.reader`) that's easy to get subtly wrong. + +**Trade-off accepted:** `podman save` ships **every layer every time**, so each deploy +moves the full image even for a one-line change (~20–40s on this lab). The registry's +layer-caching win only pays off at high deploy frequency, which a demo lab isn't. + +**Upgrade path:** If this ever becomes team-touched or high-frequency, migrating to +Artifact Registry is clean — only the *ship* step changes (`push`/`pull` instead of +`save | ssh load`); the rest of `deploy.sh` is unchanged. + +--- + +## D9 — Build locally, never on the host + +**Decision:** All image builds happen on the local workstation. + +**Why:** The `e2-micro` (shared-core, burstable) doesn't have the memory to run the Vue +multi-stage build (npm install + build) reliably, even with the swapfile. Building +locally and shipping the finished image sidesteps OOM kills entirely. + +--- + +## D10 — Frontend screens map 1:1 to attacks + +**Decision:** Give the flight-booking SPA four screens, each a natural home for one +attack, so the demo feels like poking a real product rather than hitting raw endpoints: + +- Passenger/admin lookup → SQLi (`/api/v1/users?q=`) +- Flight search results → Reflected XSS (`/api/v1/search?term=`) +- Boarding-pass download → Path Traversal (`/api/v1/download?file=`) +- Login → existing endpoint, kept as the credential-stuffing bonus + +**Why:** A believable UI makes the "exploitable without the WAF, blocked with it" +narrative land better in a live demo than curling endpoints. diff --git a/docs/DEMO_RUNBOOK.md b/docs/DEMO_RUNBOOK.md new file mode 100644 index 0000000..e518d32 --- /dev/null +++ b/docs/DEMO_RUNBOOK.md @@ -0,0 +1,61 @@ +# Demo Runbook + +> **Status: BUILT, not yet deployed to the GCP host.** All four screens and three +> attack endpoints exist in `backend/` and `frontend/` and were functionally verified +> locally (backend via `uvicorn` + `curl`, frontend via `vite dev` + a browser, talking +> to each other through the same `/api/*` path Caddy proxies in prod — including +> confirming the XSS payload actually executes and the traversal payload actually reads +> a file outside `boarding_passes/`). Not yet shipped with `./deploy.sh` — that must run +> from a real host terminal, not an AI coding-agent sandbox (see CLAUDE.md). + +Goal of the demo: type an attack into a believable flight-booking UI, watch Imperva +block it, then show the corresponding event in the Imperva portal. Three attacks, three +*distinct* portal classifications. + +## Attack matrix + +| # | Attack | Endpoint | Sample payload | Expected Imperva portal classification | +|---|-----------------|-----------------------------|------------------------------------|------------------------------------------------| +| 1 | SQL Injection | `/api/v1/users?q=` | `' OR '1'='1` | SQL Injection | +| 2 | Reflected XSS | `/api/v1/search?term=` | `` | Cross-Site Scripting (XSS) | +| 3 | Path Traversal | `/api/v1/download?file=` | `../../../../etc/passwd` | Illegal Resource Access / Directory Traversal | +| — | (bonus) Cred stuffing | `/api/v1/login` | repeated login attempts | Requires a **custom rate policy**, not a signature — not in core demo | + +Attacks 1–3 are blocked by Imperva's **default signatures** — no custom rules needed. +The bonus is intentionally excluded from the core demo because it needs a rate policy +configured in Imperva and doesn't produce a clean single-payload block (see +DECISIONS.md D7). + +## Frontend screen → attack mapping + +Each attack has a natural home in the UI so the demo looks like poking a real product: + +- **Passenger / admin lookup** → SQLi search box (`users?q=`) +- **Flight search results** → XSS search field (`search?term=`) +- **Boarding-pass download** → path-traversal file param (`download?file=`) +- **Login** → existing endpoint, kept as the credential-stuffing bonus + +## Demo flow (target) + +1. Show the app working normally (a legitimate search, a legitimate lookup). +2. For each of the three attacks: enter the payload in its screen → show the request is + blocked by Imperva (block page / rejected response). +3. Switch to the Imperva portal security dashboard → show the three distinct event + types logged, one per attack. +4. Reinforce the isolation point: the origin only accepts Imperva CIDRs, so these + attacks *cannot* reach the app except through the WAF — which is why every one of + them appears in the portal. + +## Pre-demo checklist + +- `https://flytoouter.space/` loads with Imperva's trusted edge cert. +- All three endpoints reachable *through* Imperva (legit requests return 200). +- Origin firewall confirmed Imperva-CIDR-only (`allow-imperva-waf`, `tcp:443`). +- DNS for `flytoouter.space` points at Imperva, not the raw origin IP. +- Portal logged-in and on the security events view, filtered to the demo site. + +## Notes on portal specifics + +Exact event labels and menu paths vary by Imperva account/version. Confirm the live +labels in your portal before presenting and update the classification column above to +match what your account actually shows. diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..fb3ab88 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,95 @@ +# Deploy Workflow + +Local code → finished image shipped over SSH → container swapped on the host. No +container registry (see DECISIONS.md D8 for why). + +## The two channels to the host + +Both go to the same GCP box; they're used for different steps: + +- **`podman save … | ssh … podman load`** — ships the image *bytes*. This has to use + raw `ssh` because it's a pipe into the remote Podman CLI. +- **`podman --connection gcp-lab …`** — swaps the running container over the existing + rootless socket, hands-free (no manual SSH). The alias is already configured and set + as default: + `ssh://matt@35.203.147.44:22/run/user/1001/podman/podman.sock`. + +> The socket is under **UID 1001** on this host (the original architecture doc's 1000 is +> wrong). Using the alias means you never have to think about the UID. + +## Why build locally + +The `e2-micro` can't build the Vue multi-stage image reliably (memory). Always build on +the local workstation and ship the finished image. Never build on the host. + +## `deploy.sh` + +Lives at the repo root and is run from there (so the relative `./backend` and +`./frontend` build contexts resolve). Config block at the top holds host/user/key/alias. + +Usage: + +```bash +./deploy.sh # full cycle: build both, ship both, swap both containers +VERSION=v2 ./deploy.sh # tag images v2 instead of a timestamp → named rollback point +./deploy.sh --build --ship --swap # explicit equivalent of the default +./deploy.sh --restart-caddy # after editing the Caddyfile to add/modify routes +``` + +With no arguments it runs `build → ship → swap`. `VERSION` defaults to a timestamp; set +it explicitly to create a named point you can redeploy later. + +## Steps the script performs + +1. **build** — `podman build` the backend and frontend images locally. +2. **ship** — `podman save | ssh -i matt@35.203.147.44 'podman load'` for + each image. +3. **swap** — over `--connection gcp-lab`: `rm -f` then `run -d` each container with its + name and port mapping. +4. **restart-caddy** (on demand) — `podman --connection gcp-lab restart caddy-proxy`, + needed only when the Caddyfile changed (e.g. new XSS/traversal routes). + +## IMPORTANT: keep `swap()` in sync with real run flags + +The current `swap()` recreates containers with only `--name` and `-p`. It does **not** +yet reproduce any bind mounts (e.g. the SQLite `demo.db`), config-file mounts, or +network/DNS-alias flags the real containers depend on. **Before relying on the script, +confirm the actual `podman run` flags** the current `backend-api` and `frontend-vue` +were created with and fold them into `swap()`, or a deploy will silently drop them. + +To inspect what the running containers actually use: + +```bash +podman --connection gcp-lab inspect backend-api | jq '.[0].Mounts, .[0].NetworkSettings' +podman --connection gcp-lab inspect frontend-vue | jq '.[0].Mounts, .[0].NetworkSettings' +``` + +## Caddy config / cert deploys + +The Caddyfile and certs live on the host at `~/imperva-proxy/` and are bind-mounted +read-only into `caddy-proxy`. To change routing: + +1. Edit `imperva-proxy/Caddyfile` in the repo, copy it to the host `~/imperva-proxy/`. +2. Validate before restarting so you're not debugging from a crash loop: + ```bash + podman --connection gcp-lab exec caddy-proxy caddy validate --config /etc/caddy/Caddyfile + ``` +3. `./deploy.sh --restart-caddy`. + +If the container is already crash-looping and `exec` won't attach, validate against the +mount with a throwaway container instead: + +```bash +podman run --rm -v /home/matt/imperva-proxy/Caddyfile:/etc/caddy/Caddyfile:ro,Z \ + docker.io/library/caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile +``` + +## Verifying a deploy + +- Origin health from the host (self-signed, so `-k`): + ```bash + podman --connection gcp-lab exec caddy-proxy sh -c 'wget -qO- --no-check-certificate https://localhost/ | head' + ``` + (or `curl -vk https://localhost/` directly on the host) +- Real path end to end: load `https://flytoouter.space/` in a browser — should show + Imperva's trusted edge cert while the origin serves self-signed to Imperva only. diff --git a/frontend/Containerfile b/frontend/Containerfile new file mode 100644 index 0000000..1c3ccd9 --- /dev/null +++ b/frontend/Containerfile @@ -0,0 +1,13 @@ +# Stage 1: Build the Vue App +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json . +RUN npm install +COPY . . +RUN npm run build + +# Stage 2: Serve static files with Nginx +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 8080 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c65311f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Outer Space Flights + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..cd19774 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 8080; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..0b285fa --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1242 @@ +{ + "name": "imperva-demo-frontend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "imperva-demo-frontend", + "dependencies": { + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz", + "integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.40", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz", + "integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz", + "integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.40", + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-ssr": "3.5.40", + "@vue/shared": "3.5.40", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz", + "integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz", + "integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz", + "integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz", + "integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.40", + "@vue/runtime-core": "3.5.40", + "@vue/shared": "3.5.40", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz", + "integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/shared": "3.5.40" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz", + "integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.40", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz", + "integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.40", + "@vue/compiler-sfc": "3.5.40", + "@vue/runtime-dom": "3.5.40", + "@vue/server-renderer": "3.5.40", + "@vue/shared": "3.5.40" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..edea5f3 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,6 @@ +{ + "name": "imperva-demo-frontend", + "scripts": { "dev": "vite", "build": "vite build" }, + "dependencies": { "vue": "^3.4.0" }, + "devDependencies": { "vite": "^5.2.0", "@vitejs/plugin-vue": "^5.0.0" } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..911ad9a --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,42 @@ + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..e5c66db --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,31 @@ +const BASE = '/api/v1' + +async function request(path) { + const res = await fetch(`${BASE}${path}`) + const body = await res.json().catch(() => null) + return { ok: res.ok, status: res.status, body } +} + +export function searchFlights(term) { + return request(`/search?term=${encodeURIComponent(term)}`) +} + +export function searchUsers(q) { + return request(`/users?q=${encodeURIComponent(q)}`) +} + +export async function downloadBoardingPass(file) { + const res = await fetch(`${BASE}/download?file=${encodeURIComponent(file)}`) + const text = await res.text() + return { ok: res.ok, status: res.status, text } +} + +export async function login(username, password) { + const res = await fetch(`${BASE}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }) + const body = await res.json().catch(() => null) + return { ok: res.ok, status: res.status, body } +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..fe5bae3 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './style.css' + +createApp(App).mount('#app') diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..84265a1 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,148 @@ +:root { + --bg: #0b1020; + --panel: #131a2e; + --border: #232c47; + --text: #e6e9f2; + --muted: #8a92b2; + --accent: #5b8cff; + --accent-2: #7c5bff; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + background: linear-gradient(160deg, var(--bg), #050810); + color: var(--text); + min-height: 100vh; +} + +#app { min-height: 100vh; } + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); +} + +.brand { + font-size: 1.25rem; + font-weight: 700; + background: linear-gradient(90deg, var(--accent), var(--accent-2)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +nav.tabs { + display: flex; + gap: 0.5rem; +} + +nav.tabs button { + background: transparent; + border: 1px solid var(--border); + color: var(--muted); + padding: 0.5rem 1rem; + border-radius: 999px; + cursor: pointer; + font-size: 0.9rem; +} + +nav.tabs button.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +main { + max-width: 860px; + margin: 0 auto; + padding: 2rem 1.5rem; +} + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; +} + +.panel h2 { + margin-top: 0; +} + +.field-row { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; +} + +input[type="text"], +input[type="password"] { + flex: 1; + background: #0d1326; + border: 1px solid var(--border); + color: var(--text); + padding: 0.6rem 0.8rem; + border-radius: 8px; + font-size: 0.95rem; +} + +button.primary { + background: var(--accent); + border: none; + color: white; + padding: 0.6rem 1.2rem; + border-radius: 8px; + cursor: pointer; + font-size: 0.95rem; +} + +button.primary:hover { opacity: 0.9; } + +.echo { + color: var(--muted); + margin-bottom: 1rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + text-align: left; + padding: 0.5rem 0.6rem; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; +} + +th { color: var(--muted); font-weight: 600; } + +pre.result { + background: #0d1326; + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem; + white-space: pre-wrap; + word-break: break-word; + font-size: 0.85rem; +} + +.status { + margin-top: 0.75rem; + font-size: 0.9rem; +} + +.status.error { color: #ff6b6b; } +.status.ok { color: #63e6be; } + +.hint { + color: var(--muted); + font-size: 0.85rem; + margin-top: 1.5rem; +} diff --git a/frontend/src/views/BoardingPassDownload.vue b/frontend/src/views/BoardingPassDownload.vue new file mode 100644 index 0000000..e066ac1 --- /dev/null +++ b/frontend/src/views/BoardingPassDownload.vue @@ -0,0 +1,37 @@ + + + diff --git a/frontend/src/views/FlightSearch.vue b/frontend/src/views/FlightSearch.vue new file mode 100644 index 0000000..fb45f94 --- /dev/null +++ b/frontend/src/views/FlightSearch.vue @@ -0,0 +1,56 @@ + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..3c4c14d --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/views/PassengerLookup.vue b/frontend/src/views/PassengerLookup.vue new file mode 100644 index 0000000..339e2da --- /dev/null +++ b/frontend/src/views/PassengerLookup.vue @@ -0,0 +1,55 @@ + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..37571e3 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + proxy: { + // Mirrors the Caddy `/api/*` -> backend route used in prod, so the + // frontend talks to the API the same way (same-origin) in dev and prod. + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/imperva-proxy/Caddyfile b/imperva-proxy/Caddyfile new file mode 100644 index 0000000..4591938 --- /dev/null +++ b/imperva-proxy/Caddyfile @@ -0,0 +1,15 @@ +{ + auto_https off +} + +:443 { + tls /etc/caddy/certs/origin.crt /etc/caddy/certs/origin.key + + handle /api/* { + reverse_proxy host.containers.internal:8000 + } + + handle { + reverse_proxy host.containers.internal:8080 + } +} diff --git a/imperva-proxy/certs/origin.crt b/imperva-proxy/certs/origin.crt new file mode 100644 index 0000000..d276227 --- /dev/null +++ b/imperva-proxy/certs/origin.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOjCCAiKgAwIBAgIUXT8uwyaJMEENfQOgs6SsXZ4W+C0wDQYJKoZIhvcNAQEL +BQAwGzEZMBcGA1UEAwwQZmx5dG9vdXRlci5zcGFjZTAeFw0yNjA3MjgyMDAyNTRa +Fw0zNjA3MjUyMDAyNTRaMBsxGTAXBgNVBAMMEGZseXRvb3V0ZXIuc3BhY2UwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3gl823XO7+Qx09H0rHoocm/YE +NeUYXFtWLevhptFYOpoyU+EafsogrHMJO9dy0Tnt/sA6dv+Td7oF4BxrJykbr6Fh ++xGonXGck/vTh7RE4eNClmQBZ7jJrMN0BhCtw+gvmUVgzNAz2ItqoCs37fX2EC6+ +AcApAbWIAnNmNoKa7Sc6CL+uGm6I3eW769J72rDxhm27ZUtFZZTxF8jhc64kR6TM +TDSAMLt2axlUHsYZ4GdLjsJ47rb461Cn9smUX4SFD74qSNmYDPzZbBgvmi+/UkC7 +lFsT+6FwYP/wc3eRVjVhb6j1mjG/UiVwYY59eUoKbN1Ebl/XpvJHs4skxqwLAgMB +AAGjdjB0MB0GA1UdDgQWBBTEJ04zRuHst5AwwW32lpBe43YONzAfBgNVHSMEGDAW +gBTEJ04zRuHst5AwwW32lpBe43YONzAPBgNVHRMBAf8EBTADAQH/MCEGA1UdEQQa +MBiCEGZseXRvb3V0ZXIuc3BhY2WHBCPLkywwDQYJKoZIhvcNAQELBQADggEBAGAH +EkzsBrSa30386HeOLChmbgIeCIDQLtfwPSgMtTunWTHG5fsDWIMoOOYEWT/dZdwj +8pw7FbvkKYDbnU8QufP3v+qyfvgsvAlQ9Is5+E9eTm0mmBi/zQBBZCe+vzF1EfMp +xHcvsuVwoDgaQ39Tk6rzERjpr2oIhf6UL4IFc7xP8/vmyUaIzFffxuFXMuheqGnv +uDqV+HxsJbaQnbqfRGxHmEoXNz4YhuUvT1qFgh0aeB/PDgr1T7gGvP/nrfJ0gA6D +Tvk7xnjWCeo+aKvHO6ZNqCrLqJbi5MgitdEQ5GYX4PGRH4xay9fpGlwP47oqS5Gn +XxFeG/YP/3uKDgjBOSE= +-----END CERTIFICATE-----