#13API9 · API8
Legacy Endpoints
Deprecated, shadow, and non-prod routes — inventory drift.
How to use this prompt
- 1Install SecureNow in your project (then optionally
npx securenow login):$ npm install securenow - 2Copy the prompt below and paste it into your AI coding agent (Claude Code, Cursor, Codex…) opened at the root of your project.
- 3It generates four files into
threat/13-legacy-endpoints/— openlegacy-endpoints-code-findings.html(the audit) andlegacy-endpoints-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
# Legacy Endpoints 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 the **stale / forgotten** surface, build
an exhaustive **legacy-endpoint** threat model mapped to the **OWASP API Security Top 10:2023**
(primarily **API9:2023 Improper Inventory Management** and **API8:2023 Security
Misconfiguration**), audit the code for the surface that should have been retired, and emit a
SecureNow-branded **two-track** deliverable set in **Markdown + self-contained HTML** — a
**Detection & Mitigation** runbook (the detection rules to create, the mitigation commands to
run, how to test each one) and a **Code Findings & Recommendations** audit (code-level findings,
audited **not** fixed) — plus which threats still need the SecureNow team. Every rule and command
is **grounded in the SDK actually installed in this repo** and emitted as a **ready-to-copy** unit
(SQL → saved file → full create command → dry-run test).
This is the focused **"what old thing can still be hit, and is it being watched?"** lens. It
owns the **inventory & exposure of stale surface**: zombie/deprecated versions, shadow/undocumented
routes, internet-exposed non-prod hosts, spec drift, old auth schemes left enabled, prior-era
debug/diagnostic/admin endpoints, deprecated routes that lost their current controls, and
forgotten DNS/subdomains pointing at decommissioned hosts. It deliberately does **not** re-derive
the deep API-as-surface model (consumption, injection, SSRF, parsing) — that lives in
[../14-api-security/](../14-api-security/api-security-threat-model-prompt.md). Dangling-DNS /
takeover detail defers to [../21-dns-tls-certificates/](../21-dns-tls-certificates/), and stale
auth-scheme / pre-rotation-key detail defers to [../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/).
> SecureNow is fundamentally an **API / traffic** security layer (firewall, rate-limit,
> challenge, exploit-signature instant-block, ASN enrichment, forensics). For legacy endpoints,
> its single biggest contribution is **drift detection**: it can see *live traffic to
> deprecated/shadow/non-prod/debug paths* (via the `api.deprecated.called` event + path-pattern
> traffic rules) — surfacing the surface the spec/inventory missed. But the **primary fix is to
> retire or gate the route** (an app/config change), paired with edge containment. SecureNow
> tells you the zombie is still being hit and contains the actor; it cannot un-route the zombie
> 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 Legacy Endpoints Threat Model Report (SecureNow)
You are a senior application-security engineer specializing in API inventory and attack-surface
management. Produce an **exhaustive legacy-endpoint threat model for THIS codebase**, organized
along the **OWASP API Security Top 10:2023** with a primary focus on **API9:2023 Improper
Inventory Management** and **API8:2023 Security Misconfiguration**, mapped to **SecureNow**
detections and mitigations, with a ready-to-run action plan **and** a code-level audit of every
piece of stale surface you find. You write **four files** (two tracks × MD + HTML) into
`threat/13-legacy-endpoints/` (create the folder if needed):
1. `legacy-endpoints-detection-mitigation.md` — the **operational runbook**: what to run in
SecureNow (rules to create, mitigations, tests, action plan).
2. `legacy-endpoints-detection-mitigation.html` — the same runbook as a **self-contained** HTML
page (inline CSS + copy JS, no network requests), with a **Copy button on every command block**.
3. `legacy-endpoints-code-findings.md` — the **code audit**: stale-surface issues found in the
codebase + recommendations (findings only — no code modified).
4. `legacy-endpoints-code-findings.html` — the same audit as a **self-contained** HTML page.
The two tracks **cross-link** each other: gaps/instrumentation rows in the detection report link
to the relevant code finding, and each code finding links back to the detection row it backs.
Every rule and command must be **grounded in the installed SDK** (Phase 0.5) and written as a
**ready-to-copy** unit (Phase 4 / §2).
Work in the seven 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 **inventory & exposure of stale surface**: zombie /
deprecated versions (API9), shadow / undocumented routes (API9), internet-exposed non-prod
environments (API9/API8), spec drift (API9), sensitive data still flowing through old /
unmonitored / unauthenticated endpoints (API9), old auth schemes still enabled (API8 — defer the
deep key/rotation model), prior-era debug / diagnostic / admin endpoints (API8), deprecated
routes lacking current controls (API8), and forgotten subdomains / DNS records (API9 — defer the
deep takeover model). Do **not** re-derive these — list them in a "Deferred to sibling models"
subsection and link the reports, modeling only their **traffic-observable** drift symptoms where
SecureNow adds value:
- Deep **API-as-surface** model (consumption / injection / SSRF / parsing / contract / webhook /
upload / streaming / cache) → [../14-api-security/](../14-api-security/api-security-threat-model-prompt.md).
- **Dangling subdomains / DNS records / subdomain takeover / cert drift** →
[../21-dns-tls-certificates/](../21-dns-tls-certificates/).
- **Old auth schemes / legacy tokens / pre-rotation API keys / secret rotation & cloud IAM** →
[../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/).
---
## 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 — those still matter here because the
likeliest place an attacker probes for injection is the *deprecated route that lost its
validation*, and the instant-block signature rules apply to **every** path including the zombies.
---
## 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 surface (codebase analysis)
Legacy-endpoint security *is* the inventory problem (OWASP API9). Document what is **actually
reachable**, not what the spec or the docs claim. Build the inventory along three axes: **what is
declared**, **what is deployed**, and **what is observed in traffic** — the gaps between them are
the threats. Cover at minimum:
- **Declared surface (spec / docs / source of truth)** — locate every OpenAPI/Swagger spec, GraphQL
schema, Postman collection, API reference, gateway/ingress route config, and route table the
team *believes* is the API. This is the baseline the deployed surface drifts from.
- **Deployed surface (route catalog)** — enumerate every route actually mounted (method + path),
grouped by router/controller/handler/gateway. Flag public vs internal vs admin. **Diff this
against the declared surface** — routes in code but not in the spec are **shadow APIs**; routes
in the spec but not in code are stale docs; method/param/field mismatches are **spec drift**.
- **Versions** — enumerate every version prefix (`/v1`, `/v2`, `/v3`, `/beta`, `/legacy`,
`/api-old`, date-stamped variants). For each, determine: is it still **routed/mounted**? Has it
been **superseded**? Is anything **still calling it**? A `/v1` router still `app.use()`d after
`/v2` ships is a **zombie version**.
- **Environments & hosts** — list every environment (prod / staging / dev / preview / qa / demo /
sandbox) and every hostname/subdomain the app serves or references (including `*.internal`,
`*.staging.`, `*.dev.`, `preview-*`, branch/PR preview deploys, `localhost`-style hosts that
leaked to a public DNS record). Flag any **non-prod host that is internet-reachable** and any
host referenced in config/CI/CDN that no longer resolves or points at a decommissioned target.
- **Debug / diagnostic / admin remnants** — `/__debug`, `/_status`, `/actuator`, `/metrics`,
`/health` (verbose), `/test`, `/admin` (legacy), `/internal`, `/swagger`, `/api-docs`,
GraphQL playground/introspection, profiler/heapdump/console routes, sample/scaffold routes,
feature-flag/admin toggles, "temporary" migration or backfill endpoints from a prior era.
- **Old auth schemes still enabled** — Basic auth realms, legacy session cookies, first-gen API
keys, pre-rotation keys still accepted, query-string `?api_key=` / `?token=` auth, HMAC schemes
superseded by OAuth, "service" accounts with static tokens. *Note presence and reachability
here; the deep rotation/IAM model is deferred to ../20-secrets-and-cloud-iam/.*
- **Control parity (current vs deprecated)** — for each deprecated/legacy route, check whether it
still has the controls the *current* routes have: authn/authz middleware, rate limiting, input
validation/schema, output redaction of PII/secrets, security headers, logging. Deprecated
routes commonly **lost** controls when the new version was hardened. Note every route where the
control set is weaker than its successor.
- **Sensitive data over legacy paths** — identify which deprecated/shadow/non-prod/debug routes
return or accept **sensitive data** (PII, tokens, payment, internal IDs, full objects) — and
whether those routes are **unauthenticated** or **unmonitored**. This is the highest-impact
legacy class: a live `/v1/users` that returns the full record with no authz after `/v2`
tightened it.
- **Route normalization in telemetry** — determine whether telemetry captures raw paths
(`/v1/orders/123`) or normalized templates (`/v1/orders/:id`). For drift detection you need
the **path prefix/pattern** to survive normalization — confirm `attributes_string['http.target']`
carries enough of the path to match `/v1/%`, `%/internal%`, `%/debug%`, etc.
- **Forgotten DNS / subdomains** — collect hostnames from DNS config, CDN/ingress, env files, CI,
and `CNAME`/`A` records the team controls; flag any pointing at a decommissioned or
third-party-takeoverable target. *Note candidates here; the deep dangling-DNS / takeover model
is deferred to ../21-dns-tls-certificates/.*
- **SecureNow instrumentation already present** — `securenow/register` / `securenow run` /
`securenow init` (gives traffic spans automatically — this is what makes drift detection
possible), any existing `securenow/events` `track()` calls (especially `api.deprecated.called`),
and whether the firewall is engaged. This determines what works *today* vs *after
instrumentation*.
- **Telemetry privacy & redaction** — confirm the SDK/log pipeline redacts `Authorization`,
`Cookie`, `X-API-Key`, bearer tokens, legacy `?api_key=` query values, and PII **before
ingestion** — legacy routes are the most likely to leak secrets into telemetry. If not found,
create a high-severity finding.
Output of this phase = the report's **Legacy surface & inventory** section: the **declared vs
deployed vs observed** three-way diff, the version table (prefix / mounted? / superseded-by /
still-called?), the environment & host table (env / hostname / internet-reachable? / resolves? /
points-at), the **debug/diagnostic/admin remnant list**, the **old-auth-scheme list**, the
**control-parity table** (route / has-authz / has-ratelimit / has-validation / has-redaction vs
its successor), the **sensitive-data-over-legacy** list, the **forgotten-DNS candidate list**,
the telemetry redaction status, and a short paragraph naming the real legacy 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. "GraphQL legacy schema: N/A, REST-only API"). 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 (almost all are **API9** or
**API8**; tag "—" only when nothing fits).
**A. Zombie / deprecated versions still routed (OWASP API9)**
1. `/v1` (or older) router still mounted/`app.use()`d after `/v2` shipped
2. Deprecated version still receiving live traffic (clients never migrated)
3. Deprecated version reachable but **missing controls** the current version added (authz, validation, rate-limit)
4. Multiple overlapping versions where the oldest is the least-hardened path of least resistance
5. Beta / preview / experimental routes left mounted in prod long after the experiment ended
6. Date-stamped or hash-stamped legacy route variants still resolvable (`/api/2021-…`, build-tagged paths)
**B. Shadow APIs — undocumented endpoints live in prod (OWASP API9)**
7. Route mounted in code but absent from the OpenAPI/GraphQL spec / docs
8. Internal/service-to-service route reachable from the public internet (no gateway gate)
9. "Temporary" migration / backfill / one-off admin route never removed
10. Auto-generated / scaffolded routes (CRUD generators, admin scaffolds) left exposed
11. Routes added by a dependency/framework/plugin the team didn't realize were public
12. Debug-only or feature-flagged routes that ship enabled when the flag defaults wrong
**C. Non-prod environments internet-exposed (OWASP API9 / API8)**
13. Staging / dev / preview / qa / sandbox host publicly reachable
14. `*.internal` / `*.staging.` / `preview-*` / branch-deploy host resolvable from the internet
15. Non-prod host pointing at **prod data** (shared DB / shared backend) reachable externally
16. Non-prod host with debug mode / verbose errors / relaxed auth on the public internet
17. Non-prod environment lacking the firewall / WAF / rate-limit that prod has
**D. Spec drift — documented ≠ deployed (OWASP API9)**
18. Deployed route accepts **params/fields not in the spec** (extra inputs reaching old code)
19. Deployed route accepts **methods not in the spec** (e.g. spec says GET, code allows PUT/DELETE)
20. Response returns **fields not in the spec** (over-exposure the contract doesn't declare)
21. Spec declares a route the code no longer serves (stale docs → false sense of inventory)
22. Gateway/ingress routes a path the app no longer expects (orphaned upstream mapping)
**E. Sensitive data through old / unmonitored / unauthenticated endpoints (OWASP API9)**
23. Legacy read endpoint returning full records / PII / tokens with no authz
24. Legacy write endpoint accepting state changes without the current authz/validation
25. Old export / report / bulk endpoint still live and unmonitored
26. Legacy endpoint not covered by current logging/telemetry (blind spot — abuse invisible)
27. Old unauthenticated endpoint exposing data that the current authenticated route protects
**F. Old auth schemes left enabled (OWASP API8 — defer deep key/rotation to ../20-secrets-and-cloud-iam/)**
28. Basic auth realm still accepted alongside the new scheme
29. First-generation / pre-rotation API keys still honored
30. Query-string auth (`?api_key=`, `?token=`) still accepted (leaks into logs/Referer/telemetry)
31. Legacy session cookie / token format still validated
32. Static service-account token with no expiry still accepted on a legacy path
**G. Debug / diagnostic / admin endpoints from a prior era (OWASP API8)**
33. `/actuator` / `/metrics` / `/_status` / heapdump / profiler reachable in prod
34. Swagger / OpenAPI UI / GraphQL playground / introspection enabled in prod
35. Legacy admin console / migration UI / "god mode" route still mounted
36. Verbose-error / stack-trace / version-banner exposure on a prior-era route
37. Sample / scaffold / default credentials route reachable in prod
**H. Deprecated routes lacking current security controls (OWASP API8)**
38. Deprecated route with **no rate limit** while the current route has one
39. Deprecated route with **no input validation / schema** while the current route validates
40. Deprecated route with **no output redaction** (returns PII/secrets the new route strips)
41. Deprecated route missing security headers / CORS hardening the current route has
42. Deprecated route bypassing the current authz middleware (mounted before/outside the guard)
**I. Forgotten subdomains / DNS records (OWASP API9 — defer takeover detail to ../21-dns-tls-certificates/)**
43. DNS record / subdomain pointing at a decommissioned host (dangling)
44. CNAME pointing at a deprovisioned third-party (S3/Heroku/Netlify/etc.) → takeoverable
45. Old host referenced in app/CI/CDN config that no longer resolves or is reassigned
46. Legacy API hostname (`api-old.`, `legacy.`, `v1.`) still resolving and serving the zombie
**J. Negative-space & evasion (drift surfacing through abnormal traffic)**
47. Path/encoding normalization reaching a zombie route that the WAF/rate-limit doesn't recognize (`/V1/`, `%2f`, double-encoding, trailing slash, matrix params)
48. Direct-origin / direct-IP access reaching a non-prod or internal host that bypasses the CDN/WAF
49. Method-override (`X-HTTP-Method-Override`) reaching a deprecated verb on a legacy route
50. Client-IP header spoofing (`X-Forwarded-For`) to evade per-IP limits while probing legacy paths
**K. Observable abuse (what telemetry actually catches — the workhorse rules)**
51. Live traffic hitting a **deprecated version** path (`/v1/%` after `/v2` is current) — inventory drift surfacing
52. Live traffic hitting a **shadow / non-prod / internal** path pattern (`%/internal%`, `%staging%`, `%/debug%`)
53. Live traffic hitting a **debug / diagnostic** path pattern (`%/actuator%`, `%/metrics%`, `%/swagger%`, `%/__debug%`)
54. 4xx/5xx spike on legacy paths from one IP/ASN (scanning for forgotten endpoints)
55. One IP touching an anomalous count of **distinct legacy paths** (mapping the old surface)
56. Exploit-signature match on a legacy path (SQLi/XSS/RCE probing the unhardened zombie) → instant block
57. Anomalous response sizes from a single client on a legacy read endpoint (bulk extraction via the old path)
58. New high-volume source suddenly hitting a previously-quiet legacy path (onset of zombie abuse)
**L. Deferred — modeled in sibling models (reference, do not re-derive)**
59. Deep API-as-surface model — consumption (API4), injection (C), SSRF (API7), parsing, contract/webhook/upload/streaming/cache → [../14-api-security/](../14-api-security/api-security-threat-model-prompt.md)
60. Dangling-DNS / subdomain-takeover / cert drift deep model → [../21-dns-tls-certificates/](../21-dns-tls-certificates/)
61. Old auth schemes / legacy tokens / pre-rotation key & secret-rotation / cloud-IAM deep model → [../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/)
> For 59–61, add **one** matrix row each marked *"deferred — see linked model"*, and only note
> the SecureNow traffic-observable symptom here (e.g. live traffic to a legacy path, 401/403
> spikes on an old auth scheme, requests to a dangling host pattern). The full
> detection/mitigation lives in those reports.
> **OWASP mapping:** items A1–A6, B7–B12, C13–C17, D18–D22, E23–E27, I43–I46, K51–K58 are
> **API9:2023 Improper Inventory Management**; F28–F32, G33–G37, H38–H42, C16–C17 are
> **API8:2023 Security Misconfiguration**; J47–J50 are API8/API9 evasion of inventory & control
> drift. Tag each row accordingly; use "—" only when neither fits. Each item must be either a
> matrix row 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/router/middleware/handler/config name.
- **Pattern** — quote the 1–8 relevant lines. State the missing control or stale mount precisely
(e.g. "`app.use('/v1', v1Router)` still mounted after `/v2` ships → zombie version";
"`/internal/*` router has no gateway gate → shadow API public"; "`STAGING_HOST` resolves
publicly with `DEBUG=true`"; "`/v1/users` returns full record, no `requireAuth` → sensitive
data unauthenticated"; "`basicAuth()` still accepted on `/v1` after OAuth migration";
"`/actuator` not disabled in prod profile"; "`?api_key=` query auth still honored").
- **Why exploitable** — the concrete request an attacker sends and what they achieve (e.g. "GET
`/v1/users/123` with no token returns the full PII record the `/v2` route now gates").
- **Severity** — critical / high / medium / low (impact × reachability). Unauthenticated
sensitive-data legacy routes and internet-exposed non-prod-on-prod-data are typically
critical/high.
- **Recommended fix (described, not applied)** — the specific change: e.g. "unmount the `/v1`
router or gate it behind the gateway with deprecation headers and a sunset date"; "move
`/internal/*` behind the service mesh / private network, deny at the edge"; "make non-prod
hosts require VPN/IP allowlist and disable debug mode"; "apply the current `requireAuth` +
validation + redaction middleware to the legacy route, or retire it"; "disable Basic auth and
reject pre-rotation keys"; "set the prod profile to disable `/actuator`, swagger, and
introspection"; "drop query-string auth and require `Authorization` header". Reference the
secure pattern, not a code diff. **You must not edit the codebase.**
If a control exists and is correct (old versions are gated behind the gateway with sunset
headers, non-prod is VPN-only, introspection is disabled in prod, deprecated routes share the
current middleware stack), note it as a **strength** — the posture must be honest. **Absence of
a control where stale surface exists is itself a finding** ("`/v1` router still mounted with no
deprecation gate"; "no telemetry on the legacy export route → abuse would be invisible").
Audit specifically for:
**Zombie/version flaws** — routers/handlers for old versions still mounted; conditional version
routing that never disables old branches; gateway/ingress configs still mapping deprecated
prefixes; clients/SDKs still pinned to old versions in the repo. *Recommended fixes must mention*
unmounting or gating old versions, deprecation/`Sunset` headers, gateway-level retirement, and a
documented sunset date.
**Shadow / non-prod flaws** — `/internal`, `/admin`, `/debug`, `/test` routers with no auth gate;
internal routes mounted on the same public listener; non-prod hosts with public DNS and no IP
allowlist; debug/feature-flag routes whose default ships enabled; scaffold/generator routes.
*Recommended fixes must mention* private-network/mesh isolation, edge deny rules, IP/VPN
allowlists for non-prod, removing scaffold routes, and flag-default hardening.
**Spec-drift flaws** — handlers accepting params/methods/fields not in the spec; routes missing
from the spec; gateway mappings with no matching handler; over-returning DTOs not matching the
declared response. *Recommended fixes must mention* regenerating the spec from code (or vice
versa), rejecting undeclared params/methods, response-DTO allowlisting, and CI spec-vs-route
diffing.
**Sensitive-data-over-legacy flaws** — legacy read/write/export routes returning or accepting
sensitive data without the current authz/validation/redaction; legacy routes not covered by
logging/telemetry. *Recommended fixes must mention* applying the current authz + redaction
middleware, retiring the route, and instrumenting it (so abuse is at least visible).
**Old-auth-scheme flaws** — Basic auth, legacy cookie/token formats, query-string auth, static
service tokens, pre-rotation keys still accepted. *Recommended fixes must mention* disabling the
legacy scheme, rejecting pre-rotation keys, moving auth to the `Authorization` header, and
deferring the deep rotation/IAM work to ../20-secrets-and-cloud-iam/.
**Debug/diagnostic/admin remnants** — `/actuator`/`/metrics`/heapdump/profiler/swagger/
playground/introspection/legacy-admin reachable in prod; verbose errors; default creds.
*Recommended fixes must mention* disabling in the prod profile, gating behind admin auth +
network restriction, and removing sample/default routes.
**Control-parity flaws** — deprecated routes lacking the rate-limit / validation / redaction /
headers / authz that their successors have, especially routes mounted before/outside the current
middleware chain. *Recommended fixes must mention* moving legacy routes inside the current
middleware stack or retiring them.
**Forgotten-DNS candidates** — hostnames in config/CI/CDN pointing at decommissioned or
third-party targets. *Recommended fixes must mention* removing the dangling record, reclaiming or
re-pointing the host, and deferring the deep takeover audit to ../21-dns-tls-certificates/.
(Inventory from config/DNS/manifests — **do not perform live takeover testing**.)
---
## 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. For
this domain that means: **live traffic to deprecated / shadow / non-prod / debug path patterns**
(path-pattern traffic rules + the `api.deprecated.called` event), 4xx/5xx and distinct-path
scanning of legacy surface, and exploit-signature matches on legacy paths (system signature
rules + `instant.block`). This is SecureNow's genuine contribution to inventory management.
- 🟡 **PARTIAL** — works after the customer adds the `api.deprecated.called` instrumentation at the
legacy route, **or** SecureNow can only *surface drift / contain the abuser at the edge* while
the **real fix is to retire/gate the route (app/config)**. Most legacy rows are 🟡: SecureNow
tells you the zombie is alive and stops the actor, but the route must be unmounted by the app.
- 🔴 **GAP** — SecureNow cannot detect or mitigate this from traffic/events today (e.g. a
deprecated route nobody has hit *yet*, a dangling DNS record with no traffic, an old auth scheme
no one has exercised, spec drift in fields that don't appear in path telemetry). **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**; it contains actors via firewall / rate-limit / challenge / block / signature
> instant-block, and it surfaces **drift** by matching path patterns in live traffic. It
> **cannot** see a route that nobody hits, un-route a zombie version, take down a non-prod host,
> disable Basic auth, or remove a dangling DNS record — those are **app/config fixes**. SecureNow
> detects the *abuse* (the scan of the old surface, the live hit on a deprecated path, the
> injection probe against the unhardened zombie) and contains the source; the missing control —
> **retire or gate the route** — is the primary fix. Pair the control with the app fix on every
> legacy row. A stale route that emits no traffic and no event is a 🔴 until either it gets hit or
> the app emits `api.deprecated.called`.
Use **only** the SecureNow building blocks below. Never invent CLI flags, event names, or SQL
columns.
### 4a. Instrumentation (what legacy 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. **4xx/5xx**),
methods, paths, client IPs, response sizes. **Most legacy-endpoint rules need no events** — they
match **path patterns** in live traffic to surface drift the inventory missed.
Add `securenow/events` `track()` only for the high-signal app-internal case where you want an
explicit, unambiguous marker that a deprecated route ran (never throws):
```js
const { track } = require('securenow/events');
// A deprecated/legacy endpoint or version was hit (API9 inventory drift) — the workhorse event:
track('api.deprecated.called', { ip, attributes: { route: '/v1/orders', superseded_by: '/v2/orders' } });
// A shadow / non-prod / internal route was reached (mark it where you detect it server-side):
track('api.deprecated.called', { ip, attributes: { route: '/internal/export', superseded_by: 'none', kind: 'shadow' } });
// An old auth scheme was used to authenticate (API8) — surfaces which clients still depend on it:
track('api.deprecated.called', { ip, attributes: { route: '/v1/login', superseded_by: '/v2/login', kind: 'legacy_auth', scheme: 'basic' } });
```
> Reuse the **`api.deprecated.called`** event for every legacy class — version, shadow, non-prod,
> legacy-auth — and disambiguate with the `kind` / `superseded_by` / `scheme` attributes. Do not
> invent new event names for this domain.
>
> Hash or omit any PII before it becomes an attribute value (emails, raw tokens, query-string
> keys) — see the Phase 1 **telemetry privacy & redaction** check. Legacy routes are the most
> likely to leak secrets; attributes must not become a new leak path.
Recommended event taxonomy — rules match these **exact strings**:
| Event | Emit when |
|---|---|
| `api.deprecated.called` | a deprecated endpoint, deprecated API version, shadow/internal route, or legacy auth scheme is invoked (disambiguate with `kind` / `superseded_by` / `scheme`) |
Custom `attributes` become queryable as `attributes_string['<key>']` (e.g.
`attributes_string['superseded_by']`, `attributes_string['kind']`). Ingest enriches every IP with
**ASN/org** (`client.asn`, `client.as_org`) — enabling botnet/datacenter-origin detection of
legacy-surface scanners 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 (`http.method`, response size) with a `--mode dry_run` before relying on
it; OTEL SDK versions vary (`http.method` vs `http.request.method`). **The defining trait of this
domain's rules is the path-pattern filter on `attributes_string['http.target']`** — that is how
drift surfaces in traffic.
**Traffic-based — live hit on a deprecated version path (`/v1/%` after `/v2` is current) — the workhorse:**
```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 hits,
uniqExact(attributes_string['http.target']) AS paths
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
AND timestamp >= now64(9) - INTERVAL 30 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 30 MINUTE)) - 1800
AND kind = 2
AND attributes_string['http.target'] LIKE '/v1/%'
GROUP BY ip
HAVING ip != '' AND hits >= 1
```
**Traffic-based — shadow / non-prod / internal / debug path access (inventory drift, any hit):**
```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 hits,
uniqExact(attributes_string['http.target']) AS paths
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
AND timestamp >= now64(9) - INTERVAL 30 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 30 MINUTE)) - 1800
AND kind = 2
AND (attributes_string['http.target'] LIKE '%/internal%'
OR attributes_string['http.target'] LIKE '%/debug%'
OR attributes_string['http.target'] LIKE '%/actuator%'
OR attributes_string['http.target'] LIKE '%/metrics%'
OR attributes_string['http.target'] LIKE '%/swagger%'
OR attributes_string['http.target'] LIKE '%/__debug%')
GROUP BY ip
HAVING ip != '' AND hits >= 1
```
> Tune the `LIKE` patterns to **this app's** discovered legacy/shadow/debug prefixes from Phase 1
> (e.g. add `'/api-old/%'`, `'/legacy/%'`, `'/beta/%'`, your real internal prefix). Keep the
> threshold at `hits >= 1` for confirmed-stale paths (any live hit is signal); raise it only for
> patterns that also match legitimate current traffic.
**Traffic-based — legacy-surface scanning (many distinct legacy paths + client errors from one 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,
uniqExact(attributes_string['http.target']) AS distinct_paths,
countIf(response_status_code IN ('400','401','403','404','405','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 15 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 15 MINUTE)) - 1800
AND kind = 2
AND (attributes_string['http.target'] LIKE '/v1/%'
OR attributes_string['http.target'] LIKE '/legacy/%'
OR attributes_string['http.target'] LIKE '%/internal%')
GROUP BY ip
HAVING ip != '' AND distinct_paths >= 15 AND client_errors >= 20
```
**Traffic-based — anomalous bulk extraction via a legacy read path (5xx-free, high response volume / many hits on one old path):**
```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,
attributes_string['http.target'] AS path,
count() AS hits
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 '/v1/%'
GROUP BY ip, path
HAVING ip != '' AND hits >= 300
```
**Events-based — explicit deprecated/legacy-route invocation (API9; query the logs table):**
```sql
SELECT
attributes_string['http.client_ip'] AS ip,
attributes_string['route'] AS route,
attributes_string['superseded_by'] AS superseded_by,
attributes_string['kind'] AS legacy_kind,
count() AS hits
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
AND attributes_string['event.type'] = 'api.deprecated.called'
AND timestamp >= now() - INTERVAL 30 MINUTE
GROUP BY ip, route, superseded_by, legacy_kind
HAVING ip != '' AND hits >= 1
```
> The same logs-table shape covers the legacy-auth and shadow variants — they all emit
> `api.deprecated.called`; filter or group on `attributes_string['kind']`
> (`legacy_auth` / `shadow` / version) and `attributes_string['scheme']` to split them, and raise
> the threshold (e.g. `>= 5` for legacy-auth volume) where any-hit is too noisy.
**Injection on a legacy path (catalog J/K56) — 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) and applies to **every** path,
including the unhardened zombies that lost their validation. 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 legacy attributes (`route`, `superseded_by`,
`kind`, `scheme`).
#### Emit every detection as a ready-to-copy command unit
Every detection becomes a **complete, copyable unit** — never a fragment. For each rule emit, in
order: (1) the SQL, (2) a comment line saving it to `rules/<name>.sql`, (3) the full
`securenow alerts rules create …` command, (4) the dry-run test. In Markdown each is its own
fenced block so it copies cleanly. The exact flags must match `securenow alerts rules --help` from
Phase 0.5 (`--name` / `--sql @rules/<name>.sql` / `--apps` / `--severity` / `--schedule` /
`--nlp`). Save each rule's SQL to `rules/<name>.sql` so `--sql @rules/<name>.sql` resolves. Note
pre-existing / system rules instead of duplicating them. Example unit:
```sql
-- rules/legacy-v1-traffic.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 hits,
uniqExact(attributes_string['http.target']) AS paths
FROM signoz_traces.distributed_signoz_index_v3
WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)
AND timestamp >= now64(9) - INTERVAL 30 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 30 MINUTE)) - 1800
AND kind = 2
AND attributes_string['http.target'] LIKE '/v1/%'
GROUP BY ip
HAVING ip != '' AND hits >= 1
```
```bash
securenow alerts rules create \
--name "Legacy: live traffic on deprecated /v1 path" \
--sql @rules/legacy-v1-traffic.sql \
--apps <APP_KEY> \
--severity high \
--schedule "*/5 * * * *" \
--nlp "any client hitting a /v1 path after /v2 shipped"
securenow alerts rules test <RULE_ID> --mode dry_run --wait # validate before it runs live
```
Keep the conventions above (correct tenant-scope column per table, `ip` selection,
`ts_bucket_start` + `kind = 2` guards, and the **path-pattern filter on
`attributes_string['http.target']`** that defines this domain), the `api.deprecated.called`
events-based rule shape, and the `--route /v1* --mode prefix` scoping on every legacy
block/rate-limit/challenge, in every emitted unit.
#### Test mode for false-positive-prone rules (ship FP-prone rules detect-only first)
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 (legacy-surface
scan distinct-path / client-error counts, bulk-extraction hit counts, new-volume-onset rules),
broad path-pattern `LIKE` filters that may also match current traffic, 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 (especially un-migrated straggler clients on a legacy prefix),
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 on a
legacy path, exact-match IoCs, any-hit rules on a path **confirmed stale** from Phase 1 — e.g. a
truly retired `/v1/%` no current client uses) may go straight to `prod`. In the report, **tag
every rule `test-first` or `prod-ready`** and say why. (`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 legacy endpoints, SecureNow **surfaces the drift and contains the actor at the edge**; the
**app/config fix — retire or gate the route — removes the underlying weakness**. Always pair them
on every legacy row. Once a threat is confirmed, **choose the narrowest effective mitigation(s)
from ALL of these** and combine them (e.g. rate-limit the `/v1*` prefix + block the worst scanner
IPs + challenge a NAT egress that still carries one un-migrated client). 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 — for this domain the **route-scoped block/limit/challenge
(`--route /v1* --mode prefix`)** is the workhorse: it touches only the legacy surface, never
current traffic.
| # | 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 probing for forgotten endpoints before they reach 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 — injection probes against the unhardened zombie that lost its validation. 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 /v1* --mode prefix --method ALL --app <APP_KEY> --env production --reason "..."` (`--mode exact\|prefix\|regex`, `--method GET\|POST\|…\|ALL`) | block an IP only on the legacy/`/internal`/debug prefix; least collateral — current traffic untouched. **The default legacy block.** |
| 5 | **IP block — temporary / time-boxed** | `securenow blocklist add <ip> --route /v1* --mode prefix --duration 24h --reason "..."` (`30m`,`24h`,`7d`) · reverse `securenow blocklist unblock <id> --reason "..."` | auto-expiring containment of a scanner; 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 /v1 --mode prefix --method GET --limit 60 --window 1m --key-by ip` | cap the whole legacy prefix for everyone, budgeted per IP — slows surface mapping without blocking a straggler. |
| 8 | **Rate limit — per route + IP** | `securenow ratelimit add <ip> --route /v1/export --mode prefix --method GET --limit 5 --window 1m --duration 24h` · NL `securenow ratelimit from-text "rate limit /v1* to 60/min for 24h" --yes` · test `securenow ratelimit test <ip> --path /v1/orders --method GET` | precise throttle of one client on the legacy export/read path. |
| 9 | **CAPTCHA / proof-of-work challenge** | `securenow challenge add --route /v1 --mode prefix --difficulty 16 --clearance 30m` (prefix-wide) **or** `securenow challenge add <ip> --route /v1* --difficulty 18 --clearance 30m` · test `securenow challenge test <ip> --path /v1/orders --method GET` | bot enumeration of the old surface from **shared / NAT / CGNAT** egress — a human passes once, a scanner can't. Prefer over a hard block when the IP may carry a real un-migrated client. |
| 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()`) | a session authenticated via a legacy/Basic/query-string scheme is compromised — kill the stolen session, not the IP. |
| 12 | **Trusted IP (suppress)** | `securenow trusted add <ip> --label "Legacy partner still on /v1"` | a sanctioned straggler you can't migrate yet — suppresses detection **and** mitigation. NOT deny-by-default; never use `allowlist` for this. |
| 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 legacy 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 legacy-path rule quiet for a known legitimate straggler without weakening it. |
| 15 | **App / config / code fix (PRIMARY for every legacy row)** | *described in the Code-Findings report, never auto-applied* | the actual fix: **unmount or gate the deprecated version**, put `/internal` behind the mesh, IP/VPN-restrict non-prod, disable `/actuator`/swagger/introspection in prod, drop Basic/query-string auth, apply the current middleware to the legacy route, remove the dangling DNS record, regenerate the spec from code. SecureNow contains; the fix removes. |
**Choosing per threat** — by **confidence**: exploit-signature/exact IoC on a legacy path →
instant-block or route-scoped block; probable bot enumerating the old surface on shared egress →
**challenge** (`--route /v1* --mode prefix`); noisy/legit-mixed legacy traffic (a straggler mixed
with scanners) → **route-scoped rate-limit (test-mode first)**; a session compromised via a
legacy auth scheme → **revoke**; a known un-migratable partner → **trusted / fp**. By **blast
radius**: always scope to the narrowest `route`/`method`/`IP`/`duration` that stops the abuse —
for legacy that means **`--route /v1* --mode prefix`** so you never touch current traffic; on
NAT/CGNAT/shared IPs prefer challenge/rate-limit over a hard block; recommend **notify-only** for
the *first* sighting of live legacy-path traffic (the right response is usually migrate-the-client-
and-retire-the-route, not block a straggler). Always pair the edge mitigation with the
**app/config fix — retire or gate the route** (Code-Findings report), which is primary on every
legacy row; SecureNow can only surface the drift and contain the actor.
### 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 deprecated-route event — exercise the events-based legacy rule end to end:
securenow event send api.deprecated.called --ip 203.0.113.50 \
--attrs route=/v1/orders,superseded_by=/v2/orders,test=true
# Synthetic legacy-auth + shadow variants (same event, different kind):
securenow event send api.deprecated.called --ip 203.0.113.50 \
--attrs route=/v1/login,kind=legacy_auth,scheme=basic,test=true
securenow event send api.deprecated.called --ip 203.0.113.51 \
--attrs route=/internal/export,kind=shadow,superseded_by=none,test=true
# Validate a rule query without waiting for the schedule:
securenow alerts rules test <RULE_ID> --mode dry_run --wait
# Traffic-based rules (deprecated/shadow/debug path patterns) — generate spans, then check pipeline:
securenow test-span "threat-model.legacy.v1.smoke"
securenow forensics "requests to /v1, /internal, /debug, /actuator paths by IP in the last hour" --env production
# Injection on a legacy path — confirm a payload triggers the system rule + instant block (staging):
# send a request carrying a benign-but-matching marker (e.g. /v1/search?q=' OR '1'='1) to a staging URL,
# then verify the block fired:
securenow firewall test-ip 203.0.113.50 --app <APP_KEY> --env production
# Mitigation verification (scope to the legacy prefix):
securenow ratelimit test 203.0.113.50 --path /v1/orders --method GET
securenow challenge test 203.0.113.50 --path /v1/orders --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>
```
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 **two tracks**, each as Markdown **and** self-contained HTML, into
`threat/13-legacy-endpoints/`:
- **Detection & Mitigation** (`legacy-endpoints-detection-mitigation.md` / `.html`) — the
operational runbook: what to run in SecureNow.
- **Code Findings & Recommendations** (`legacy-endpoints-code-findings.md` / `.html`) — the code
audit: stale-surface issues in the codebase + recommendations (findings only — never applied).
The two tracks **cross-link**: the gaps / instrumentation rows in the Detection report link to
the relevant code finding, and each code finding links back to the Detection row it backs.
### 5a. Detection & Mitigation report — sections (both `.md` and `.html`), in order
1. **Executive summary** — stats line (threats modeled · covered · partial · gaps · rules to
create · mitigations), top 3 detectable legacy risks for this stack (e.g. "live `/v1/users`
returns PII with no authz", "staging host internet-reachable on prod data", "`/actuator`
exposed in prod"), installed `securenow` version + app key + firewall state, and a one-line
OWASP API Top 10:2023 coverage note (API9 + API8 owned here; deep API surface / DNS / secrets
deferred to sibling models).
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 which **system signature rules** (SQLi/XSS/RCE) are present.
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 **Mode** cell tags each rule **`test-first`** or
**`prod-ready`** (per Phase 4b). The **Mitigation** cell must pick **specific, scoped**
mitigation(s) from the Phase 4c toolbox — name the toolbox row(s) and the scope (e.g.
"route-scoped block `--route /v1* --mode prefix`" + "app fix: unmount the `/v1` router"), never
a generic "block the IP." Include the deferred rows (59–61) pointing to the sibling models, then
the "Out of scope" N/A list.
4. **Detection rules to create** — each as the **ready-to-copy command unit** from Phase 4 (SQL →
save to `rules/<name>.sql` → full `securenow alerts rules create …` → `--mode dry_run` test).
**Mark each rule `test-first` or `prod-ready`** (Phase 4b); for every `test-first` rule include
the promotion step — create/run it in **`--mode test`**, observe real traffic for **3–7 days**,
tune the threshold + add `securenow fp` exclusions, then **`--mode prod`** to arm mitigation
(`securenow alerts rules update <RULE_ID> --mode test` → … → `--mode prod`). Injection-class
rows reference the **system signature rules + `instant.block`**, not duplicate SQL. Note rules
that already exist (from Phase 0) instead of duplicating them.
5. **Instrumentation the detections need** — only the `track('api.deprecated.called')` calls 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 Phase 4c 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 revoke · trusted ·
allowlist · fp exclusion · **app/config fix**) + the "Choosing per threat" guidance + per-threat
ready-to-copy mitigation command + reversibility. Make explicit that the app/config fix —
**retire or gate the route** — is primary on every legacy row, and the SecureNow control is
drift-surfacing + edge containment. Scope every legacy block/rate-limit/challenge with
`--route /v1* --mode prefix`.
7. **Action plan (copy-paste, ordered)** — ① engage firewall + enable signature `instant.block`,
② add `api.deprecated.called` instrumentation at the legacy routes, ③ create the path-pattern
drift rules for this app's discovered legacy prefixes — **FP-prone rules created in `--mode
test`** (detect-only), high-precision rules straight to `--mode prod`, ④ enable automations /
challenge rules scoped to the legacy prefixes, ⑤ test, ⑥ verify in dashboard, ⑦ **promote the
test-first rules after N days** — observe 3–7 days of real traffic, tune thresholds + add
`securenow fp` exclusions, then `securenow alerts rules update <RULE_ID> --mode prod` to arm
mitigation, ⑧ schedule the **retire/gate** app/config work from the code-findings report (the
actual fix). Real commands only, `<APP_KEY>` already substituted.
8. **Testing & validation** — per-rule recipe from Phase 4d: `securenow event send …` /
`test-span` / `--mode dry_run` + expected outcome (which rule fires, which notification
appears) + cleanup, using TEST-NET IPs (`192.0.2.0/24` / `198.51.100.0/24` / `203.0.113.0/24`).
9. **Response runbooks** — per notification type (live deprecated-path traffic, shadow/debug path
hit, legacy-surface scan, injection on a legacy path): confirm TP (straggler client to migrate,
or attacker?) → respond command (copy) → reverse command (copy).
10. **Known gaps & SecureNow feature requests** — each 🔴 (un-hit deprecated route, dangling DNS
record with no traffic, unexercised old auth scheme, field-level spec drift invisible to path
telemetry): why it's not coverable today, the interim app/config fix (link to the code report),
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 (both `.md` and `.html`), in order
State clearly at the top: *"Findings only — no application code was modified."*
1. **Executive summary** — findings by severity (critical / high / medium / low), top 3 code
risks for this stack, one-paragraph posture verdict.
2. **Legacy surface & inventory** — the Phase 1 inventory for this domain: the **declared vs
deployed vs observed** three-way diff + the version table + environment/host table +
debug/diagnostic remnant list + old-auth-scheme list + control-parity table +
sensitive-data-over-legacy list + forgotten-DNS candidates + telemetry redaction status.
3. **Threat catalog** — the exhaustive Phase 2 catalog (grouped A–L, each tagged OWASP/CWE,
modeled or explicit N/A), including the deferred rows (59–61) pointing to the sibling models.
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). This is the section the user
reviews to plan their own retirements.
5. **Strengths** — controls already present and correct (old versions gated with sunset headers,
non-prod VPN-only, introspection disabled in prod, deprecated routes inside the current
middleware stack) — the posture must be honest.
6. **App / config fixes (primary remediation)** — the config/code changes that remove the root
cause (described, not applied): unmount or gate the deprecated version, put `/internal` behind
the mesh, IP/VPN-restrict non-prod, disable `/actuator`/swagger/introspection in prod, drop
Basic/query-string auth, apply the current middleware to the legacy route, remove the dangling
DNS record, regenerate the spec from code — each linked to the Detection-report row it backs.
7. **Instrumentation recommendations** — the `track('api.deprecated.called')` calls to add and the
exact file:line to add them (disambiguated by `kind` / `superseded_by` / `scheme`), so the
detection rules light up. Cross-link to the Detection report's "Instrumentation" section.
8. **Appendix** — files reviewed, resolved SDK version (from Phase 0.5), date, link to the
detection-mitigation report.
### 5c. HTML skeletons — SecureNow branding (self-contained; offline; copy buttons)
Both HTML files share the `<head>` (brand tokens + copy-button styles) and the copy `<script>` at
the end of `<body>`. Change only the `<title>`, the sidebar subtitle, the header, and the section
content per track. **Wrap EVERY command/SQL block as a `.cmd`** (so it gets a Copy button). No
external fonts / scripts / network.
#### 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 — Legacy Endpoints — SecureNow" OR "Code Findings — Legacy Endpoints — 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 · Legacy Endpoints" OR "Code Findings · Legacy Endpoints" --></div>
<!-- one <a href="#…"> per section of THIS track (5a or 5b) -->
</nav>
<main>
<header class="top"><h1><!-- report title for this track --></h1>
<p><code><!-- app name / domain --></code> · <span class="pill">securenow <!-- installed version --></span></p></header>
<div class="stats"><!-- 5 .stat cards; numbers MUST equal the table/finding counts of THIS track --></div>
<!-- <section id="…"> blocks mirroring the Markdown sections of THIS track (5a or 5b) -->
<footer>Generated by the SecureNow legacy-endpoints 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>
```
#### Track A — Detection & Mitigation HTML body
Title `Legacy Endpoints — Detection & Mitigation`, sidebar subtitle
`Detection & Mitigation · Legacy Endpoints`, one nav link + `<section>` per **5a** section. The
five `.stat` cards = threats modeled · covered · partial · gaps · rules to create (numbers MUST
equal the matrix counts). The header `<p>` shows the surface, e.g.
`<span class="pill">multi-version REST · /v1 + /v2 + staging exposed</span>`. **Every** SQL /
create / mitigation / test block uses the copyable wrapper:
```html
<div class="cmd"><button class="copy" type="button">Copy</button><pre>securenow alerts rules create \
--name "Legacy: live traffic on deprecated /v1 path" \
--sql @rules/legacy-v1-traffic.sql --apps <APP_KEY> --severity high \
--schedule "*/5 * * * *" --nlp "any client hitting a /v1 path after /v2 shipped"</pre></div>
```
#### Track B — Code Findings HTML body
Title `Legacy Endpoints — Code Findings & Recommendations`, sidebar subtitle
`Code Findings · Legacy Endpoints`, one nav link + `<section>` per **5b** section. The five
`.stat` cards = findings critical · high · medium · low · strengths (numbers MUST equal the
findings table counts). Lead with the note *"Findings only — no application code was modified."*
Prose may omit copy buttons, but **any example/fix command still wraps in `.cmd`**.
Badge usage (both tracks): severity → `<span class="b crit|high|med|low">`, coverage →
`<span class="c cov|part|gap">COVERED|PARTIAL|GAP</span>`, OWASP tag →
`<span class="owasp">API9</span>`, CWE tag → `<span class="cwe">CWE-1059</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 of each track.
---
## Quality bar (the report is rejected if any of these fail)
- Every catalog item A1–K58 is either a matrix row or an explicit N/A line; each modeled row
carries its OWASP API Top 10:2023 tag (almost always **API9** or **API8**, or "—").
- The drift-surfacing rows (deprecated-version traffic, shadow/non-prod/debug path patterns,
legacy-surface scanning, `api.deprecated.called` events) are each modeled with the path-pattern
signal and the matching event where instrumentation helps.
- The deferred rows (59–61) point to the sibling models — deep API surface to
[../14-api-security/](../14-api-security/api-security-threat-model-prompt.md), dangling-DNS /
takeover to [../21-dns-tls-certificates/](../21-dns-tls-certificates/), old-auth-scheme /
secret-rotation / cloud-IAM to [../20-secrets-and-cloud-iam/](../20-secrets-and-cloud-iam/) —
and are **not** re-derived here.
- 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 (correct table column) and selects an
`ip` column; traffic queries keep the `ts_bucket_start` + `kind = 2` guards and the
path-pattern filter on `attributes_string['http.target']`.
- Injection coverage on legacy paths 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 challenge …`, `firewall`, the `--route /v1* --mode prefix` block scope,
and the `api.deprecated.called` event).
- Detection vs. fix is honest: every legacy row pairs the SecureNow control (drift-surfacing +
edge containment) with the **app/config fix — retire or gate the route**, which is primary.
- Every 🔴 gap appears in the gaps section with an interim app/config fix (retire/gate/remove)
**and** the "contact the SecureNow team" line.
- The action plan runs top-to-bottom with `<APP_KEY>` substituted in, ending with the
retire/gate work scheduled.
- **Phase 0.5 ran**: the resolved installed `securenow` version appears in both reports' appendix,
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 → `rules/<name>.sql` → full
`securenow alerts rules create …` → `--mode dry_run` test); flags match `alerts rules --help`.
- **Four** files are written to `threat/13-legacy-endpoints/` (detection-mitigation `.md` + `.html`,
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** (`.cmd` wrapper + the shared copy `<script>`).
- 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.
- The Detection report's mitigation section presents the **full toolbox** (§6 / Phase 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** (legacy default: `--route /v1* --mode
prefix`) — 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 (signature matches,
exact IoCs, any-hit on a confirmed-stale path) are `prod-ready`. The action plan creates the
test-first rules in `--mode test` and has an explicit "promote after N days" step.
- The stats cards in each HTML match that track's table/finding counts.
- A one-line summary is printed back: per-track file paths, threat counts, rules-to-create count,
code findings by severity, gaps, OWASP coverage, and the resolved SDK version.
<!-- ════════════════ END OF PROMPT ════════════════ -->