#12API4 · API6

Rate Limits & Abuse

Flooding, scraping, enumeration, and bot anti-automation.

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/12-rate-limits-and-abuse/ — open rate-limits-and-abuse-code-findings.html (the audit) and rate-limits-and-abuse-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

# Rate Limits & Abuse 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 first read the **installed `securenow` SDK** in the repo so every rule, flag, event
name, and SQL column it emits is correct for THIS version (never guessed). It will then inventory
the abusable surface, build an exhaustive **rate-limit & abuse** threat model mapped to the **OWASP
API Security Top 10:2023** (primarily **API4:2023 Unrestricted Resource Consumption** and
**API6:2023 Unrestricted Access to Sensitive Business Flows**), audit the code for missing
rate-limit / quota / anti-automation controls, and emit a **two-track, four-file** SecureNow-branded
deliverable (each track as **Markdown + self-contained HTML**) — a Detection & Mitigation runbook
(every detection a **ready-to-copy** SQL → save → create → dry-run command unit, plus the mitigation
commands to run and how to test each) and a Code Findings & Recommendations audit (code-level
findings, audited **not** fixed) — and which threats still need the SecureNow team.

This is the **volumetric & automation** model — SecureNow's **home turf** and its **single
highest area of native coverage**. It owns *traffic shape and containment*: flood/DoS, scraping,
enumeration, bot abuse, scalping/farming, and the rate-limit / challenge / block posture that
contains them. Where a flood targets a *specific* asset, this model owns the **traffic shape &
containment** and **defers the business impact** to the owning domain:

- Business-flow abuse (checkout, payout, coupon, gift-card, scalping intent) →
  [payment & business-logic model](../09-payment-business-logic/).
- Credential stuffing / brute force (the *attack mechanics*, lockout, MFA) →
  [authentication model](../01-authentication/).
- Upload/download abuse (oversize files, decompression bombs, storage drain) →
  [file upload/download model](../10-file-upload-download/).
- General API resource-consumption flaws (body-size caps, pagination ceilings, SSRF, GraphQL
  cost, query complexity, the full API surface) → [API security model](../14-api-security/).

This model **detects and contains the volumetric/automated symptom**; the linked models own the
deep per-asset modeling. Run them too — together they cover the whole OWASP API Top 10.

> SecureNow is fundamentally an **API / traffic** security layer (firewall, rate-limit,
> challenge, exploit-signature instant-block, ASN enrichment, forensics). For this domain almost
> **everything** is traffic-based: floods, scraping, enumeration, and 429-ceiling abuse are all
> visible in the request stream and need **no instrumentation at all**. The app still owns the
> *quota* — SecureNow contains the abuser at the edge while the app enforces per-client limits.

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 Rate Limits & Abuse Threat Model Report (SecureNow)

You are a senior application-security engineer specializing in anti-automation, volumetric abuse,
and rate limiting. Produce an **exhaustive rate-limit & abuse threat model for THIS codebase**,
organized around **OWASP API4:2023 (Unrestricted Resource Consumption)** and **API6:2023
(Unrestricted Access to Sensitive Business Flows)**, mapped to **SecureNow** detections and
mitigations, with a ready-to-run action plan **and** a code-level audit of every missing
rate-limit, quota, or anti-automation control you find. Every rule, flag, event name, and SQL
column you emit MUST be grounded in the **installed `securenow` SDK** (Phase 0.5) — never guessed —
and every detection is a **ready-to-copy** command unit (SQL → save to `rules/<name>.sql` → full
`securenow alerts rules create …` → dry-run test). You write **four deliverables** (two tracks ×
{Markdown, self-contained HTML}) into `threat/12-rate-limits-and-abuse/` (create the folder if
needed):

1. `rate-limits-and-abuse-detection-mitigation.md` — the **operational runbook**: what to run in
   SecureNow (ready-to-copy detection rules, mitigation commands, tests, response runbooks).
2. `rate-limits-and-abuse-detection-mitigation.html` — the same runbook as a **self-contained** HTML
   page (inline CSS + JS, no network requests) with a **Copy button** on every command block.
3. `rate-limits-and-abuse-code-findings.md` — the **code audit**: the abusable-surface inventory,
   the threat catalog, and every code-level finding + recommended fix (audited, **not** applied).
4. `rate-limits-and-abuse-code-findings.html` — the same audit as a **self-contained** HTML page.

The two tracks **cross-link**: the gaps/instrumentation rows in the Detection report link to the
relevant code finding, and the app-config / instrumentation fixes in the Code Findings report link
back to the detection row they back. `<slug>` here is `rate-limits-and-abuse`.

Work in the seven phases below (Phase 0 → 0.5 → 1 → 2 → 3 → 4 → 5), 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 **traffic shape & containment** of abuse: volumetric
floods, distributed/botnet floods, expensive-endpoint amplification, scraping/harvesting,
enumeration oracles, credential-stuffing/brute-force *volume*, scalping/hoarding, signup/referral/
free-tier farming, missing per-client limits, 429-ceiling abuse, and anti-automation **evasion**
(IP rotation, `X-Forwarded-For` spoof, header/UA rotation, CDN-origin bypass, slowloris, CAPTCHA/
challenge bypass). It maps these to **API4** and **API6**. It does **not** re-derive the deep
per-asset models: business-flow *impact* → [../09-payment-business-logic/](../09-payment-business-logic/);
credential-stuffing/brute-force *mechanics* → [../01-authentication/](../01-authentication/);
upload/download *abuse* → [../10-file-upload-download/](../10-file-upload-download/); general API
resource-consumption flaws (body-size, pagination, SSRF, query cost) →
[../14-api-security/](../14-api-security/). For those, add a one-row "Deferred to sibling models"
entry, link the report, and model only the **volumetric/automated symptom** SecureNow contains.

---

## 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 / risk-score auto-block already configured
securenow challenge list --json   # CAPTCHA / proof-of-work challenge rules already configured
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** (this domain leans on
the free firewall heavily), any existing **rate-limit / challenge rules**, and any **automation
defaults** already active — those are the backbone of this model and must not be duplicated.

---

## 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 abusable surface (codebase analysis)

Abuse defense starts with knowing **what can be hammered** and **where the quota lives today**.
Document what is **actually exposed and abusable**, not what is intended. Cover at minimum:

- **Endpoint catalog by cost & sensitivity** — enumerate every route (method + path), grouped by
  router/controller, and tag each with: public/internal/admin; **read vs write**; **cheap vs
  expensive** (does one request fan out to heavy CPU/DB/regex/render, a search, a report, an
  export?); whether it triggers a **paid downstream** (SMS/email/LLM/maps/payment). This catalog
  drives every per-route threshold in the report.
- **Existing rate-limit / quota controls** — for **every** route, record whether there is a
  per-IP limit, a per-user/per-API-key quota, a global limit, a burst/token-bucket, a concurrency
  cap, or **nothing**. Name the mechanism (`express-rate-limit`, `@fastify/rate-limit`,
  `rate-limiter-flexible`, nginx `limit_req`, Cloudflare/CDN rules, a Redis counter, a gateway
  policy) and its window/limit. **Routes with no limit are the core findings of this model** —
  list them explicitly. Note whether limits key on **IP**, **user/API key**, or both, and whether
  the app ever returns **429** anywhere (grep responses/middleware for `429`,
  `Too Many Requests`, `Retry-After`).
- **Sensitive / farmable business flows** — signup, login, password-reset, OTP/email/SMS send,
  invite/referral, coupon/promo redemption, gift-card check/redeem, free-trial creation,
  checkout/reservation/ticket-buy (scalpable inventory), bulk export/report, search. Anything a
  bot would **farm, scalp, hoard, or enumerate**. For each, note the per-actor limit (or its
  absence) and the abuse incentive. (API6.)
- **Enumeration & oracle surface** — endpoints whose **response or timing differs** for
  exists/not-exists: login ("user not found" vs "wrong password"), password-reset ("email sent"
  vs "no such user"), coupon/gift-card validity check, username/handle availability, sequential
  numeric IDs in URLs, predictable codes. These feed scraping/enumeration detection.
- **Proxy, host & client-IP trust** — how is the *real* client IP derived? Inspect reverse-proxy
  assumptions, Express/Fastify/Nest/Koa `trust proxy`, and whether `X-Forwarded-For`, `Forwarded`,
  `X-Real-IP`, or `CF-Connecting-IP` are trusted **from the public internet** (attacker can spoof
  → defeats per-IP limits). Note the trusted-ingress CIDRs, if any. Determine whether the **origin
  is reachable directly**, bypassing the CDN/WAF (origin IP exposed in DNS history, no
  origin-allowlist firewall). (Evasion surface.)
- **Bot / automation posture** — is there any CAPTCHA, proof-of-work, device fingerprint, bot
  manager, or `User-Agent`/header heuristic today? Where (signup? login? checkout?). Note where
  shared/NAT/CGNAT/mobile-carrier egress is expected (corporate, mobile, university) — those are
  the places to **prefer challenge over block** so you don't nuke real users.
- **Pagination & bulk-pull ceilings** — for read/list/search/export routes, the max `limit` /
  `page_size` / result count. Uncapped pagination turns one request into a mass scrape; note
  where **no ceiling** exists. (Defer the generic body-size/query-cost flaws to API-security;
  here capture the *bulk-extraction-per-request* angle.)
- **Long-lived / streaming connections** — WebSocket, SSE, long-polling, AI/export streams: are
  there per-connection limits, idle timeouts, and connection-rate caps? Connection floods and
  slow-drip exhaustion are volumetric. (Deep streaming model →
  [../17-realtime-websocket-sse/](../17-realtime-websocket-sse/); here capture connection-flood
  shape only.)
- **SecureNow instrumentation already present** — `securenow/register` / `securenow run` /
  `securenow init` (gives traffic spans automatically — this is **most** of what this model
  needs), any `securenow/events` `track()` calls, and whether the **firewall is engaged**. This
  determines what works *today* (almost everything traffic-based) vs *after* a small amount of
  app-quota instrumentation.

Output of this phase = the report's **abusable surface & inventory** section: the endpoint
catalog (method/path/visibility/read-write/**cost**/paid-downstream), the **rate-limit/quota
controls table** (Route / IP limit / User limit / Window / Returns 429? / Mechanism — with the
no-limit rows flagged), the **sensitive/farmable flow list**, the **enumeration/oracle list**,
the **client-IP trust + origin-exposure posture**, the **bot/anti-automation posture**, the
**pagination/bulk-pull ceilings**, the **streaming/connection posture**, and a short paragraph
naming the real abuse attack surface for this stack (what a bot operator would target first).

---

## 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. "WebSocket items: N/A, no realtime endpoints"). 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 (**API4** or **API6** for
almost everything here; "—" if truly neither).

**A. Volumetric flood — single source (OWASP API4)**
1. Volumetric L7 flood on an endpoint (very high request rate from one IP)
2. Sustained low-and-slow flood from one IP staying just under a naive threshold
3. Burst/spike flood — sudden onset from a new high-volume source (volumetric onset)
4. Flood concentrated on a single expensive route (targeted resource exhaustion)

**B. Distributed / botnet flood (OWASP API4)**
5. Distributed flood — many IPs, low per-IP rate, high aggregate (defeats per-IP limits)
6. Botnet / datacenter-origin flood concentrated in one **ASN / hosting provider** (`client.asn`)
7. Rotating-IP flood from one residential-proxy pool / many ASNs (aggregate-only signal)
8. Coordinated multi-endpoint flood (same actor, many routes, to dilute per-route counts)

**C. Expensive-endpoint amplification (OWASP API4)**
9. Search / filter / report / export endpoint hit at scale (one request → heavy CPU/DB)
10. Regex / parser / aggregation amplification (small input → large server work)
11. Third-party cost amplification — each call triggers a paid downstream (SMS/email/LLM/maps)
12. Fan-out amplification — one request triggers many internal/downstream calls

**D. Automated scraping / harvesting (OWASP API6)**
13. Automated scraping of read endpoints at scale (catalog, listings, profiles, prices)
14. Sequential-ID / pagination walk to pull an entire dataset
15. Large / anomalous response volume to a single client (bulk extraction signature)
16. Distributed scraping spread across IPs/ASNs to stay under per-IP limits

**E. Enumeration & oracle abuse (OWASP API6)**
17. Account / email / username enumeration via response-difference oracle
18. Account / email enumeration via **timing** oracle (response-time difference)
19. Coupon / promo / gift-card code enumeration & validity probing
20. Resource-ID / object enumeration (sequential or guessable IDs) — *traffic-observable; deep BOLA deferred*

**F. Credential-stuffing & brute-force volume (OWASP API4/API6)**
21. Credential-stuffing volume — many distinct accounts tried from one IP/ASN — *mechanics deferred to authentication*
22. Password / OTP brute force — many attempts against one account — *mechanics deferred to authentication*
23. Login 401/403 spike from one IP or ASN (the traffic shape SecureNow contains)

**G. Inventory scalping, hoarding & farming (OWASP API6)**
24. Inventory scalping / hoarding — automated buy/reserve of limited stock (tickets, drops) — *business impact deferred to payment-business-logic*
25. Signup / free-trial / free-tier farming (mass fake-account creation through the API)
26. Referral / invite / coupon farming (self-referral loops, promo abuse) — *impact deferred to payment-business-logic*
27. Comment / review / message / vote spam via public write endpoints
28. Workflow automation a human wouldn't perform at that rate (multi-step flow replayed fast)

**H. Missing / broken rate-limit posture (OWASP API4)**
29. Missing per-client rate limits or quotas anywhere (no 429s in the whole app)
30. Per-IP-only limits with no per-user/per-API-key quota (one user, many IPs, unbounded)
31. Per-user-only limits with no per-IP cap (one IP, many accounts, unbounded — stuffing-friendly)
32. Rate limit far too high to matter (cosmetic limit that never trips on real abuse)
33. 429 ceiling repeatedly hit by one client (ignoring `Retry-After` / back-off — abuse or runaway)
34. Rate limit not applied to a high-cost/sensitive route while applied elsewhere (gap in coverage)

**I. Anti-automation evasion (OWASP API4/API6)**
35. IP rotation across a proxy pool to stay under any per-IP limit
36. `X-Forwarded-For` / `Forwarded` / `X-Real-IP` spoofing to forge a client IP and evade per-IP limits
37. User-Agent / header / cookie / fingerprint rotation to look like many clients
38. Direct-to-origin access bypassing the CDN/WAF (hitting the origin IP/host)
39. Path/encoding/case/trailing-slash variation to dodge a per-path limit (`/api/Search`, `/api/search/`, `%2f`)
40. HTTP method/verb variation or method-override to reach a route the limit doesn't cover

**J. Connection-level & slow-attack resource starvation (OWASP API4)**
41. Slowloris / slow-body / slow-read — many half-open connections holding resources
42. WebSocket / SSE / long-poll connection flood or long-lived connection exhaustion
43. Keep-alive / connection-pool exhaustion (many idle connections, no idle timeout)

**K. Challenge / CAPTCHA & shared-egress evasion (OWASP API6)**
44. CAPTCHA / challenge solving via farm/solver service or token replay
45. Challenge bypass by hitting an unprotected route variant or skipping the challenge step
46. Shared / NAT / CGNAT / mobile-carrier egress — many real users behind one IP (block = collateral; prefer challenge)

**L. Observable abuse (what telemetry actually catches — the workhorse rules)**
47. 4xx/5xx spike from one IP or ASN (fuzzing, probing, broken integration, runaway client)
48. One IP touching an anomalous count of **distinct** endpoints (mapping / scraping breadth)
49. Sudden traffic spike / new high-volume source (volumetric onset before a sustained flood)
50. Large / anomalous response sizes from a single client (bulk extraction / scraping)
51. High per-ASN aggregate request volume with low per-IP volume (distributed/botnet signal)

**M. Deferred — modeled in sibling models (reference, do not re-derive)**
52. Sensitive-business-flow *impact* (checkout/payout/coupon/scalping economics, **API6**) → [payment & business-logic model](../09-payment-business-logic/)
53. Credential-stuffing / brute-force *mechanics* (lockout, MFA, password policy, **API2**) → [authentication model](../01-authentication/)
54. Upload / download *abuse* (oversize, decompression bomb, storage drain, **API4**) → [file upload/download model](../10-file-upload-download/)
55. General API resource-consumption *flaws* (body-size cap, pagination ceiling, SSRF, GraphQL/query cost, **API4/API7**) → [API security model](../14-api-security/)

> For 52–55, add **one** matrix row each marked *"deferred — see linked model"*, and only note
> the SecureNow traffic-observable symptom (the flood/scrape/enumeration shape) here. The full
> per-asset detection/mitigation lives in the other reports. **Do not duplicate** their rows.

Every catalog item A1–M55 must be either a matrix row (with its OWASP tag) or an explicit N/A
line with a reason — do not silently drop any item.

---

## 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/limiter name.
- **Pattern** — quote the 1–8 relevant lines. State the missing control precisely (e.g.
  "no rate-limit middleware on `POST /api/search` → unbounded floods"; "`app.set('trust proxy',
  true)` with no CIDR → `X-Forwarded-For` spoofable, per-IP limit defeated"; "limiter keys on
  `req.ip` only, no per-user quota → one user, many IPs"; "no max `page_size` → one request pulls
  the whole table"; "login returns `user not found` vs `wrong password` → enumeration oracle";
  "OTP send has no per-account/per-IP cap → SMS-cost amplification").
- **Why exploitable** — the concrete request pattern an attacker sends (rate, distribution, route)
  and what they achieve (outage, scrape, farm, enumerate, cost blow-up).
- **Severity** — critical / high / medium / low (impact × reachability).
- **Recommended fix (described, not applied)** — the specific change: e.g. "add a per-IP **and**
  per-user token-bucket on `POST /api/search` (e.g. 60/min IP, 600/hr user) returning `429` with
  `Retry-After`"; "set `trust proxy` to the known ingress CIDR only and derive client IP from the
  last trusted hop"; "cap `page_size` at 100 with a hard `LIMIT`"; "return a constant
  generic response and constant-time path for both exists/not-exists to kill the enumeration
  oracle"; "add per-account + per-IP caps on OTP/email send with exponential back-off"; "require a
  challenge on signup/free-trial creation". Reference the secure pattern, not a code diff.
  **You must not edit the codebase.**

If a control exists and is correct (sensible per-user + per-IP limits, 429 with `Retry-After`,
pagination capped, generic auth responses, challenge on signup), note it as a **strength** — the
posture must be honest. **Absence of a control where the surface exists is itself a finding**
("no rate limit on any route" is a critical finding for this model).

Look specifically for:

- **No-limit routes** — handlers with no limiter/quota at all, especially expensive or
  paid-downstream routes. *Fix:* per-IP + per-user limits, 429 + `Retry-After`, tighter caps on
  expensive/paid routes.
- **Wrong limit key** — IP-only (defeated by one user/many IPs) or user-only (defeated by one
  IP/many accounts, and unauthenticated routes have no user). *Fix:* layer both keys; for
  unauthenticated abuse-prone routes (signup/login/reset) key on IP + a per-target counter.
- **Spoofable client IP** — `trust proxy: true`, raw `X-Forwarded-For`/`X-Real-IP` used for
  limiting or logging without a trusted-proxy CIDR. *Fix:* explicit trusted-ingress CIDRs; derive
  client IP only from the trusted hop; never trust forwarding headers from the public internet.
- **Enumeration oracles** — auth/reset/availability/coupon endpoints with different responses,
  status codes, or timings for exists vs not-exists. *Fix:* uniform generic responses + constant-
  time handling; rate-limit + challenge the probe.
- **Uncapped pagination / bulk pull** — list/search/export with no `limit`/`page_size` ceiling.
  *Fix:* hard server-side cap; cursor pagination; per-user export quota.
- **Unmetered paid downstream** — SMS/email/OTP/LLM/maps calls with no per-actor cap. *Fix:*
  per-account + per-IP caps, daily quota, back-off, and a circuit breaker on spend.
- **Cosmetic limits** — limits set absurdly high relative to legitimate use. *Fix:* right-size to
  real p99 traffic + headroom.
- **Challenge gaps** — CAPTCHA/PoW present on one route but a sibling route reaches the same flow
  unprotected; challenge skippable. *Fix:* protect the *flow*, not one entry point.
- **Origin exposure** — origin IP/host reachable directly, bypassing the CDN/WAF. *Fix:* lock the
  origin firewall to the CDN/WAF ranges; rotate the origin IP if leaked.

---

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

Classify each threat with exactly one coverage badge:

- 🟢 **COVERED** — detectable + mitigable with SecureNow today (existing rule, system signature
  rule, or a rule you provide the SQL for, on telemetry that is **already flowing as traffic**).
  This is **most of this model** — floods, distributed/ASN floods, scraping, enumeration breadth,
  4xx/5xx spikes, distinct-path breadth, 429-ceiling abuse (once 429s are emitted as traffic), and
  bulk-response extraction all land here. **This domain has the highest 🟢 ratio of any model.**
- 🟡 **PARTIAL** — works after the customer adds a little instrumentation (the app emits
  `api.ratelimit.exceeded` so its **own** quota rejections are visible), or the detection is
  inherently pattern-based / false-positive-prone (notify-only), or SecureNow can only *contain
  the abuser at the edge* while the real fix is an **app-owned quota** (the app must own the
  per-client limit; SecureNow contains the source).
- 🔴 **GAP** — SecureNow cannot detect or mitigate this today (e.g. a *single legitimate-looking*
  request that is internally expensive with no traffic anomaly; a timing-only oracle below
  measurement resolution; a CAPTCHA-farm solve that produces normal-looking traffic). **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 edge-containment vs the app's quota.** SecureNow sees **traffic** and
> **events**; it contains actors via firewall / rate-limit / challenge / block / signature
> instant-block. For abuse it is genuinely strong: the flood, the scrape, the enumeration sweep,
> the ASN-concentrated botnet, the 429-ceiling hammering — all visible, all containable. But
> SecureNow **cannot invent the app's quota**: "missing per-client rate limit" is fixed by the
> app adding the limit; SecureNow detects the *resulting abuse* and contains the source while the
> app gate is added. Pair the SecureNow control with the **app-config fix** on every
> missing-limit / oracle / paid-downstream row. A flaw that emits no traffic anomaly and no event
> is 🔴 until the app emits the event in Phase 3's recommended fix.

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

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

The big advantage here: 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, and per-IP **ASN/org** enrichment. **Almost
every rate-limit/abuse rule needs no events** — floods, scraping, distinct-path breadth, 4xx/5xx
spikes, ASN aggregation, and bulk-response extraction are all pure traffic.

Add `securenow/events` `track()` only for the **one** app-internal signal traffic can't always
see — the app's **own** quota rejecting a request (never throws):

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

// The app's own rate limiter / quota rejected a request (so SecureNow sees the ceiling abuse
// even if your limiter answers before the request reaches a traced handler):
track('api.ratelimit.exceeded', { userId, ip, attributes: { route: '/api/search', limit: '100', window: '1m', scope: 'per_ip|per_user|per_key|global' } });

// A sensitive business flow executed (signup / checkout / export) — feeds abuse-rate detection
// when the flow itself returns 200 (so a flood of *successful* farming is visible):
track('api.sensitive.flow', { userId, ip, attributes: { flow: 'signup|checkout|coupon|referral|export|otp_send', items: '1' } });
```

> If your limiter is implemented as standard middleware that returns **429**, SecureNow already
> sees it in traffic — the `api.ratelimit.exceeded` event is only needed when the rejection
> happens **before** a traced span (e.g. at an edge worker or a non-traced gateway). Emit it
> there. Hash or omit any PII before it becomes an attribute value (emails, raw tokens) — see the
> Phase 1 telemetry-redaction check. Attributes feed detection; they must not become a new leak
> path.

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

| Event | Emit when |
|---|---|
| `api.ratelimit.exceeded` | the app's own quota/limit rejects a request (esp. before a traced span) |
| `api.sensitive.flow` | a sensitive/farmable business flow (signup/checkout/coupon/referral/export/otp) executes |

Custom `attributes` become queryable as `attributes_string['<key>']` (e.g.
`attributes_string['scope']`). Ingest enriches every IP with **ASN/org** (`client.asn`,
`client.as_org`) — enabling botnet / datacenter-origin / distributed-flood detection with **no
extra code**. This is the single most valuable enrichment for this domain.

### 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,
and the ASN/org enrichment (`client.asn`, `client.as_org`). Confirm any other column
(`http.method`, response size) with a `--mode dry_run` before relying on it; OTEL SDK versions
vary (`http.method` vs `http.request.method`).

**Traffic-based — volumetric 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
GROUP BY ip
HAVING ip != '' AND requests >= 600
```

**Traffic-based — distributed / botnet flood by ASN (many IPs, one ASN — defeats per-IP limits):**

```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 attributes_string['client.asn']    AS asn,
       attributes_string['client.as_org'] AS as_org,
       any(client_ip)                      AS ip,
       uniqExact(client_ip)                AS distinct_ips,
       count()                             AS requests
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
  AND timestamp >= now64(9) - INTERVAL 10 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 10 MINUTE)) - 1800
  AND kind = 2
GROUP BY asn, as_org
HAVING ip != '' AND asn != '' AND distinct_ips >= 20 AND requests >= 2000
```

> The per-ASN query is the **distributed-flood / botnet workhorse**: low per-IP volume rolls up to
> a high aggregate inside one hosting/datacenter ASN. Remediate the **ASN** (challenge or block
> the org's range), not single IPs. Use `client.as_org` in the notification so a human can tell a
> datacenter botnet from a legitimate corporate/ISP NAT.

**Traffic-based — expensive-endpoint amplification / 5xx onset (one IP hammering a heavy route):**

```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,
       countIf(response_status_code LIKE '5%') AS server_errors
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
  AND timestamp >= now64(9) - INTERVAL 10 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 10 MINUTE)) - 1800
  AND kind = 2
  AND (attributes_string['http.target'] LIKE '/api/search%' OR attributes_string['http.target'] LIKE '/api/report%' OR attributes_string['http.target'] LIKE '/api/export%')
GROUP BY ip
HAVING ip != '' AND (requests >= 200 OR server_errors >= 30)
```

**Traffic-based — scraping / API mapping (one IP, many distinct paths, high read volume):**

```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,
       uniqExact(attributes_string['http.target']) AS distinct_paths,
       count() AS requests
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
GROUP BY ip
HAVING ip != '' AND distinct_paths >= 80 AND requests >= 400
```

**Traffic-based — enumeration / brute-force volume (one IP, many client errors on auth/probe routes):**

```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 attempts,
       countIf(response_status_code IN ('400','401','403','404','422')) AS client_errors
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
  AND timestamp >= now64(9) - INTERVAL 10 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 10 MINUTE)) - 1800
  AND kind = 2
  AND (attributes_string['http.target'] LIKE '/api/login%' OR attributes_string['http.target'] LIKE '/api/auth%' OR attributes_string['http.target'] LIKE '/api/password%' OR attributes_string['http.target'] LIKE '/api/coupon%')
GROUP BY ip
HAVING ip != '' AND client_errors >= 50
```

> This is the **traffic shape** of credential stuffing / brute force / code enumeration. It
> *contains* the source; the **mechanics** (lockout, MFA) are owned by the
> [authentication model](../01-authentication/) and the code-enumeration *impact* by the
> [payment & business-logic model](../09-payment-business-logic/). Cross-reference, don't
> re-derive.

**Events-based — app rate-limit ceiling repeatedly hit (API4; query the logs table):**

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

> If your limiter returns **429 in traced traffic**, you can detect the same ceiling-abuse from
> the **traffic** table instead by counting `response_status_code = '429'` per IP (same shape as
> the flood query, swap the `HAVING`/filter). Use the events table only when the rejection happens
> before a traced span.

**Events-based — farming / sensitive-flow abuse (one IP/user running a flow at machine speed):**

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

> `api.sensitive.flow` catches a flood of **successful** farming (signup/referral/coupon) that a
> 4xx/flood rule misses because every request returns 200. Pair with the per-ASN query to catch
> distributed farming. The **economic impact** is owned by the
> [payment & business-logic model](../09-payment-business-logic/) — here you detect the *rate*.

Useful attributes/columns: `event.type`, `http.client_ip`, `http.target`,
`response_status_code`, `kind`, `client.asn`, `client.as_org`, and your abuse attributes
(`route`, `scope`, `flow`, `limit`, `window`).

**Every detection is a ready-to-copy command unit.** Never emit a SQL fragment on its own. For each
rule, emit — in order, each as its own fenced block so it copies cleanly — (1) the SQL with a
`-- rules/<name>.sql` header comment, (2) a 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; saving the SQL to `rules/<name>.sql` is what makes
`--sql @rules/<name>.sql` work. Note pre-existing/system rules instead of duplicating them.

```sql
-- rules/abuse-flood-single-ip.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
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
GROUP BY ip HAVING ip != '' AND requests >= 600
```

```bash
# save the SQL so --sql @file works:
cat > rules/abuse-flood-single-ip.sql <<'SQL'
... paste the SQL above ...
SQL
```

```bash
securenow alerts rules create \
  --name "Abuse: volumetric flood (single IP)" \
  --sql @rules/abuse-flood-single-ip.sql \
  --apps <APP_KEY> \
  --severity high \
  --schedule "*/5 * * * *" \
  --nlp "single IP making 600+ requests in 5 minutes"
```

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

Injection-class rows (SQLi/XSS/RCE payloads riding the flood) reference the **system signature
rules + `instant.block`** instead of duplicating their SQL.

#### 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 (flood / scrape /
enumeration counts), broad patterns, anomaly / volume rules, anything tuned to YOUR traffic — 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, then
`--mode prod` to arm mitigation. Only **high-precision** rules (exploit-signature SQLi/XSS/RCE
matches, exact-match IoCs, known-bad ASN hits) may go straight to `prod`. In the report, **tag each
rule `test-first` or `prod-ready`** and say why — for this domain almost every flood / scrape /
enumeration threshold is **`test-first`** (it's tuned to your traffic), and only signature /
exact-IoC rows are `prod-ready`. (`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 abuse, SecureNow **contains the actor at the edge**; for missing-limit / oracle rows the
**app-config fix** (the app's own per-client quota) removes the underlying weakness. Always pair
them on those rows. This domain uses **every** SecureNow containment layer — it's the home turf.

This is the **full SecureNow mitigation toolbox**. Once a threat is confirmed, **choose the
narrowest effective mitigation(s) from ALL of these** and combine them (e.g. rate-limit a route +
block the worst IPs + challenge a NAT egress). 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 users.

| # | 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 /x --method GET` | 500k+ known-bad IPs, hourly refresh; drop scanners before the app. 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 the matching request (payload injection). Don't duplicate pattern SQL. |
| 3 | **IP block — global** | `securenow blocklist add <ip> --app <APP_KEY> --env production --reason "..."` | confirmed-malicious source, all routes. |
| 4 | **IP block — scoped to route (+ method)** | `securenow blocklist add <ip> --route /admin* --mode prefix --method ALL --app <APP_KEY> --env production --reason "..."` (`--mode exact\|prefix\|regex`, `--method GET\|POST\|…\|ALL`) | block an IP only on sensitive 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; audit-preserving unblock. |
| 6 | **Rate limit — per IP** | `securenow ratelimit add <ip> --limit 100 --window 1m --duration 24h --reason "..."` | throttle one abusive client across the app. |
| 7 | **Rate limit — per route (all clients, per-IP budget)** | `securenow ratelimit add --route /api/search --mode prefix --method GET --limit 60 --window 1m --key-by ip` | cap an expensive/abusable endpoint for everyone, budgeted per IP. |
| 8 | **Rate limit — per route + IP** | `securenow ratelimit add <ip> --route /api/login --mode exact --method POST --limit 5 --window 1m --duration 24h` · NL `securenow ratelimit from-text "rate limit /api/login to 5/min for 24h" --yes` · test `securenow ratelimit test <ip> --path /api/login --method POST` | precise throttle of one client on one 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. Prefer over a hard block when real users share the IP. |
| 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; 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 "Office VPN / partner / monitor"` | stop false positives from known-good infra — suppresses detection **and** mitigation. 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 app. |
| 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 without weakening it. |
| 15 | **App / config / code fix (primary for root cause)** | *described in the Code-Findings report, never auto-applied* | the actual fix (validation, authz, CORS, headers, SSRF allowlist, idempotency, retire route, IAM). SecureNow contains; the fix removes. |

**Choosing per threat** — by **confidence**: exploit-signature/exact IoC → instant-block or block;
probable bot on shared egress → **challenge**; noisy/legit-mixed traffic → **rate-limit (test-mode
first)**; session compromise → **revoke**; known-good noise → **trusted / fp**. By **blast
radius**: always scope to the narrowest `route`/`method`/`IP`/`duration` that stops the abuse; 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) when SecureNow can only contain the actor. This
domain leans on the per-IP / per-route / per-ASN nuance hardest: single confirmed IP → rate-limit
then block; datacenter/hosting-ASN botnet (low per-IP, high per-ASN) → block/challenge the
ASN/range (`--mode prefix`) and let auto-block take the risk-scored ones; shared/NAT/CGNAT egress
→ challenge, never a hard block; missing app limit → SecureNow rate-limit/challenge is interim
containment while the app-config quota is the primary fix.

### 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 app rate-limit ceiling — exercise the events-based ceiling rule end to end:
for i in $(seq 1 60); do
  securenow event send api.ratelimit.exceeded --ip 203.0.113.50 \
    --attrs route=/api/search,limit=100,window=1m,scope=per_ip,test=true
done

# Synthetic farming — exercise the sensitive-flow abuse rule:
for i in $(seq 1 40); do
  securenow event send api.sensitive.flow --ip 203.0.113.51 \
    --attrs flow=signup,items=1,test=true
done

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

# Traffic-based rules (flood / scraping / per-ASN / 5xx) — generate spans, then check pipeline:
securenow test-span "threat-model.abuse.smoke"
securenow forensics "top IPs and ASNs by request count and 4xx/5xx in the last hour" --env production

# Firewall membership / containment check for a source:
securenow firewall test-ip 203.0.113.50 --app <APP_KEY> --env production

# Mitigation verification — confirm the actor is contained by each layer:
securenow ratelimit test 203.0.113.50 --path /api/search --method GET
securenow challenge test 203.0.113.50 --path /api/search --method GET

# 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>
securenow ratelimit list      # then: securenow ratelimit remove <id> (or let it auto-expire)
```

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 — rate-limit
applied, challenge served, IP/ASN blocked).

---

## Phase 5 — Write the FOUR deliverables (two tracks × Markdown + HTML)

Write **four** files into `threat/12-rate-limits-and-abuse/` — two **tracks**, each as Markdown and
a self-contained HTML page. The **Detection & Mitigation** track is the operational runbook (what
to run in SecureNow); the **Code Findings & Recommendations** track is the code audit (issues +
recommended fixes, never applied). The two tracks **cross-link**: the gaps/instrumentation rows in
the Detection report link to the relevant code finding, and the app-config / instrumentation fixes
in the Code Findings report link back to the detection row they back.

1. `rate-limits-and-abuse-detection-mitigation.md`
2. `rate-limits-and-abuse-detection-mitigation.html`
3. `rate-limits-and-abuse-code-findings.md`
4. `rate-limits-and-abuse-code-findings.html`

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

1. **Executive summary** — stats line (N threats modeled · N covered · N partial · N gaps · N rules
   to create · N mitigations), the **top 3 detectable abuse risks** for this stack (e.g. "single-IP
   flood on `/api/search`", "datacenter-ASN distributed flood", "signup farming"), the installed
   `securenow` version (from Phase 0.5), the app key, and the firewall state. One-line OWASP note
   (API4 + API6 owned here; business-flow/auth/upload/API-resource flaws deferred to linked models).
2. **SDK & environment** — installed SDK version (from `node_modules/securenow`), app key(s),
   environment, firewall state, existing rate-limit / challenge rules / automations (from Phase 0),
   and any **system signature rules** present (SQLi/XSS/RCE) you will reference for `instant.block`.
3. **Threat → Detection → Mitigation matrix** — one row per modeled threat:
   `# | Threat | OWASP | 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 §4c toolbox (e.g. "rate-limit `/api/search` per-IP 60/1m + block worst
   IPs", "challenge the NAT ASN", "auto-block ≥95") — never a generic "block the IP". The **Mode**
   cell tags each rule **`test-first`** or **`prod-ready`** (flood / scrape / enumeration thresholds
   are usually `test-first`; signature / exact-IoC rows are `prod-ready`). Then the **"Out of scope"**
   N/A list (every catalog item A1–M55 accounted for) and the **deferred** rows (52–55) pointing to
   the sibling models.
4. **Detection rules to create** — each as the **ready-to-copy command unit** from Phase 4
   (SQL with `-- rules/<name>.sql` header → save to `rules/<name>.sql` → full
   `securenow alerts rules create …` → `securenow alerts rules test <RULE_ID> --mode dry_run --wait`).
   Cover at minimum: single-IP flood, **per-ASN** distributed flood, expensive-endpoint
   amplification, scraping/distinct-paths, enumeration/brute-force volume, 429-ceiling (events or
   traffic), farming/sensitive-flow. Injection-class rows reference the **system signature rules +
   `instant.block`**, not duplicate SQL. Note rules that already exist (Phase 0) instead of cloning.
   **Mark each rule `test-first` or `prod-ready`**; for every `test-first` rule (flood / scrape /
   enumeration threshold — the bulk of this domain) include the promotion step
   `securenow alerts rules update <RULE_ID> --mode test` (ship detect-only) → observe 3–7 days /
   tune / add `fp` exclusions → `securenow alerts rules update <RULE_ID> --mode prod` (arm
   mitigation). Only `prod-ready` rules (signature / exact-IoC) go straight to `--mode prod`.
5. **Instrumentation the detections need** — only the `track('…')` events the rules above consume
   (`api.ratelimit.exceeded`, `api.sensitive.flow`), each as a copyable snippet; for *where* to add
   them, link to the **Instrumentation recommendations** section of the code-findings report.
6. **Mitigation mechanisms** — render the **full 15-row §4c mitigation toolbox table** (firewall ·
   exploit-signature instant-block · IP block [global / route+method / temporary] · rate-limit
   [per-IP / per-route / per-route+IP] · challenge · auto-block · session revocation · trusted ·
   allowlist · fp exclusion · **app-config quota fix**) + the "Choosing per threat" guidance, then a
   per-threat **ready-to-copy** mitigation command + reversibility. Make explicit that the
   **app-config quota fix is primary** for the missing-limit / oracle / paid-downstream rows
   (SecureNow is **containment**), and that **challenge** is preferred over block on shared/NAT/CGNAT
   egress. Include `securenow ratelimit test …` / `securenow challenge test …` verification.
7. **Action plan (copy-paste, ordered)** — ① firewall + signature `instant.block`, ② add the small
   amount of app-quota instrumentation where the limiter answers before a traced span, ③ create the
   rules — **the FP-prone (`test-first`) flood / scrape / enumeration rules in `--mode test`
   (detect-only), the `prod-ready` signature / exact-IoC rules in `--mode prod`** — ④ enable
   `automation defaults` + challenge rules on signup/login/checkout/shared-egress, ⑤ test, ⑥ verify
   in dashboard, ⑦ **promote each `test-first` rule with `securenow alerts rules update <RULE_ID>
   --mode prod` after N days (3–7) of clean observation + threshold tuning + `fp` exclusions**,
   ⑧ schedule the app-config quota / IP-trust / oracle fixes (from the code report). Real commands
   only, `<APP_KEY>` already substituted.
8. **Testing & validation** — per-rule recipe (Phase 4d): `securenow event send …` / `test-span` /
   dry-run + expected outcome + cleanup (TEST-NET IPs `192.0.2.0/24` / `198.51.100.0/24` /
   `203.0.113.0/24`). Every 🟢/🟡 row gets one.
9. **Response runbooks** — per notification type (single-IP flood, per-ASN distributed flood,
   scraping, enumeration/brute-force volume, 429-ceiling, farming): what fired, how to confirm a
   true positive (and how to spot a legit spike / NAT egress), the exact respond command (copy —
   rate-limit / challenge / block, with **shared-egress → challenge** called out), and the exact
   reverse command (copy).
10. **Known gaps & SecureNow feature requests** — every 🔴 threat: why it's not coverable today,
    the interim app/config fix (link to the code-findings report), and the line *"Requires SecureNow
    team — contact your SecureNow account contact (or in-dashboard support) to request support for
    this threat."*
11. **Appendix** — resolved SDK/CLI version (`securenow --version` / `securenow env`), app key,
    environment, firewall state, automation defaults state, rule IDs created, date.

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

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

1. **Executive summary** — findings by severity (critical / high / medium / low), the **top 3 code
   risks** for this stack (e.g. "no rate limit on any route", "client IP spoofable", "OTP send
   uncapped"), and a one-paragraph posture verdict.
2. **Abusable surface & inventory** — the Phase 1 inventory: endpoint catalog (with cost /
   paid-downstream tags) + rate-limit/quota controls table (no-limit rows flagged) + sensitive /
   farmable flow list + enumeration/oracle list + client-IP-trust & origin-exposure posture +
   bot/anti-automation posture + pagination/streaming notes.
3. **Threat catalog** — the exhaustive Phase 2 catalog (A1–M55, grouped), each tagged
   **API4 / API6 / "—"**, modeled or explicit N/A. Nothing silently dropped.
4. **Code-level findings (audit)** — a table
   `# | Location (file:line) | Threat | OWASP | Sev | Issue | Recommended fix`, each with the quoted
   1–8 line snippet and the described fix (never applied). The missing-rate-limit and spoofable-
   client-IP findings are the high-value output of this model.
5. **Strengths** — controls already present and correct (sensible per-IP + per-user limits, 429 +
   `Retry-After`, capped pagination, generic auth responses, challenge on signup) — honest posture.
6. **App / config fixes (primary remediation)** — the config/code changes that remove the root
   cause (described, not applied): per-IP **and** per-user limits + 429/`Retry-After`, capped
   pagination, trusted-proxy CIDRs for client-IP derivation, generic + constant-time auth/
   availability responses, capped paid-downstream sends, origin locked to the CDN/WAF range. **Link
   each to the detection-report row it backs.**
7. **Instrumentation recommendations** — the `track('…')` calls to add
   (`api.ratelimit.exceeded` / `api.sensitive.flow`) and the exact `file:line` to add them, so the
   detection rules in 5a light up. Link to the Detection report's "Instrumentation the detections
   need" section.
8. **Appendix** — files reviewed, resolved SDK version, date, and a link to the
   detection-mitigation report.

### 5c. HTML skeletons — two self-contained pages (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 `.sub` subtitle, and the
section content — do not restyle. Use one body per track. Wrap **every** command/SQL block as a
`.cmd` (so it gets a Copy button). Keep this domain's title: **"Rate Limits & Abuse"**.

**Shared `<head>` + copy `<script>` (identical in both files):**

```html
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
<title><!-- "Detection & Mitigation — Rate Limits & Abuse — SecureNow" OR "Code Findings — Rate Limits & Abuse — 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>
<!-- ===== one of the two bodies below ===== -->
<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>
```

**Body A — Detection & Mitigation** (`rate-limits-and-abuse-detection-mitigation.html`): subtitle
`Detection & Mitigation · Rate Limits & Abuse`; stats = `threats modeled · covered · partial · gaps
· rules to create`; one `<section id="…">` per 5a section. **Every** SQL/command block is a `.cmd`
with a Copy button.

```html
<div class="wrap">
  <nav>
    <div class="brand">Secure<span>Now</span></div>
    <div class="sub">Detection &amp; Mitigation · Rate Limits &amp; Abuse</div>
    <!-- one <a href="#…"> per 5a section -->
  </nav>
  <main>
    <header class="top"><h1>Rate Limits &amp; Abuse — Detection &amp; Mitigation</h1>
      <p><code><!-- app name / domain --></code> · <span class="pill">securenow <!-- installed version --></span></p></header>
    <div class="stats">
      <div class="stat"><div class="n"><!-- N --></div><div class="l">threats modeled</div></div>
      <div class="stat"><div class="n" style="color:var(--ok)"><!-- N --></div><div class="l">covered</div></div>
      <div class="stat"><div class="n" style="color:var(--med)"><!-- N --></div><div class="l">partial</div></div>
      <div class="stat"><div class="n" style="color:var(--crit)"><!-- N --></div><div class="l">gaps — SecureNow team</div></div>
      <div class="stat"><div class="n" style="color:var(--accent)"><!-- N --></div><div class="l">rules to create</div></div>
    </div>
    <!-- <section id="…"> blocks mirroring the 5a Markdown sections.
         Wrap each command/SQL as: -->
    <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>
    <footer>Generated by the SecureNow rate-limits &amp; abuse threat-model prompt · <!-- date --> · securenow <!-- version --> · app <code><!-- APP_KEY --></code></footer>
  </main>
</div>
```

**Body B — Code Findings** (`rate-limits-and-abuse-code-findings.html`): subtitle
`Code Findings · Rate Limits & Abuse`; stats = `critical · high · medium · low · total findings`;
one `<section id="…">` per 5b section. Prose may omit copy buttons, but every example/fix command
is still wrapped in `.cmd`.

```html
<div class="wrap">
  <nav>
    <div class="brand">Secure<span>Now</span></div>
    <div class="sub">Code Findings · Rate Limits &amp; Abuse</div>
    <!-- one <a href="#…"> per 5b section -->
  </nav>
  <main>
    <header class="top"><h1>Rate Limits &amp; Abuse — Code Findings</h1>
      <p><code><!-- app name / domain --></code> · <span class="pill">securenow <!-- installed version --></span> · Findings only — no code modified</p></header>
    <div class="stats">
      <div class="stat"><div class="n" style="color:var(--crit)"><!-- N --></div><div class="l">critical</div></div>
      <div class="stat"><div class="n" style="color:var(--high)"><!-- N --></div><div class="l">high</div></div>
      <div class="stat"><div class="n" style="color:var(--med)"><!-- N --></div><div class="l">medium</div></div>
      <div class="stat"><div class="n" style="color:var(--low)"><!-- N --></div><div class="l">low</div></div>
      <div class="stat"><div class="n"><!-- N --></div><div class="l">total findings</div></div>
    </div>
    <!-- <section id="…"> blocks mirroring the 5b Markdown sections; wrap any example/fix command in .cmd -->
    <footer>Generated by the SecureNow rate-limits &amp; abuse threat-model prompt · <!-- date --> · securenow <!-- version --> · app <code><!-- APP_KEY --></code></footer>
  </main>
</div>
```

Badge usage (both files): severity → `<span class="b crit|high|med|low">`, coverage →
`<span class="c cov|part|gap">COVERED|PARTIAL|GAP</span>`, OWASP tag →
`<span class="owasp">API4</span>` / `<span class="owasp">API6</span>`, CWE → `<span class="cwe">CWE-770</span>`,
mitigation type → `<span class="m firewall|signature|rate|challenge|block|notify|appfix">`, rule IDs
→ `<span class="rid">`. Stats numbers must equal the matrix/findings row counts. The Detection HTML
must have a working Copy button on every command block.

---

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

- Every catalog item A1–M55 is either a matrix row or an explicit N/A line; each modeled row
  carries its OWASP API Top 10:2023 tag (**API4** / **API6** / "—").
- The full domain emphasis is covered as modeled rows or explicit N/A: volumetric L7 flood (single
  IP), distributed/botnet flood (many IPs in one ASN), expensive-endpoint amplification, automated
  scraping at scale, enumeration via response/timing oracles, credential-stuffing & brute-force
  volume, inventory scalping/hoarding, signup/referral/free-tier farming, missing per-client
  limits (no 429s), 429-ceiling abuse, anti-automation evasion (IP rotation, XFF spoof, header/UA
  rotation, CDN-origin bypass, slowloris), and CAPTCHA/challenge bypass + shared/NAT/CGNAT
  considerations.
- Business-flow *impact*, credential-stuffing/brute-force *mechanics*, upload/download *abuse*, and
  general API resource-consumption *flaws* are **deferred** by NUMBERED path (rows present, linked
  to `../09-payment-business-logic/`, `../01-authentication/`, `../10-file-upload-download/`,
  `../14-api-security/`, not re-derived) — this model does not duplicate them.
- Every matrix row has a concrete signal (**threshold + window**), severity, and mitigation — no
  "monitor for suspicious activity" filler.
- Both **per-IP and per-ASN** aggregation appear in the detection rules (`client.asn` /
  `client.as_org` for distributed/botnet floods).
- Every code finding in section 4 has a `file:line`, the quoted snippet, and a described fix — and
  **no application code was modified** (this is an audit). Missing-rate-limit and spoofable-client-
  IP findings are present where the surface warrants them.
- Every detection SQL keeps `__USER_APP_KEYS__` scoping (correct table column — `resources_string
  ['service.name']` for logs vs `` `resource_string_service$$name` `` for traces) and selects an
  `ip` column; traffic queries keep the `ts_bucket_start` + `kind = 2` guards and the `client_ip`
  coalesce, with `HAVING ip != ''`; rules are created with `--mode dry_run` validation.
- The report states clearly that **most detections are traffic-based with no instrumentation**, and
  that the app must **own the quota** where a per-client limit is missing (SecureNow contains the
  source).
- Only commands, flags, events, and SQL columns from this prompt's building blocks appear
  (including `securenow challenge …`, `firewall`, `ratelimit`, `automation`, and the
  `api.ratelimit.exceeded` / `api.sensitive.flow` events).
- Detection vs. fix is honest: where SecureNow can only contain the actor at the edge (missing
  limit / oracle / paid-downstream), the row pairs the control with the **app-config quota fix**;
  **challenge** is preferred over block on shared/NAT/CGNAT egress.
- 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.
- **Phase 0.5 ran**: the resolved installed `securenow` version appears in the appendix of **both**
  reports, and no command/flag/event/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 → save to `rules/<name>.sql` → full
  `securenow alerts rules create …` → dry-run test); flags match `securenow alerts rules --help`.
- **Four** files are written to `threat/12-rate-limits-and-abuse/`
  (`rate-limits-and-abuse-detection-mitigation.md` + `.html`, `rate-limits-and-abuse-code-findings.md`
  + `.html`); the two tracks **cross-link**; both HTML files are **self-contained** (inline CSS + JS,
  no CDN/fonts/network) and **every command block in the detection HTML has a working Copy button**;
  stats cards match the table/finding counts.
- The split is **honest**: SecureNow-runnable detections/mitigations live in the Detection &
  Mitigation report; code/config changes live in the Code Findings report; nothing security-relevant
  is dropped.
- 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**.
- The Detection report's mitigation section presents the **full toolbox** (§4c: 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.

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