diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..d1db99b
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,196 @@
+# Architecture, Key Takeaways & Security Automation Roadmap
+
+This document complements `REPORT.md` with visual architecture, the reasoning behind the technical
+choices, and where this work extends into a broader security-automation capability. Diagrams are
+Mermaid (render natively on GitHub).
+
+---
+
+## 1. Part 1 — how the CORS attack works (current, vulnerable)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant V as Victim browser (logged into NC)
+ participant E as Attacker site www.evil.com
+ participant NC as NC webapp /json-cors-origin/*
+
+ V->>E: 1. Visits malicious blog (phishing / malvertising / compromised widget)
+ E-->>V: 2. Serves front.js (runs in victim's browser)
+ V->>NC: 3. fetch() POST /json-cors-origin/appInit Origin: https://www.evil.com:3000 Cookie: superSecretSession (sent automatically)
+ NC-->>V: 4. 200 OK Access-Control-Allow-Origin: https://www.evil.com:3000 (reflected!) Access-Control-Allow-Credentials: true + full user PII payload
+ Note over V,E: Same-Origin Policy defeated: browser lets evil.com JS read the response
+ V->>E: 5. Exfiltrate stolen account data
+ E-->>V: 6. Render "we stole your data" / blackmail message
+```
+
+**Root cause in one line:** the server reflects any `Origin` into `Access-Control-Allow-Origin`
+*and* sets `Access-Control-Allow-Credentials: true`, while the session cookie is neither `HttpOnly`
+nor `SameSite`-restricted — so any site the victim visits can make authenticated calls and read the
+result.
+
+---
+
+## 2. Target hardened architecture (GCP, Cloud Run)
+
+The same app deployed the way it should be, addressing **Part 1** (CORS/cookies) and **Part 2**
+(trusted client IP) at the right layers.
+
+```mermaid
+flowchart TB
+ subgraph Internet
+ U[Legitimate user]
+ Atk[Attacker / bot]
+ end
+
+ subgraph Edge["GCP Edge"]
+ LB["External HTTPS Load Balancer (terminates TLS, sets true client IP)"]
+ CA["Cloud Armor WAF + rate-based ban rules (keyed on real client IP)"]
+ end
+
+ subgraph Run["Cloud Run service"]
+ APP["Express app trust proxy = fixed hop count CORS allow-list + HttpOnly/SameSite cookies"]
+ end
+
+ U --> LB
+ Atk --> LB
+ LB --> CA
+ CA -->|allowed| APP
+ CA -. blocked / rate-limited .-> Atk
+
+ APP --> SM["Secret Manager (session keys, tokens)"]
+```
+
+**Why each control sits where it does**
+
+| Concern | Fix | Layer | Why here |
+|---|---|---|---|
+| CORS data theft (Part 1) | Origin allow-list, no reflect-with-credentials; `HttpOnly`+`SameSite` cookies | App | Origin/cookie policy is application logic |
+| Spoofable `req.ip` (Part 2) | `trust proxy` = exact hop count (not `true`); or trust a Cloud-Armor-verified header | App + Edge | Only the edge knows the real client IP |
+| Volumetric abuse / brute force | Cloud Armor rate-based ban rules on the true client IP | Edge | Stop it before it consumes Cloud Run instances (cost + spoof-proof) |
+| Secrets | Secret Manager, not env files in the image | Platform | Rotation + least privilege |
+
+---
+
+## 3. Part 3 — log pipeline to ELK SIEM
+
+```mermaid
+flowchart LR
+ APP["Cloud Run app structured JSON logs (stdout/stderr)"] --> CL["Cloud Logging"]
+ CL -->|Log Router sink inclusion filter| PS["Pub/Sub topic"]
+ PS --> DLQ["Dead-letter topic (failed deliveries)"]
+ PS --> LS["Logstash / Elastic Agent (GCP Pub/Sub input) parse + geoip + redact PII"]
+ LS --> ES[("Elasticsearch ILM: hot/warm/cold")]
+ ES --> KB["Kibana dashboards + detection rules + alerting"]
+ KB --> SB["Slack alert (Part 5 bot)"]
+```
+
+**Design rationale (the follow-up-meeting talking points):**
+- **No agent on Cloud Run** — the platform already ships stdout/stderr to Cloud Logging; the app's
+ only job is to emit *structured* JSON so fields are queryable without regex.
+- **Pub/Sub decouples produce from consume** — if ELK is down/upgrading, messages buffer (durable
+ retention) instead of dropping; a **dead-letter topic** catches un-processable events.
+- **Scales independently** — Cloud Run, Pub/Sub, and Elasticsearch each scale on their own; a
+ traffic spike buffers in Pub/Sub rather than forcing ES to scale in lockstep.
+- **Filter at the sink** — only security-relevant logs cross the boundary (cost + signal-to-noise).
+- **Redact PII in Logstash before ES** — this app handles reproductive-health data; never index raw.
+
+---
+
+## 4. Part 5 — what was built: detection → Slack SOAR loop
+
+This is the flow implemented in this repo (`app.js` hook + `slackbot/`), proven end-to-end.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant Atk as Attacker request (credentialed cross-origin)
+ participant APP as Webapp (app.js) /json-cors-origin/*
+ participant BOT as Slackbot backend (Bolt, Socket Mode)
+ participant DB as SQLite security_events
+ participant SL as Slack channel
+ participant SEC as Security engineer
+
+ Atk->>APP: POST with Origin != own + session cookie
+ APP->>APP: Detect attack signature log "SECURITY ALERT"
+ APP->>BOT: POST /internal/alert (localhost)
+ BOT->>DB: INSERT event (status = pending)
+ BOT->>SL: Post interactive Block Kit alert [Cyber attack] [Infra instability] [False positive]
+ SEC->>SL: Clicks "Cyber attack"
+ SL->>BOT: action event over Socket Mode
+ BOT->>DB: UPDATE status + classified_by + classified_at
+ BOT->>SL: chat.update -> discreet resolved state "✅ Confirmed cyber attack · marked by "
+ Note over DB: Full audit trail retained for analytics
+```
+
+**Why these choices:**
+- **Socket Mode** — no public URL/ngrok; runs anywhere, minimal attack surface (outbound-only).
+- **Real detection hook, not a fake button** — the alert is a genuine side-effect of the exploit,
+ so it doubles as a working detection PoC for Part 4.
+- **`node:sqlite`** — zero-build persistence (no native compile), a real queryable audit store; the
+ same schema maps 1:1 onto a managed DB (Firestore/Cloud SQL) for production.
+- **State machine in the record** (`pending → classified`, with actor + timestamp) — this is what
+ makes it auditable and analyzable, which is exactly the gap the challenge calls out about plain
+ alerts + emoji reactions.
+
+---
+
+## 5. Key takeaways
+
+| # | Takeaway |
+|---|---|
+| 1 | **The CORS bug is a configuration + cookie-hardening problem, not a code-logic bug** — reflect-origin-with-credentials plus a JS-readable, cross-site cookie is the lethal combination. Fixing either layer (allow-list *or* `HttpOnly`+`SameSite`) breaks the attack. |
+| 2 | **`trust proxy: true` is a foot-gun** — it trusts an unbounded, attacker-influenced XFF chain. Trust must be pinned to the exact, known infrastructure hops, and the *real* client IP only exists at the edge. |
+| 3 | **Rate limiting belongs at the edge** (Cloud Armor) where the client IP is spoof-proof and abuse is stopped before it costs you Cloud Run instances; app-level limits are for per-user/per-key logic, not primary IP defense. |
+| 4 | **Detection should ride on generic, always-on data enriched with a few targeted fields**, with the detection *rule* living in the SIEM — not bespoke, bypassable logic buried in the app. |
+| 5 | **Alerts without workflow are not auditable.** Turning an alert into a stateful, classified, attributable record (who, what, when) is what makes it defensible to an auditor and useful for analytics — the core point of Part 5. |
+
+---
+
+## 6. Further value: security-automation capabilities this unlocks
+
+The Part 5 bot is deliberately a seed for a broader **SOAR** (Security Orchestration, Automation &
+Response) capability. Natural extensions, roughly in maturity order:
+
+```mermaid
+flowchart LR
+ A["Alert + manual triage (today: this PoC)"] --> B["Enrichment on alert (GeoIP, threat intel, user/session context)"]
+ B --> C["Guided response (runbook links, one-click 'revoke session' button)"]
+ C --> D["Auto-remediation (block Origin/IP at Cloud Armor, force logout, rotate token)"]
+ D --> E["Analytics + audit (MTTR, false-positive rate, attack trends from the event store)"]
+```
+
+- **Enrichment** — before a human looks, attach GeoIP, ASN, threat-intel reputation, and the
+ affected user/session so the Slack card is decision-ready.
+- **One-click response actions** — add buttons that *act*: revoke the leaked session, add the
+ attacker Origin/IP to a Cloud Armor deny rule, open a Jira/PagerDuty incident — all from Slack.
+- **Closed-loop auto-remediation** — for high-confidence signatures, act first and post the alert as
+ a notification of what was already contained.
+- **Analytics from the event store** — because every event is persisted with its lifecycle, you get
+ real metrics: MTTR, alert volume/trends, false-positive rate, most-targeted endpoints — the
+ "show an auditor" and "analytics" gaps the brief explicitly calls out.
+- **Detection-as-code** — express detections as versioned SIEM rules (peer-reviewed, tested),
+ not ad-hoc queries.
+
+---
+
+## 7. Tech stack
+
+| Area | Chosen / recommended | Notes |
+|---|---|---|
+| Demo webapp | Node.js + Express 5, EJS | As given; hardened per §2 |
+| Slack bot | `@slack/bolt` (Socket Mode) | Outbound-only, no public endpoint |
+| Bot persistence (PoC) | `node:sqlite` (built-in) | No native build; maps to managed DB in prod |
+| Bot persistence (prod) | Firestore or Cloud SQL (Postgres) | Managed, HA, backups |
+| Hosting | GCP Cloud Run | Autoscaling, stateless containers |
+| Edge / WAF | Cloud HTTPS Load Balancer + Cloud Armor | True client IP, rate limiting, WAF rules |
+| Secrets | GCP Secret Manager | Rotation, least privilege |
+| Log transport | Cloud Logging → Log Router sink → Pub/Sub (+ DLQ) | Durable, decoupled |
+| SIEM | ELK (Logstash → Elasticsearch → Kibana) | Detection rules, dashboards, alerting |
+| Security testing | OWASP ZAP (baseline + active) | Evidence in `evidence/` |
+| Incident routing | Slack (SOAR front-end) → Jira / PagerDuty | Stay-in-Slack workflow |
+
+---
+
+*See `REPORT.md` for the full written answers and `evidence/` for reproductions, the ZAP report,
+and the Slack screenshot.*
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..52f31fc
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,52 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this repo is
+
+This is `cors-demo`, a security *training* application (branded "Natural Cycles Security Training") that intentionally demonstrates client-side/CORS data-theft vulnerabilities so engineers can see the attack in action. Almost every file here is deliberately insecure — that is the point of the app. Do not "fix" the vulnerabilities in `app.js` or the views unless a task explicitly asks for that; the misconfigured CORS/cookie logic is the product, not a bug.
+
+## Running the app
+
+- Install: `pnpm install`
+- Start: `pnpm start` (runs `node app.js`)
+- Serves HTTPS on port 3000 using the checked-in self-signed cert (`www.nc.com.key` / `www.nc.com.pem`)
+- Browse to `https://localhost:3000` (or `https://www.nc.com:3000`) and accept the self-signed cert warning
+- The attack scenarios require simulating two origins on one machine. Add to `/etc/hosts`:
+ ```
+ 127.0.0.1 www.nc.com www.evil.com
+ ```
+- Third-party cookies must be allowed in the browser for the CORS scenario to work
+- There are no build steps, no test suite, and no linter configured in this repo — `pnpm start` is the only script
+
+## Architecture
+
+Single-file Express ("Express 5") server (`app.js`) with EJS views, no database/session store despite `mongodb`/`monk` being listed as dependencies (unused in current `app.js`). Everything hinges on a small set of middleware behaviors near the top of `app.js`:
+
+1. **`POST -> GET` override hack**: any POST to a path matching `*appInit` is rewritten to `GET` before continuing, so a real backend JSON endpoint can be simulated by Express's static file server.
+2. **Fake-JSON-via-static-file trick**: paths starting with `/json-` get `Content-Type: application/json` forced, then fall through to `express.static('public')`, which serves flat files under `public/json-default/`, `public/json-cors-origin/`, `public/json-cors-wildcard/` (e.g. `public/json-cors-origin/appInit`). These static files are the "backend response" payloads (fake user data, PII, etc.) used to prove data exfiltration worked. The `objects/` directory holds the canonical copies of these JSON fixtures (`appInit`, `postInit`, `webSignupInit`) that the `public/json-*` files are copied from.
+3. **CORS header branches by path prefix**, controlled by the module-level `allowOrigin` variable (`'*' | 'origin' | 'null'`) near the top of `app.js`:
+ - `/json-default/*` — no CORS headers set at all (baseline/safe comparison case)
+ - `/json-cors-wildcard/*` — sets `Access-Control-Allow-Origin` to `*` or to the reflected `Origin` depending on `allowOrigin`, plus `Allow-Credentials: true` (a known-bad combination the demo exploits)
+ - `/json-cors-origin/*` — always reflects the request's `Origin` header (or defaults to `https://:3000`) back as `Access-Control-Allow-Origin`, with credentials allowed — this is the "vulnerable" explicit-origin scenario
+ - There's a dead/disabled branch (`false && ...`) left in place showing an earlier variant of the wildcard logic; it's unreachable and not worth "cleaning up" unless asked.
+4. **Cookie-gated JSON routes**: `blockIfNoCookie` rejects any `/json-*` request with HTTP 400 unless a `superSecretSession` cookie is present, simulating an authenticated backend call that will be stolen via CORS.
+5. **Login**: `GET /login` sets `superSecretSession` as a non-httpOnly cookie (`res.cookie(...)`), so it can also be read/exfiltrated from JS (used by the local-storage scenario).
+6. **Local-storage theft scenario**: `PUT /local-storage/yummy` (with a matching `OPTIONS` preflight handler) reflects `Origin` back and accepts arbitrary JSON bodies, simulating an attacker endpoint receiving stolen `localStorage` contents.
+
+### View/route structure
+
+- `views/index.ejs` — landing page linking to the active demo scenarios. The Local Storage scenario section is currently commented out (see `54b5224 feat: hide local storage scenario`); the routes and views for it (`views/local-storage/*`, `public/steal.js`) still exist and work, just aren't linked from the homepage.
+- `views/cors/allow-origin/{index,webapp,attacker}.ejs` — the CORS attack walkthrough:
+ - `webapp.ejs` — a fake "target" site (`www.nc.com`) where a victim logs in (sets the `superSecretSession` cookie via JS, `SameSite=None; Secure`) and stores "secret" data
+ - `attacker.ejs` — the malicious third-party page (served from `www.evil.com`) that loads `public/front.js`, which fires a cross-origin, credentialed AJAX request at `/json-cors-origin/appInit` (or `/json-cors-wildcard/appInit` depending on `mode` attribute on the script tag) and displays whatever "stolen" data comes back
+- `views/local-storage/{index,webapp,vulnerable}.ejs` + `public/steal.js` — parallel scenario for exfiltrating `localStorage` (rather than a cookie-authenticated API) to an attacker-controlled `PUT` endpoint
+- `public/cookie-steal.js` — a minimal image-beacon cookie exfiltration payload, referenced conceptually rather than wired into a specific route
+
+### Key files to read together when modifying a scenario
+
+Because the attack flow spans server route, static fixture data, and two+ EJS views, always check all of these before changing one:
+- `app.js` (routing + CORS header logic for the scenario's path prefix)
+- `objects/` and the matching `public/json-*/` copy (the payload "stolen")
+- the relevant `views//*.ejs` pair (victim/webapp page + attacker page)
+- the relevant `public/*.js` (e.g. `front.js`, `steal.js`) that performs the cross-origin call
diff --git a/REPORT.md b/REPORT.md
new file mode 100644
index 0000000..b316ea3
--- /dev/null
+++ b/REPORT.md
@@ -0,0 +1,174 @@
+# Security & Infrastructure Engineer Challenge — Report
+
+Author: Purushotham Muktha
+Target: `cors-demo` vulnerable webapp (this repo), run locally per `README.md`
+Evidence referenced below lives in `evidence/`.
+Architecture diagrams, key takeaways, the security-automation roadmap, and the tech stack are in
+**`ARCHITECTURE.md`**.
+
+---
+
+## Part 1: CORS Vulnerability
+
+### Setup
+
+Ran the app per `README.md` (`pnpm install && pnpm start`), added the required `/etc/hosts` entries, and walked the **"Theft successful - Set allow to Origin"** scenario end to end: visited the attacker blog, logged into the fake NC webapp, returned to the attacker blog, and observed the threatening "stolen data" message render successfully.
+
+Full reproduction (browser steps + an automated curl equivalent + captured request/response headers) is in **`evidence/cors-attack-repro.md`**.
+
+### 1. Why does the attack work?
+
+The `/json-cors-origin/*` route in `app.js` (lines 78–92) builds its CORS response by **reflecting whatever `Origin` header the requester sends** back as `Access-Control-Allow-Origin`, and unconditionally sets `Access-Control-Allow-Credentials: true`:
+
+```js
+if (req.get("Origin") != null) {
+ res.setHeader('Access-Control-Allow-Origin', req.get("Origin"));
+} else {
+ res.setHeader('Access-Control-Allow-Origin', "https://" + req.hostname + ":3000");
+}
+res.setHeader('Access-Control-Allow-Credentials', 'true');
+```
+
+This is the canonical CORS misconfiguration: reflect-origin + allow-credentials together mean *any* website on the internet can make a `fetch`/XHR request with `credentials: 'include'` and have the browser both send the victim's session cookie **and** allow the JS to read the response. There is no origin allow-list, so the Same-Origin Policy provides zero protection here. Compounding it, the session cookie (`superSecretSession`, set in `app.js:105` and again client-side in `views/cors/allow-origin/webapp.ejs`) is **not `HttpOnly`** and uses `SameSite=None`, so it's both readable by JS and sent on cross-site requests — there's no secondary defense layer (no `SameSite=Lax/Strict`, no CSRF token, no per-origin allow-list) to fall back on once the CORS check is bypassed.
+
+### 2. How could an attacker get the user to visit the malicious blog?
+
+Realistic delivery vectors for a logged-in NC user, any of which just need the victim's browser to load attacker-controlled JS while the session cookie is still valid (no interaction with the NC app itself required):
+- **Phishing / social engineering**: an email, SMS, or social media post ("read this health article") linking to the attacker's page.
+- **Malvertising**: a paid ad network serving the attacker's page/script on otherwise legitimate sites.
+- **Compromised third-party content**: a supply-chain compromise of a JS library, widget, or ad tag embedded in a site the victim already trusts (the victim never has to knowingly visit "evil.com" — the payload can be injected into a page they already trust).
+- **Open redirect / link shortener abuse**: hiding the true destination behind a trusted-looking shortened or redirected URL.
+- **Watering hole**: compromising a site frequented by the target demographic (e.g. a fertility/wellness forum) and injecting the theft script there.
+
+None of these require compromising Natural Cycles' own infrastructure — the vulnerability lives entirely in the CORS/cookie configuration, so *any* page the victim's browser loads while authenticated is a viable delivery mechanism.
+
+### 3. How can this attack be prevented?
+
+In priority order:
+1. **Never reflect `Origin` when `Access-Control-Allow-Credentials: true`.** Maintain an explicit allow-list of known, trusted origins (e.g. `https://app.naturalcycles.com`) and only set `Access-Control-Allow-Origin` to the request's `Origin` if it matches an entry in that list — otherwise omit the header entirely.
+2. **Make the session cookie `HttpOnly`** so client-side JS (attacker's or otherwise) can never read it directly, and set **`SameSite=Lax` or `Strict`** so it isn't attached to cross-site requests at all in modern browsers — this alone would have blocked this specific attack even with the CORS misconfiguration in place.
+3. **Prefer token-based auth for API calls** (e.g. short-lived bearer tokens sent explicitly by client code) over ambient cookie auth for endpoints that must be reachable cross-origin — tokens aren't automatically attached the way cookies are.
+4. **Defense in depth**: CSRF tokens on state-changing requests, `Vary: Origin` (already present) to avoid cache poisoning across origins, and monitoring for the "credentialed request from unlisted Origin" signature (see Part 4).
+
+---
+
+## Part 2: X-Forwarded-For IP Spoofing
+
+Full reproduction and captured output is in **`evidence/xff-spoof-repro.md`**.
+
+### 1. Is the audit logging vulnerable to XFF spoofing?
+
+**Yes, confirmed.** `app.js:14` sets `app.set('trust proxy', true)`, which tells Express to trust *every* hop declared in `X-Forwarded-For` and take the left-most (client-supplied) address as `req.ip` — used directly in the audit log at `app.js:111`. Sending `curl` requests with an arbitrary `X-Forwarded-For: 6.6.6.6` (or a fabricated multi-hop chain) causes that exact value to appear as the "received from" IP, with no real proxy in the path validating it. An attacker can make every request appear to originate from any IP they choose, defeating both the audit trail and any IP-based controls built on `req.ip`.
+
+### 2. Fix for a GCP + Load Balancer deployment
+
+The core problem is `trust proxy: true` trusts an *unbounded* number of hops. The fix is to trust **only the exact number of hops that are actually real infrastructure**, not a blanket `true`:
+
+- On Cloud Run behind an external HTTPS Load Balancer, the request path is: `client -> GCLB -> Cloud Run's own front end -> your container`. GCLB appends the true client IP to `X-Forwarded-For` before forwarding; Cloud Run's ingress may append again. That means there are a small, fixed number of *trusted* hops closest to the app — set `app.set('trust proxy', N)` to that exact count (verified against current GCP documentation for Cloud Run + LB) rather than `true`. With a numeric hop count, Express walks in from the right and only trusts that many entries, so anything the original client injected into `X-Forwarded-For` themselves gets correctly ignored.
+- Stronger option: don't parse `X-Forwarded-For` for trust decisions at all. Put a **Cloud Armor security policy** in front of the Load Balancer and have it inject a header GCLB/Cloud Armor controls (Cloud Armor exposes the verified client IP it evaluated its rules against); trust that header unconditionally since the client can never write to it (Cloud Armor/GCLB overwrite it at the edge, stripping anything the client sent under that name).
+- Either way: **never leave `trust proxy` as the boolean `true`** in a production deployment — it silently trusts an unbounded, attacker-influenced chain.
+
+### 3. IP-based rate limiting recommendation, pros & cons
+
+**Recommendation: rate-limit at the edge, in Cloud Armor, not in the app.**
+
+Configure a Cloud Armor **rate-based ban rule** on the Load Balancer, keyed on the client IP as GCLB itself observed the TCP connection (never on a header value) — e.g. threshold N requests/min per IP, then temporary ban.
+
+| | Edge rate limiting (Cloud Armor) | App-level rate limiting (e.g. `express-rate-limit` keyed on `req.ip`) |
+|---|---|---|
+| **Pros** | Sees the *real* client IP directly from the TCP/TLS connection — immune to header spoofing; blocks traffic before it consumes app/container resources or scales up Cloud Run instances (cost control); scales independently of the app; centrally managed across all services behind the LB | Can implement business-logic-aware limits (e.g. per authenticated user/API key, not just per IP; different limits per route) |
+| **Cons** | Coarser-grained — can't easily key on application concepts like "logged-in user ID"; another GCP resource to configure/pay for | Only as trustworthy as the `trust proxy` configuration behind it (must be fixed per above); consumes app compute for requests that get limited anyway; a single Cloud Run instance's in-memory limiter doesn't share state across autoscaled instances without an external store (e.g. Redis) |
+
+**Best practice**: use both, in layers — Cloud Armor for coarse, IP-based, pre-app protection (immune to spoofing once `X-Forwarded-For`/trust proxy is fixed per above), and app-level limiting for finer-grained per-user/per-API-key logic where IP alone isn't the right key.
+
+---
+
+## Part 3: Data Pipeline for Logs (ELK SIEM, Cloud Run)
+
+**Assumptions**: app is deployed on Google Cloud Run; SIEM is a self-managed ELK stack (Elasticsearch + Logstash/Kibana, not Elastic Cloud).
+
+**Design:**
+
+```
+Cloud Run container (stdout/stderr, structured JSON logs)
+ | (automatic — Cloud Run always forwards container stdout/stderr)
+ v
+ Google Cloud Logging
+ | (Log Router: a Cloud Logging *sink* with an inclusion filter,
+ | e.g. resource.type="cloud_run_revision" AND relevant severity/labels)
+ v
+ Pub/Sub topic (+ a dead-letter topic for failed deliveries)
+ |
+ v
+ Logstash (or Elastic Agent's GCP Pub/Sub input) subscribed to the topic
+ | (parse/enrich: geoip on client IP, field renames, drop noisy fields)
+ v
+ Elasticsearch (ILM policy: hot/warm/cold + retention matching compliance needs)
+ |
+ v
+ Kibana (SIEM detections, dashboards, alerting)
+```
+
+**Why this shape:**
+- **No agent to install** on Cloud Run itself — Cloud Run's platform already captures stdout/stderr into Cloud Logging automatically, so the app only needs to log **structured JSON** (not free-text) so fields like `origin`, `sourceIp`, `path`, `cookiePresent` are queryable without regex parsing downstream.
+- **Cloud Logging sink -> Pub/Sub** is the standard GCP-native export path and decouples "how logs are produced" from "how ELK consumes them" — if Logstash/ES is down or being upgraded, Pub/Sub retains messages (configurable retention, e.g. 7 days) instead of losing them, and a dead-letter topic catches anything Logstash can't process after N delivery attempts, so malformed events don't silently vanish.
+- **Reliability**: Pub/Sub gives at-least-once delivery with retries; combine with Logstash's persistent queue (or the Elastic Agent's own on-disk buffering) so a Logstash restart doesn't drop in-flight messages. Idempotent ingestion (e.g. a stable `_id` for dedup) handles the at-least-once (rather than exactly-once) delivery semantics.
+- **Scalability**: Cloud Run autoscaling and Pub/Sub both scale independently of each other and of Elasticsearch — a traffic spike doesn't require scaling ES in lockstep; Pub/Sub simply buffers until Logstash/ES catches up. Logstash workers can be scaled horizontally (multiple consumers on the same subscription) if ingest volume grows.
+- **Cost/relevance filtering**: apply the inclusion filter at the Cloud Logging sink stage (not "ship everything") so only security-relevant log entries (auth events, CORS/audit log lines, 4xx/5xx, admin actions) cross into the pipeline, keeping ES storage and Logstash CPU proportional to what's actually useful for detection.
+- **Sensitive data**: since some of this app's payloads are PII-shaped (health/reproductive data per the demo's fixtures), redact or tokenize sensitive fields either at the app's structured-logging layer or in a Logstash filter *before* the data lands in Elasticsearch, not after.
+
+---
+
+## Part 4: Identifying the Attack Through Log Search
+
+**Given the pipeline from Part 3, how would you search for the Part 1 attack?**
+
+**Answer: a hybrid — generic infrastructure logs as the reliable base signal, enriched with a small number of targeted fields logged by the app itself, not full bespoke in-app detection logic.**
+
+Rationale (per the hint):
+- **Pure generic logging** (just HTTP access logs: method, path, status, IP, user-agent) is always-on, requires zero app changes, and can't be bypassed by a bug in app-level detection code — but it can't see the actual CORS-theft signature at all, because the interesting fact ("this credentialed request's `Origin` doesn't match our own domain, and we reflected it back") lives in header *semantics* that generic access logs don't usually capture verbatim (many LB/proxy access log formats don't log the `Origin` request header or the `Access-Control-Allow-Origin` response header by default).
+- **Pure in-app detection** (e.g. hand-rolled logic buried in a route handler) is fragile: easy to accidentally bypass with a code change elsewhere, adds attack-surface-specific logic that has to be re-implemented per route/service, and if the detection logic itself has a bug, you silently lose visibility with no generic fallback.
+- **The practical middle ground**: keep detection *data* generic (log the handful of raw header values at the access-log/app-log layer — `Origin`, `Referer`, whether a session cookie was present, the actual `Access-Control-Allow-Origin` value the server sent back, `User-Agent`, source IP, path, status) and build the *detection query/alert* in the SIEM (Kibana) as a saved search/detection rule over that generic data, rather than hardcoding "if this looks like an attack, alert" inside `app.js`. This keeps the app's job simple (log good structured facts) and keeps the actual detection logic centralized, versioned, and adjustable in the SIEM without a code deploy.
+
+**Specific data points useful for detecting this attack:**
+- Requests to `/json-*` (or any authenticated API) where the `Origin` header is present **and** does not match the known set of first-party origins.
+- Whether a session/auth cookie was present on that same request (i.e. it was a *credentialed* cross-origin request, not just any cross-origin hit).
+- The actual `Access-Control-Allow-Origin` value the server sent back — if it equals the request's `Origin` (reflection) rather than a fixed value, that's the smoking gun of the misconfiguration being actively exploited, not just probed.
+- Volume/burst pattern: a spike of distinct, previously-unseen `Origin` values hitting the same authenticated endpoint in a short window (mass/automated exploitation vs. a one-off manual test).
+- Correlate by session/cookie value across origins: the *same* session cookie being presented from multiple, unrelated `Origin` values in a short time window is a strong indicator the session has been stolen and replayed, independent of whether this specific CORS bug is the vector.
+
+---
+
+## Part 5: Operationalizing Attack Response on Slack
+
+Implementation writeup is in **`slackbot/README.md`**; end-to-end evidence (console logs + DB
+states from a real attack) is in **`evidence/part5-slackbot-repro.md`**.
+
+**What was built** (`slackbot/`, a standalone Node service):
+- A **Bolt** app in **Socket Mode** — connects to Slack with just a Bot Token + App-Level Token,
+ no public URL/ngrok, so it runs entirely on localhost.
+- **Real trigger, not a fake button**: `app.js`'s vulnerable `/json-cors-origin/*` route now also
+ detects the Part 1 attack signature — a *credentialed cross-origin request whose `Origin` isn't
+ one of the app's own* — and fires a fire-and-forget notification to the bot's localhost-only
+ `POST /internal/alert` endpoint. So the Slack alert is a genuine side-effect of the exploit
+ happening.
+- **Interactive Block Kit message** with three buttons — *Cyber attack*, *Infrastructure
+ instability*, *False positive* — matching the PDF's example.
+- **On classification**: the bot records who classified it (Slack user ID) and when, then
+ `chat.update`s the message in place to a **discreet resolved state**
+ (`✅ Confirmed cyber attack · marked by at