diff --git a/.gitignore b/.gitignore index 652c9e6e..3da1c077 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ Thumbs.db __pycache__/ *.py[cod] +# Screenshot generator runtime output +docs/screenshots/runtime/ + diff --git a/README.md b/README.md index 816a9c75..ac8a7e54 100644 --- a/README.md +++ b/README.md @@ -188,15 +188,15 @@ See [**START_HERE.md**](START_HERE.md) for guided navigation. - [x] **#12** — Architecture documentation - [x] **#13** — CI/CD pipeline (GitHub Actions + SonarQube + Telegram notifications) -### 🚀 Phase 2 — Active/Planned (Issues #14–#20) +### ✅ Phase 2 — Completed (Issues #14–#20) - [x] **#14** — Support additional event types (`UserProfileChanged`, `EntityCreated`, `EntityUpdated`, `DataDeleted`) - [x] **#15** — Authentication & Authorization (JWT + Spring Security + RBAC) - [x] **#16** — Advanced filtering, search, date range picker, CSV export - [x] **#17** — Event timeline visualization -- [ ] **#18** — Reconciliation Service (batch integrity checking + Quartz scheduler) -- [ ] **#19** — Kubernetes manifests + Helm chart -- [ ] **#20** — Live demo scenario + Q&A documentation +- [x] **#18** — Reconciliation Service (batch integrity checking + Quartz scheduler) +- [x] **#19** — Kubernetes manifests + Helm chart +- [x] **#20** — Live demo scenario + Q&A documentation **Full roadmap and issue tracking:** [**docs/ROADMAP.md**](docs/ROADMAP.md) @@ -226,6 +226,7 @@ All documentation is located in the `docs/` directory: | [**docs/DEPLOYMENT.md**](docs/DEPLOYMENT.md) | Quickstart + environment setup | | [**docs/ROADMAP.md**](docs/ROADMAP.md) | Complete GitHub Issues roadmap (Phase 1–4) | | [**docs/TESTING_SCENARIOS.md**](docs/TESTING_SCENARIOS.md) | Live demo scenarios + curl commands + screenshots | +| [**docs/LIVE_DEMO_QA.md**](docs/LIVE_DEMO_QA.md) | Interview-ready live demo script + architecture Q&A | | [**docs/EVENT_HASH_CANONICAL_MIGRATION.md**](docs/EVENT_HASH_CANONICAL_MIGRATION.md) | Event hash canonicalization + recovery procedures | | [**CONTRIBUTING.md**](CONTRIBUTING.md) | Contribution workflow, PR guidelines | diff --git a/START_HERE.md b/START_HERE.md index d001143f..79eb7e1f 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -25,10 +25,13 @@ Use this file as the single entry point for project setup and work planning. ## 4) Current status -- MVP Phase 1 is complete (Issues #1–#13) -- Next open implementation item is Issue #14 (additional event types) +- MVP Phase 1 is complete (Issues #1-#13) +- Phase 2 items (#14-#20) are complete in roadmap; use docs below for demo preparation and next planning ## 5) Next action -Confirm local prerequisites from section 1, then start with the next open issue in [`docs/ROADMAP.md`](docs/ROADMAP.md). +Confirm local prerequisites from section 1, then: + +1. Use [`docs/LIVE_DEMO_QA.md`](docs/LIVE_DEMO_QA.md) for interview-ready demo flow. +2. Use [`docs/ROADMAP.md`](docs/ROADMAP.md) to plan the next issue beyond phase 2. diff --git a/docs/LIVE_DEMO_QA.md b/docs/LIVE_DEMO_QA.md new file mode 100644 index 00000000..3b98142d --- /dev/null +++ b/docs/LIVE_DEMO_QA.md @@ -0,0 +1,238 @@ +# Live Demo Scenario and Q&A (Issue #20) + +This document is an interview-ready script for demonstrating the Distributed Audit Ledger end to end. + +## Audience and Goal + +- Audience: interviewer, tech lead, architect, or security engineer. +- Goal: prove immutable audit flow from command API to on-chain anchoring and read-side integrity checks. +- Duration: 12-20 minutes (core path: ~10 minutes, Q&A: ~10 minutes). + +## Demo Narrative (What to Say) + +1. "I send a command to the command service." +2. "The command service emits a domain event to Kafka." +3. "Event store persists immutable JSON payload + canonical SHA-256 hash." +4. "Audit writer anchors the same hash in the smart contract." +5. "Query service exposes audit logs and integrity status (`ON_CHAIN`, `MISMATCH`, `PENDING`)." + +## Prerequisites + +- Docker stack from `deploy/` is running. +- Backend services are running on ports `8081`-`8084`. +- `AUDIT_LEDGER_CONTRACT_ADDRESS` is configured for blockchain-aware services. +- Tools: `curl`, `jq`. +- Optional (for Steps 5–6): `docker` CLI, `psql` client. + +Quick health check: + +```bash +curl http://localhost:8081/actuator/health +curl http://localhost:8082/actuator/health +curl http://localhost:8083/actuator/health +curl http://localhost:8084/actuator/health +``` + +## Demo Script (Core Path) + +### Step 0: Obtain JWT token + +```bash +TOKEN=$(curl -s -X POST http://localhost:8081/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123!"}' | jq -r '.accessToken') +``` + +### Step 1: Send one command + +```bash +CMD=$(curl -s -X POST http://localhost:8081/commands/user/login \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"userId":"demo.user@example.com"}') +echo "$CMD" +EVENT_ID=$(echo "$CMD" | jq -r '.eventId') +``` + +Expected: response contains `success=true` and `eventId`. + +### Step 2: Read events from query service + +```bash +curl -s "http://localhost:8084/api/audit-logs?userId=demo.user@example.com&limit=5" \ + -H "Authorization: Bearer $TOKEN" +``` + +Expected: + +- At least one event with `eventType=USER_LOGGED_IN`. +- `eventHash` is present. +- Note: `integrityStatus` in the list always shows `PENDING` by default. The live on-chain status is resolved by the integrity-check endpoint (Step 4). + +### Step 3: Resolve audit ID for integrity check + +Use the `eventId` captured in Step 1 to find the exact record: + +```bash +AUDIT_ID=$(curl -s "http://localhost:8084/api/audit-logs?userId=demo.user@example.com&limit=20" \ + -H "Authorization: Bearer $TOKEN" | jq -r --arg eid "$EVENT_ID" '.[] | select(.eventId == $eid) | .id') +echo "$AUDIT_ID" +``` + +Expected: numeric ID (for example `42`). + +### Step 4: Run integrity check + +```bash +curl -s "http://localhost:8084/api/audit-logs/$AUDIT_ID/integrity-check" \ + -H "Authorization: Bearer $TOKEN" +``` + +Expected: `status` eventually becomes `ON_CHAIN`. + +If `MISMATCH` appears immediately after command, wait a few seconds and retry (asynchronous anchor timing). + +### Step 5: Show Kafka flow proof (optional) + +```bash +docker exec dal-kafka kafka-topics --bootstrap-server localhost:9092 --list +``` + +Expected topics include `user.login.events`. + +### Step 6: Show DB evidence (optional) + +```bash +psql -h localhost -U postgres -d audit_ledger \ + -c "SELECT id, event_id, event_type, user_id, event_hash, created_at FROM audit.events ORDER BY id DESC LIMIT 5;" +``` + +Expected: latest event row with non-empty `event_hash`. + +## UI Walkthrough (2-4 minutes) + +Use screenshot pack and/or running Angular app: + +1. Command accepted response: `docs/screenshots/01-command-accepted.png` +2. Audit list response: `docs/screenshots/02-audit-logs-list.png` +3. Integrity `ON_CHAIN`: `docs/screenshots/03-integrity-on-chain.png` +4. Integrity `MISMATCH`: `docs/screenshots/04-integrity-mismatch.png` +5. Kafka topics: `docs/screenshots/05-kafka-topics.png` +6. Postgres events: `docs/screenshots/06-postgres-audit-events.png` +7. Health endpoints: `docs/screenshots/07-health-endpoints.png` +8. Angular dashboard: `docs/screenshots/08-angular-dashboard.png` + +## Demo Variants + +### Fast path (5-7 minutes) + +- Run Step 1, 2, 4 only. +- Explain asynchronous eventual consistency in one sentence. + +### Deep technical path (15-20 minutes) + +- Run full core path. +- Add Kafka consumer groups and lag checks. +- Discuss canonical hash serialization and why it prevents DB/on-chain divergence. + +## Typical Interview Q&A + +### 1) Why blockchain here if you already store data in PostgreSQL? + +- PostgreSQL stores operational history. +- Blockchain adds external tamper-evidence with immutable anchoring. +- Integrity endpoint cross-checks DB hash against on-chain evidence. + +### 2) How do you guarantee hash consistency across services? + +- Both `event-store-service` and `audit-writer-service` use `CanonicalObjectMapperFactory.create()`. +- Canonical JSON (stable field order and timestamp formatting) guarantees byte-identical hashing. + +### 3) Why Kafka between services? + +- Decouples write pipeline stages. +- Supports backpressure/retries and independent scaling. +- Preserves event-driven architecture boundaries. + +### 4) What does `PENDING` mean? + +`PENDING` appears in two different contexts with different meanings: + +- **Audit log list** (`GET /api/audit-logs`): `integrityStatus` is always `"PENDING"` — it is a hard-coded placeholder set by `AuditEventDtoMapper.defaultIntegrityStatus()`. It says nothing about whether `event_hash` is present or the record is valid; it simply means "not checked yet". +- **Integrity-check endpoint** (`GET /api/audit-logs/{id}/integrity-check`): `PENDING` means the `event_hash` column is null or blank in `audit.events`. This is a legacy or corrupt-row condition — the event-store service writes `event_hash` synchronously during persistence, so a blank hash indicates the row was never properly processed. `PENDING` is **not** returned when the blockchain is unreachable; that scenario results in an error response instead. + +### 5) What does `MISMATCH` mean? + +- Stored DB hash is not observed on-chain. +- Could indicate delayed anchoring, write failure, or tampering. + +### 6) How do you handle duplicate blockchain writes? + +- Contract rejects duplicate hashes. +- Service uses retries + dead-letter topic for failed processing. + +### 7) What are your failure isolation points? + +- Command acceptance is isolated from downstream anchoring latency. +- Event store and audit writer consume asynchronously and can recover independently. + +### 8) Which service owns schema migrations? + +- Only `event-store-service` owns Flyway migrations for `audit.events`. +- Other services must not introduce independent Flyway schemas. + +### 9) Is this exactly-once processing? + +- End-to-end exactly-once is hard in distributed systems. +- Design is effectively-once at business level via idempotency keys, duplicate hash rejection, and unique constraints. + +### 10) How do you scale read traffic? + +- `query-service` is read-optimized and stateless. +- Scale horizontally behind load balancer. +- Keep writes and reads separated (CQRS). + +### 11) Why WebFlux + Reactor? + +- Non-blocking model for I/O heavy services. +- Better resource use under concurrent traffic. +- Fits event-driven architecture. + +### 12) How do you secure this in production? + +- JWT + role-based access control. +- Secret management for private keys and contract addresses. +- TLS, network policies, audit logging, and limited DB privileges. + +## Risk/Trade-off Talking Points + +- Eventual consistency: integrity status may lag right after command acceptance. +- Operational complexity: adds Kafka + blockchain infra. +- Cost/performance: anchoring every event can be expensive on public networks; batching is a future optimization. + +## Troubleshooting During Demo + +### Integrity never reaches ON_CHAIN + +- Check `AUDIT_LEDGER_CONTRACT_ADDRESS` for `audit-writer-service` and `query-service`. +- Verify Ganache RPC is reachable at `http://127.0.0.1:8545`. +- Ensure private key env var is set for `audit-writer-service`. + +### No events in query service + +- Verify Kafka topic exists: `user.login.events`. +- Check event-store consumer logs and PostgreSQL connectivity. + +### 401/403 from APIs + +- Re-authenticate and include JWT token. +- Confirm role permissions for requested endpoint. + +## Related Docs + +- `docs/ARCHITECTURE.md` +- `docs/CQRS_FLOW.md` +- `docs/TESTING_SCENARIOS.md` +- `docs/DEPLOYMENT.md` +- `docs/EVENT_HASH_CANONICAL_MIGRATION.md` + diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5f5c01cd..8f5a7ae7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -542,6 +542,7 @@ CREATE TABLE audit.events ( **ID:** #20 **Labels:** `docs` **Depends on:** #12, #18 +**Status:** ✅ Done **Description:** Подготовить готовый сценарий для демонстрации на собеседовании. @@ -553,13 +554,17 @@ CREATE TABLE audit.events ( - Потенциальные вопросы и ответы **Subtasks:** -- [ ] #20.1 - Написать guide -- [ ] #20.2 - Prepare curl команды -- [ ] #20.3 - Prepare скриншоты UI -- [ ] #20.4 - Q&A документ +- [x] #20.1 - Написать guide +- [x] #20.2 - Prepare curl команды +- [x] #20.3 - Prepare скриншоты UI +- [x] #20.4 - Q&A документ **Expected PR:** PR-20 (Demo scenario documentation) +**Deliverables:** +- `docs/LIVE_DEMO_QA.md` +- `docs/TESTING_SCENARIOS.md` + --- ## Labels Reference diff --git a/docs/screenshots/01-command-accepted.png b/docs/screenshots/01-command-accepted.png index 082f2482..2f4d13c6 100644 Binary files a/docs/screenshots/01-command-accepted.png and b/docs/screenshots/01-command-accepted.png differ diff --git a/docs/screenshots/02-audit-logs-list.png b/docs/screenshots/02-audit-logs-list.png index 9b241801..c7e540e6 100644 Binary files a/docs/screenshots/02-audit-logs-list.png and b/docs/screenshots/02-audit-logs-list.png differ diff --git a/docs/screenshots/03-integrity-on-chain.png b/docs/screenshots/03-integrity-on-chain.png index d737f008..328dd6a6 100644 Binary files a/docs/screenshots/03-integrity-on-chain.png and b/docs/screenshots/03-integrity-on-chain.png differ diff --git a/docs/screenshots/04-integrity-mismatch.png b/docs/screenshots/04-integrity-mismatch.png index e9c825cf..9e505f5b 100644 Binary files a/docs/screenshots/04-integrity-mismatch.png and b/docs/screenshots/04-integrity-mismatch.png differ diff --git a/docs/screenshots/05-kafka-topics.png b/docs/screenshots/05-kafka-topics.png index c409fd30..7bab150d 100644 Binary files a/docs/screenshots/05-kafka-topics.png and b/docs/screenshots/05-kafka-topics.png differ diff --git a/docs/screenshots/06-postgres-audit-events.png b/docs/screenshots/06-postgres-audit-events.png index c2b62be2..b9d76452 100644 Binary files a/docs/screenshots/06-postgres-audit-events.png and b/docs/screenshots/06-postgres-audit-events.png differ diff --git a/docs/screenshots/07-health-endpoints.png b/docs/screenshots/07-health-endpoints.png index 9ac31f2c..8394818e 100644 Binary files a/docs/screenshots/07-health-endpoints.png and b/docs/screenshots/07-health-endpoints.png differ diff --git a/docs/screenshots/08-angular-dashboard.png b/docs/screenshots/08-angular-dashboard.png index 0db1b9f4..416b1837 100644 Binary files a/docs/screenshots/08-angular-dashboard.png and b/docs/screenshots/08-angular-dashboard.png differ diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md index 486c58c5..1b49b0b7 100644 --- a/docs/screenshots/README.md +++ b/docs/screenshots/README.md @@ -22,6 +22,37 @@ Use lowercase kebab-case names and keep files in PNG format: - Keep terminal width wide enough so commands/results are readable - Prefer one screenshot per scenario outcome +## Runtime Regeneration + +Prerequisites: + +```pwsh +pip install Pillow +``` + +Requires **Python 3.9+**, a running local stack (`deploy/docker-compose.yml`) and all four backend services on ports `8081`–`8084`. Credentials are read from env vars `DEMO_USERNAME` (default: `admin`) and `DEMO_PASSWORD` (default: `admin123!`). + +The script connects to Docker containers and PostgreSQL using the following env vars (shown with defaults matching `deploy/docker-compose.yml`): + +| Env var | Default | Purpose | +|-----------------------|----------------|------------------------------------| +| `DEMO_USERNAME` | `admin` | Auth login username | +| `DEMO_PASSWORD` | `admin123!` | Auth login password | +| `POSTGRES_CONTAINER` | `dal-postgres` | Docker container name for psql | +| `POSTGRES_DB` | `audit_ledger` | Database name | +| `POSTGRES_USER` | `postgres` | PostgreSQL user | +| `KAFKA_CONTAINER` | `dal-kafka` | Docker container name for Kafka | +| `SCREENSHOT_TIMESTAMP`| `1` | Set `0` for deterministic PNGs | +| `CAPTURE_OUTPUT` | `docs/screenshots/runtime/capture.json` | Path for `capture.json` output | + +Use the generator to refresh screenshots from live local services: + +```pwsh +python docs/screenshots/generate_runtime_screenshots.py +``` + +The script stores raw capture data in `docs/screenshots/runtime/capture.json` (gitignored). + ## Current Status The screenshot pack files are present in this folder: @@ -37,3 +68,7 @@ The screenshot pack files are present in this folder: If needed for final demo polish, replace generated images with runtime-captured screenshots while keeping the same filenames. +Note for `08-angular-dashboard.png`: +- If Angular app on `http://localhost:4200` is running, the screenshot will include a live frontend probe result. +- If frontend is not running, the file captures the probe error so the demo pack still remains complete and reproducible. + diff --git a/docs/screenshots/generate_runtime_screenshots.py b/docs/screenshots/generate_runtime_screenshots.py new file mode 100644 index 00000000..56102763 --- /dev/null +++ b/docs/screenshots/generate_runtime_screenshots.py @@ -0,0 +1,310 @@ +import json +import os +import subprocess +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import List, Optional +from urllib import request + +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[2] +SCREEN_DIR = ROOT / "docs" / "screenshots" +RUNTIME_DIR = SCREEN_DIR / "runtime" +RUNTIME_DIR.mkdir(exist_ok=True) + +POSTGRES_CONTAINER = os.environ.get("POSTGRES_CONTAINER", "dal-postgres") +POSTGRES_DB = os.environ.get("POSTGRES_DB", "audit_ledger") +POSTGRES_USER = os.environ.get("POSTGRES_USER", "postgres") +KAFKA_CONTAINER = os.environ.get("KAFKA_CONTAINER", "dal-kafka") + +W, H = 1366, 768 +BG = (9, 15, 25) +FG = (236, 239, 244) +MUTED = (148, 163, 184) +ACCENT = (96, 165, 250) + + +def now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def http_json(method: str, url: str, body=None, token: Optional[str] = None, user_agent: Optional[str] = None): + payload = None if body is None else json.dumps(body).encode("utf-8") + headers = {"Content-Type": "application/json"} + if token: + headers["Authorization"] = f"Bearer {token}" + if user_agent: + headers["User-Agent"] = user_agent + req = request.Request(url=url, data=payload, headers=headers, method=method) + with request.urlopen(req, timeout=15) as resp: + text = resp.read().decode("utf-8") + return resp.status, text, json.loads(text) + + +def http_text(url: str): + req = request.Request(url=url, method="GET") + with request.urlopen(req, timeout=10) as resp: + text = resp.read().decode("utf-8", errors="replace") + return resp.status, text + + +def run_cmd(args: List[str]) -> str: + result = subprocess.run(args, check=True, capture_output=True, text=True) + return result.stdout.strip() + + +def psql(sql: str) -> str: + return run_cmd([ + "docker", "exec", POSTGRES_CONTAINER, + "psql", "-U", POSTGRES_USER, "-d", POSTGRES_DB, + "-At", "-c", sql, + ]) + + +def font(size: int, bold: bool = False): + candidates = [ + # Windows + "C:/Windows/Fonts/consolab.ttf" if bold else "C:/Windows/Fonts/consola.ttf", + "C:/Windows/Fonts/arialbd.ttf" if bold else "C:/Windows/Fonts/arial.ttf", + # Linux (DejaVu / Liberation) + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf" if bold else "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf", + # macOS + "/System/Library/Fonts/Supplemental/Menlo Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Menlo.ttc", + "/Library/Fonts/Courier New Bold.ttf" if bold else "/Library/Fonts/Courier New.ttf", + ] + for c in candidates: + p = Path(c) + if p.exists(): + return ImageFont.truetype(str(p), size=size) + return ImageFont.load_default() + + +def wrap_text(draw: ImageDraw.ImageDraw, text: str, fnt, width: int) -> List[str]: + lines = [] + for raw in text.splitlines() or [""]: + current = "" + for part in raw.split(" "): + candidate = part if not current else current + " " + part + if draw.textlength(candidate, font=fnt) <= width: + current = candidate + else: + if current: + lines.append(current) + current = part + lines.append(current) + return lines + + +def render(path: Path, title: str, subtitle: str, body: str): + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + t_font = font(56, bold=True) + s_font = font(30) + b_font = font(27) + + x = 56 + y = 48 + d.text((x, y), title, fill=FG, font=t_font) + y += 78 + d.text((x, y), subtitle, fill=MUTED, font=s_font) + y += 66 + + block = [ + "Distributed Audit Ledger - Runtime Screenshot Pack", + *([f"Generated: {now_iso()}"] if os.environ.get("SCREENSHOT_TIMESTAMP", "1") not in ("0", "false", "no") else []), + f"File: {path.name}", + "", + ] + block.extend(wrap_text(d, body, b_font, W - 2 * x)) + + for line in block: + color = ACCENT if line.startswith("$") else FG + d.text((x, y), line, fill=color, font=b_font) + y += 36 + if y > H - 40: + break + + img.save(path, format="PNG") + + +def pretty(obj) -> str: + return json.dumps(obj, ensure_ascii=False, indent=2) + + +def main(): + out = {} + + # 1) Auth token + password = os.environ.get("DEMO_PASSWORD", "admin123!") + _, _, auth_obj = http_json( + "POST", + "http://localhost:8081/auth/login", + {"username": os.environ.get("DEMO_USERNAME", "admin"), "password": password}, + ) + token = auth_obj["accessToken"] + out["auth"] = auth_obj + + # 2) Command accepted + _, _, cmd_obj = http_json( + "POST", + "http://localhost:8081/commands/user/login", + {"userId": "demo.user@example.com", "ipAddress": "127.0.0.1", "userAgent": "runtime-capture"}, + token=token, + user_agent="runtime-capture", + ) + event_id = cmd_obj.get("eventId") + if not event_id: + raise RuntimeError(f"Command did not return an eventId (success={cmd_obj.get('success')}). Response: {cmd_obj}") + out["command"] = cmd_obj + + # 3) Poll query list + logs = [] + for _ in range(20): + _, _, logs_obj = http_json( + "GET", + "http://localhost:8084/api/audit-logs?userId=demo.user@example.com&limit=20", + token=token, + ) + logs = logs_obj if isinstance(logs_obj, list) else [] + if any(row.get("eventId") == event_id for row in logs): + break + time.sleep(1) + + if not logs: + raise RuntimeError("No audit logs returned by query-service") + + selected = next((r for r in logs if r.get("eventId") == event_id), None) + if selected is None: + raise RuntimeError( + f"Event {event_id} not found in query-service after polling. " + "Check event-store consumer lag and query-service connectivity." + ) + audit_id = int(selected["id"]) + out["list"] = logs + out["selectedAuditId"] = audit_id + + # 4) Integrity ON_CHAIN polling + integrity_obj = None + for _ in range(20): + _, _, integrity_obj = http_json( + "GET", + f"http://localhost:8084/api/audit-logs/{audit_id}/integrity-check", + token=token, + ) + if integrity_obj.get("status") == "ON_CHAIN": + break + time.sleep(1) + out["integrityOnChain"] = integrity_obj + + # 5) Tamper to force mismatch and then restore (always restore in finally) + original_hash = psql(f"SELECT event_hash FROM audit.events WHERE id={audit_id};") + tampered = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + psql(f"UPDATE audit.events SET event_hash='{tampered}' WHERE id={audit_id};") + try: + _, _, mismatch_obj = http_json( + "GET", + f"http://localhost:8084/api/audit-logs/{audit_id}/integrity-check", + token=token, + ) + out["integrityMismatch"] = mismatch_obj + finally: + # Always restore — use NULL when original hash was blank to avoid permanent corruption + if original_hash: + psql(f"UPDATE audit.events SET event_hash='{original_hash}' WHERE id={audit_id};") + else: + psql(f"UPDATE audit.events SET event_hash=NULL WHERE id={audit_id};") + + # 6) Kafka topics + topics = run_cmd([ + "docker", "exec", KAFKA_CONTAINER, + "kafka-topics", "--bootstrap-server", "localhost:9092", "--list", + ]).splitlines() + out["kafkaTopics"] = topics + + # 7) Postgres rows snapshot + rows = psql("SELECT id,event_id,event_type,user_id,event_hash,created_at FROM audit.events ORDER BY id DESC LIMIT 5;") + out["postgresRows"] = rows + + # 8) Health snapshot + health = {} + for port in [8081, 8082, 8083, 8084]: + try: + code, text = http_text(f"http://localhost:{port}/actuator/health") + health[str(port)] = {"statusCode": code, "body": text} + except Exception as ex: + health[str(port)] = {"error": str(ex)} + out["health"] = health + + # 9) Frontend quick probe + frontend = {} + try: + code, text = http_text("http://localhost:4200") + frontend["statusCode"] = code + frontend["preview"] = text[:600] + except Exception as ex: + frontend["error"] = str(ex) + out["frontend"] = frontend + + capture_path = Path(os.environ.get("CAPTURE_OUTPUT", str(RUNTIME_DIR / "capture.json"))) + capture_path.parent.mkdir(parents=True, exist_ok=True) + # Redact auth tokens before persisting to avoid accidental credential disclosure + safe_out = {**out, "auth": {k: "[REDACTED]" if "token" in k.lower() else v + for k, v in out.get("auth", {}).items()}} + capture_path.write_text(pretty(safe_out), encoding="utf-8") + + render( + SCREEN_DIR / "01-command-accepted.png", + "Command Accepted Response", + "POST /commands/user/login -> 202 Accepted", + "$ curl -X POST http://localhost:8081/commands/user/login ...\n" + pretty(cmd_obj), + ) + render( + SCREEN_DIR / "02-audit-logs-list.png", + "Audit Logs List", + "GET /api/audit-logs?userId=demo.user@example.com", + "$ curl \"http://localhost:8084/api/audit-logs?...\" ...\n" + pretty(logs[:3]), + ) + render( + SCREEN_DIR / "03-integrity-on-chain.png", + "Integrity Check: ON_CHAIN", + f"GET /api/audit-logs/{audit_id}/integrity-check", + "$ curl http://localhost:8084/api/audit-logs/{audit_id}/integrity-check ...\n" + pretty(integrity_obj), + ) + render( + SCREEN_DIR / "04-integrity-mismatch.png", + "Integrity Check: MISMATCH", + f"DB hash tampered for audit.events.id={audit_id}", + "$ psql UPDATE audit.events SET event_hash='0123...';\n" + pretty(mismatch_obj), + ) + render( + SCREEN_DIR / "05-kafka-topics.png", + "Kafka Topics", + f"docker exec {KAFKA_CONTAINER} kafka-topics --list", + f"$ docker exec {KAFKA_CONTAINER} kafka-topics --bootstrap-server localhost:9092 --list\n" + "\n".join(topics), + ) + render( + SCREEN_DIR / "06-postgres-audit-events.png", + "PostgreSQL audit.events", + "Latest rows from audit.events", + "$ psql -c \"SELECT ... FROM audit.events ORDER BY id DESC LIMIT 5;\"\n" + rows, + ) + render( + SCREEN_DIR / "07-health-endpoints.png", + "Service Health Endpoints", + "GET /actuator/health for ports 8081-8084", + pretty(health), + ) + render( + SCREEN_DIR / "08-angular-dashboard.png", + "Angular Dashboard Probe", + "Frontend endpoint check at http://localhost:4200", + pretty(frontend), + ) + + +if __name__ == "__main__": + main() +