#02A01 · API1 BOLA · API5 BFLA

Authorization

BOLA/IDOR, broken function-level auth, mass assignment, privilege escalation, multi-tenant.

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/02-authorization/ — open authorization-code-findings.html (the audit) and authorization-detection-mitigation.html (the defenses) in your browser.

🔒No repository upload to SecureNow is required. Your coding agent/provider determines where code is processed. The prompts request self-contained HTML reports that can be opened offline; review the generated output and keep sensitive findings in an environment you control.

Topic-specific review guide

Prove every object and function check runs on the server

Guide updated

Map who may do what to which object

Document the access-control model the code actually implements: roles, ownership rules, tenancy, and where each decision is made, whether in middleware, decorators, a policy engine, or inline conditions. List every entry surface that reaches data, including REST routes, GraphQL resolvers, background jobs, webhooks, exports, and admin tooling. Record how the tenant or active organization is derived; a value read from a header, parameter, or body is a different trust boundary from one taken from the server-side session. If the product has teams and invitations, include the membership lifecycle: who may invite, which role an invite grants, and what is revoked when a member is removed.

Check ownership, role, and field scope in code

Ask your coding agent to follow each handler from identity resolution to the database query. A query scoped by object ID alone, without the owner or tenant, is the typical IDOR pattern, and nested references such as an item under another user's order need the same check. Confirm that write, update, and delete methods enforce the same role checks as reads, and that paths are normalized before the authorization decision. Look for mass assignment of fields such as role, tenantId, or ownerId, and for responses that return properties the caller should not see. For each finding, request a file and line reference and the exact request a second test account would send.

Know what a successful bypass looks like in telemetry

A successful authorization bypass often returns a normal 200 and emits no failure signal. Request traces can still show the shape of abuse: one client walking many distinct IDs on the same route template, a burst of 401 and 403 responses across restricted paths, or a 403 followed by a 200 on the same resource. Traces cannot show that an allowed request returned an object the caller does not own. That question needs an application event recording the decision with privacy-safe caller, owner, and tenant identifiers. Until such an event exists, treat a single quiet IDOR on an unguessable ID as invisible to runtime monitoring.

Pair each finding with a negative test

The deliverable should pair every finding with the missing server-side check, the request that demonstrates it using two isolated test accounts, and the expected 403 or 404 after the change. Rerun legitimate sharing, team, and admin workflows to confirm they still work. Rate limits, challenges, or an IP block can slow enumeration, but they cannot add an ownership check or revoke a removed member's tokens. Record agent tool calls, background jobs, and entry points the review could not reach as open gaps. An engineer who owns the data model should review the evidence before approving a change.

The prompt

# Authorization 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` SDK/CLI installed and logged in. The agent will analyze the codebase, build an
exhaustive **authorization** (access-control) threat model, audit the code for access-control
flaws, and emit a SecureNow-branded **two-track** deliverable set in **Markdown + self-contained
HTML** — a **Detection & Mitigation** runbook (the rules to create, the mitigation commands to
run, how to test each one) and a **Code Findings & Recommendations** audit (the access-control
flaws in the code, audited and **not** fixed) — plus which threats still need the SecureNow team.

Every alert rule and CLI command the agent emits is **grounded in the `securenow` SDK actually
installed in `node_modules/securenow`** (Phase 0.5) — never a guessed flag, subcommand, event
name, or SQL column — and every detection is written as a **ready-to-copy unit**
(SQL → save to `rules/<name>.sql` → full `securenow alerts rules create …` → dry-run test).

Authorization ≠ authentication. Authentication proves *who you are*; authorization decides
*what you are allowed to do with it*. This model assumes the caller is already
authenticated and asks: can they reach data or actions that are not theirs? This is OWASP
**A01:2021 Broken Access Control** (the #1 web risk) and OWASP API Security **API1 (BOLA)**,
**API3 (BOPLA)**, **API5 (BFLA)**. Run the [authentication threat model](../01-authentication/authentication-threat-model-prompt.md)
first if you have not — the two are complementary.

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 an Authorization Threat Model Report (SecureNow)

You are a senior application-security engineer specializing in access control. Produce an
**exhaustive authorization threat model for THIS codebase**, mapped to **SecureNow**
detections and mitigations, with a ready-to-run action plan **and** a code-level audit of
every access-control flaw you find. Every rule and command you emit is grounded in the
**installed** `securenow` SDK (Phase 0.5), and every detection is a **ready-to-copy unit**
(SQL → `rules/<name>.sql` → full `securenow alerts rules create …` → dry-run test).

You write **FOUR** files into `threat/02-authorization/` (create the folder if needed) — two
tracks, each as Markdown **and** a self-contained HTML page (inline CSS + JS, no network
requests, with offline **copy buttons** on every command block), using the SecureNow branding
skeletons at the end:

**Track A — Detection & Mitigation (the operational runbook: what to run in SecureNow)**
1. `authorization-detection-mitigation.md` — the runbook in Markdown.
2. `authorization-detection-mitigation.html` — the same, self-contained HTML with copy buttons.

**Track B — Code Findings & Recommendations (the code audit + recommendations)**
3. `authorization-code-findings.md` — the code-level findings in Markdown.
4. `authorization-code-findings.html` — the same, self-contained HTML.

The two tracks **cross-link** each other: every gap/instrumentation row in the Detection report
links to the relevant code finding, and each code finding links back to the detection row it
backs.

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

---

## 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
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.

---

## 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 — Map the authorization model (codebase analysis)

Document what access control is **actually implemented**, not what is intended. Cover at
minimum:

- **Model** — RBAC, ABAC, ACLs, ownership-based, scopes/claims, or ad-hoc. Where is the
  policy defined (middleware, decorators, per-route guards, a policy engine, inline `if`s)?
- **Principals & roles** — the set of roles/permissions, how a request's identity and role
  are resolved (session, JWT claim, DB lookup), and whether the role is **re-verified
  server-side** or trusted from the token/client.
- **Tenancy** — is the app multi-tenant? How is `tenantId`/`orgId`/`accountId` derived — from
  the server-side session, or from a client-supplied header/param/body? Is **every** data
  query scoped by tenant?
- **Collaboration, teams & invitations** — can users share a workspace/team/org, invite others,
  hold a role **within a group**, or occupy a billed seat? If so, map the whole membership
  lifecycle, because it is the richest authorization bug-bounty surface in modern SaaS: **(a)
  Invites** — who may invite, is the granted role **capped by the inviter's** role, is the
  invite token unguessable/expiring/single-use, is it **bound to the invited identity** (and
  accepted only by that verified email), and does accept read the org/role from the **server-
  stored invite** (never from the request body)? **(b) Membership & roles** — can a member
  change **their own** role or another member's role, is every member-management route re-scoped
  to an org the actor **administers**, is the last owner/admin protected, and on removal/
  downgrade are the member's **sessions, API keys, tokens, webhooks and share links revoked**
  (not just the row deleted)? **(c) Org context** — how is the "active org/workspace" chosen
  (session vs a client header/cookie/param like `X-Org-Id`/`active_org`), and is membership in
  that org **re-verified on every request** before its data or actions are served? **(d)
  Provisioning & automation** — ownership transfer, seat/billing gates, per-org service
  accounts / scoped API keys / bots, SCIM/SSO/JIT group→role mapping, and staff/admin
  **impersonation** ("log in as user").
- **Object ownership** — for each resource type, how is "does this principal own / may access
  this object" enforced? Is it a per-record check, or only a "is logged in" check?
- **Enforcement points** — list every entry surface and whether each enforces authz: REST
  routes, GraphQL resolvers, gRPC, WebSocket handlers, background jobs/cron, webhooks,
  server actions, file/object-storage access, admin tooling, internal/service endpoints.
- **Sensitive functions & objects** — admin endpoints, role/permission management, billing,
  exports/reports, bulk/batch endpoints, impersonation, data deletion, integration tokens.
- **Method & path handling** — is authz enforced per HTTP method (not just GET)? Are paths
  normalized (encoding, `..`, trailing slash, case) **before** the authz decision?
- **Field-level control** — can clients set protected fields (mass assignment: `role`,
  `isAdmin`, `tenantId`, `ownerId`, `balance`, `verified`)? Do responses over-return fields a
  role shouldn't see?
- **AI agents, MCP & tool-calling** — does the app expose or consume LLM agents, an MCP
  server/client, or LLM function/tool-calling? For each: list the callable tools/functions and,
  for each, whether the call is **authorized against the end user's permissions** (not just the
  agent's service identity). Is authorization delegated **on-behalf-of** the user (the agent
  acts with the user's scope / a down-scoped token), or does it run with ambient/standing
  credentials? Are high-impact tools gated by human approval / step-up? For retrieval (RAG), is
  every fetched document/row/memory scoped to the requesting user **and** tenant? Are agent/MCP
  tokens audience-bound so one server's token can't be replayed on another? Can untrusted
  content reaching the agent (documents, tool output, memory) alter which tools it may call?
- **SecureNow instrumentation already present** — `securenow/register` or `securenow run`,
  `securenow/sessions` `guard()`, and any `securenow/events` `track('authz.*' / 'admin.*' /
  'tool.*' / 'agent.*')` calls. This determines which detections work *today* vs *after
  instrumentation*.

Output of this phase = the report's **Scope & authorization model** section: a component
table (Model / Roles / Tenancy / Ownership checks / Enforcement points / Sensitive surfaces /
Field control) plus a short paragraph naming the real access-control attack surface for this
stack.

---

## Phase 2 — Enumerate threats (exhaustive catalog)

Evaluate **every** threat below against the discovered model. 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. "cross-tenant: N/A, single-tenant app"). Never silently drop an item.
Add stack-specific threats you discover that are not listed — this catalog is the floor, not
the ceiling.

**A. Object-level authorization — BOLA / IDOR (OWASP API1)**
1. IDOR read: fetch another user's object by manipulating an ID (path/query/body)
2. IDOR write/delete: modify or delete another user's object
3. BOLA enumeration at scale: one principal walking many object IDs
3a. ID brute-forcing (scanning the id space for valid objects): many distinct ids with a **high miss ratio**
    (mostly `404/403`) — the statistical fingerprint of hunting for which objects exist
3b. IDOR dump / bulk exfiltration: many distinct **successful** (`200`) objects on a `:id` route — pulling data
    once valid ids are known (a normal user reads only a handful of their own)
3c. Sequential / incrementing id walk: a dense, near-consecutive id range from one client (`/orders/1001,1002,…`)
4. Predictable identifiers (sequential ints, UUIDv1, base64'd PKs) that enable enumeration
4a. Cross-owner access rate: a principal granted an anomalous count of objects whose owner isn't them — the
    *definitive* IDOR breach signal (needs the `authz.granted` + `owner_id` event)
5. Nested/indirect reference not re-checked (`/orders/{mine}/items/{not-mine}`)
6. List/search/export endpoints that return objects across owners

**B. Function-level authorization — BFLA (OWASP API5)**
7. Missing authz on admin/privileged endpoints (force-browse to `/admin`, `/internal`)
8. Method-based BFLA: read is checked but `POST`/`PUT`/`PATCH`/`DELETE` is not
9. Hidden/undocumented endpoints reachable without a role check (debug, internal API)
10. Role enforced in the UI only, not server-side (client-side authorization)
11. New or changed endpoint ships **without** an authz check (missing-by-default)

**C. Object property-level authorization — BOPLA / mass assignment (OWASP API3)**
12. Mass assignment: client sets protected fields (`role`, `isAdmin`, `tenantId`, `balance`)
13. Excessive data exposure: response returns fields the role/owner shouldn't see (PII, flags)
14. Per-field write without per-field authz (changing `price`, `ownerId`, `status` via PATCH)

**D. Privilege escalation**
15. Vertical escalation: user → admin (forced role param, privileged default, role tampering)
16. Horizontal escalation: act as another same-level user across an account/tenant boundary
17. Escalation via token/claim tampering (role/scope/tenant in JWT trusted, not re-verified)
18. Stale privilege: role downgrade/removal not reflected until token expiry
19. Self-service privilege grant (a user can assign themselves a role/permission)

**E. Multi-tenancy & data isolation**
20. Cross-tenant object access (query not scoped by the server-side tenant)
21. Client-supplied tenant trusted (tenant from header/param/body overrides the session)
22. Shared-cache / shared-connection / shared-key cross-tenant bleed
23. Cross-tenant via list/search/aggregate endpoints that omit the tenant filter
24. Cross-tenant via background jobs / exports / webhooks where authz context is dropped

**F. Policy-model flaws (RBAC / ABAC)**
25. Default-allow: access denied only on an explicit rule; new resources are open
26. Over-broad roles / wildcard permissions / role explosion
27. Wrong-subject check: caller's role checked against the wrong resource/owner
28. Confused deputy: a privileged service performs an action for an unprivileged caller with no re-check
29. Inconsistent enforcement across entry points (REST vs GraphQL vs gRPC vs cron vs webhook)
30. Time-of-check / time-of-use: authz decision is stale by the time the action executes

**G. Token / session / claim authorization**
31. Scope/audience not enforced (token valid but issued for a different scope/app/tenant)
32. Over-privileged API keys (no scopes, no per-resource or per-tenant limits)
33. Key/token reuse across tenants or environments (prod key works in another tenant)
34. No step-up / re-authorization on sensitive actions (delete account, change role, payout)
35. Authorization cached past a permission revocation (revoked grant still works)

**H. Workflow / business-logic authorization**
36. Force-browsing a multi-step flow (skip approval/payment/verification step)
37. Parameter tampering changing ownership/price/quantity/recipient (business-logic authz)
38. Replay of an authorized action beyond its allowed count/window
39. Race / TOCTOU double-use of a single-use or limited authorization (double-spend)

**I. Indirect & inherited access surfaces**
40. GraphQL: nested resolver authz bypass; introspection exposing privileged mutations
41. Batch / bulk endpoints bypassing per-item authz
42. File / object storage: signed-URL abuse, predictable blob keys, path traversal to others' files
43. SSRF leveraging the server's identity to reach internal authorized services
44. Server-side export/render running with the server's privileges over user-controlled refs
45. Webhook / callback / integration endpoints acting with elevated, unscoped trust

**J. Negative-space & bypass techniques**
46. Path normalization/encoding bypass (`/admin/..;/`, `%2e%2e`, double-encoding, trailing slash, case)
47. HTTP method override (`X-HTTP-Method-Override`, verb tunneling) bypassing method-scoped authz
48. Reaching the same handler via an alternate host/route that skips the gateway authz (direct origin)
49. HTTP parameter pollution changing the authorized subject or resource

**K. Cross-origin & response handling**
50. CORS misconfiguration enabling authenticated cross-origin reads (reflected origin + credentials)
51. Per-user authorized responses cached on a shared cache/CDN/proxy → cross-user leak
52. Authorization-relevant data leaked in errors/responses enabling targeted access

**L. Administrative & infrastructure surfaces**
53. Unprotected admin panels / internal tools / dashboards reachable from the internet
54. Debug/diagnostic endpoints exposed (actuator, `/metrics`, `/__debug`, GraphQL playground)
55. Cloud metadata / internal service endpoints reachable via the app's identity (IMDS, k8s API)

**M. Observable abuse (what telemetry can actually catch)**
56. 401/403 spike from one IP or principal (authz probing / forced browsing)
57. One principal touching an anomalous count of **distinct** object IDs (BOLA enumeration)
58. Access to admin/sensitive paths by a non-admin role
59. Cross-tenant access attempts (tenant mismatch at the authz layer)
60. Privilege-elevation events at an anomalous rate, off-hours, or self-granted

**N. AI agents, MCP servers & LLM tool-calling (agentic authorization)**
Evaluate these whenever the app exposes or consumes LLM agents, an MCP server/client, function/tool-calling, or autonomous jobs. If the app has no agentic surface, mark the whole section N/A in one line — do not silently drop it.
61. Confused-deputy agent: an agent/tool runs with its own broad service identity and performs an action the requesting **user** is not authorized for — no end-user authz re-check at the tool/function boundary
62. Missing on-behalf-of (OBO) / delegation: the agent's action does not carry or down-scope to the end user's permissions; it uses ambient/standing service credentials instead of the user's authorization
63. Excessive agency: the agent is granted tools/scopes far beyond the task; a single tool call can reach privileged operations with no per-action authz gate
64. Tool-call argument tampering: the model (or caller) sets privileged targets/fields in a tool call (`tenantId`, `userId`, `role`, resource path, `ownerId`) and the tool trusts them without re-authorizing against the end user — mass assignment at the tool boundary
65. Prompt-injection-driven escalation: untrusted content (a document, email, web page, prior tool output, agent memory) steers the agent into calling a privileged tool or crossing a tenant/owner boundary
66. MCP tool-authorization gap: a connected MCP client/user can `tools/call` tools it should not — authz is coarse ("connected") rather than per-tool / per-argument / per-user; `tools/list` exposes privileged or internal tools
67. MCP/agent token confused deputy & audience: a token minted for one MCP server or resource is accepted by another (no audience / resource-indicator binding, RFC 8707); over-broad or long-lived agent OAuth consent/API keys, not scoped per tenant or per resource
68. RAG / retrieval authorization: the agent retrieves documents, rows, or memory the requesting user may not see — the vector store / knowledge base / tool output is not scoped to the user's permissions, or a shared index/cache bleeds across tenants
69. No human-in-the-loop / step-up for high-impact agent actions: destructive or privileged actions (delete, pay, change role, grant access, bulk export) execute autonomously with no approval gate or re-authorization
70. Autonomous / background agent over-privilege: scheduled or self-triggered agent runs act with standing elevated credentials over user-controlled references (the server-side export / SSRF analog, agent edition); the agent identity is not scoped per tenant/request
71. Observable agentic abuse: one agent/service identity touching an anomalous count of **distinct** objects, tenants, or tools; a tool-call to a privileged tool by an under-scoped agent/user; a spike in denied tool calls (agent probing)

**O. Collaboration, teams & organizations — membership-lifecycle authorization**
Evaluate whenever the app has shared workspaces/teams/orgs, invitations, roles held **within a group**, or seats/billing tied to membership. This is the single richest authorization surface in modern SaaS bug-bounty reports. If the app is strictly single-user with no sharing, mark the whole section N/A in one line — do not silently drop it.
*Invitations*
72. Invite privilege escalation: the granted role is **not capped by the inviter's** role (a member invites as admin/owner), or a principal who should not invite at all can
73. Invite role / org mass assignment: `role`, `orgId`, or permissions are read from the **invite-accept request body** or a client-controlled invite field instead of the **server-stored invite** (accept with `role=admin`, or point acceptance at a different org)
74. Invitation token weakness: guessable/sequential/short tokens, no expiry, **single-use not enforced** (reuse re-joins or re-escalates), token not invalidated on revoke or role change
75. Invite–identity rebinding: an invite issued to `alice@` is accepted by a **different** logged-in account because the invited email is not verified/bound — or an attacker changes their own email to the invited address to claim the invite (and its role)
76. Cross-tenant / wrong-org invite: accepting an invite, or an org-scoped invite endpoint, adds the caller to or reads an **org other than the one in the token** (`orgId` taken from body/param)
77. Invite enumeration & leakage: tokens in URLs/referrers/logs, predictable invite IDs, a pending-invite list that leaks member emails, or an accept/status endpoint that acts as an existence oracle
78. Self-join / domain capture: a user adds themselves to an org by POSTing membership with a known `orgId` (no invite required), or "anyone with `@company.com` auto-joins" lets an attacker who registers a matching address join an org
*Membership & role lifecycle*
79. Self-escalation within a team: a member raises **their own** role to admin/owner (the role-change route doesn't forbid self-target or doesn't cap the new role to the actor's level)
80. Horizontal membership tampering: a member edits another member's role, or manages membership of an org they do **not** administer, because `orgId`/actor-role is not re-checked on the member-management route
81. Last-owner / quorum bypass: removing or demoting the final owner/admin orphans the org, or a **race** demotes two admins to zero (TOCTOU on the admin set)
82. Removal doesn't revoke access: a removed or downgraded member keeps live **sessions, API keys, tokens, webhooks, or share links**; the lower role isn't enforced until token expiry (stale privilege, org edition)
83. Ownership-transfer abuse: transfer completes without the **new** owner's consent or the current owner's step-up; transfer initiated from a lower role; a pending transfer leaves two owners or zero
84. Seat / billing authorization: bypassing seat limits (add members with no paid seat), reaching paid-tier capability by switching into an org that has it, or a downgraded org retaining elevated grants
*Org context & scoping*
85. Active-org context tampering: the "current org/workspace" is taken from a client **header/cookie/param** (`X-Org-Id`, `active_org`) and trusted **without re-verifying membership**, so switching context serves another org's data or actions
86. Org-scoped IDOR: a resource belongs to org B but the handler checks only "is the caller a member of *some* org" or personal ownership — never that the object's org equals the caller's active, authorized org
87. Role-scope confusion: a role meant to be **per-org** is treated as global (admin in org A ⇒ admin everywhere), or cross-org inheritance via nested teams/groups
88. Per-org service accounts & scoped keys: an org API key, bot, or integration (webhook, OAuth app, CI token) acts **outside its org or above its role**; tokens not scoped per org/resource
89. SCIM / SSO / JIT provisioning authz: IdP-supplied groups mapped to roles without validation; JIT-provisioned users land on a too-high default role; a SCIM endpoint authorized only by a shared token allows cross-org membership writes
90. Support / impersonation / "log in as user": staff or org-admin impersonation without re-auth, scoping, consent, or audit; an impersonation token usable beyond its intended target or window
*Observable (telemetry)*
91. Observable membership abuse: a spike in failed/denied **invite-accept** or membership calls (probing orgIds/tokens); one principal admitted into or touching an anomalous number of **distinct** orgs; **self-targeted or privilege-raising** role-change events; an invite accepted where the **accepting account ≠ the invited identity**; active-org switches to orgs the principal is not a member of

---

## 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/resolver name.
- **Pattern** — quote the 1–8 vulnerable lines. State the access-control rule that is missing
  or wrong (e.g. "query scoped by `id` only, never by `req.user.tenantId`").
- **Why exploitable** — the concrete request an attacker sends and what they get.
- **Severity** — critical / high / medium / low (impact × reachability).
- **Recommended fix (described, not applied)** — the specific change: e.g. "scope the query
  `where: { id, ownerId: req.user.id }` and 404 on miss"; "replace `Object.assign(user, body)`
  with an explicit allow-list of writable fields"; "derive `tenantId` from the session, never
  from `req.body.tenantId`"; "add a server-side role guard on the `DELETE` handler";
  "normalize and decode the path before the authz decision". Reference the secure pattern, not
  a code diff. **You must not edit the codebase.**

If a check exists and is correct, note it as a **strength** (it belongs in the report so the
posture is honest). Absence of a finding for a catalog item where the surface exists is itself
a finding ("no ownership check on …").

---

## 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, or a rule you
  provide the SQL for, on telemetry that is already flowing).
- 🟡 **PARTIAL** — works after the customer adds instrumentation (`track('authz.*')` events),
  or the detection is inherently pattern-based / false-positive-prone (notify-only), or
  SecureNow can only *contain the abuser* while the real fix is app-level.
- 🔴 **GAP** — SecureNow cannot detect or mitigate this today. **Still include it**: give the
  app-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 authorization specifically.** Unlike authentication, a *successful*
> access-control bypass often looks like a normal `200` and emits no failure signal. SecureNow
> detects **patterns of abuse** (enumeration, probing, cross-tenant attempts, admin-path
> access by the wrong role) and **contains the actor** (rate-limit / challenge / block /
> revoke). It does **not** replace the missing server-side check — that is always an **app
> fix**, listed alongside the detection. A single quiet IDOR with no `authz.*` event is a 🔴
> 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.

#### Detection-source tiers — classify every detection by what it needs

Every authorization detection falls into one of three tiers. **Tag each rule in the report with its
tier**, and prefer the lowest tier that works — a detection that needs no customer code lands for every
app the moment it is plugged in (episode 2), before any instrumentation (episode 3) or code fix (episode 4).

- **Tier 1 — runtime, from traces alone (NO app code).** Built only from the HTTP traffic SecureNow
  captures automatically (method, path, path-params, status, IP, ASN) plus the **API map** route
  templates. Catches the *shapes* of access-control abuse without a single `track()` call:
  - **object / ID enumeration (IDOR scraping):** one client hitting many **distinct** object ids on the
    same route template (`/api/orders/:id`);
  - **privilege boundary opened (sudden escalation):** the same client/session gets a **403 then a 200**
    on the same restricted template — a boundary that was denied and then opened (a mid-session role grant,
    a stolen higher-privilege session, or a gate that regressed);
  - **BFLA / forced-browsing probing:** a 401/403 burst across many **distinct** restricted templates;
  - **sensitive-path access:** any hit on `/admin`, `/internal`, `/actuator`-style prefixes;
  - **sequential-id walk:** monotonic id params from one client.
  These are 🟢 COVERED on day one. They cannot read *semantics* (an authorized-looking `200` on a UUID the
  attacker already knows is invisible) — that is Tier 2.
- **Tier 2 — runtime, with the optional `authz.*` / `tool.*` bridge (one event per decision).** The app
  emits the authorization *decision* (§4a), so SecureNow sees what traces cannot: **caller ≠ object_owner
  on an allow** (true IDOR on non-sequential ids), cross-tenant denials, self-service elevation, step-up
  bypass, and the agentic `tool.call` / `agent.action` surface. 🟡 until the event ships; then 🟢.
- **Tier 3 — code-level (audit only, Phase 3).** The missing/weak check itself — no runtime signal fixes
  it; it is an **app fix**. Every Tier-1/Tier-2 detection names the Tier-3 finding it contains.

The matrix in the report must show, per threat, **which tier(s) apply** so the reader sees what lands with
zero code vs what needs the bridge vs what is a pure code fix.

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

Authorization decisions are mostly invisible to traffic alone, so the high-value detections
need the app to emit an **authorization-decision event** at each enforcement point. Add these
with `securenow/events` — fire-and-forget, never throws:

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

// Emit at every authorization decision (especially DENY, and GRANT on sensitive resources):
track('authz.denied', {
  userId, sessionId, ip,
  attributes: {
    decision: 'deny',
    role: 'member',
    tenant: tenantId,            // the principal's tenant
    resource_type: 'invoice',
    object_id: requestedId,
    owner_id: record?.ownerId,   // the object's real owner (reveals cross-owner attempts)
    action: 'read',
    route: '/api/invoices/:id',
    reason: 'not_owner',
  },
});

// Privilege / role change — actor and target:
track('authz.elevation', { userId: actorId, ip, attributes: { target: targetUserId, from_role: 'member', to_role: 'admin', self: String(actorId === targetUserId) } });

// Sensitive admin function invoked:
track('admin.action', { userId, ip, attributes: { role, action: 'export_all_users', route: '/admin/export' } });

// Collaboration / teams — the membership lifecycle (invites, role changes, org-context switch).
// `org_ref`/`account_ref` are keyed references (never raw ids/emails). These let detections catch
// invite privilege-escalation, invite-identity rebinding, self-escalation, and org-context abuse:
track('membership.invite.created',  { userId: inviterId, ip, attributes: { org_ref, inviter_role: 'member', invited_role: 'admin', invited_ref } });
track('membership.invite.accepted', { userId: accepterId, ip, attributes: { org_ref, granted_role: 'admin', invited_ref, identity_match: String(accepterInvitedRef === invitedRef) } });
track('membership.role.changed',    { userId: actorId, ip, attributes: { org_ref, target_ref: targetMemberRef, from_role: 'member', to_role: 'admin', self: String(actorId === targetMemberId), actor_role: 'member' } });
track('membership.removed',         { userId: actorId, ip, attributes: { org_ref, target_ref: targetMemberRef, sessions_revoked: 'true' } });
// Active-org context — emit the chosen org AND whether the principal is actually a verified member:
track('org.context.switched',       { userId, ip, attributes: { org_ref, member: 'false', source: 'header' } });

// Agentic surface: emit at the tool/function boundary — the authz decision for THIS call, the
// end user it acts for, and the target. (reveals confused-deputy, excessive agency, tool BOLA):
track('tool.call', {
  userId: endUserId,                 // the user the agent acts on behalf of (not the agent's service id)
  attributes: {
    agent: 'support-copilot', tool: 'refund.issue',
    decision: 'deny', reason: 'not_user_resource',
    on_behalf_of: endUserId, tenant: tenantId,
    target_ref: requestedObjectId, object_owner: record?.ownerId,
    approved_by: null,               // human-approval ref for high-impact tools, else null
  },
});

// Sessions: one line — emits `session.seen` and enforces revocations (needed to kill a
// session/user you catch escalating or crossing tenants):
const securenow = require('securenow/sessions');
app.use(securenow.guard());
```

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

| Event | Emit when |
|---|---|
| `authz.denied` | any access decision resolves to DENY |
| `authz.granted` | a **sensitive** access is granted (admin/billing/export/cross-owner-ok) |
| `authz.elevation` | a role/permission is granted or changed |
| `authz.tenant.mismatch` | the request's tenant ≠ the resource's tenant at the authz layer |
| `admin.action` | a privileged/admin function executes |
| `membership.invite.created` | an invite is issued — record `org_ref`, `inviter_role`, `invited_role`, `invited_ref` (catches invite privilege-escalation) |
| `membership.invite.accepted` | an invite is accepted — record `granted_role`, `invited_ref`, `identity_match` (catches invite-identity rebinding / wrong-account accept) |
| `membership.role.changed` | a member's role changes — record `target_ref`, `from_role`, `to_role`, `self`, `actor_role` (catches self-escalation) |
| `membership.removed` | a member is removed/downgraded — record whether `sessions_revoked` |
| `org.context.switched` | the active org/workspace changes — record `org_ref` and `member` (catches active-org context tampering) |
| `tool.call` | an LLM/MCP tool or function is invoked — record the authz `decision`, `on_behalf_of`, `tool`, `target_ref`, `object_owner` |
| `agent.action` | an autonomous/background agent run takes a privileged or cross-tenant action |
| `session.seen` | automatic via `guard()` |

Custom `attributes` become queryable as `attributes_string['<key>']` (e.g.
`attributes_string['object_id']`). Ingest enriches every event IP with **ASN/org**
(`client.asn`, `client.as_org`). HTTP traffic (status codes incl. **401/403**, paths, IPs) is
captured automatically once the app runs under `securenow run` / `securenow/register` /
`securenow init` — traffic-based rules (probing, admin-path access, force-browsing) need **no
events at all**.

### 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** (`signoz_logs.distributed_logs_v2`) → `resources_string['service.name'] IN (__USER_APP_KEYS__)`
- **traces** (`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.

**Events-based — BOLA enumeration** (one principal hitting many distinct objects; query the
**logs** table):

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['enduser.id']     AS principal,
  uniqExact(attributes_string['object_id']) AS distinct_objects,
  count() AS attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] IN ('authz.denied','authz.granted')
  AND attributes_string['resource_type'] != ''
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, principal
HAVING ip != '' AND distinct_objects >= 30
```

**Events-based — cross-tenant attempts / not-owner denials**:

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['enduser.id']     AS principal,
  count() AS cross_tenant_attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] IN ('authz.tenant.mismatch','authz.denied')
  AND attributes_string['reason'] IN ('cross_tenant','not_owner','wrong_tenant')
  AND timestamp >= now() - INTERVAL 30 MINUTE
GROUP BY ip, principal
HAVING ip != '' AND cross_tenant_attempts >= 5
```

**Events-based — self-service / anomalous privilege elevation**:

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['enduser.id']     AS actor,
  count() AS elevations
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'authz.elevation'
  AND (attributes_string['self'] = 'true' OR attributes_string['to_role'] IN ('admin','owner','superuser'))
  AND timestamp >= now() - INTERVAL 60 MINUTE
GROUP BY ip, actor
HAVING ip != '' AND elevations >= 1
```

**Events-based — invitation abuse** (invite privilege-escalation, or an invite accepted by an account that is **not** the invited identity — the classic SaaS invite-rebinding bug; needs `membership.invite.*`):

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['enduser.id']     AS actor,
  attributes_string['org_ref']        AS org,
  attributes_string['event.type']     AS kind,
  attributes_string['granted_role']   AS granted_role,
  count() AS n
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] IN ('membership.invite.created','membership.invite.accepted')
  AND (
       -- invite granting a role above the inviter's, or an admin/owner invite
       (attributes_string['event.type'] = 'membership.invite.created'
         AND attributes_string['invited_role'] IN ('admin','owner'))
       -- invite accepted by an account that is not the invited identity
    OR (attributes_string['event.type'] = 'membership.invite.accepted'
         AND attributes_string['identity_match'] = 'false')
  )
  AND timestamp >= now() - INTERVAL 60 MINUTE
GROUP BY ip, actor, org, kind, granted_role
HAVING ip != ''
```

**Events-based — membership self-escalation & active-org context abuse** (a member raising their own role, or switching into an org they are **not** a member of — active-org tampering; needs `membership.role.changed` / `org.context.switched`):

```sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['enduser.id']     AS actor,
  attributes_string['org_ref']        AS org,
  countIf(attributes_string['event.type'] = 'membership.role.changed'
            AND attributes_string['self'] = 'true'
            AND attributes_string['to_role'] IN ('admin','owner'))              AS self_escalations,
  countIf(attributes_string['event.type'] = 'org.context.switched'
            AND attributes_string['member'] = 'false')                          AS nonmember_switches
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] IN ('membership.role.changed','org.context.switched')
  AND timestamp >= now() - INTERVAL 60 MINUTE
GROUP BY ip, actor, org
HAVING ip != '' AND (self_escalations >= 1 OR nonmember_switches >= 3)
```

**Traffic-based — forced-browsing / authz probing** (401/403 spike, 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 denied,
       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 15 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 15 MINUTE)) - 1800
  AND kind = 2
  AND response_status_code IN ('401','403')
GROUP BY ip
HAVING denied >= 25 AND distinct_paths >= 8
```

**Traffic-based — sensitive/admin-path access** (alert on any access to privileged prefixes;
combine with role events to catch the wrong principal):

```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 15 MINUTE
  AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - INTERVAL 15 MINUTE)) - 1800
  AND kind = 2
  AND (attributes_string['http.target'] LIKE '/admin%' OR attributes_string['http.target'] LIKE '/internal%' OR attributes_string['http.target'] LIKE '%/actuator%')
GROUP BY ip
HAVING hits >= 1
```

**Traffic-based — object/ID enumeration (Tier 1, trace-only, no events)**. Collapse each request path to
its **route template** (prefer `http.route` set by the framework, e.g. Express `/api/orders/:id`; else
normalize `http.target` by replacing UUID and numeric segments), then flag one client hitting many
**distinct** object ids on one template — the IDOR-scraping shape, with zero app code:

```sql
WITH
  coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip,
  splitByChar('?', attributes_string['http.target'])[1] AS path,
  if(attributes_string['http.route'] != '', attributes_string['http.route'],
     replaceRegexpAll(replaceRegexpAll(lower(path),
       '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', '/:uuid'),
       '/[0-9]+', '/:id')) AS route_tmpl
SELECT client_ip AS ip, route_tmpl, uniqExact(path) AS distinct_objects, 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
  AND (route_tmpl LIKE '%/:id%' OR route_tmpl LIKE '%/:uuid%')
GROUP BY ip, route_tmpl
HAVING ip != '' AND distinct_objects >= 25
```

**Traffic-based — IDOR hunting: scan vs dump (Tier 1, statistical, trace-only).** Plain distinct-count is not
enough: it can't tell a legitimate power user from an attacker, and it can't separate the two IDOR phases.
Split the distinct objects **by response status** and compute the **hit/miss ratio** per client+template — the
statistical fingerprint of someone hunting IDORs:

- **Scanning for valid ids** (brute-forcing the id space): many **distinct** ids with a **high miss ratio**
  (mostly `404/403/401`), a few `200`s — the attacker is finding which objects exist.
- **Dumping** (exfiltration once ids are known): many **distinct `200`** objects — the attacker is pulling
  data. A normal user reads a handful of their *own* objects, so a high distinct-success count on a `:id`
  template is the anomaly.

```sql
WITH
  coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip,
  splitByChar('?', attributes_string['http.target'])[1] AS path,
  if(attributes_string['http.route'] != '', attributes_string['http.route'],
     replaceRegexpAll(replaceRegexpAll(lower(path),
       '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', '/:uuid'),
       '/[0-9]+', '/:id')) AS route_tmpl
SELECT
  client_ip AS ip, route_tmpl,
  uniqExact(path)                                          AS distinct_ids,
  uniqExactIf(path, response_status_code = '200')          AS distinct_found,       -- objects actually read
  countIf(response_status_code IN ('404','403','401'))     AS misses,
  count()                                                  AS attempts,
  round(misses / greatest(attempts, 1), 2)                 AS miss_ratio,           -- ~1 = scanning, ~0 = dumping
  multiIf(miss_ratio >= 0.5 AND distinct_ids >= 20, 'scan',
          distinct_found >= 40, 'dump', 'watch')           AS pattern
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 (route_tmpl LIKE '%/:id%' OR route_tmpl LIKE '%/:uuid%')
GROUP BY ip, route_tmpl
HAVING ip != '' AND ((miss_ratio >= 0.5 AND distinct_ids >= 20) OR distinct_found >= 40)
```

**Traffic-based — sequential / incrementing id walk (Tier 1, trace-only).** The lowest-effort IDOR hunt on
integer keys: `/orders/1001`, `/1002`, `/1003`… A dense, near-consecutive id range from one client is a walk,
even at a volume below the enumeration threshold (and even if every response is `200`):

```sql
WITH
  coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip,
  splitByChar('?', attributes_string['http.target'])[1] AS path,
  if(attributes_string['http.route'] != '', attributes_string['http.route'],
     replaceRegexpAll(lower(path), '/[0-9]+', '/:id')) AS route_tmpl,
  toUInt64OrZero(extract(path, '/([0-9]{2,})(?:/|$)')) AS oid
SELECT client_ip AS ip, route_tmpl,
       uniqExact(oid)            AS distinct_ids,
       max(oid) - min(oid)       AS id_span            -- span ≈ count ⇒ a consecutive walk, not scattered access
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 oid > 0
GROUP BY ip, route_tmpl
HAVING ip != '' AND distinct_ids >= 15 AND id_span <= distinct_ids * 3
```

**Events-based — cross-owner access rate (the *real* IDOR breach; needs `authz.granted` + `object_owner`).**
The signals above are heuristics on traffic — they flag the *behaviour*. The definitive signal needs one event:
emit `authz.granted` on reads of owned objects carrying `owner_id`, then count **allows where the owner isn't
the caller**. A legitimate `200` on your own object never fires this; an IDOR that the app wrongly allowed does:

```sql
SELECT
  attributes_string['enduser.id'] AS principal,
  attributes_string['http.client_ip'] AS ip,
  count()                                   AS cross_owner_reads,
  uniqExact(attributes_string['object_id']) AS distinct_objects
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'authz.granted'
  AND attributes_string['owner_id'] != ''
  AND attributes_string['owner_id'] != attributes_string['enduser.id']   -- accessed someone else's object
  AND timestamp >= now64() - INTERVAL 30 MINUTE
GROUP BY principal, ip
HAVING ip != '' AND cross_owner_reads >= 5
```

**Traffic-based — privilege boundary opened: 403 → 200 (Tier 1, trace-only)**. The detector for
*sudden escalation* when no role-change event fires: the same client, on the same restricted template,
is first **denied (403)** and then **allowed (200)** within the window — a boundary that just opened
(a mid-session role grant, a stolen higher-privilege session replayed, or a gate that regressed). Pure
traffic, no events. Test-first (re-auth and fixed-typo retries can trip it; tune per app):

```sql
WITH
  coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], '')) AS client_ip,
  splitByChar('?', attributes_string['http.target'])[1] AS path,
  if(attributes_string['http.route'] != '', attributes_string['http.route'],
     replaceRegexpAll(replaceRegexpAll(lower(path),
       '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', '/:uuid'),
       '/[0-9]+', '/:id')) AS route_tmpl
SELECT ip, route_tmpl, denied_at, allowed_at
FROM (
  SELECT client_ip AS ip, route_tmpl,
         minIf(timestamp, response_status_code = '403') AS denied_at,
         maxIf(timestamp, response_status_code = '200') AS allowed_at
  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 response_status_code IN ('200','403')
  GROUP BY ip, route_tmpl
)
WHERE ip != '' AND denied_at > 0 AND allowed_at > denied_at
```

Useful attributes: `event.type`, `enduser.id`, `session.id`, `http.client_ip`, `http.target`,
`http.route`,
`response_status_code`, `client.asn`, `client.as_org`, and your authz attributes
(`decision`, `role`, `tenant`, `resource_type`, `object_id`, `owner_id`, `action`, `reason`).

#### Emit every detection as a ready-to-copy command unit

Each detection becomes a **complete, copyable unit** — never a fragment. For every rule emit, in
order: (1) the SQL, (2) a 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; in the Detection HTML each is wrapped in a `.cmd` with a Copy
button. The exact flags **must match `securenow alerts rules --help` from Phase 0.5** — never a
guessed flag. Save each rule's SQL to `rules/<name>.sql` so `--sql @rules/<name>.sql` resolves.
Note pre-existing/system rules (from Phase 0) instead of duplicating them. Example unit:

```sql
-- rules/authz-bola-enum.sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['enduser.id']     AS principal,
  uniqExact(attributes_string['object_id']) AS distinct_objects,
  count() AS attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] IN ('authz.denied','authz.granted')
  AND attributes_string['resource_type'] != ''
  AND timestamp >= now() - INTERVAL 15 MINUTE
GROUP BY ip, principal
HAVING ip != '' AND distinct_objects >= 30
```

```bash
securenow alerts rules create \
  --name "Authz: BOLA object enumeration (single principal)" \
  --sql @rules/authz-bola-enum.sql \
  --apps <APP_KEY> \
  --severity high \
  --schedule "*/15 * * * *" \
  --nlp "one principal accessing 30+ distinct object IDs in 15 minutes"

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

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

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

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

**Rule of thumb:** any detection that can **false-positive** — heuristic thresholds (BOLA
enumeration / 401-403 probe / cross-tenant-attempt 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 (shared-NAT 403 bursts, bulk-export users, internal
scanners), then `--mode prod` to arm mitigation. Only **high-precision** rules (exploit-signature
SQLi/XSS/RCE matches, exact-match IoCs, self-service `to_role=admin` elevation with `self=true`,
known-bad ASN hits) may go straight to `prod`. In the report, **tag each 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. The full SecureNow mitigation toolbox (select per threat)

For authorization abuse, the SecureNow control plane **contains the actor**; the **app fix**
restores the missing check. Always pair them. Once a threat is confirmed, **choose the narrowest
effective mitigation(s) from ALL of these** and combine them (e.g. rate-limit the `/api/invoices`
enumeration 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 (e.g. `/admin*`, `/internal*`); 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/invoices --mode prefix --method GET --limit 30 --window 5m --key-by ip` | cap an expensive/enumerable endpoint for everyone, budgeted per IP. |
| 8 | **Rate limit — per route + IP** | `securenow ratelimit add <ip> --route /api/invoices --mode exact --method GET --limit 30 --window 5m --duration 24h` · NL `securenow ratelimit from-text "rate limit /api/invoices to 30/5m for 24h" --yes` · test `securenow ratelimit test <ip> --path /api/invoices --method GET` | precise throttle of one client on one route. |
| 9 | **CAPTCHA / proof-of-work challenge** | `securenow challenge add --route /api/invoices --difficulty 16 --clearance 30m` (route-wide) **or** `securenow challenge add <ip> --route /api/invoices --difficulty 18 --clearance 30m` · test `securenow challenge test <ip> --path /api/invoices --method GET` | BOLA enumeration / scraping of authorized endpoints 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 session <id> --reason "..."` / `securenow revoke user <id>` / `revoke list` / `revoke restore <id>` (SDK `securenow/sessions` `guard()` / `isRevoked()`) | a principal caught escalating privilege or crossing tenants — kill the stolen/abusing session, not the IP. Needs `guard()` in the app. |
| 12 | **Trusted IP (suppress)** | `securenow trusted add <ip> --label "Office VPN / partner / pentest box / monitor"` | stop false positives from known-good infra (internal scanners, QA) — 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 missing check: ownership scope, tenant scope, server-side role guard, field allow-list, path normalization, step-up re-auth, retire route. SecureNow contains; the fix removes. |

**Choosing per threat** — by **confidence**: exploit-signature/exact IoC → instant-block or
block; self-service `to_role=admin` elevation with `self=true` → block + `revoke`; probable BOLA
bot on shared egress → **challenge**; noisy/legit-mixed traffic (403 spikes from shared NAT,
bulk-export users) → **rate-limit (test-mode first)**; session/tenant 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) — SecureNow can only contain the actor; the app fix restores the missing
authorization check. All commands here are verified against the installed SDK in Phase 0.5.

### 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 authz events — exercise an events-based rule end to end (BOLA enumeration):
for i in $(seq 1 35); do
  securenow event send authz.denied --user attacker@example.com \
    --session test-$i --ip 203.0.113.77 \
    --attrs decision=deny,resource_type=invoice,object_id=inv-$i,owner_id=victim,action=read,reason=not_owner,test=true
done

# Synthetic privilege elevation:
securenow event send authz.elevation --user attacker@example.com --ip 203.0.113.77 \
  --attrs target=attacker@example.com,from_role=member,to_role=admin,self=true,test=true

# Synthetic collaboration/teams abuse — invite rebinding + member self-escalation + active-org tampering:
securenow event send membership.invite.accepted --user attacker@example.com --ip 203.0.113.88 \
  --attrs org_ref=org-acme,granted_role=admin,invited_ref=victim,identity_match=false,test=true
securenow event send membership.role.changed --user attacker@example.com --ip 203.0.113.88 \
  --attrs org_ref=org-acme,target_ref=attacker,from_role=member,to_role=admin,self=true,actor_role=member,test=true
for i in $(seq 1 5); do
  securenow event send org.context.switched --user attacker@example.com --ip 203.0.113.88 \
    --attrs org_ref=org-$i,member=false,source=header,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 (forced browsing / admin paths) — simulate, then check the pipeline:
securenow test-span "threat-model.authz.smoke"
securenow forensics "401 and 403 responses by IP in the last hour" --env production

# Mitigation verification:
securenow challenge test 203.0.113.77 --path /api/invoices --method GET
securenow ratelimit test 203.0.113.77 --path /api/invoices --method GET
securenow firewall test-ip 203.0.113.77 --app <APP_KEY> --env production
securenow revoke list

# 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 deliverables (two tracks)

Write **two tracks**, each as Markdown **and** self-contained HTML, into `threat/02-authorization/`:

- **Track A — Detection & Mitigation** (the operational runbook: what to run in SecureNow):
  `authorization-detection-mitigation.md` + `authorization-detection-mitigation.html`.
- **Track B — Code Findings & Recommendations** (the code audit + recommendations):
  `authorization-code-findings.md` + `authorization-code-findings.html`.

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. Both HTML
files are self-contained (inline CSS + JS, no network) with **copy buttons** on every command
block. Stats cards must equal the table/finding row counts in each report.

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

1. **Executive summary** — stats line (threats modeled · covered · partial · gaps · rules to
   create · mitigations), the top 3 *detectable* access-control risks, the installed `securenow`
   version + app key + firewall state.
2. **SDK & environment** — installed SDK version (from `node_modules/securenow` via Phase 0.5),
   app key(s), environment, firewall state, existing rules / automations / challenge rules (from
   Phase 0), and any system signature rules present.
3. **Threat → Detection → Mitigation matrix** — one row per modeled threat:
   `# | Threat | OWASP/CWE | Coverage 🟢/🟡/🔴 | Tier (1 trace / 2 event / 3 code) | Detection rule | Signal (threshold+window) | Schedule | Sev | Mode | Mitigation`.
   The **Tier** cell states what the detection needs (1 = traces only / no app code, 2 = `authz.*`/`tool.*`
   bridge, 3 = code fix only) — so the reader sees at a glance what lands with zero code vs the bridge vs a
   pure app fix. Severity ∈ {critical, high, medium, low}. Each row's **Mitigation** cell must pick
   **specific, scoped mitigation(s)** from the Phase 4c toolbox (by number/name, scoped to the
   route/method/IP/duration that applies) — never a generic "block the IP." The **Mode** cell
   tags the rule **`test-first`** (false-positive-prone — heuristic thresholds, broad patterns,
   anomaly/volume) or **`prod-ready`** (high-precision — exact IoC, signature, self-service
   `to_role=admin`+`self=true`); test-first rules carry the `--mode test` → observe (3–7 days) →
   `--mode prod` promotion workflow. Then the "Out of scope" N/A list and any rows deferred to a
   sibling threat model.
4. **Detection rules to create** — each as the **ready-to-copy unit** from Phase 4 (SQL → save to
   `rules/<name>.sql` → full `securenow alerts rules create …` → `alerts rules test … --mode
   dry_run`). Tag each rule **`test-first`** or **`prod-ready`** (per Phase 4b-test); for every
   `test-first` rule include the `--mode test` → observe (3–7 days) → `--mode prod` promotion step
   in its unit. Note rules that already exist (from Phase 0) instead of duplicating them; injection-
   class rows reference the system signature rules + `instant.block` rather than duplicate SQL.
5. **Instrumentation the detections need** — only the `track('authz.*')` / `guard()` events the
   rules above consume, each as a copyable snippet; point to the **code-findings report** for
   *where* (file:line) to add them.
6. **Mitigation mechanisms** — render the **full Phase 4c mitigation toolbox table** (all 15 rows:
   free firewall · exploit-signature `instant.block` · IP block [global / route+method / temporary]
   · rate-limit [per-IP / per-route / route+IP] · challenge · auto-block · revoke · trusted ·
   allowlist · fp · **app-config fix**) plus, per threat, the **specific, scoped** ready-to-copy
   mitigation command(s) selected from it and their reversibility. Make explicit that the app fix is
   primary and the SecureNow control is containment.
7. **Action plan (copy-paste, ordered)** — ① firewall + signature instant-block, ② add
   instrumentation, ③ create rules — **create every false-positive-prone (`test-first`) rule in
   `--mode test`** (detect-only), prod-ready rules armed, ④ enable automations / challenge, ⑤ test,
   ⑥ verify, ⑦ **promote to prod after N days** — an explicit step that, after 3–7 days of observed
   real traffic + threshold tuning + `securenow fp` exclusions, runs
   `securenow alerts rules update <RULE_ID> --mode prod` on each test-first rule, ⑧ schedule the
   app/config fixes (from the code report). Real commands only, `<APP_KEY>` already substituted.
8. **Testing & validation** — per-rule recipe (`securenow event send …` / `test-span` / dry-run)
   + expected outcome + cleanup, using TEST-NET IPs (`192.0.2`/`198.51.100`/`203.0.113`).
9. **Response runbooks** — per notification type: confirm TP → respond command (copy) → reverse
   command (copy).
10. **Known gaps & SecureNow feature requests** — each 🔴: why it is not coverable today, the
    interim fix (link to the code report), and the "contact the SecureNow team" line.
11. **Appendix** — resolved SDK/CLI version, app key, environment, rule IDs created, date.

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

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

1. **Executive summary** — findings by severity (critical / high / medium / low), the top 3 code
   risks, one-paragraph posture verdict.
2. **Surface & inventory** — the Phase 1 inventory for this domain (model / roles / tenancy /
   ownership checks / enforcement points / sensitive surfaces / field control).
3. **Threat catalog** — the exhaustive Phase 2 catalog (grouped A–M, each tagged OWASP/CWE,
   modeled or explicit N/A). Drop nothing.
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).
5. **Strengths** — access controls already present and correct (honest posture).
6. **App / config fixes (primary remediation)** — the config/code changes that remove the root
   cause (described, not applied): ownership scope, tenant scope, server-side role guard, field
   allow-list, path normalization, step-up re-auth — each linked to the detection-report row it
   backs.
7. **Instrumentation recommendations** — the `track('authz.*')` / `guard()` calls to add and the
   exact file:line to add them, so the detection rules light up.
8. **Appendix** — files reviewed, resolved SDK version, date, link to the detection-mitigation
   report.

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

Both HTML files share the `<head>` below (brand tokens + copy-button styles) and the copy
`<script>` at the end of `<body>`. Produce **two** files from it: change only the `<title>`, the
sidebar subtitle, the header, the stats cards, and the section content. **Wrap EVERY
command/SQL/code block as a `.cmd`** (so it gets a Copy button). Stats numbers must equal the
table/finding row counts.

```html
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
<title><!-- "Detection & Mitigation — Authorization — SecureNow" OR "Code Findings — Authorization — 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 · Authorization" OR "Code Findings · Authorization" --></div>
    <!-- one <a href="#…"> per section of THIS track -->
  </nav>
  <main>
    <header class="top"><h1><!-- report title --></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 --></div>
    <!-- <section id="…"> blocks mirroring the Markdown sections of THIS track (5a or 5b) -->
    <footer>Generated by the SecureNow authorization threat-model prompt · <!-- date --> · securenow <!-- version --> · app <code><!-- APP_KEY --></code></footer>
  </main>
</div>
<script>
document.querySelectorAll('.copy').forEach(function(b){b.addEventListener('click',function(){
  var pre=b.parentElement.querySelector('pre'); if(!pre)return; var t=pre.innerText;
  function done(){b.textContent='Copied';b.classList.add('done');setTimeout(function(){b.textContent='Copy';b.classList.remove('done');},1500);}
  function fb(){var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();try{document.execCommand('copy');}catch(e){}document.body.removeChild(ta);done();}
  if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(done,fb);}else{fb();}
});});
</script>
</body></html>
```

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

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

Badge usage: severity `<span class="b crit|high|med|low">`; coverage
`<span class="c cov|part|gap">COVERED|PARTIAL|GAP</span>`; OWASP `<span class="owasp">API1</span>`;
CWE `<span class="cwe">CWE-639</span>`; mitigation
`<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 Code-Findings
HTML may omit copy buttons on prose, but still wraps any example/fix command in `.cmd`.

---

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

- Every catalog item A1–M60 is either a matrix row or an explicit N/A line.
- Every matrix row has a concrete signal (threshold + window), severity, and mitigation — no
  "monitor for suspicious activity" filler.
- Every code finding 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 and selects an `ip` column.
- Only commands, flags, events, and SQL columns from this prompt's building blocks appear
  (including `securenow challenge …`).
- Detection vs. fix is honest: where SecureNow can only contain the actor, the row says so and
  pairs the control with the **app fix**.
- Every 🔴 gap appears in the gaps section with an interim app 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 **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 …` → dry-run test); flags match `alerts rules --help`.
- **Four** files are written to `threat/02-authorization/` (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**; the stats cards match the table/finding counts.
- 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** (Phase 4c §6: firewall ·
  instant-block · block [global / route / method / temporary] · rate-limit [IP / route / IP+route] ·
  challenge · auto-block · revoke · trusted · allowlist · fp · app-fix), and **each modeled threat's
  matrix row selects specific, scoped mitigation(s) from it** — never a generic "block the IP."
- **Every false-positive-prone rule is tagged `test-first`** and carries the `--mode test` → observe
  (3–7 days) → `--mode prod` promotion workflow; only high-precision rules are `prod-ready`. The
  action plan creates the test-first rules in `--mode test` and has an explicit "promote after N
  days" step.
- A one-line summary is printed back: per-track file paths, threat counts, rules-to-create count,
  code findings by severity, gaps, and the resolved SDK version.

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