#16API6 · API8 · API7 SSRF

Webhooks

Inbound signature/replay verification and outbound SSRF on dispatch.

Download .md

How to use this prompt

  1. 1
    Install SecureNow in your project (then optionally npx securenow login):
    $ npm install securenow
  2. 2Copy the prompt below and paste it into your AI coding agent (Claude Code, Cursor, Codex…) opened at the root of your project.
  3. 3It generates four files into threat/16-webhooks/ — open webhooks-code-findings.html (the audit) and webhooks-detection-mitigation.html (the defenses) in your browser.

🔒Runs entirely in your environment — your codebase is never uploaded or shared. The generated HTML reports are self-contained and work offline.

The prompt

# Webhooks Threat Model — Generator Prompt

A **copy-paste prompt** for customers. Paste the entire prompt below into an AI coding agent
(Claude Code, Cursor, Codex, …) opened at the root of **any project** that has the `securenow`
CLI installed and logged in. The agent will inventory every webhook surface — **inbound**
provider callbacks the app must authenticate before acting on, and **outbound** webhooks the app
dispatches to customer-supplied URLs — build an exhaustive **webhook security** threat model
mapped to **OWASP API Security Top 10:2023** (**API6** Unrestricted Access to Sensitive Business
Flows · **API8** Security Misconfiguration · **API7** SSRF), audit the code for webhook-layer
flaws, and emit a SecureNow-branded **two-track** deliverable set in **Markdown + self-contained
HTML** — a **Detection & Mitigation** runbook (the rules to create, the mitigation commands to run,
how to test each one) and a **Code Findings & Recommendations** audit (every webhook-layer flaw,
audited and **not** fixed) — including which threats still need the SecureNow team. Every rule and
command is **grounded in the SDK actually installed in the repo** and emitted as a **ready-to-copy
unit** (SQL → save → create → dry-run).

This model owns **one trust boundary in two directions**. **Inbound:** is this callback really
from the provider, is it fresh, and have I processed it before? — signature verification on the
**raw body**, constant-time comparison, timestamp tolerance, nonce / event-id replay protection,
and idempotent processing. **Outbound:** where am I sending this, and what am I leaking? — the
customer-supplied destination URL is an **SSRF & data-egress sink**, plus per-endpoint secret
custody, rotation, and retry idempotency. It is a **deep dive** beneath the general
[API security model](../14-api-security/api-security-threat-model-prompt.md), which models webhook
signature/replay only as a single catalog row each; here every webhook threat is fully derived.

> SecureNow is fundamentally an **API / traffic** security layer (firewall, rate-limit,
> challenge, exploit-signature instant-block, ASN enrichment, forensics). For webhooks its
> **native** strengths are: rate-limiting / blocking abusive or runaway senders, and three
> first-class webhook events plus an SSRF event — `api.webhook.signature_failed`,
> `api.webhook.replay_blocked`, `api.webhook.duplicate_ignored`, and `ssrf.blocked` — that turn
> app-internal verification decisions into edge-detectable signals. The **actual fixes**
> (verify-before-trust, raw-body HMAC, constant-time compare, replay/idempotency, and the
> outbound SSRF allowlist) are **app changes** SecureNow detects the *abuse* against and contains
> the *source* of, but cannot apply for you.

Requirements on the customer machine: `npm i -g securenow && securenow login` (admin auth +
app runtime connected). Everything else is discovered by the agent.

---

<!-- ════════════════ COPY EVERYTHING BELOW THIS LINE ════════════════ -->

# Generate a Webhooks Threat Model Report (SecureNow)

You are a senior application-security engineer specializing in webhook and integration security.
Produce an **exhaustive webhook security threat model for THIS codebase**, covering **both
directions of the webhook trust boundary** — inbound provider callbacks and outbound dispatch to
customer-supplied URLs — organized along the relevant **OWASP API Security Top 10:2023** codes
(**API6**, **API8**, **API7/SSRF**), mapped to **SecureNow** detections and mitigations, with a
ready-to-run action plan **and** a code-level audit of every webhook-layer flaw you find. You
write **four** deliverables — **two tracks**, each as Markdown + self-contained HTML — into
`threat/16-webhooks/` (create the folder if needed):

1. `webhooks-detection-mitigation.md` — the **operational runbook**: what to run in SecureNow
   (detection rules to create, mitigation commands, testing & validation, response runbooks).
2. `webhooks-detection-mitigation.html` — the same runbook as a **self-contained** HTML page
   (inline CSS + a no-network **copy button** on every command block), using the SecureNow
   branding skeleton at the end.
3. `webhooks-code-findings.md` — the **code audit**: every webhook-layer flaw in this codebase with
   `file:line`, the quoted snippet, severity, and the recommended fix (described, **never applied**).
4. `webhooks-code-findings.html` — the same audit as a self-contained HTML page.

The two tracks **cross-link**: the gaps / instrumentation rows in the detection runbook link to the
relevant code finding, and the code findings link back to the detection row they back. Every alert
rule and CLI command is **grounded in the SDK actually installed in this repo** (Phase 0.5) and
written as a **ready-to-copy unit** (SQL → `rules/<name>.sql` → full create command → dry-run).

Work in the phases below, in order. **Never invent facts**: if something is not in the
codebase or not returned by a CLI command, say "not found" — do not guess. **Do not modify
application code.** You are auditing: every code-level fix is *described in the report*, never
applied to the repo.

**Scope discipline.** This model owns the webhook trust boundary end to end: inbound
authenticity (signature, raw-body, constant-time, replay, idempotency, source-IP trust, forged
event types/amounts), inbound flooding / cost amplification, and outbound URL-target safety
(SSRF), data egress, retry idempotency, and signing-secret custody. For surrounding concerns,
**reference and defer** — do not re-derive them — using the numbered sibling models:

- **Webhook-driven money moves** (a forged `charge.succeeded` that credits a balance, refund
  abuse, payout via callback) → defer the business-logic / fraud depth to
  [../09-payment-business-logic/](../09-payment-business-logic/). This model owns *whether the
  callback is authentic and replay-safe*; the payment model owns *what the money move means*.
- **Signing-secret custody, storage, and rotation mechanics** (where the secret lives, KMS/secret
  manager, IAM, leakage in CI) → defer to
  [../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/). This model owns *that a
  per-endpoint secret exists and is used for constant-time HMAC*; the secrets model owns *how it's
  stored and rotated*.
- **The general API surface** (rate limits everywhere, CORS, headers, schema validation, the full
  SSRF catalog) → defer to [../14-api-security/](../14-api-security/). This model owns the
  *webhook-specific* slice only.

---

## Phase 0 — Verify SecureNow tooling

Run and record (use `--json` where supported):

```bash
securenow doctor              # connectivity must be healthy
securenow whoami              # admin auth + runtime app
securenow status --json       # app key(s), environment, firewall state
securenow alerts rules --json # detection rules that already exist (incl. system signature rules)
securenow automation --json   # blocklist automations that already exist
securenow challenge list --json   # CAPTCHA / proof-of-work challenge rules
securenow env --json          # resolved SDK config (service name, endpoints)
```

If the CLI is missing or not logged in, **stop** and tell the user to run
`npm i -g securenow && securenow login`, then re-run this prompt. Capture the **app key**
(UUID) — every rule and command in the report must use it. If multiple apps exist, ask the user
which app this codebase maps to before continuing. Note the **firewall state** and any **system
signature rules** (SQLi/XSS/RCE) already present — a forged webhook body is still a request body,
so payload-borne injection inside a callback is caught by those signature rules; do not duplicate
them.

---

## Phase 0.5 — Ground every rule & command in the INSTALLED SDK

Before writing any SQL or CLI, read the SecureNow SDK that is actually installed in this repo so
every alert rule and command is correct for THIS version — never guess flags, subcommands, event
names, or SQL columns:

```bash
cat node_modules/securenow/package.json    # installed SDK version (record it in both reports)
ls node_modules/securenow                  # exported modules: events, sessions, register, run, …
ls node_modules/securenow/dist 2>/dev/null # built entrypoints / bundled CLI
npx securenow --help                       # top-level commands available in this version
npx securenow alerts rules --help          # exact create flags: --name/--sql/--apps/--severity/--schedule/--nlp
npx securenow event --help                 # `event send` shape for synthetic tests
npx securenow ratelimit --help; npx securenow challenge --help
npx securenow blocklist --help; npx securenow automation --help; npx securenow trusted --help
```

If `node_modules/securenow` is absent, run `npm ls securenow`; if still missing, tell the user to
`npm i securenow` (or `npm i -g securenow`) and stop. EVERY command, flag, `track('…')` event
name, and SQL column you emit MUST be one the installed SDK/CLI actually exposes. If the installed
version lacks a capability this prompt references, emit the rule but annotate it
`# requires securenow >= <version>` instead of a broken command. Record the resolved version in
the appendix of BOTH reports.

In Phase 4 and Phase 5, treat `node_modules/securenow` + `--help` as the source of truth: the
`securenow/events` `track()` signatures, the `securenow alerts rules` SQL columns, and every
mitigation subcommand are discoverable there. Cross-check before emitting.

---

## Phase 1 — Inventory the webhook surface (codebase analysis)

Webhook security starts with knowing **every callback you receive and every URL you call out
to**. Document what is **actually wired**, not what is intended. Cover at minimum:

### Inbound (provider → you)

- **Endpoint catalog** — enumerate every inbound webhook/callback route (method + path), grouped
  by router/controller. For each: the **provider** (stripe / shopify / github / meta / twilio /
  slack / sendgrid / custom / …), whether it is publicly routable, and which downstream action it
  triggers (order, ship, credit, email/SMS, job enqueue, account change, money move).
- **Authenticity mechanism per endpoint** — exactly how each callback is verified:
  - **Signature scheme** — HMAC-SHA256 header (`Stripe-Signature`, `X-Hub-Signature-256`,
    `X-Shopify-Hmac-Sha256`, …), JWT, mTLS, basic-auth, shared bearer token, or **none**.
  - **Raw-body verification** — is the HMAC computed over the **exact raw request bytes**, or over
    a re-serialized / already-`JSON.parse`d object? (A re-serialized body breaks signature
    integrity and invites parser-differential bypass.)
  - **Verify-before-trust ordering** — is the signature checked **before** any field of the body
    is read, parsed into business logic, or persisted? Or is the body parsed/acted on first and
    the signature "checked" later (or in a way that doesn't gate the action)?
  - **Comparison function** — is the signature compared with a **constant-time** primitive
    (`crypto.timingSafeEqual`, `hmac.compare_digest`, …) or with `===` / `==` / string equality
    (timing-oracle)?
  - **Secret source** — per-endpoint secret vs one shared secret across all providers/endpoints;
    where it is read from (env, secrets manager, hardcoded — flag hardcoded as a finding).
- **Replay & freshness controls** — does each endpoint enforce a **timestamp tolerance**
  (reject events older than N minutes), a **nonce**, and **event-id deduplication** (have I seen
  this `event.id` before)? Where is the dedupe state stored (DB unique constraint, Redis with
  TTL, in-memory — flag in-memory/none)?
- **Idempotent processing** — is the side effect (ship, credit, order, email) guarded so that
  processing the **same event twice** is a no-op? Unique constraint, idempotency key, "already
  processed" check, or nothing?
- **Source-IP trust** — is a provider IP allowlist used? Is it the **only** trust mechanism
  (no signature)? IP allowlists drift and are spoofable behind misconfigured proxies — note if
  it's load-bearing.
- **Trusted event fields** — which fields of the inbound payload drive money/state moves
  (`amount`, `currency`, `event.type`, `customer`, `status`)? Are they trusted **as sent**, or
  re-fetched from the provider API by ID before acting (the safe pattern)?
- **Webhook flood / cost posture** — is there any per-sender rate limit on inbound endpoints? Does
  each accepted callback trigger expensive or paid downstream work (DB writes, email/SMS, LLM,
  third-party calls) with no cap?

### Outbound (you → customer-supplied URL)

- **Dispatch sinks** — every place the server **sends** a webhook to a URL that is
  **customer-controlled or user-influenced** (customer-registered endpoint URL, "test webhook",
  callback URL in an integration config). Each is an **SSRF sink** (API7).
- **URL-target safety** — before dispatch, is the destination URL validated against an
  **allowlist / denylist**? Are **internal / private / link-local / metadata** targets blocked
  (`127.0.0.0/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254.0.0/16` incl. cloud IMDS
  `169.254.169.254`, `[::1]`, `[::]`, `0.0.0.0`, `fc00::/7`, `fe80::/10`)? Is the scheme
  restricted to `https` (and `http` only where intended)? Are non-standard ports blocked?
- **SSRF bypass resistance** — does validation resolve DNS and check the **resolved IP** (not
  just the hostname), pin the connection to that IP, and **disable or re-validate redirects**?
  Or can DNS rebinding, a `30x` redirect to an internal host, or decimal/hex/octal/`[::]` IP
  encodings slip past? Is there a metadata-header strip (no `Metadata-Flavor`, no IMDSv2 token
  echo)?
- **Egress payload** — what data leaves in the outbound body? Could a misconfigured / hijacked
  destination receive PII, secrets, tokens, or another tenant's data? Is the payload minimized /
  signed so the receiver can verify *you*?
- **Outbound retry semantics** — on failure, does the dispatcher retry? Do retries carry a
  **stable event id / idempotency key** so the receiver can dedupe, or does each retry look like
  a fresh event (receiver-side duplication)? Is there a retry ceiling and backoff?
- **Outbound signing** — do you sign outbound payloads with a **per-endpoint secret** so the
  receiver can verify authenticity? Is there a **rotation** mechanism (overlapping keys), or is
  it one global secret that can never rotate without breaking every customer?

### Telemetry & instrumentation

- **Telemetry privacy & redaction** — confirm the SecureNow SDK / log pipeline redacts webhook
  **signing secrets**, signature headers, bearer tokens, and any PII in webhook bodies before
  ingestion. Webhook secrets must never become an attribute value or a log field. If not found,
  create a high-severity finding.
- **SecureNow instrumentation already present** — `securenow/register` / `securenow run` /
  `securenow init` (gives traffic spans automatically), any existing `securenow/events` `track()`
  calls (especially `api.webhook.*` / `ssrf.blocked`), and whether the firewall is engaged. This
  determines what works *today* vs *after instrumentation*.

Output of this phase = the report's **Webhook surface & inventory** section: the **inbound
endpoint catalog** (route / provider / action / signature scheme / raw-body? / verify-order /
compare-fn / secret-source / timestamp tolerance / nonce / event-id dedupe / idempotency /
source-IP trust), the **outbound dispatch sink list** (sink / URL source / allowlist? /
private-range block? / redirect handling / scheme/port restriction / egress payload / retry
idempotency / outbound signing & rotation), the **telemetry redaction status**, and a short
paragraph naming the real webhook attack surface for this stack.

---

## Phase 2 — Enumerate threats (exhaustive catalog)

Evaluate **every** threat below against the discovered surface. Each item is either **modeled**
(a row in the threat matrix) or **explicitly N/A** (one line in an "Out of scope" subsection with
the reason — e.g. "Outbound items: N/A, app only receives webhooks, never dispatches"). Never
silently drop an item. Add stack-specific threats you discover that are not listed — this catalog
is the floor, not the ceiling. Tag each modeled row with its **OWASP API Top 10:2023** code
(**API6 / API8 / API7**, or "—").

**A. Inbound authenticity — signature (OWASP API8)**
1. No signature verification at all — endpoint trusts any POST (forged-callback path)
2. Weak / bypassable verification — verification short-circuits on missing header, empty
   signature accepted, `if (sig && sig !== expected)` (skips when absent), wrong header read
3. Signature verified **after** the body is parsed and trusted / acted on / persisted
   (verify-after-trust)
4. HMAC computed over a **re-serialized / re-parsed** body, not the **raw bytes** (signature
   integrity broken; parser-differential bypass)
5. Non-constant-time signature comparison (`===` / `==` / string equality) → timing oracle on the
   expected MAC
6. Wrong / weak algorithm or self-asserted algorithm (MD5/SHA1, `alg:none`-style JWT, attacker
   picks the scheme)
7. One shared signing secret across all providers/endpoints, or a hardcoded secret in source
   (defer custody depth to ../20-secrets-and-cloud-iam/, model the *that-it's-used* slice here)

**B. Inbound replay & idempotency (OWASP API6 / API8)**
8. Replay — a captured legitimate callback re-sent and re-accepted (no timestamp tolerance)
9. No nonce / no `event.id` deduplication — the same event id processed more than once
10. Duplicate processing → **double ship / double credit / double order / double email-or-SMS /
    double job execution / state corruption** (the business impact of 8–9)
11. Stale-event acceptance — event far in the past accepted (no freshness window) enabling delayed
    replay
12. Dedupe state not durable (in-memory only, lost on restart / not shared across instances) →
    replay window reopens

**C. Inbound trust-boundary abuse (OWASP API6)**
13. Source-IP allowlist used as the **only** trust mechanism (no signature) — spoofable behind a
    misconfigured proxy, drifts as provider IPs change
14. Forged **event type** in an unauthenticated/under-verified callback (attacker sends
    `invoice.paid` / `subscription.active` to unlock entitlements)
15. Forged **amount / currency / customer / status** trusted as-sent instead of re-fetched from
    the provider by id (payment-fraud path — defer the money-move depth to
    ../09-payment-business-logic/)
16. Provider/tenant confusion — one provider's callback accepted on another's endpoint, or a
    callback attributed to the wrong tenant/account
17. Webhook **flooding** from a malicious or runaway sender (high callback rate to one endpoint)
18. Webhook **cost amplification** — each accepted callback triggers paid/expensive downstream
    work (email/SMS/LLM/DB) with no cap → bill blow-up / DoS-by-cost

**D. Outbound SSRF & target safety (OWASP API7)**
19. SSRF to **cloud metadata** via a customer-supplied webhook URL (IMDS `169.254.169.254`,
    GCP/Azure metadata endpoints) → credential theft
20. SSRF to **internal / private / link-local** services (`127/8`, `10/8`, `172.16/12`,
    `192.168/16`, `[::1]`, `fc00::/7`, `fe80::/10`)
21. Blind / out-of-band SSRF — dispatcher reaches an attacker-chosen host and the response/timing
    leaks internal reachability
22. SSRF **filter bypass** — DNS rebinding, `30x` redirect to internal, decimal/hex/octal IP,
    `[::]`, `0`, `0.0.0.0`, userinfo `@`, unexpected scheme/port; validation checks hostname but
    not the **resolved IP**

**E. Outbound data egress & receiver integrity (OWASP API8)**
23. Sensitive payload (PII / secrets / tokens / cross-tenant data) sent to an attacker-controlled
    or misconfigured destination URL
24. No outbound signing — receiver cannot verify the webhook came from you; payload is forgeable
    en route / by a MITM
25. Outbound **retries without idempotency** — each retry carries no stable event id → receiver
    processes the same event multiple times (receiver-side duplication)
26. Unbounded retries / no backoff — a dead destination causes a retry storm (self-inflicted DoS,
    or amplification toward a victim URL)

**F. Secret custody at the webhook boundary (OWASP API8 — depth deferred)**
27. No **per-endpoint** outbound secret (one global secret) → cannot rotate without breaking all
    customers; one leak compromises everyone
28. No rotation mechanism (no overlapping-key window) for inbound or outbound secrets
29. Signing secret **leakage** — secret in logs, telemetry attributes, client bundles, error
    bodies, or source control
> For 27–29, model the *webhook-boundary* slice (that a per-endpoint secret exists, is used for
> constant-time HMAC, is rotatable, and never leaks into telemetry). **Defer** storage/KMS/IAM
> custody to [../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/).

**G. Payload-borne exploits inside a callback (defer to system signature rules)**
30. SQLi / XSS / RCE / SSTI / path-traversal payloads smuggled **inside** a webhook body field
    that the app stores or renders → caught by the **system signature rules + `instant.block`**
    (a forged body is still a request body); do not author duplicate pattern SQL.

**H. Observable abuse (what telemetry actually catches — the workhorse rules)**
31. Burst of **signature failures** from one IP/ASN on a webhook route (forgery probing / brute on
    the secret) → `api.webhook.signature_failed`
32. **Replay-blocked** bursts from one IP (replay attack in progress) → `api.webhook.replay_blocked`
33. **Duplicate-ignored** bursts (mis-behaving sender or replay attempt) → `api.webhook.duplicate_ignored`
34. **SSRF-blocked** outbound attempts — any hit is high-signal → `ssrf.blocked`
35. Volumetric flood on a webhook endpoint (high request rate, one IP) — traffic, no events needed
36. 4xx/5xx spike on a webhook route from one IP/ASN (broken integration or fuzzing) — traffic

**I. Deferred — modeled in sibling models (reference, do not re-derive)**
37. Webhook-driven **money-move** business logic (refund abuse, balance credit semantics, payout)
    → [../09-payment-business-logic/](../09-payment-business-logic/) (**API6**)
38. Signing-secret **storage / KMS / IAM / rotation mechanics** →
    [../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/) (**API8**)
39. General API surface — rate limits everywhere, CORS, headers, full SSRF catalog →
    [../14-api-security/](../14-api-security/)

> For 37–39, add **one** matrix row each marked *"deferred — see linked model"*, and only note the
> SecureNow traffic-observable symptom here (e.g. signature-failure spike, SSRF-block hit). The
> full detection/mitigation lives in the other reports.

---

## Phase 3 — Audit the code (findings only — do not fix)

For **each** modeled threat that maps to real code, locate the responsible code and record a
**finding** for the report's "Code-level findings" section. A finding is:

- **Location** — `file:line` (clickable), the route/handler/middleware name (inbound handler or
  outbound dispatcher).
- **Pattern** — quote the 1–8 relevant lines. State the missing control precisely.
- **Why exploitable** — the concrete request an attacker sends (or the URL they register) and what
  they achieve.
- **Severity** — critical / high / medium / low (impact × reachability).
- **Recommended fix (described, not applied)** — the specific change, referencing the secure
  pattern, not a code diff. **You must not edit the codebase.**

Look specifically for:

**Inbound signature flaws** — signature header not read, or compared with `===`/`==`/string
equality; verification that short-circuits when the header is absent or empty
(`if (sig && sig !== expected) reject` — skips when `sig` is falsy); HMAC computed over
`JSON.stringify(req.body)` or a parsed object instead of the **raw bytes**; `express.json()` /
body-parser consuming the stream before the verifier can read the raw body; the route handler
reading/persisting body fields **before** calling the verifier; weak/self-asserted algorithm; a
single shared secret or a hardcoded secret literal. *Recommended fixes must mention* raw-body
capture (e.g. `express.raw()` / a verify hook that stashes `rawBody`), constant-time comparison
(`crypto.timingSafeEqual` / `hmac.compare_digest`), a fixed strong algorithm, per-endpoint
secrets from a secret store, and **verify-before-any-parse/trust** ordering.

**Inbound replay / idempotency flaws** — no timestamp extracted or no tolerance check; no nonce;
no `event.id` dedupe; dedupe stored only in memory or with no shared/durable store; the side
effect (ship/credit/order/email/job) executed without a "have I processed this event id?" guard
or DB unique constraint. *Recommended fixes must mention* a timestamp tolerance window
(e.g. ±5 min), event-id deduplication backed by a durable unique constraint or Redis-with-TTL,
and idempotent side effects keyed on the provider event id.

**Inbound trust-boundary flaws** — source-IP allowlist as the only trust; `event.type` /
`amount` / `status` read straight from the payload to drive money/state moves instead of
re-fetching the object from the provider API by id; one provider's verifier reused on another
endpoint; tenant derived from an untrusted body field. *Recommended fixes must mention* signature
as the primary trust (IP allowlist at most as defense-in-depth), re-fetching authoritative state
from the provider by id before acting on value/entitlement, per-provider verifiers, and deriving
tenant from the verified secret/endpoint, not the body.

**Outbound SSRF flaws** — `fetch`/`axios`/`http.request` to a customer-supplied URL with no host
allowlist/denylist; validation on the **hostname string** but not the **resolved IP**; redirects
followed without re-validation (`maxRedirects` left default, `follow: true`); no private/link-
local/metadata range block; scheme/port unrestricted; no DNS-pin / no rebinding defense.
*Recommended fixes must mention* validating the **resolved IP** against a blocklist of
private/link-local/metadata ranges, pinning the connection to the validated IP, disabling or
re-validating redirects, restricting scheme to `https` and port to 443 (or an explicit list), and
emitting `ssrf.blocked` on every denial.

**Outbound egress / retry / signing flaws** — full payload (PII/secrets/cross-tenant) sent to an
arbitrary URL; no HMAC signature on the outbound body; retries that do not carry a stable event
id / idempotency key; unbounded retries with no backoff; one global outbound secret with no
per-endpoint scoping or rotation path. *Recommended fixes must mention* payload minimization,
signing outbound bodies with a per-endpoint secret (and a delivery/event id header for receiver
idempotency), bounded retries with exponential backoff, and an overlapping-key rotation window.

**Telemetry redaction flaws** — webhook signing secrets, signature headers, bearer tokens, or
PII from webhook bodies appearing in logs / SecureNow event attributes / error responses.
*Recommended fixes must mention* redacting secret headers and PII before ingestion and hashing or
omitting `event_id` / `target_host` derivations that could leak data.

If a control exists and is correct (raw-body HMAC + constant-time compare + timestamp tolerance +
event-id dedupe; outbound resolved-IP allowlist with redirects disabled), note it as a
**strength** — the posture must be honest. Absence of a control where the surface exists is itself
a finding ("inbound endpoint has no signature verification at all").

---

## Phase 4 — Map every modeled threat to SecureNow detection + mitigation

Classify each threat with exactly one coverage badge:

- 🟢 **COVERED** — detectable + mitigable with SecureNow today, on telemetry already flowing or
  via the first-class webhook events below. Flooding, signature-failure bursts, replay-block
  bursts, SSRF-block hits, and payload-borne injection inside a callback land here.
- 🟡 **PARTIAL** — works after the customer adds instrumentation (`track('api.webhook.*')` /
  `ssrf.blocked`), or SecureNow can only *contain the abusive sender at the edge* while the real
  fix is app-level (the verifier, the SSRF allowlist, the idempotency guard). **Pair the control
  with the app fix on every such row.**
- 🔴 **GAP** — SecureNow cannot detect or mitigate this today. **Still include it**: give the
  app/config-level fix, then add the line *"Requires SecureNow team — contact your SecureNow
  account contact (or in-dashboard support) to request support for this threat."* Collect all gaps
  in the report's "Known gaps & SecureNow feature requests" section.

> **Be honest about what is edge-detectable vs an app fix.** SecureNow sees **traffic** and
> **events** and contains actors via firewall / rate-limit / challenge / block / signature
> instant-block. It **cannot** see a verifier that compares with `===`, an SSRF sink with no
> guard, or a missing event-id dedupe **until the app emits the corresponding event** in the
> recommended fix. A weak verifier with no failures yet emits no signal; the fix is the app's
> verifier, and SecureNow detects the forgery *attempts* (`api.webhook.signature_failed`) and the
> SSRF *attempts* (`ssrf.blocked`) once the app emits them, then rate-limits / blocks the source.
> Pair edge-containment **with** the app fix on every inbound-authenticity, replay, SSRF, and
> egress row.

Use **only** the SecureNow building blocks below. Never invent CLI flags, event names, or SQL
columns.

### 4a. Instrumentation (what webhook detections feed on)

Once the app runs under `securenow run` / `securenow/register` / `securenow init`, **HTTP traffic
is captured automatically** — status codes (incl. **429** and **5xx**), methods, paths, client
IPs, response sizes. Volumetric floods and 4xx/5xx spikes on webhook routes need **no events**.

Add `securenow/events` `track()` for the app-internal verification decisions traffic can't see
(these calls **never throw** — a telemetry failure must never break webhook processing). **Reuse
these exact event names — do not invent new ones:**

```js
const { track } = require('securenow/events');

// Inbound webhook authenticity / replay / idempotency:
track('api.webhook.signature_failed', { ip, attributes: { route: '/webhooks/stripe', provider: 'stripe|shopify|github|meta|custom', reason: 'missing|invalid|expired_timestamp|bad_format' } });
track('api.webhook.replay_blocked',   { ip, attributes: { route: '/webhooks/stripe', provider: 'stripe|shopify|github|meta|custom', event_id: '<hash_or_id>' } });
track('api.webhook.duplicate_ignored',{ ip, attributes: { route: '/webhooks/stripe', provider: 'stripe|shopify|github|meta|custom', event_id: '<hash_or_id>' } });

// Outbound dispatch blocked by your SSRF guard (API7) — any hit is high-signal:
track('ssrf.blocked', { ip, attributes: { route: '/webhooks/dispatch', target_host: '169.254.169.254', reason: 'link_local|private|metadata|redirect|dns_rebind|bad_scheme' } });
```

Emit at the enforcement point:

- `api.webhook.signature_failed` — the verifier rejected the callback (missing/invalid signature,
  expired timestamp, malformed header). Emit **before** returning the 4xx.
- `api.webhook.replay_blocked` — timestamp/nonce/event-id replay protection rejected a callback.
- `api.webhook.duplicate_ignored` — an already-processed `event.id` was ignored (idempotency
  no-op).
- `ssrf.blocked` — the outbound dispatcher refused a destination URL (private/link-local/metadata
  target, disallowed scheme/port, or a redirect/DNS-rebind to an internal IP).

> **Hash or omit any PII or secret before it becomes an attribute value.** Never put the signing
> secret, the raw signature, the full target URL with credentials, or customer PII into an
> attribute. Use a hash of `event_id` and the bare `target_host` only — attributes feed detection,
> they must not become a new leak path (see the Phase 1 telemetry-redaction check).

Recommended webhook event taxonomy — rules match these **exact strings**:

| Event | Emit when |
|---|---|
| `api.webhook.signature_failed` | an inbound webhook signature is missing / invalid / expired / malformed |
| `api.webhook.replay_blocked` | a webhook is rejected by timestamp / nonce / event-id replay protection |
| `api.webhook.duplicate_ignored` | an already-processed webhook event-id is ignored (idempotency no-op) |
| `ssrf.blocked` | an outbound webhook dispatch is denied by the SSRF allowlist/guard |

Custom `attributes` become queryable as `attributes_string['<key>']` (e.g.
`attributes_string['target_host']`, `attributes_string['provider']`, `attributes_string['route']`,
`attributes_string['reason']`, `attributes_string['event_id']`). Ingest enriches every IP with
**ASN/org** (`client.asn`, `client.as_org`) — enabling botnet/datacenter-origin detection on
forgery floods with no extra code.

### 4b. Detection rules — SQL conventions

Two query shapes. Both **must** keep the tenant scope and **must** select an `ip` column (per-IP
aggregation is what remediation/auto-block keys on). **The tenant-scope column differs by table**
— using the wrong one fails with `UNKNOWN_IDENTIFIER`:

- **logs/events** (`signoz_logs.distributed_logs_v2`) → `resources_string['service.name'] IN (__USER_APP_KEYS__)`
- **traces/HTTP** (`signoz_traces.distributed_signoz_index_v3`) → `` `resource_string_service$$name` IN (__USER_APP_KEYS__) ``

When grouping by `ip`, add `HAVING ip != '' AND …` so rows with no client IP don't aggregate into
an empty-key bucket. Traffic columns proven available: `response_status_code`, `kind` (server span
= 2), `ts_bucket_start`, `attributes_string['http.target']`, the `client_ip` coalesce below.
Confirm any other column with a `--mode dry_run` before relying on it.

**Events-based — inbound webhook forgery probing (signature failures from one IP):**

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['provider']       AS provider,
  attributes_string['route']          AS route,
  count() AS failures
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'api.webhook.signature_failed'
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, provider, route
HAVING ip != '' AND failures >= 5
```

**Events-based — inbound replay attack in progress (replay-blocked bursts from one IP):**

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['route']          AS route,
  count() AS blocked
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'api.webhook.replay_blocked'
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, route
HAVING ip != '' AND blocked >= 3
```

**Events-based — duplicate-delivery / replay noise (duplicate-ignored bursts):**

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['route']          AS route,
  count() AS duplicates
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'api.webhook.duplicate_ignored'
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, route
HAVING ip != '' AND duplicates >= 10
```

**Events-based — outbound SSRF attempt blocked (API7) — any hit is high-signal:**

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['target_host']    AS target_host,
  attributes_string['reason']         AS reason,
  count() AS attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'ssrf.blocked'
  AND timestamp >= now() - INTERVAL 30 MINUTE
GROUP BY ip, target_host, reason
HAVING ip != '' AND attempts >= 1
```

**Combined webhook-abuse signal (all three webhook events, lower per-signal threshold):**

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['event.type']     AS signal,
  attributes_string['route']          AS route,
  count() AS attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] IN ('api.webhook.signature_failed','api.webhook.replay_blocked','api.webhook.duplicate_ignored')
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, signal, route
HAVING ip != '' AND attempts >= 5
```

**Traffic-based — webhook endpoint flood (single IP), no events needed:**

```sql
WITH coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip
SELECT client_ip AS ip,
       count() AS requests,
       uniqExact(attributes_string['http.target']) AS distinct_paths
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
  AND timestamp >= now64(9) - INTERVAL 5 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 5 MINUTE)) - 1800
  AND kind = 2
  AND attributes_string['http.target'] LIKE '%/webhook%'
GROUP BY ip
HAVING ip != '' AND requests >= 300
```

**Traffic-based — 4xx/5xx spike on a webhook route (broken/forging integration):**

```sql
WITH coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip
SELECT client_ip AS ip,
       countIf(response_status_code IN ('400','401','403','422')) AS client_errors,
       countIf(response_status_code LIKE '5%') AS server_errors,
       count() AS total
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
  AND timestamp >= now64(9) - INTERVAL 15 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 15 MINUTE)) - 1800
  AND kind = 2
  AND attributes_string['http.target'] LIKE '%/webhook%'
GROUP BY ip
HAVING ip != '' AND (client_errors >= 30 OR server_errors >= 30)
```

**Payload-borne injection inside a callback (catalog G) — use the SecureNow system signature
rules, don't write SQL.** SQLi / XSS / RCE detection ships as **system signature rules** with
synchronous **`instant.block`** (blocks a matching request in ~2.6s on ingest). A forged webhook
body is still a request body, so a payload smuggled in a webhook field is caught by these rules.
Confirm they're present and enabled for this app via `securenow alerts rules --json`; enable
`instant.block` rather than authoring duplicate pattern SQL.

Useful attributes/columns: `event.type`, `http.client_ip`, `http.target`, `response_status_code`,
`kind`, `client.asn`, `client.as_org`, and your webhook attributes (`provider`, `route`, `reason`,
`target_host`, `event_id`).

**Emit every detection as a complete, ready-to-copy unit — never a fragment.** For each rule,
output in order, each as its own fenced block so it copies cleanly: (1) the SQL with a
`-- rules/<name>.sql` first line, (2) the line saving it to `rules/<name>.sql`, (3) the full
`securenow alerts rules create …` command, (4) the dry-run test. The exact flags MUST match
`securenow alerts rules --help` from Phase 0.5 — never invent flags. Save each rule's SQL to
`rules/<name>.sql` so `--sql @rules/<name>.sql` resolves. Note pre-existing / system rules (from
Phase 0) instead of duplicating them. Example unit:

```sql
-- rules/webhook-signature-failures.sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['provider']       AS provider,
  attributes_string['route']          AS route,
  count() AS failures
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'api.webhook.signature_failed'
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, provider, route
HAVING ip != '' AND failures >= 5
```

```bash
# save the SQL so --sql @file resolves
mkdir -p rules
cat > rules/webhook-signature-failures.sql <<'SQL'
-- (paste the SQL block above)
SQL
```

```bash
securenow alerts rules create \
  --name "Webhook: inbound signature-failure burst (single IP)" \
  --sql @rules/webhook-signature-failures.sql \
  --apps <APP_KEY> \
  --severity high \
  --schedule "*/5 * * * *" \
  --nlp "single IP causing 5+ webhook signature failures in 15 minutes"

securenow alerts rules test <RULE_ID> --mode dry_run --wait   # validate before it runs live
```

### 4b-test. Test mode for false-positive-prone rules

Alert rules have a lifecycle **mode**: `test` = **detect-only, NO mitigation** vs `prod` = full
(mitigation / auto-action armed) — plus a **status** (`Active | Disabled | Paused`). Manage with:

```bash
securenow alerts rules update <RULE_ID> --mode test     # detect-only: fires notifications, takes NO action
# …observe real traffic for several days; tune the threshold; add securenow fp exclusions for any FPs…
securenow alerts rules update <RULE_ID> --mode prod      # promote: arm the mitigation / auto-action
securenow alerts rules update <RULE_ID> --status Paused  # or --enable / --disable / --pause shortcuts
```

**Rule of thumb:** any detection that can **false-positive** — heuristic thresholds (webhook flood /
signature-failure / replay-block / duplicate-ignored counts), broad patterns, anomaly / volume
rules, anything tuned to YOUR traffic (a real provider's legitimate retries can legitimately inflate
signature-failure and duplicate-ignored volume) — must ship in **`--mode test` first**. Run it
detect-only for **3–7 days of real traffic**, review what it flags, raise/lower the threshold and
add `securenow fp` exclusions for legitimate hits (e.g. a provider's published egress IPs), then
`--mode prod` to arm mitigation. Only **high-precision** rules (exploit-signature SQLi/XSS/RCE
matches inside a callback body, exact-match IoCs, `ssrf.blocked` hits where any single event is
high-signal, known-bad ASN hits) may go straight to `prod`. **Tag every rule `test-first` or
`prod-ready` and say why** — in the matrix, the rule unit, and the action plan. (`securenow alerts
rules test <id> --mode dry_run --wait` is the separate one-off *query* validation — run it before
either.)

### 4c. Mitigation commands (the only allowed remediation surface)

For webhook abuse, SecureNow **contains the actor at the edge**; the **app fix** removes the
underlying weakness. Always pair them on inbound-authenticity, replay, SSRF, and egress rows —
the SecureNow control stops the *flood/forgery source*, the app fix stops the *forgery from
working*.

Use the **full SecureNow mitigation toolbox** below. Once a threat is confirmed, **choose the
narrowest effective mitigation(s) from ALL of these** and combine them — for webhooks especially
**route-scoped rate-limit / block on the receiver path** (`--route /webhooks/<provider> --mode
prefix --method POST`) so a forging or runaway sender is throttled without touching other traffic.
Re-check every command/flag against the installed SDK in Phase 0.5 (`securenow <cmd> --help`);
annotate `# requires securenow >= <ver>` if absent. Scope by **app / env / route / method / IP /
duration** to avoid hitting real provider retries.

| # | Mitigation | Command (ready-to-copy) | Use / scope |
|---|---|---|---|
| 1 | **Free firewall (network)** | `securenow firewall enable --app <APP_KEY> --env production` · `securenow run --firewall-only` · test `securenow firewall test-ip <ip> --path /webhooks/stripe --method POST` | 500k+ known-bad IPs, hourly refresh; drop scanners before they hit a webhook endpoint. No app change. |
| 2 | **Exploit-signature instant block** | enable the `instant` config on the system SQLi/XSS/RCE signature rules (dashboard / MCP `securenow_alert_rule_instant_update`); custom rule → create with `--execution-mode instant` | synchronous ~2.6s block of a payload smuggled inside a callback body (catalog G). Don't duplicate pattern SQL. |
| 3 | **IP block — global** | `securenow blocklist add <ip> --app <APP_KEY> --env production --reason "..."` | confirmed-forging source, all routes. |
| 4 | **IP block — scoped to route (+ method)** | `securenow blocklist add <ip> --route /webhooks* --mode prefix --method POST --app <APP_KEY> --env production --reason "..."` (`--mode exact\|prefix\|regex`, `--method GET\|POST\|…\|ALL`) | block an IP only on the webhook receiver paths; least collateral. |
| 5 | **IP block — temporary / time-boxed** | `securenow blocklist add <ip> --duration 24h --reason "..."` (`30m`,`24h`,`7d`) · reverse `securenow blocklist unblock <id> --reason "..."` | auto-expiring containment of a probing sender; audit-preserving unblock. |
| 6 | **Rate limit — per IP** | `securenow ratelimit add <ip> --limit 100 --window 1m --duration 24h --reason "..."` | throttle one abusive sender across the app. |
| 7 | **Rate limit — per route (all clients, per-IP budget)** | `securenow ratelimit add --route /webhooks/stripe --mode prefix --method POST --limit 60 --window 1m --key-by ip` | cap an expensive/floodable webhook endpoint for everyone, budgeted per IP. |
| 8 | **Rate limit — per route + IP** | `securenow ratelimit add <ip> --route /webhooks/stripe --mode exact --method POST --limit 5 --window 1m --duration 24h` · NL `securenow ratelimit from-text "rate limit /webhooks/stripe to 5/min for 24h" --yes` · test `securenow ratelimit test <ip> --path /webhooks/stripe --method POST` | precise throttle of one sender on one receiver route. |
| 9 | **CAPTCHA / proof-of-work challenge** | `securenow challenge add --route /login --difficulty 16 --clearance 30m` (route-wide) **or** `securenow challenge add <ip> --route /api/search --difficulty 18 --clearance 30m` · test `securenow challenge test <ip> --path /login --method GET` | bot scraping / business-flow abuse from **shared / NAT / CGNAT** egress — a human passes once, a script can't. **Rarely fits machine-to-machine webhooks** (no human to solve it); reserve for human-facing callback pages, otherwise prefer rate-limit/block. |
| 10 | **Auto-block (risk-scored)** | `securenow automation defaults --yes` (≥95→7d, 90–94→72h, 85–89→24h) · custom `securenow automation create --conditions '[...]' --actions '[...]'` · preview `securenow automation dry-run <id>` | hands-off blocking by risk score of forgery-probing sources; actions include block / rate_limit / requireCaptcha. |
| 11 | **Session revocation** | `securenow revoke …` (SDK `securenow/sessions` `guard()` / `isRevoked()`) | session theft / account takeover — kill the stolen session, not the IP. |
| 12 | **Trusted IP (suppress)** | `securenow trusted add <ip> --label "Stripe webhook egress / partner batch sender"` | stop false positives from a provider's published webhook source IPs — suppresses detection **and** mitigation so legit retries never trip the abuse rules. NOT deny-by-default. |
| 13 | **Allowlist (deny-by-default)** | `securenow allowlist add <ip> --label "..." --reason "..."` ⚠️ once any entry exists, ONLY listed IPs reach the app | lockdown of an internal/admin-only surface. Never for a public webhook endpoint. |
| 14 | **False-positive exclusion** | `securenow fp create --conditions '[...]' --rule-scope this_rule --reason "..."` · `securenow fp mark <notification-id> <ip> --rule-scope this_rule` · preview `securenow fp dry-run --conditions '[...]'` | keep a noisy rule quiet (e.g. a real provider doing legit retries that re-deliver events) without weakening it. |
| 15 | **App / config / code fix (primary for root cause)** | *described in the Code-Findings report, never auto-applied* | the actual fix: raw-body HMAC + constant-time compare, verify-before-trust, timestamp tolerance + durable event-id dedupe + idempotent side effects, re-fetch authoritative state from the provider by id, outbound resolved-IP allowlist with redirects disabled, per-endpoint secrets + rotation, payload minimization + outbound signing. SecureNow contains the abusive sender; the fix removes the weakness. |

**Choosing per threat** — by **confidence**: exploit-signature/exact IoC → instant-block or block;
probable bot on shared egress → **challenge** (rarely fits machine-to-machine webhooks); noisy/
legit-mixed traffic (signature-failure or duplicate-ignored bursts where a real provider's retries
can inflate the count) → **rate-limit (test-mode first)**; session compromise → **revoke**;
known-good provider noise → **trusted / fp**. By **blast radius**: always scope to the narrowest
`route`/`method`/`IP`/`duration` that stops the abuse — for webhooks prefer a **route-scoped
rate-limit/block on the receiver path** over a global one; on NAT/CGNAT/shared IPs prefer
challenge/rate-limit over a hard block. Always pair an edge mitigation with the **app/config fix**
(Code-Findings report) on every inbound-authenticity, replay, SSRF, and egress row — SecureNow can
only contain the actor at the edge, the app fix stops the forgery from working.

### 4d. Testing every detection and mitigation

Only test against apps/environments the user owns; prefer `--env local`/staging. For synthetic
source IPs use TEST-NET ranges (`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`).

```bash
# Synthetic inbound forgery probing — exercise the signature-failure rule end to end:
for i in $(seq 1 6); do
  securenow event send api.webhook.signature_failed --ip 203.0.113.51 \
    --attrs route=/webhooks/stripe,provider=stripe,reason=invalid,test=true
done

# Synthetic replay attack:
for i in $(seq 1 4); do
  securenow event send api.webhook.replay_blocked --ip 203.0.113.51 \
    --attrs route=/webhooks/stripe,provider=stripe,event_id=evt_test_123,test=true
done

# Synthetic duplicate-delivery noise:
for i in $(seq 1 12); do
  securenow event send api.webhook.duplicate_ignored --ip 198.51.100.20 \
    --attrs route=/webhooks/shopify,provider=shopify,event_id=evt_test_456,test=true
done

# Synthetic outbound SSRF block (metadata target):
securenow event send ssrf.blocked --ip 203.0.113.50 \
  --attrs route=/webhooks/dispatch,target_host=169.254.169.254,reason=link_local,test=true

# Validate a rule query without waiting for the schedule:
securenow alerts rules test <RULE_ID> --mode dry_run --wait

# Traffic-based rules (webhook flood / 4xx-5xx spike) — generate spans, then check pipeline:
securenow test-span "threat-model.webhook.smoke"
securenow forensics "requests and 4xx/5xx to /webhooks in the last hour" --env production

# Payload-borne injection inside a callback — confirm a benign-but-matching marker in a webhook
#   field triggers the system signature rule + instant block on a staging URL, then verify:
securenow firewall test-ip 203.0.113.50 --app <APP_KEY> --env production

# Mitigation verification:
securenow ratelimit test 203.0.113.51 --path /webhooks/stripe --method POST

# Confirm + clean up:
securenow notifications list --limit 10
securenow blocklist list      # then: securenow blocklist unblock <id> --reason "threat-model test"
securenow challenge list      # then: securenow challenge remove <id>
```

Every 🟢/🟡 threat row in the report must have a concrete test recipe (commands + expected
outcome: which rule fires, which notification appears, what the mitigation does).

---

## Phase 5 — Write the four reports (two tracks)

Write **four** files into `threat/16-webhooks/`: the **Detection & Mitigation** runbook
(`.md` + `.html`) and the **Code Findings & Recommendations** audit (`.md` + `.html`). The two
tracks **cross-link** each other: the gaps / instrumentation rows in the detection runbook link to
the relevant code finding, and each code finding links back to the detection row it backs. Stats
numbers in each HTML must equal that track's table/finding counts. Record the resolved installed
`securenow` version (from Phase 0.5) in the appendix of **both** reports.

### 5a. Detection & Mitigation report — sections, in order (both `.md` and `.html`)

1. **Executive summary** — stats line (N threats modeled · N covered · N partial · N gaps ·
   N rules to create · N mitigations), top 3 **detectable** webhook risks for this specific stack
   (e.g. "signature-failure brute on the Stripe endpoint", "outbound dispatch has no SSRF guard →
   `ssrf.blocked` not emitted", "no event-id dedupe → replay-block bursts"), installed `securenow`
   version + app key + firewall state, and a one-line OWASP note (API6/API8/API7 owned here;
   payment money-moves + secret custody deferred to siblings).
2. **SDK & environment** — installed SDK version (from `node_modules/securenow`, Phase 0.5),
   app key(s), environment, firewall state, existing rules / automations / challenge rules (from
   Phase 0), and the system signature rules present (SQLi/XSS/RCE + `instant.block` state).
3. **Threat → Detection → Mitigation matrix** — one row per modeled threat:
   `# | Threat | OWASP/CWE | Coverage 🟢/🟡/🔴 | Detection rule | Signal (threshold+window) | Schedule | Sev | Mode | Mitigation`.
   Severity ∈ {critical, high, medium, low}. The **Mitigation** cell must pick **specific, scoped**
   mitigation(s) from the §6 toolbox (e.g. "route-scoped rate-limit on `/webhooks/stripe` + app fix:
   raw-body HMAC")  — never a generic "block the IP". The **Mode** cell tags each rule **`test-first`
   or `prod-ready`** per the 4b-test rule of thumb (heuristic/volume webhook-count rules →
   `test-first`; `ssrf.blocked` / exploit-signature / exact-IoC → `prod-ready`). Then the "Out of
   scope" N/A list and the deferred-to-sibling rows (payment money-moves, secret custody, general
   API) — present, linked, not re-derived.
4. **Detection rules to create** — each as the **ready-to-copy unit** from Phase 4 (SQL with a
   `-- rules/<name>.sql` first line → the `cat > rules/<name>.sql` save → full
   `securenow alerts rules create …` → `securenow alerts rules test <RULE_ID> --mode dry_run --wait`).
   **Mark each rule `test-first` or `prod-ready`** (per 4b-test); for every `test-first` rule include
   the `securenow alerts rules update <RULE_ID> --mode test` → observe (3–7 days) →
   `securenow alerts rules update <RULE_ID> --mode prod` promotion step in its unit. Injection-class
   rows reference the **system signature rules + `instant.block`**, not duplicate SQL. Note
   pre-existing rules (from Phase 0) instead of duplicating them.
5. **Instrumentation the detections need** — only the `track('api.webhook.signature_failed' /
   '…replay_blocked' / '…duplicate_ignored' / 'ssrf.blocked')` events the rules above consume, each
   as a copyable snippet; point to the **code-findings report** for *where* (file:line) to add them.
6. **Mitigation mechanisms** — render the **full §6 mitigation toolbox table** (all 15 rows: free
   firewall · exploit-signature instant-block · IP block [global / route+method / temporary] ·
   rate-limit [per-IP / per-route / route+IP] · challenge · auto-block · session revocation · trusted ·
   allowlist · fp exclusion · **app/config fix**) + the "Choosing per threat" paragraph + a per-threat
   ready-to-copy mitigation command (scoped to the narrowest route/method/IP/duration) + reversibility.
   Make explicit that the **app fix is primary** for inbound authenticity, replay, idempotency, and
   outbound SSRF — the SecureNow control is containment of the abusive sender. State that
   false-positive-prone rules are armed via the **`--mode test` → observe → `--mode prod` promotion**
   (see item 4 / 4b-test), not enabled with mitigation on day one.
7. **Action plan (copy-paste, ordered)** — ① engage the firewall + enable signature instant-block,
   ② add the webhook event instrumentation at each verifier and the outbound SSRF guard,
   ③ create the rules — **every `test-first` (FP-prone) rule created in `--mode test`** (detect-only),
   `prod-ready` rules created armed — ④ enable automations / challenge, ⑤ test, ⑥ verify in dashboard,
   ⑦ **promote the test-first rules after N days** (3–7 days of real traffic, threshold tuned + `fp`
   exclusions added): `securenow alerts rules update <RULE_ID> --mode prod`, ⑧ schedule the
   app/config fixes from the **code-findings report** (raw-body HMAC + constant-time + verify-first,
   timestamp/dedupe/idempotency, outbound resolved-IP allowlist, per-endpoint secrets + rotation).
   Real commands only, `<APP_KEY>` already substituted.
8. **Testing & validation** — per-rule recipe from 4d: `securenow event send …` / `test-span` /
   dry-run + expected outcome + cleanup (TEST-NET IPs `192.0.2`/`198.51.100`/`203.0.113`).
9. **Response runbooks** — for each notification type (signature-failure burst, replay-blocked
   burst, duplicate-ignored burst, SSRF-blocked, webhook flood, webhook 4xx/5xx spike): what fired,
   how to confirm a true positive (e.g. is the source a real provider IP? check `securenow trusted`),
   the exact command to respond (rate-limit / block / trusted / signature), and the exact command to
   reverse.
10. **Known gaps & SecureNow feature requests** — every 🔴 threat: why it's not coverable today,
    the interim app/config fix (**link to the relevant code finding**), and the "contact the
    SecureNow team" line.
11. **Appendix** — resolved SDK/CLI version (from Phase 0.5), app key, environment, firewall state,
    rule IDs created, date, link to the code-findings report.

### 5b. Code Findings & Recommendations report — sections, in order (both `.md` and `.html`)

State at the top: *"Findings only — no application code was modified."*

1. **Executive summary** — findings by severity (critical / high / med / low), top 3 code risks
   (e.g. "verify-after-parse on the Stripe endpoint", "HMAC over `JSON.stringify(req.body)`",
   "outbound dispatch has no resolved-IP allowlist"), one-paragraph posture verdict.
2. **Surface & inventory** — the Phase 1 inventory for this domain: the **inbound endpoint catalog**
   (route / provider / action / signature scheme / raw-body? / verify-order / compare-fn /
   secret-source / timestamp tolerance / nonce / event-id dedupe / idempotency / source-IP trust) +
   the **outbound dispatch sink list** (sink / URL source / allowlist? / private-range block? /
   redirect handling / scheme-port restriction / egress payload / retry idempotency / outbound
   signing & rotation) + telemetry redaction status + a short attack-surface paragraph.
3. **Threat catalog** — the exhaustive Phase 2 catalog (A1–I39), grouped, each tagged OWASP/CWE,
   modeled or explicit N/A.
4. **Code-level findings (audit)** — table
   `# | Location (file:line) | Threat | OWASP/CWE | Sev | Issue | Recommended fix`, each with the
   quoted 1–8 line snippet and the described fix (**never applied**). Each finding links to the
   detection-report row it backs.
5. **Strengths** — controls already present and correct (raw-body HMAC + constant-time compare +
   timestamp tolerance + event-id dedupe; outbound resolved-IP allowlist with redirects disabled) —
   the posture must be honest.
6. **App / config fixes (primary remediation)** — the config/code changes that remove the root
   cause (described, not applied): verify-before-trust + raw-body HMAC + constant-time compare,
   timestamp tolerance + durable event-id dedupe + idempotent side effects, re-fetch authoritative
   state from the provider by id, outbound resolved-IP allowlist with redirects disabled,
   per-endpoint secrets + rotation, payload minimization + outbound signing. Each linked to the
   detection-report row it backs.
7. **Instrumentation recommendations** — the `track('api.webhook.*')` / `ssrf.blocked` calls to add
   and the **exact file:line** to add them (the enforcement point in each verifier and the outbound
   SSRF guard), so the detection rules light up.
8. **Appendix** — files reviewed, resolved SDK version (from Phase 0.5), date, link to the
   detection-mitigation report.

### HTML skeletons — two self-contained files (offline; inline CSS + copy JS; no network)

Both HTML files share the `<head>` (brand tokens + copy-button styles) and the copy `<script>` at
the end of `<body>` below. Change only the `<title>`, the sidebar subtitle, and the section content
(one body per track). **Wrap EVERY command/SQL block as a `.cmd`** so it gets a Copy button. The
Detection HTML title is "Detection & Mitigation — Webhooks — SecureNow"; the Code-Findings HTML
title is "Code Findings — Webhooks — SecureNow".

```html
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
<title><!-- "Detection & Mitigation — Webhooks — SecureNow" OR "Code Findings — Webhooks — SecureNow" --></title>
<style>
  :root{--bg:#0f1419;--panel:#161c24;--panel2:#1b2330;--border:#26303d;--txt:#dbe3ec;--muted:#8b97a7;
    --accent:#3ea6ff;--accent2:#16c79a;--crit:#ff5c6c;--high:#ff9f43;--med:#f7c948;--low:#8b97a7;
    --ok:#16c79a;--info:#3ea6ff;--rev:#b388ff;}
  *{box-sizing:border-box}html{scroll-behavior:smooth}
  body{margin:0;background:var(--bg);color:var(--txt);font:15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
  a{color:var(--accent);text-decoration:none}
  code{background:#0b0f14;border:1px solid var(--border);border-radius:5px;padding:.08em .4em;font:13px/1.4 ui-monospace,"SF Mono",Menlo,Consolas,monospace;color:#9fe0c0}
  .wrap{display:grid;grid-template-columns:240px 1fr;max-width:1280px;margin:0 auto}
  nav{position:sticky;top:0;align-self:start;height:100vh;overflow:auto;padding:28px 18px;border-right:1px solid var(--border);background:var(--panel)}
  nav .brand{font-weight:700;font-size:15px;letter-spacing:.3px}nav .brand span{color:var(--accent)}
  nav .sub{color:var(--muted);font-size:12px;margin-bottom:22px}
  nav a{display:block;color:var(--muted);padding:7px 10px;border-radius:7px;font-size:13.5px}
  nav a:hover{background:var(--panel2);color:var(--txt)}
  main{padding:36px 40px 80px;min-width:0}
  header.top h1{margin:0 0 6px;font-size:26px}header.top p{margin:0;color:var(--muted)}
  .pill{display:inline-block;font-size:11px;font-weight:600;padding:3px 9px;border-radius:999px;border:1px solid var(--border);color:var(--muted);background:var(--panel)}
  .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin:26px 0 34px}
  .stat{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 18px}
  .stat .n{font-size:26px;font-weight:700}.stat .l{color:var(--muted);font-size:12.5px;margin-top:2px}
  section{margin:0 0 40px}
  h2{font-size:18px;margin:0 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
  h2 .num{color:var(--accent);font-weight:700;margin-right:8px}
  table{width:100%;border-collapse:collapse;font-size:13.5px;background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
  th,td{text-align:left;padding:11px 13px;border-bottom:1px solid var(--border);vertical-align:top}
  th{background:var(--panel2);color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.4px}
  tr:last-child td{border-bottom:none}tr:hover td{background:#19212c}
  .rid{font:12px ui-monospace,Menlo,Consolas,monospace;color:#7fd1ff;white-space:nowrap}
  .b{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:6px;white-space:nowrap}
  .b.crit{background:rgba(255,92,108,.15);color:var(--crit);border:1px solid rgba(255,92,108,.35)}
  .b.high{background:rgba(255,159,67,.13);color:var(--high);border:1px solid rgba(255,159,67,.32)}
  .b.med{background:rgba(247,201,72,.13);color:var(--med);border:1px solid rgba(247,201,72,.32)}
  .b.low{background:rgba(139,151,167,.13);color:var(--low);border:1px solid rgba(139,151,167,.32)}
  .c{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:6px;white-space:nowrap}
  .c.cov{background:rgba(22,199,154,.13);color:var(--ok);border:1px solid rgba(22,199,154,.35)}
  .c.part{background:rgba(247,201,72,.13);color:var(--med);border:1px solid rgba(247,201,72,.32)}
  .c.gap{background:rgba(255,92,108,.15);color:var(--crit);border:1px solid rgba(255,92,108,.35)}
  .owasp,.cwe{display:inline-block;font:11px ui-monospace,Menlo,Consolas,monospace;color:var(--accent);border:1px solid rgba(62,166,255,.3);border-radius:6px;padding:1px 6px;white-space:nowrap}
  .cwe{color:var(--rev);border-color:rgba(179,136,255,.3)}
  .m{display:inline-block;font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;border:1px solid var(--border)}
  .m.block{color:var(--crit);border-color:rgba(255,92,108,.35)}.m.rate{color:var(--info);border-color:rgba(62,166,255,.35)}
  .m.challenge{color:var(--accent2);border-color:rgba(22,199,154,.35)}.m.firewall{color:var(--ok);border-color:rgba(22,199,154,.35)}
  .m.signature{color:var(--crit);border-color:rgba(255,92,108,.35)}.m.notify{color:var(--muted)}.m.appfix{color:var(--high);border-color:rgba(255,159,67,.35)}
  .card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px}
  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  pre{background:#0b0f14;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow:auto;font:13px ui-monospace,Menlo,Consolas,monospace;color:#cfe8da;margin:0}
  .cmd{position:relative;margin:10px 0}
  .copy{position:absolute;top:8px;right:8px;font:11px ui-monospace,Menlo,Consolas,monospace;color:var(--muted);background:var(--panel2);border:1px solid var(--border);border-radius:6px;padding:3px 9px;cursor:pointer}
  .copy:hover{color:var(--txt);border-color:var(--accent)}.copy.done{color:var(--ok);border-color:var(--ok)}
  .flow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:6px 0 14px}
  .flow .step{background:var(--panel2);border:1px solid var(--border);border-radius:9px;padding:8px 12px;font-size:13px}.flow .arr{color:var(--accent);font-weight:700}
  .note{border-left:3px solid var(--high);background:rgba(255,159,67,.06);padding:10px 14px;border-radius:0 8px 8px 0;color:#e7d3bd;font-size:13.5px;margin:10px 0}
  footer{color:var(--muted);font-size:12px;border-top:1px solid var(--border);padding-top:18px;margin-top:30px}
  @media(max-width:880px){.wrap{grid-template-columns:1fr}nav{display:none}.stats,.grid2{grid-template-columns:1fr 1fr}main{padding:24px 18px}}
</style></head>
<body>
<div class="wrap">
  <nav>
    <div class="brand">Secure<span>Now</span></div>
    <div class="sub"><!-- "Detection & Mitigation · Webhooks" OR "Code Findings · Webhooks" --></div>
    <!-- one <a href="#…"> per section of THIS track (5a or 5b) -->
  </nav>
  <main>
    <header class="top"><h1><!-- report title --></h1>
      <p><code><!-- app name / domain --></code> · <span class="pill">securenow <!-- installed version from Phase 0.5 --></span></p></header>
    <div class="stats"><!-- 5 .stat cards; numbers MUST equal this track's table/finding counts --></div>
    <!-- <section id="…"> blocks mirroring the Markdown sections of THIS track (5a or 5b) -->
    <footer>Generated by the SecureNow webhooks threat-model prompt · <!-- date --> · securenow <!-- version --> · app <code><!-- APP_KEY --></code></footer>
  </main>
</div>
<script>
document.querySelectorAll('.copy').forEach(function(b){b.addEventListener('click',function(){
  var pre=b.parentElement.querySelector('pre'); if(!pre)return; var t=pre.innerText;
  function done(){b.textContent='Copied';b.classList.add('done');setTimeout(function(){b.textContent='Copy';b.classList.remove('done');},1500);}
  function fb(){var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();try{document.execCommand('copy');}catch(e){}document.body.removeChild(ta);done();}
  if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(done,fb);}else{fb();}
});});
</script>
</body></html>
```

Every SQL/command block in the **Detection & Mitigation** HTML uses the copyable wrapper:

```html
<div class="cmd"><button class="copy" type="button">Copy</button><pre>securenow alerts rules create \
  --name "..." --sql @rules/&lt;name&gt;.sql --apps &lt;APP_KEY&gt; --severity high \
  --schedule "*/5 * * * *" --nlp "..."</pre></div>
```

Badge usage: severity → `<span class="b crit|high|med|low">`; coverage →
`<span class="c cov|part|gap">COVERED|PARTIAL|GAP</span>`; OWASP → `<span class="owasp">API8</span>`;
CWE → `<span class="cwe">CWE-347</span>`; mitigation type →
`<span class="m firewall|signature|rate|challenge|block|notify|appfix">`; rule IDs →
`<span class="rid">`. Stats numbers must equal each track's matrix/findings row counts. The
Code-Findings HTML may omit copy buttons on prose, but still wraps any example/fix command in
`.cmd`.

---

## Quality bar (the report is rejected if any of these fail)

- **Phase 0.5 ran**: the resolved installed `securenow` version appears in **both** reports'
  appendix, and no command / flag / event / SQL column is emitted that the installed SDK/CLI does
  not expose (else it is annotated `# requires securenow >= <version>`).
- Every detection rule is a **complete copyable unit** (SQL → `rules/<name>.sql` → full
  `securenow alerts rules create …` → `--mode dry_run` test); flags match `alerts rules --help`.
- **Four** files are written to `threat/16-webhooks/` (`webhooks-detection-mitigation.md` + `.html`,
  `webhooks-code-findings.md` + `.html`); the two tracks **cross-link**; both HTML files are
  self-contained (inline CSS/JS, no network) and **every command block in the detection HTML has a
  working Copy button** (`.cmd` wrapper).
- The split is honest: SecureNow-runnable detections/mitigations live in the **Detection** report;
  code/config changes live in the **Code-Findings** report; nothing security-relevant is dropped.
- Every catalog item A1–I39 is either a matrix row or an explicit N/A line; each modeled row
  carries its OWASP API Top 10:2023 tag (**API6 / API8 / API7**, or "—") and a CWE where applicable.
- **Both directions** of the trust boundary are fully modeled: INBOUND signature
  (missing/weak/bypassable, verify-after-trust, non-raw-body, non-constant-time, weak algo, shared
  secret), replay & idempotency (no timestamp tolerance/nonce/event-id dedupe, duplicate
  processing → double ship/credit/order/email, non-durable dedupe), trust-boundary abuse (IP
  allowlist as the only trust, forged event types/amounts, provider/tenant confusion, flooding,
  cost amplification); OUTBOUND SSRF (metadata/internal/link-local, blind, filter bypass), data
  egress, retries-without-idempotency, secret custody (per-endpoint, rotation, leakage).
- Webhook signature/replay/idempotency rows use the **exact** events
  `api.webhook.signature_failed` / `api.webhook.replay_blocked` / `api.webhook.duplicate_ignored`,
  and outbound SSRF rows use `ssrf.blocked` — no invented event names.
- Payment money-moves, signing-secret storage/rotation custody, and the general API surface are
  **deferred** to the numbered siblings (../09-payment-business-logic/, ../20-secrets-and-cloud-iam/,
  ../14-api-security/) — rows present, linked, **not** re-derived.
- Every matrix row has a concrete signal (threshold + window), severity, and mitigation — no
  "monitor for suspicious activity" filler.
- Every code finding in the Code-Findings report has a `file:line`, the quoted snippet, and a
  described fix — and **no application code was modified** (this is an audit).
- Every detection SQL keeps `__USER_APP_KEYS__` scoping with the **correct table column**
  (`resources_string['service.name']` for logs/events vs `` `resource_string_service$$name` `` for
  traces) and selects an `ip` column with `HAVING ip != ''`; traffic queries keep the
  `ts_bucket_start` + `kind = 2` guards; rules validate with `--mode dry_run`.
- Payload-borne injection inside a callback references the **system signature rules +
  `instant.block`**, not duplicate pattern SQL.
- Only commands, flags, events, and SQL columns from this prompt's building blocks appear
  (including `securenow ratelimit / blocklist / trusted / firewall` and the `api.webhook.*` /
  `ssrf.blocked` events).
- Detection vs. fix is honest: where SecureNow can only contain the abusive sender at the edge
  (every inbound-authenticity, replay, SSRF, and egress row), the row **pairs the edge control
  with the app fix**.
- The Detection report's mitigation section presents the **full toolbox** (§6: firewall · instant-block ·
  block [global / route / method / temporary] · rate-limit [IP / route / IP+route] · challenge · auto-block ·
  revoke · trusted · allowlist · fp · app-fix), and **each modeled threat's matrix row selects specific,
  scoped mitigation(s) from it** — never a generic "block the IP."
- **Every false-positive-prone rule is tagged `test-first`** and carries the `--mode test` → observe (3–7
  days) → `--mode prod` promotion workflow; only high-precision rules are `prod-ready`. The action plan
  creates the test-first rules in `--mode test` and has an explicit "promote after N days" step.
- Every 🔴 gap appears in the gaps section with an interim app/config fix **and** the "contact the
  SecureNow team" line.
- The action plan runs top-to-bottom with `<APP_KEY>` substituted in.
- Each HTML is self-contained (no CDN, no fonts, no external scripts) and its stats cards match
  that track's table/findings counts.
- A one-line summary is printed back to the user: **per-track file paths**, threat counts,
  rules-to-create count, code findings by severity, gaps, and the resolved SDK version.

<!-- ════════════════ END OF PROMPT ════════════════ -->