# Secrets & Cloud IAM 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 installed (`node_modules/securenow`) and the CLI logged in. The agent will first
**ground every rule and command in the SecureNow SDK actually installed in the repo** (never
guessing flags, subcommands, event names, or SQL columns), inventory every place the project
**stores, distributes, and exposes secrets**, map its **cloud identity (IAM)** posture, build an
exhaustive **secrets & cloud-IAM** threat model mapped to the **OWASP Top 10:2021 + API
Security Top 10:2023**, audit the code/config for credential and IAM flaws, and emit a
SecureNow-branded **two-track** deliverable set in **Markdown + self-contained HTML** (with
offline copy buttons) — a **Detection & Mitigation** runbook (the few detection rules SecureNow
*can* run here as **ready-to-copy** command units, the mitigation/containment commands, how to
test each one) and a **Code Findings & Recommendations** report (the code-level findings, audited,
**not** fixed) — including which threats need a **secret-store / cloud-IAM config fix** or the
SecureNow team.

> **Read this first — coverage reality.** SecureNow is fundamentally an **API / traffic**
> security layer. This domain is mostly **storage, distribution, and cloud-control-plane**
> posture, which produces little or no inbound HTTP traffic. So unlike the API/auth models, this
> one is **LOW–MEDIUM native coverage**: SecureNow natively catches **SSRF-to-cloud-metadata
> attempts** (the `ssrf.blocked` event — IMDS `169.254.169.254`, GCP/Azure metadata → credential
> theft, **API7:2023**) and **credential exposure inside telemetry** (via the SDK redaction
> posture — **A02:2021**). **Everything else** — hardcoded secrets, frontend-exposed keys, missing
> vault/KMS, no rotation, over-broad IAM roles, privilege-escalation paths, cross-account trust,
> long-lived static keys — is a **secret-store / cloud-IAM configuration fix** that this report
> *audits and prescribes* but SecureNow does **not** mitigate at the edge. The deliverable here is
> heavily an **audit + findings + config-fix plan**, with edge containment paired on the one
> surface (IMDS SSRF) where SecureNow adds real signal.

This is one of a family of threat models. **Secrets & Cloud IAM** owns *how credentials and cloud
identity are stored, scoped, and exposed*. It **defers** CI/CD pipeline secret handling to the
[supply-chain & CI/CD model](../22-supply-chain-cicd/), secrets-in-logs leakage to the
[storage & logs model](../23-storage-and-logs/), compute/network/cloud-resource posture to the
[cloud-infrastructure model](../19-cloud-infrastructure/), and inbound webhook signature
**verification** to the [webhooks model](../16-webhooks/) — though it owns the **secret** behind
those signatures. Run those too; together they cover the platform surface.

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 Secrets & Cloud IAM Threat Model Report (SecureNow)

You are a senior application-security engineer specializing in secrets management and cloud
identity. Produce an **exhaustive secrets & cloud-IAM threat model for THIS codebase**, organized
along **OWASP Top 10:2021 — A02 Cryptographic Failures, A05 Security Misconfiguration, A07
Identification & Authentication Failures — and OWASP API Security Top 10:2023 — API7 SSRF (cloud
metadata)**, mapped to **SecureNow** detections/mitigations where they exist and to
**secret-store / cloud-IAM config fixes** everywhere else, with a ready-to-run action plan **and**
a code-level audit of every credential and IAM flaw you find. **Ground every alert rule, flag,
event name, and SQL column in the `securenow` SDK that is actually installed in this repo**
(`node_modules/securenow`, Phase 0.5) — never guess. Emit every detection as a **ready-to-copy**
unit (SQL → save to `rules/<name>.sql` → full `securenow alerts rules create …` → dry-run test).

You write **four** deliverables — **two tracks** — into `threat/20-secrets-and-cloud-iam/`
(create the folder if needed):

1. `secrets-and-cloud-iam-detection-mitigation.md` — the **operational runbook**: what to run in
   SecureNow (the few detection rules to create as ready-to-copy command units, the
   mitigation/containment commands, testing, response runbooks).
2. `secrets-and-cloud-iam-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**, using the SecureNow branding skeleton given at the end of this prompt.
3. `secrets-and-cloud-iam-code-findings.md` — the **code audit**: credential & IAM issues found in
   the codebase/config + recommended fixes (described, never applied; secret values fingerprinted,
   never printed).
4. `secrets-and-cloud-iam-code-findings.html` — the same audit as a self-contained HTML page.

The two tracks **cross-link** each other: gap/instrumentation rows in the detection report link
to the relevant code finding, and each code finding (the secret-store / cloud-IAM config fix)
links back to the detection-report row it backs.

Work in the phases below, in order. **Never invent facts**: if a secret, IAM policy, or
control is not in the codebase/config or not returned by a CLI command, say "not found" — do not
guess, and **do not print any real secret value** into the report (mask to a fingerprint: first 4
chars + length + entropy class). **Do not modify application code, secrets, or cloud config.** You
are auditing: every fix is *described in the report*, never applied.

**Scope discipline.** This model owns: secret storage/distribution/exposure (source, config,
images, git history, frontend bundles, URLs, telemetry), secret-manager/KMS/rotation posture,
cloud-IAM role/policy scope, IAM privilege-escalation paths, cross-account trust & confused-deputy,
SSRF→cloud-metadata credential theft, long-lived vs short-lived credentials, machine-identity
over-scoping, and the **secrets behind** webhook/API/signing. It does **not** re-derive: CI/CD
pipeline secret handling (**defer to** [../22-supply-chain-cicd/](../22-supply-chain-cicd/)),
secrets surfacing in **logs/log stores** (**defer to**
[../23-storage-and-logs/](../23-storage-and-logs/)), compute/network/bucket-resource
configuration (**defer to** [../19-cloud-infrastructure/](../19-cloud-infrastructure/)), and
inbound webhook signature **verification logic** (**defer to**
[../16-webhooks/](../16-webhooks/)). List each in a "Deferred to sibling models" subsection,
link those reports, and model only the **observable abuse** SecureNow can add (the IMDS-fetch
attempt, the credential string surfacing in telemetry).

---

## 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) + redaction posture
```

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. From `securenow env --json` note the
**SDK version and redaction posture** (does the pipeline strip `Authorization`, `Cookie`,
`X-API-Key`, bearer tokens, secrets, connection strings before ingest?) — this is the backbone of
the "credential exposure in telemetry" coverage and is the single most important SecureNow control
in this domain. Note the **firewall state**; the firewall and signature rules do little for
secrets/IAM directly (this domain emits almost no inbound traffic) — the SSRF-to-metadata path is
the exception.

---

## 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 (e.g. `ssrf.blocked`, `api.upstream.error`), the
`securenow alerts rules` SQL columns, and every mitigation subcommand are discoverable there.
Cross-check before emitting.

---

## Phase 1 — Inventory the secrets & cloud-IAM surface (codebase + config analysis)

Secrets security starts with finding **every credential and every identity**. Document what is
**actually present and reachable**, not what policy intends. **Do not exfiltrate or print secret
values — fingerprint them.** Cover at minimum:

- **Secret material catalog** — enumerate every secret-shaped string and its home: API keys,
  cloud access keys (`AKIA…`/`ASIA…`, GCP service-account JSON, Azure client secrets),
  DB/connection strings, JWT/HMAC signing secrets, webhook signing secrets, OAuth client secrets,
  TLS private keys, SSH keys, encryption keys, third-party tokens (Stripe, Twilio, SendGrid,
  OpenAI…). For each: **where** (source file, `.env`, config map, container image layer,
  Dockerfile `ENV`/`ARG`, k8s manifest, Terraform, CI variable, committed file), **scope**
  (per-env? shared? per-tenant?), **lifetime** (static/long-lived vs short-lived/rotated),
  **and storage** (plaintext file, env var, secret manager, KMS-encrypted, vault).
- **Hardcoded-secret scan** — grep source, config, fixtures, tests, notebooks, IaC, and
  `Dockerfile`s for embedded credentials and high-entropy strings. Check `.env*`,
  `*.pem`/`*.key`, `serviceAccount*.json`, `*.tfvars`, `docker-compose*.yml`, k8s `Secret`
  manifests with base64 (note: base64 ≠ encryption). Flag any committed `.env` that should be
  gitignored.
- **Git history exposure** — determine whether secrets were **ever committed** (even if later
  removed): scan history/blobs for the patterns above. A removed-but-committed secret is still
  leaked unless rotated. (Deep history/CI-variable analysis: defer to
  [../22-supply-chain-cicd/](../22-supply-chain-cicd/) — but list the finding here.)
- **Frontend / client exposure** — identify any secret reachable by a browser or mobile client:
  values in client bundles, **source maps**, public env (`NEXT_PUBLIC_*`, `VITE_*`,
  `REACT_APP_*`, `EXPO_PUBLIC_*`, `PUBLIC_*`), inline `<script>` config, mobile app strings/asset
  bundles, and any server key mistakenly shipped where a publishable/anon key belongs. Distinguish
  **publishable** keys (safe) from **secret/service** keys (must never ship).
- **Secrets in URLs / transit side-channels** — find credentials placed in query strings, path
  segments, `Referer`-leaking URLs, redirect targets, analytics/marketing pixels, third-party
  script params, or pre-signed URLs that embed long-lived creds. (Secrets landing in **logs**:
  defer to [../23-storage-and-logs/](../23-storage-and-logs/); secrets landing in **SecureNow
  telemetry**: owned here via the redaction posture.)
- **Secret-manager / vault / KMS posture** — is there a secret manager (AWS Secrets Manager / SSM
  Parameter Store SecureString, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, Doppler,
  1Password, Infisical)? Are secrets **encrypted at rest** (KMS/CMK) or plaintext? Is there a
  **rotation** policy/automation, or are keys static forever? Note where **none** exists.
- **Cloud identity inventory (IAM)** — enumerate IAM principals the app uses: roles, instance
  profiles, service accounts, machine identities, and any **static user access keys**. For each,
  capture the **attached policies** (managed + inline) and their **action/resource scope**.
  Source from IaC (Terraform/CloudFormation/Pulumi/CDK), k8s `ServiceAccount`/IRSA annotations,
  and runtime config. If IAM lives only in the cloud console (not IaC), say so and audit what is
  discoverable.
- **Policy-scope analysis** — for every policy, flag **wildcards**: `Action: "*"`,
  `"<service>:*"` (e.g. `s3:*`, `iam:*`), `Resource: "*"`, `Effect: Allow` with no condition,
  `NotAction`/`NotResource` traps, and admin-equivalent managed policies
  (`AdministratorAccess`, `PowerUserAccess`). Note least-privilege violations precisely.
- **IAM privilege-escalation paths** — look for escalation primitives granted to the app's
  principal: `iam:PassRole` (esp. with `Resource:"*"`), `iam:CreatePolicyVersion` /
  `SetDefaultPolicyVersion` / `AttachUserPolicy` / `PutUserPolicy` (policy self-edit),
  `sts:AssumeRole` chains, `lambda:CreateFunction`+`PassRole`, `ec2:RunInstances`+`PassRole`,
  `iam:CreateAccessKey` for other users. Map who-can-become-whom.
- **Cross-account trust & confused-deputy** — inspect trust policies (`AssumeRolePrincipals`):
  third-party/cross-account roles **without an `sts:ExternalId` condition**, overly broad
  `Principal: {"AWS":"*"}`, missing `aws:SourceArn`/`aws:SourceAccount` conditions on
  service-to-service trust. These are confused-deputy holes.
- **Credential lifetime & distribution** — long-lived static cloud keys baked into env/images vs
  short-lived role credentials (instance profile / IRSA / workload identity / OIDC federation /
  `AssumeRoleWithWebIdentity`). Flag any static `AKIA…` user key in app config — that is the
  highest-value standing credential.
- **Machine-identity / service-account scoping** — service accounts shared across environments
  (one SA for dev+prod), one over-scoped SA used by everything, secrets reused between tenants,
  and absence of **per-tenant** signing/API secrets (one global HMAC = one leak compromises all
  tenants).
- **SSRF → cloud-metadata sinks** — every place the server fetches a **user-influenced URL**
  (webhook delivery, link/URL preview, image/PDF proxy, import-from-URL, SVG/HTML render, SSO
  metadata fetch, avatar-by-URL). Each is a path to `169.254.169.254` (AWS IMDS),
  `metadata.google.internal` / `169.254.169.254` (GCP), `169.254.169.254/metadata` (Azure, needs
  `Metadata:true` header). Check for an **SSRF guard** (host/IP allowlist, link-local/private-range
  block, redirect disabling) and for **IMDSv2 enforcement** (`HttpTokens: required`,
  `HttpPutResponseHopLimit: 1`) which neutralizes IMDSv1 SSRF.
- **Webhook / API / signing secrets** — locate the secrets behind signature verification (the
  *verification logic* is deferred to [../16-webhooks/](../16-webhooks/); the **secret** is owned
  here): is it hardcoded, per-tenant, rotated, stored in a manager? A leaked signing secret lets an
  attacker forge valid webhooks.
- **Telemetry privacy & redaction (SecureNow's primary control here)** — confirm whether the
  SecureNow SDK / log pipeline redacts `Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`,
  bearer tokens, API keys, connection strings, cloud access keys, signing secrets, and
  secret-shaped body/header/attribute fields **before ingestion**. If redaction is absent or
  partial, that is a **high-severity finding** (secrets would flow into SecureNow itself).
- **SecureNow instrumentation already present** — `securenow/register` / `securenow run` /
  `securenow init`, any `securenow/events` `track()` calls (esp. an existing `ssrf.blocked`), and
  the firewall state. This determines what works *today* vs *after instrumentation*.

Output of this phase = the report's **Secrets & IAM surface & inventory** section: the
**secret-material catalog** (kind / location / scope / lifetime / storage — values fingerprinted,
never shown), the **hardcoded-secret + git-history findings**, the **frontend-exposure list**, the
**secret-manager/KMS/rotation posture**, the **IAM principal & policy table** (principal / policy /
wildcards / escalation primitives), the **cross-account-trust posture**, the **credential-lifetime
posture**, the **SSRF→metadata sink list**, the **per-tenant-secret posture**, the **telemetry
redaction status**, and a short paragraph naming the real credential-theft 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. "GCP/Azure metadata items: N/A, AWS-only deployment"). Never silently drop an
item. Add stack-specific threats you discover — this catalog is the floor, not the ceiling. Tag
each modeled row with its framework code: **A02:2021**, **A05:2021**, **A07:2021**, or
**API7:2023**, or "—".

**A. Hardcoded & embedded secrets (A02:2021 / A05:2021)**
1. Secret hardcoded in application source (API key, token, password literal)
2. Secret in committed config / `.env` / `.tfvars` / `docker-compose` checked into the repo
3. Secret baked into a container image layer (Dockerfile `ENV`/`ARG`, copied `.env`, build cache)
4. Secret present in **git history** (committed then removed but never rotated)
5. Secret in fixtures, seeds, tests, notebooks, or sample/demo files reachable in prod builds
6. Secret in k8s `Secret`/ConfigMap stored only base64-encoded (no encryption-at-rest / not in a manager)

**B. Frontend / client-side secret exposure (A02:2021)**
7. Secret/service key shipped in a client JS/CSS bundle
8. Secret leaked through a published **source map**
9. Secret in public/build-time env (`NEXT_PUBLIC_*`, `VITE_*`, `REACT_APP_*`, `EXPO_PUBLIC_*`)
10. Secret embedded in a mobile app binary / asset bundle / strings
11. Server (secret) key used where a publishable/anon key belongs (confusion of key class)

**C. Secrets in URLs, transit & side-channels (A02:2021)**
12. Credential/token in a query string or path segment (leaks via history, proxies, referrer)
13. Secret leaked through the `Referer` header to a third party
14. Secret sent to analytics/marketing pixels or third-party scripts as a parameter
15. Credentials embedded in a pre-signed/long-lived URL handed to clients
16. Secret captured by SecureNow telemetry without redaction (owned here — redaction posture)
17. Secret leaked into application logs / log store (**deferred → [../23-storage-and-logs/](../23-storage-and-logs/)**)

**D. Secret storage, encryption & rotation (A02:2021 / A05:2021)**
18. No secret manager / vault — secrets in plaintext env/files
19. Secrets at rest not KMS/CMK-encrypted (plaintext or weak/base64 "encryption")
20. No rotation policy — long-lived static secrets that never expire
21. Single shared secret across all environments (dev/staging/prod) or all services
22. No per-tenant secrets — one global signing/HMAC/API secret (one leak = all tenants compromised)
23. Secrets distributed to too many consumers / over-broad read access on the secret store

**E. Over-broad IAM roles & wildcard policies (A05:2021)**
24. `Action: "*"` / `Resource: "*"` policy (admin-equivalent) attached to the app principal
25. Service-wildcard action (`s3:*`, `iam:*`, `dynamodb:*`) far beyond what the app needs
26. Admin-equivalent managed policy (`AdministratorAccess` / `PowerUserAccess`) on a runtime role
27. `Effect: Allow` with no `Condition` where one is warranted (no IP/MFA/tag/account guard)
28. `NotAction` / `NotResource` allow-trap granting more than intended

**F. IAM privilege-escalation paths (A05:2021)**
29. `iam:PassRole` (esp. with `Resource:"*"`) → pass a higher-privileged role to a service
30. Policy self-edit: `iam:CreatePolicyVersion`/`SetDefaultPolicyVersion`/`AttachUserPolicy`/`PutUserPolicy`
31. `sts:AssumeRole` chain reaching a more-privileged role from the app principal
32. Compute+PassRole combo (`lambda:CreateFunction`/`ec2:RunInstances`/`glue:*` + `PassRole`)
33. `iam:CreateAccessKey`/`UpdateLoginProfile` on other principals → standing credential minting

**G. Cross-account trust & confused-deputy (A05:2021)**
34. Third-party/cross-account `AssumeRole` trust **without `sts:ExternalId`** (confused deputy)
35. Trust policy `Principal: {"AWS":"*"}` or whole-account principal where a role ARN is needed
36. Service trust missing `aws:SourceArn`/`aws:SourceAccount` condition (service confused-deputy)
37. Over-broad cross-account read on the secret store / KMS key policy

**H. SSRF → cloud metadata → credential theft (API7:2023)**
38. SSRF to AWS IMDSv1 `169.254.169.254` → steal instance-role credentials
39. SSRF to GCP metadata (`metadata.google.internal` / `169.254.169.254`) → SA token theft
40. SSRF to Azure IMDS (`169.254.169.254/metadata`, `Metadata:true`) → managed-identity token
41. SSRF filter bypass to metadata (DNS rebinding, redirect-to-metadata, decimal/hex/octal IP, `[::]`)
42. IMDSv1 still enabled (no `HttpTokens: required` hop-limit-1) — SSRF directly yields creds

**I. Credential lifetime & machine identity (A07:2021 / A05:2021)**
43. Long-lived static cloud user key (`AKIA…`) in app config instead of a role/instance profile
44. No short-lived/federated creds (no IRSA / workload-identity / OIDC `AssumeRoleWithWebIdentity`)
45. Service account / machine identity shared across environments (one SA for dev+prod)
46. One over-scoped service account used by every component (no per-component identity)
47. Standing human/CI credentials with programmatic access and no MFA / no expiry

**J. Webhook / API / signing-secret leakage (A02:2021)**
48. Webhook **signing secret** hardcoded / shared / not rotated (forge valid webhooks if leaked)
49. API/HMAC signing secret global instead of per-tenant (one leak forges for all tenants)
50. Verification *logic* gaps — **deferred → [../16-webhooks/](../16-webhooks/)** (secret owned here)

**K. Negative-space & evasion**
51. Decoy/old key left active after "rotation" (rotated in app but old key still valid in cloud)
52. Secret in an environment variable readable by a child process / subprocess / SSRF-reachable endpoint
53. Metadata endpoint reachable from a sidecar/proxy/debug container that bypasses IMDSv2
54. Encoded/obfuscated secret (base64/hex/split-string) evading naive scanners but trivially recovered

**L. Deferred — modeled in sibling models (reference, do not re-derive)**
55. CI/CD pipeline secret storage, masked-var leakage, pipeline OIDC → [../22-supply-chain-cicd/](../22-supply-chain-cicd/)
56. Secrets surfacing in logs / log aggregation / log retention → [../23-storage-and-logs/](../23-storage-and-logs/)
57. Compute/network/bucket/security-group resource posture → [../19-cloud-infrastructure/](../19-cloud-infrastructure/)
58. Inbound webhook signature **verification** correctness → [../16-webhooks/](../16-webhooks/)

> For 55–58, add **one** matrix row each marked *"deferred — see linked model"*. Note only the
> SecureNow-observable symptom where one exists (e.g. a leaked-secret string surfacing in telemetry
> is caught by the redaction posture owned here; the rest live in the linked report).

**M. Observable abuse (what SecureNow telemetry actually catches — the workhorse rules here)**
59. SSRF-to-metadata attempt blocked by the app's guard (`ssrf.blocked`, `target_host` = link-local) — the one high-signal native detection
60. Repeated outbound-fetch attempts targeting `169.254.169.254` / metadata hostnames from one IP
61. A credential-shaped string appearing in ingested telemetry attributes (redaction-gap canary)
62. Spike of upstream auth failures (`api.upstream.error` 401/403) → a rotated/leaked/expired key in use
63. A request carrying a known secret pattern in URL/params reaching the edge (signature-style match)

> Tag each modeled row with **A02:2021 / A05:2021 / A07:2021 / API7:2023** (storage/exposure →
> A02; manager/rotation/IAM/trust/misconfig → A05; metadata SSRF → API7; credential-lifetime →
> A07). Each item must be a matrix row or an explicit N/A line with a reason — do not silently
> drop any item.

---

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

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

- **Location** — `file:line` (clickable): the source/config/IaC file and line, the role/policy
  name, the env-var name, the Dockerfile/manifest, the SSRF sink handler.
- **Pattern** — quote the 1–8 relevant lines, **with secret values fingerprinted** (first 4 chars
  + length + entropy class — never the full value). State the missing control precisely (e.g.
  "`const STRIPE_KEY = "sk_live_…"` → hardcoded secret in source"; "`Action: "*", Resource: "*"`
  → admin-equivalent inline policy on the runtime role"; "`fetch(req.body.url)` with no host
  allowlist → SSRF to IMDS"; "trust policy has no `sts:ExternalId` → confused deputy";
  "`HttpTokens: optional` → IMDSv1 enabled"; "`NEXT_PUBLIC_API_SECRET` → secret in client bundle").
- **Why exploitable** — the concrete path an attacker takes and what they achieve (which
  credential is stolen, which resource it grants, blast radius).
- **Severity** — critical / high / medium / low (impact × reachability; a live `sk_live`/`AKIA`
  in a public bundle = critical).
- **Recommended fix (described, not applied)** — the specific change: e.g. "move to AWS Secrets
  Manager / Vault and reference at runtime"; "**rotate immediately** — assume the committed key is
  compromised — then purge history"; "replace the static `AKIA…` user key with an instance
  profile / IRSA short-lived role"; "scope the policy to the exact actions+ARNs the app uses,
  remove `*`"; "add an `sts:ExternalId` condition to the cross-account trust"; "enforce IMDSv2
  (`HttpTokens: required`, hop-limit 1)"; "add an SSRF allowlist that blocks link-local
  `169.254.0.0/16` and private ranges and disables redirects"; "split global HMAC into per-tenant
  signing secrets"; "remove the secret from public env; use a server-only var and a server proxy".
  Reference the secure pattern, not a code diff. **You must not edit code, secrets, or cloud
  config.** For any **already-exposed** secret, the fix MUST start with *rotate now*.

If a control exists and is correct (secrets in a manager, KMS-encrypted, IMDSv2 enforced,
least-privilege scoped policies, per-tenant secrets, SSRF allowlist present), note it as a
**strength** — the posture must be honest. Absence of a control where the surface exists is itself
a finding ("no secret manager — all secrets plaintext in `.env`").

Look specifically for:

**Hardcoded / git-history secret flaws** — literals in source/config/IaC/Dockerfiles; committed
`.env`; base64-only k8s secrets; secrets in history. *Fixes must mention* a secret manager,
**immediate rotation of any exposed secret**, history purge (BFG/`filter-repo`), gitignore +
pre-commit secret scanning. (Deep history & CI scanning: defer to
[../22-supply-chain-cicd/](../22-supply-chain-cicd/).)

**Frontend-exposure flaws** — secret/service keys in bundles, source maps, or public env;
server keys shipped client-side. *Fixes must mention* removing the secret from the client, using a
publishable/anon key client-side with a server proxy for privileged calls, disabling source-map
upload of secrets, and rotating any leaked key.

**Storage / rotation flaws** — plaintext at rest, no KMS, no rotation, shared/global secrets.
*Fixes must mention* KMS/CMK encryption, automated rotation, per-environment + per-tenant
isolation, and least-privilege read access on the store.

**IAM scope flaws** — wildcard actions/resources, admin managed policies, condition-less allows.
*Fixes must mention* least-privilege (exact actions + resource ARNs), removing `*`, adding
conditions (account/source-ARN/tag/MFA), and replacing admin policies with scoped ones.

**Privilege-escalation flaws** — `PassRole` (esp. `Resource:"*"`), policy self-edit, AssumeRole
chains, compute+PassRole. *Fixes must mention* constraining `PassRole` to specific role ARNs,
removing self-edit permissions from runtime roles, and breaking AssumeRole chains.

**Cross-account / confused-deputy flaws** — missing `ExternalId`, `Principal:"*"`, missing
`SourceArn`/`SourceAccount`. *Fixes must mention* mandatory `sts:ExternalId`, specific role-ARN
principals, and service source-condition guards.

**SSRF→metadata flaws** — user-influenced outbound fetch with no allowlist; IMDSv1 enabled;
redirects followed; metadata reachable from sidecars. *Fixes must mention* an outbound-URL
allowlist that blocks `169.254.0.0/16` + private/link-local ranges, disabling redirects, DNS-rebind
protection, and **enforcing IMDSv2** (`HttpTokens: required`, `HttpPutResponseHopLimit: 1`) as the
durable fix. **Pair with the SecureNow `ssrf.blocked` event** so the attempt is detectable.

**Credential-lifetime flaws** — static `AKIA…` keys, no federation, shared/over-scoped SAs.
*Fixes must mention* instance-profile/IRSA/workload-identity short-lived creds, OIDC federation
for CI, per-component service accounts, and MFA + expiry on any human/CI programmatic creds.

**Telemetry-redaction flaws** — pipeline ingesting secrets unredacted. *Fixes must mention*
configuring the SecureNow SDK redaction list to drop auth headers, cookies, API keys, connection
strings, cloud keys, and secret-shaped attributes **before** ingest, and hashing/omitting any
secret before it becomes a `track()` attribute.

---

## Phase 4 — Map every modeled threat to SecureNow detection + mitigation (or config fix)

Classify each threat with exactly one coverage badge. **Be honest — this domain is config-fix-heavy.**

- 🟢 **COVERED** — detectable + actionable with SecureNow today. In this domain this is
  **narrow**: SSRF-to-metadata **attempts** via the `ssrf.blocked` event (you provide the
  detection SQL), and **credential exposure in telemetry** via the SDK **redaction posture**
  (a config of SecureNow itself). Do not over-claim 🟢.
- 🟡 **PARTIAL** — SecureNow can **detect/contain at the edge** but the **real fix is a
  secret-store / cloud-IAM config change**: e.g. it can rate-limit/block the IP probing the SSRF
  sink, or alert on repeated upstream-auth failures (leaked-key-in-use), while the durable fix is
  the SSRF allowlist + IMDSv2 / rotating the key. Also use 🟡 when detection needs instrumentation
  the customer must add (`ssrf.blocked`, `api.upstream.error`).
- 🔴 **GAP** — SecureNow cannot detect or mitigate this (it never touches the cloud control plane,
  the secret store, git history, or client bundles). **This is most of this domain.** Still
  include each: give the **secret-store / cloud-IAM config fix** as primary, 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.

> **Honesty rule for this model.** SecureNow sees **traffic** and **events** and contains actors
> via firewall / rate-limit / challenge / block / signature. It **does not** read your IAM
> policies, your secret manager, your git history, or your frontend bundle. A wildcard IAM policy,
> a hardcoded key, a missing `ExternalId`, an unrotated secret, an IMDSv1 setting — these are
> **config/app fixes**, full stop. The **only** native signals are: the **SSRF attempt to
> metadata** (an outbound fetch your guard blocks and emits as `ssrf.blocked`), a
> **credential string surfacing in telemetry** (caught/prevented by the redaction posture), and
> the **edge containment** of the IP probing your SSRF sink. **Pair every edge-containment row
> with the config fix**, and never present an IAM/secret-store fix as something SecureNow performs.

Use **only** the SecureNow building blocks below. Never invent CLI flags, event names, or SQL
columns — treat `node_modules/securenow` + the `--help` output from Phase 0.5 as the source of
truth for every flag, event name, and SQL column.

**Every detection becomes a ready-to-copy command unit.** For **each** modeled threat that becomes
a SecureNow detection (in this domain: SSRF-to-metadata, upstream-credential-rejection, and the
redaction-gap canary — few, by design), emit a **complete, copyable unit** — never a fragment. For
each rule emit, in order: the SQL, a line saving it to `rules/<name>.sql`, the full
`securenow alerts rules create …` command, and the dry-run test. In Markdown each is its own fenced
block (so it copies cleanly). Example:

````markdown
**Rule: Secrets — SSRF to cloud metadata (IMDS credential theft)**  · 🟢 COVERED · API7:2023 · critical

```sql
-- rules/secrets-ssrf-metadata.sql
SELECT
  attributes_string['http.client_ip'] AS ip,
  attributes_string['target_host']    AS target_host,
  attributes_string['route']          AS route,
  count() AS attempts
FROM signoz_logs.distributed_logs_v2
WHERE resources_string['service.name'] IN (__USER_APP_KEYS__)
  AND attributes_string['event.type'] = 'ssrf.blocked'
  AND attributes_string['target_host'] IN ('169.254.169.254','metadata.google.internal','169.254.170.2','[fd00:ec2::254]')
  AND timestamp >= now() - INTERVAL 30 MINUTE
GROUP BY ip, target_host, route
HAVING ip != '' AND attempts >= 1
```

```bash
securenow alerts rules create \
  --name "Secrets: SSRF to cloud metadata (IMDS credential theft)" \
  --sql @rules/secrets-ssrf-metadata.sql \
  --apps <APP_KEY> --severity critical --schedule "*/5 * * * *" \
  --nlp "an outbound fetch to 169.254.169.254 or cloud metadata was blocked by the SSRF guard"

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

The exact flags must match `securenow alerts rules --help` from Phase 0.5. Save each rule's SQL to
`rules/<name>.sql` so `--sql @rules/<name>.sql` works. Keep the SQL conventions from 4b (correct
tenant-scope column per table + `ip` column + `HAVING ip != ''`; the `ts_bucket_start` + `kind = 2`
guards on traffic queries). **Note pre-existing/system rules** discovered in Phase 0 instead of
duplicating them. The SSRF-to-metadata detection **reuses the `ssrf.blocked` event verbatim** (no
invented secrets-specific event name); the upstream-credential signal reuses `api.upstream.error`.
If the installed SDK lacks a flag this prompt references, emit the rule but annotate it
`# requires securenow >= <version>`. For the **mitigation** side, use the layers table from 4c:
every per-threat mitigation is itself a ready-to-copy command (with `<APP_KEY>` substituted) plus
its reversibility note. **Never put a secret value into a rule, attribute, or command — fingerprint
it; the secret-store / cloud-IAM config fix stays primary for everything that emits no telemetry.**

### 4a. Instrumentation (what the few detections feed on)

Once the app runs under `securenow run` / `securenow/register` / `securenow init`, HTTP traffic is
captured automatically — but secrets/IAM posture emits little inbound traffic, so the high-value
signal here is **events your app emits at its own guards** (never throws). The most important is
the **reused `ssrf.blocked` event** for IMDS/metadata attempts:

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

// Your SSRF guard denied an outbound fetch to cloud metadata (API7) — the #1 native signal here.
// Reuse the SAME 'ssrf.blocked' event the API-security model uses; target_host carries the IMDS IP.
track('ssrf.blocked', { ip, attributes: { route: '/api/preview', target_host: '169.254.169.254', reason: 'link_local|metadata|private_range|dns_rebind' } });

// A consumed upstream/cloud API rejected the credential (leaked/rotated/expired key in use) (A07):
track('api.upstream.error', { ip, attributes: { upstream: 'aws_s3|stripe|gcp|azure', status: '401|403', route: '/api/sync' } });
```

> **Do NOT put secret values into attributes.** Hash or omit any credential before it becomes an
> attribute (target host is fine; a token is not). This is the same telemetry-privacy rule from the
> Phase 1 redaction check — instrumentation must not become a new leak path.

The SecureNow API-security event taxonomy already defines these strings; **reuse them verbatim**
(do not coin secrets-specific event names):

| Event | Emit when |
|---|---|
| `ssrf.blocked` | an outbound fetch to cloud metadata / link-local / private range is denied by your SSRF guard (IMDS credential-theft attempt) |
| `api.upstream.error` | a consumed upstream/cloud call returns 401/403 — a leaked/rotated/expired credential is being used |

Custom `attributes` become queryable as `attributes_string['<key>']` (e.g.
`attributes_string['target_host']`). Ingest enriches every IP with **ASN/org** (`client.asn`,
`client.as_org`) — so an SSRF probe from a datacenter ASN is distinguishable from a real user.

**Redaction is the other half of instrumentation here.** The single most important "instrumentation"
step in this domain is verifying (Phase 1) and hardening the SecureNow SDK **redaction list** so
that `Authorization`, `Cookie`, `X-API-Key`, bearer tokens, API keys, connection strings, and
cloud access keys are stripped **before ingest**. That is what makes "credential exposure in
telemetry" a 🟢 rather than a self-inflicted leak.

### 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 containment keys on). **The tenant-scope column differs by table** — using the
wrong one fails with `UNKNOWN_IDENTIFIER`:

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

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

**Events-based — SSRF-to-metadata attempt blocked (API7) — any hit is high-signal (the marquee rule):**

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

**Events-based — repeated upstream credential rejection (leaked/rotated/expired key in use) (A07):**

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

**Traffic-based — outbound/edge probing of an SSRF sink with metadata-shaped targets in the URL
(catches probing even before the app emits `ssrf.blocked`):**

```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 '%169.254.169.254%'
       OR attributes_string['http.target'] LIKE '%metadata.google.internal%'
       OR attributes_string['http.target'] LIKE '%/latest/meta-data%'
       OR lower(attributes_string['http.target']) LIKE '%metadata%')
GROUP BY ip
HAVING ip != '' AND hits >= 1
```

> **Redaction-gap canary (notify-only).** If you want an early warning that a secret pattern is
> reaching ingest despite redaction, a notify-only rule can match secret-shaped tokens in
> `http.target` (e.g. `LIKE '%AKIA%'`, `LIKE '%sk_live_%'`, `LIKE '%password=%'`). Treat any hit
> as a **redaction failure to fix**, not an attacker to block — route it to the runbook. Keep the
> match patterns generic; never store the matched value.

**Secrets/IAM posture is NOT a SQL detection.** Hardcoded keys, wildcard policies, missing
`ExternalId`, no rotation, IMDSv1 — none of these emit telemetry. They are **config-audit findings**
from Phase 3, fixed in the secret store / cloud IAM. Do not author SQL pretending to detect them;
mark them 🔴 and prescribe the config fix.

Create + validate each rule as the **ready-to-copy command unit** (save the SQL to
`rules/<name>.sql`, then create, then dry-run — see the unit template at the top of Phase 4):

```bash
securenow alerts rules create \
  --name "Secrets: SSRF to cloud metadata (IMDS credential theft)" \
  --sql @rules/secrets-ssrf-metadata.sql \
  --apps <APP_KEY> \
  --severity critical \
  --schedule "*/5 * * * *" \
  --nlp "an outbound fetch to 169.254.169.254 or cloud metadata was blocked by the SSRF guard"

securenow alerts rules test <RULE_ID> --mode dry_run --wait
```

The exact flags must match `securenow alerts rules --help` from Phase 0.5; `--sql @rules/<name>.sql`
reads the SQL you saved to disk. Note pre-existing/system rules instead of duplicating them.

### 4c. The full SecureNow mitigation toolbox (select per threat)

For this domain the **secret-store / cloud-IAM config fix (row 15) is the primary remediation** for
nearly every threat; SecureNow **contains the actor** only where there is traffic — essentially the
**SSRF-to-metadata probe** on a URL-fetch sink. Always pair the edge mitigation with the config fix.
Once a threat is confirmed, **choose the narrowest effective mitigation(s) from ALL of these** and
combine them (e.g. rate-limit the `/api/preview` sink + block the worst IP + 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 probing your SSRF/metadata sink 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 a request carrying a metadata IP / known secret-probe payload in the URL. Don't duplicate pattern SQL. |
| 3 | **IP block — global** | `securenow blocklist add <ip> --app <APP_KEY> --env production --reason "..."` | confirmed-malicious source (e.g. a confirmed SSRF/metadata-theft attempt), all routes. |
| 4 | **IP block — scoped to route (+ method)** | `securenow blocklist add <ip> --route /api/preview --mode prefix --method ALL --app <APP_KEY> --env production --reason "..."` (`--mode exact\|prefix\|regex`, `--method GET\|POST\|…\|ALL`) | block an IP only on the SSRF/URL-fetch path; 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/preview --mode prefix --method GET --limit 60 --window 1m --key-by ip` | cap the URL-fetch endpoint for everyone, budgeted per IP. |
| 8 | **Rate limit — per route + IP** | `securenow ratelimit add <ip> --route /api/preview --mode exact --method POST --limit 5 --window 1m --duration 24h` · NL `securenow ratelimit from-text "rate limit /api/preview to 5/min for 24h" --yes` · test `securenow ratelimit test <ip> --path /api/preview --method POST` | precise throttle of one client repeatedly probing the SSRF sink. |
| 9 | **CAPTCHA / proof-of-work challenge** | `securenow challenge add --route /api/preview --difficulty 16 --clearance 30m` (route-wide) **or** `securenow challenge add <ip> --route /api/preview --difficulty 18 --clearance 30m` · test `securenow challenge test <ip> --path /api/preview --method GET` | bot probing of a URL-fetch endpoint 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 of SSRF probers by risk score; actions include block / rate_limit / requireCaptcha. |
| 11 | **Session revocation** | `securenow revoke …` (SDK `securenow/sessions` `guard()` / `isRevoked()`) | if a leaked credential drove a session takeover — kill the stolen session, not the IP. |
| 12 | **Trusted IP (suppress)** | `securenow trusted add <ip> --label "Internal scanner / partner / monitor"` | stop false positives from known-good infra (internal vuln scanners, partner batch jobs) — 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 the SSRF/upstream-error rules quiet for a known integration without weakening them. |
| 15 | **Secret-store / cloud-IAM config / code fix (primary for root cause)** | *described in the Code-Findings report, never auto-applied* | the actual fix: move secrets to a manager + **rotate exposed ones**, KMS-encrypt, add rotation, scope IAM least-privilege, remove wildcards/`PassRole`, add `sts:ExternalId`, **enforce IMDSv2** + SSRF allowlist, replace static `AKIA…` keys with short-lived role creds, split global → per-tenant secrets, harden the SDK redaction list. SecureNow contains; the fix removes. |

**Choosing per threat** — by **confidence**: a request carrying a metadata IP / exact secret-probe
payload → instant-block or block; a probable bot probing the SSRF sink on shared egress →
**challenge**; noisy/legit-mixed probing → **rate-limit (test-mode first)**; a leaked credential
driving a session takeover → **revoke**; known-good scanner 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. **In this domain almost every
threat's real fix is row 15** — hardcoded keys, wildcard IAM, missing `ExternalId`, no rotation,
IMDSv1, frontend exposure emit no telemetry and cannot be contained at the edge; edge mitigation
(rows 1–14) applies mainly to the **SSRF-to-metadata** sink, where the durable fix is **always** the
SSRF allowlist + **IMDSv2** (edge containment buys time, it does not close the hole). Always pair an
edge mitigation with the **secret-store / cloud-IAM config fix (Code-Findings report)**. For the
**redaction-gap canary** and **upstream-credential-rejection** rules, recommend **notify-only** (no
auto action) — they signal a **config problem to fix** (a redaction gap, a leaked/rotated key), not
an attacker to block — and give the exact runbook command the human runs after confirming.

### 4c-bis. 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 (the
repeated-upstream-rejection count, the SSRF-probe-hit count), 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 (e.g. a known integration whose key briefly 403s, an internal scanner
hitting the URL-fetch sink), then `--mode prod` to arm mitigation. Only **high-precision** rules
(the marquee `ssrf.blocked`-to-metadata rule — any hit is a genuine credential-theft attempt — and
exact-match IoCs) may go straight to `prod`. **Tag every rule in the report `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.)

### 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`). **Never
send a real secret value, even in a test.**

```bash
# Synthetic SSRF-to-metadata block — exercise the marquee rule end to end:
securenow event send ssrf.blocked --ip 203.0.113.50 \
  --attrs route=/api/preview,target_host=169.254.169.254,reason=link_local,test=true

# Synthetic GCP / Azure metadata variants:
securenow event send ssrf.blocked --ip 203.0.113.51 \
  --attrs route=/api/import,target_host=metadata.google.internal,reason=metadata,test=true

# Synthetic upstream credential rejection (leaked/rotated key in use):
for i in $(seq 1 12); do
  securenow event send api.upstream.error --ip 203.0.113.52 \
    --attrs upstream=aws_s3,status=403,route=/api/sync,test=true
done

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

# Traffic-based SSRF-probe rule — generate spans, then check the pipeline:
securenow test-span "threat-model.secrets.ssrf.smoke"
securenow forensics "requests whose path contains 169.254.169.254 in the last hour" --env production

# Mitigation verification on the SSRF sink:
securenow ratelimit test 203.0.113.50 --path /api/preview --method GET
securenow challenge test 203.0.113.50 --path /api/preview --method GET
securenow firewall test-ip 203.0.113.50 --app <APP_KEY> --env production

# 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). 🔴 rows have a
**config-fix verification** instead (e.g. "after enforcing IMDSv2, confirm an IMDSv1 `curl
169.254.169.254/latest/meta-data` from the box returns 401 — done out of band, not via SecureNow").

---

## Phase 5 — Write the deliverables (two tracks, four files)

Write all four files into `threat/20-secrets-and-cloud-iam/`. The two tracks cross-link each other:
the detection report's gap and instrumentation rows link to the relevant code finding, and each
code finding (the secret-store / cloud-IAM config fix) links back to the detection-report row it
backs. **Never render or print a real secret value in either track — fingerprint only** (first 4
chars + length + entropy class).

### 5a. Detection & Mitigation report — `secrets-and-cloud-iam-detection-mitigation.{md,html}`

The **operational runbook**: what to run in SecureNow. Be explicit up top that this domain is
**config-fix-heavy** — SecureNow native coverage is **LOW–MEDIUM** here (primarily IMDS-SSRF
detection via `ssrf.blocked` + telemetry redaction; the bulk of remediation is secret-store /
cloud-IAM config work that lives in the code-findings track). Sections, in order (same in .md and
.html):

1. **Executive summary** — stats line (threats modeled · covered · partial · gaps · rules to
   create · mitigations), top 3 credential-theft risks for this stack, installed `securenow`
   version + app key + firewall state, and the one-line honest coverage note (which framework codes
   A02/A05/A07/API7 are owned here vs deferred, and "native coverage LOW–MEDIUM — IMDS-SSRF +
   redaction; the rest is config").
2. **SDK & environment** — installed SDK version (from `node_modules/securenow`), app key(s),
   environment, firewall state, existing rules/automations/challenge rules (from Phase 0), system
   signature rules present, and the **SDK redaction posture** (the single most important SecureNow
   control in this domain).
3. **Threat → Detection → Mitigation matrix** — one row per modeled threat:
   `# | Threat | Framework (A02/A05/A07/API7) | Coverage 🟢/🟡/🔴 | Detection rule (or "config-audit finding" → link to code report) | Signal (threshold+window, or "config audit") | Schedule | Sev | Mitigation (edge containment + config fix) | Mode (test-first / prod-ready)`.
   Severity ∈ {critical, high, medium, low}. **Each row's Mitigation cell must pick specific,
   scoped mitigation(s) by number from the 4c toolbox** (e.g. "9 challenge `/api/preview` + 15
   config fix: SSRF allowlist + IMDSv2") — never a generic "block the IP", and for the config-fix-
   heavy rows lead with **row 15**. **Tag every detection rule `test-first` or `prod-ready`** in the
   Mode column (FP-prone heuristic/threshold rules → `test-first`; the high-precision marquee
   `ssrf.blocked`-to-metadata rule → `prod-ready`). Then the "Out of scope" N/A list and the deferred
   sibling rows (55–58) pointing to `../22-supply-chain-cicd/`, `../23-storage-and-logs/`,
   `../19-cloud-infrastructure/`, `../16-webhooks/`.
4. **Detection rules to create** — each as the **ready-to-copy unit** from Phase 4 (SQL → save to
   `rules/<name>.sql` → `securenow alerts rules create …` → dry-run test). This domain yields
   **few** SQL rules by design (SSRF-to-metadata, upstream-credential-rejection, redaction-gap
   canary). SSRF-to-metadata **reuses the `ssrf.blocked` event verbatim**; injection-class rows
   reference the **system signature rules + `instant.block`**, not duplicate SQL. Note rules that
   already exist from Phase 0. **Mark each rule `test-first` or `prod-ready`** (per 4c-bis); for
   every `test-first` rule include the promotion workflow inline —
   `securenow alerts rules update <RULE_ID> --mode test` → observe 3–7 days + tune + `securenow fp`
   exclusions → `securenow alerts rules update <RULE_ID> --mode prod`. The high-precision marquee
   `ssrf.blocked`-to-metadata rule is `prod-ready` and may go straight to `--mode prod`.
5. **Instrumentation the detections need** — only the `track('…')` events the rules above consume
   (`ssrf.blocked` at every URL-fetch sink, `api.upstream.error` on consumed cloud/upstream calls),
   each as a copyable snippet, **plus the redaction-list hardening step** (the other half of
   instrumentation here); point to the code-findings report for *where* to add them. **Do not put a
   secret value into an attribute — hash or omit it.**
6. **Mitigation mechanisms** — render the **full 4c toolbox table verbatim (all 15 rows)**: free
   firewall · exploit-signature instant-block · IP block (global / route+method / temporary) ·
   rate-limit (IP / route / route+IP) · challenge · auto-block · session revocation · trusted ·
   allowlist · fp exclusion · **secret-store / cloud-IAM config fix (row 15, primary)** — plus the
   "Choosing per threat" paragraph and per-threat ready-to-copy mitigation command + reversibility.
   Make explicit that the **config fix (row 15) is primary** for nearly every row and SecureNow's
   role is containment of the SSRF prober + preventing telemetry leakage (the SDK redaction posture).
7. **Action plan (copy-paste, ordered)** — ① verify + harden the SDK redaction list (close the
   telemetry-leak path first), ② add the `ssrf.blocked` event at every URL-fetch sink + enforce
   IMDSv2, ③ create the rules — **FP-prone rules (upstream-credential-rejection, redaction-gap
   canary, SSRF-probe-count) in `--mode test`** and the high-precision `ssrf.blocked`-to-metadata
   rule `--mode prod`, ④ enable automations / challenge / rate-limit on the SSRF sink, ⑤ test, ⑥
   verify in dashboard, ⑦ **promote the test-first rules with an explicit "promote after N days
   (3–7) of clean observation" step** — `securenow alerts rules update <RULE_ID> --mode prod` once
   thresholds are tuned and FPs excluded, ⑧ schedule the **secret-store & cloud-IAM config-fix
   backlog** from the code report (rotate exposed secrets → manager/KMS → least-privilege IAM →
   `ExternalId` → short-lived creds → per-tenant secrets). Real commands, `<APP_KEY>` substituted.
8. **Testing & validation** — per-rule recipe: `securenow event send …` / `test-span` / dry-run +
   expected outcome + cleanup (TEST-NET IPs 192.0.2/198.51.100/203.0.113). **Never send a real
   secret value, even in a test.** 🔴 rows get a **config-fix verification** instead (e.g. "after
   enforcing IMDSv2, confirm an IMDSv1 `curl 169.254.169.254/latest/meta-data` returns 401 — out of
   band, not via SecureNow").
9. **Response runbooks** — per notification type (SSRF-to-metadata, upstream-credential-rejection,
   redaction-gap canary): confirm TP → respond command (copy) → reverse command (copy). For the
   canary/upstream signals the response is **config remediation** (rotate the key, fix the redaction
   list), **not** an IP block — make that explicit.
10. **Known gaps & SecureNow feature requests** — **most of this domain.** Each 🔴 (hardcoded
    secrets, frontend exposure, missing manager/KMS/rotation, wildcard IAM, privesc paths,
    cross-account trust, static keys, per-tenant secrets): why it's not coverable at the edge today,
    the **interim secret-store / cloud-IAM config fix** (link to the code report), and the
    "contact the SecureNow team" line.
11. **Appendix** — resolved SDK/CLI version, app key, environment, redaction posture summary,
    firewall state, rule IDs created, date.

### 5b. Code Findings & Recommendations report — `secrets-and-cloud-iam-code-findings.{md,html}`

State at top: *"Findings only — no application code, secret, or cloud config was modified, and no
secret value is printed (values are fingerprinted)."* Sections, in order (same in .md and .html):

1. **Executive summary** — findings by severity (critical/high/med/low), top 3 code/config risks,
   one-paragraph posture verdict (any exposed `sk_live`/`AKIA` in a public bundle = critical).
2. **Surface & inventory** — the Phase 1 inventory for this domain: the secret-material catalog
   (kind / location / scope / lifetime / storage — **fingerprinted**) + hardcoded/git-history list
   + frontend-exposure list + secret-manager/KMS/rotation posture + IAM principal/policy table
   (principal / policy / wildcards / escalation primitives) + cross-account-trust posture +
   credential-lifetime posture + SSRF→metadata sink list + per-tenant-secret posture + telemetry
   redaction status.
3. **Threat catalog** — the exhaustive Phase 2 catalog 1–63 (grouped A–M, each tagged
   A02:2021 / A05:2021 / A07:2021 / API7:2023 or "—", modeled or explicit N/A), with the deferred
   siblings (55–58) referenced by numbered path, not re-derived.
4. **Code-level findings (audit)** — table
   `# | Location (file:line) | Threat | Framework | Sev | Issue | Recommended fix`, each with the
   **fingerprinted** 1–8 line snippet and the described fix (never applied). Any **already-exposed**
   secret's fix MUST begin with *rotate now*. Each finding links to the detection-report matrix row
   it backs.
5. **Strengths** — controls already present and correct (secrets in a manager, KMS-encrypted,
   IMDSv2 enforced, least-privilege scoped policies, per-tenant secrets, SSRF allowlist, redaction
   list configured) — honest posture.
6. **App / config fixes (primary remediation)** — the config/code changes that remove the root
   cause (described, not applied): move secrets to a manager + rotate exposed ones, KMS-encrypt, add
   rotation, scope IAM least-privilege, remove wildcards/`PassRole`, add `sts:ExternalId`, enforce
   IMDSv2, replace static `AKIA…` keys with short-lived role creds, split global → per-tenant
   secrets, harden the SDK redaction list. Each linked to the detection-report row it backs.
7. **Instrumentation recommendations** — the `track('…')` calls to add (`ssrf.blocked` at each
   URL-fetch sink, `api.upstream.error` on consumed cloud/upstream calls) and the exact file:line to
   add them (from Phase 1/3), so the detection rules light up — never letting a secret become an
   attribute value.
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>`. Change only the `<title>`, the sidebar subtitle, and the
section content. Wrap **EVERY** command/SQL block as a `.cmd` (so it gets a Copy button).

**Shared `<head>` + `<body>` shell + copy `<script>`** (identical for 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 — Secrets & Cloud IAM — SecureNow" OR "Code Findings — Secrets & Cloud IAM — 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 · Secrets & Cloud IAM" OR "Code Findings · Secrets & Cloud IAM" --></div>
    <!-- one <a href="#…"> per section -->
  </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 secrets &amp; cloud-IAM 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>
```

**File 2 — `secrets-and-cloud-iam-detection-mitigation.html`** uses the shell above with:
- `<title>`: `Detection & Mitigation — Secrets & Cloud IAM — SecureNow`
- sidebar subtitle: `Detection & Mitigation · Secrets & Cloud IAM`
- `<h1>`: `Secrets & Cloud IAM — Detection & Mitigation`
- the 11 sections of **5a**, with **every** SQL/command block wrapped in the copyable `.cmd`
  pattern below.
- 5 stat cards: threats modeled · covered · partial · gaps · rules to create (numbers MUST equal
  the matrix counts).

**File 4 — `secrets-and-cloud-iam-code-findings.html`** uses the same shell with:
- `<title>`: `Code Findings — Secrets & Cloud IAM — SecureNow`
- sidebar subtitle: `Code Findings · Secrets & Cloud IAM`
- `<h1>`: `Secrets & Cloud IAM — Code Findings & Recommendations`
- the 8 sections of **5b**; prose may omit copy buttons, but any example/fix command is still
  wrapped in `.cmd`.
- 5 stat cards: total findings · critical · high · medium · low (numbers MUST equal the
  findings-table counts).

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 critical \
  --schedule "*/5 * * * *" --nlp "..."</pre></div>
```

Badge usage: severity `<span class="b crit|high|med|low">`; coverage
`<span class="c cov|part|gap">COVERED|PARTIAL|GAP</span>`; framework tag
`<span class="owasp">A02:2021</span>` (or `A05:2021` / `A07:2021` / `API7:2023`); mitigation
`<span class="m firewall|signature|rate|challenge|block|notify|appfix">` (use `appfix` for the
secret-store / cloud-IAM config fix **and** the redaction posture); 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`. **Never render a real secret
value in either HTML file — fingerprint only.**

---

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

- Every catalog item 1–63 is either a matrix row or an explicit N/A line; each modeled row carries
  its framework tag (**A02:2021 / A05:2021 / A07:2021 / API7:2023**, or "—").
- Coverage badges are **honest**: 🟢 is reserved for IMDS-SSRF detection (`ssrf.blocked`) and the
  telemetry-redaction posture; secret storage, rotation, IAM scope, privesc, cross-account trust,
  and static-key threats are 🟡 (edge-containment + config fix) or 🔴 (config fix only) — the report
  does **not** over-claim native coverage in this config-fix-heavy domain.
- Every IMDS/SSRF row **pairs** edge containment (firewall / rate-limit / challenge / block on the
  sink) **with** the durable config fix (SSRF allowlist + IMDSv2).
- The deferred siblings are referenced by **numbered path** and not re-derived: CI/CD secrets →
  `../22-supply-chain-cicd/`, secrets-in-logs → `../23-storage-and-logs/`, compute/network →
  `../19-cloud-infrastructure/`, webhook signature verification → `../16-webhooks/`.
- Every matrix row has a concrete signal (threshold + window) **or** an explicit "config-audit
  finding", plus severity and mitigation — no "monitor for suspicious activity" filler.
- Every code/config finding has a `file:line`, a **fingerprinted** snippet (no real secret value
  printed anywhere), and a described fix — and **no code, secret, or cloud config was modified**
  (this is an audit). Any exposed-secret fix begins with *rotate now*.
- Every detection SQL keeps `__USER_APP_KEYS__` scoping with the **correct table column**
  (`resources_string['service.name']` for logs vs `` `resource_string_service$$name` `` for traces),
  the `client_ip` coalesce, and `HAVING ip != ''`; traffic queries keep the `ts_bucket_start` +
  `kind = 2` guards; rules are validated with `--mode dry_run`.
- SSRF-to-metadata detection **reuses the `ssrf.blocked` event verbatim** (no invented
  secrets-specific event name); upstream-credential signal reuses `api.upstream.error`.
- Only commands, flags, events, and SQL columns from this prompt's building blocks appear
  (`firewall`, `ratelimit`, `challenge`, `blocklist`, `automation`, `fp`, `trusted`, `event send`,
  `alerts rules`, `ssrf.blocked`, `api.upstream.error`).
- The **SDK redaction posture** is treated as a first-class mitigation and appears in the action
  plan as step ①.
- Every 🔴 gap appears in the gaps section with an interim **secret-store / cloud-IAM config fix**
  **and** the "contact the SecureNow team" line.
- The action plan runs top-to-bottom with `<APP_KEY>` substituted in.
- **Phase 0.5 ran**: the resolved installed `securenow` version appears in **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/20-secrets-and-cloud-iam/` (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 in each HTML match its table/finding counts; **no secret value is
  rendered** in either file (fingerprint only).
- The split is honest: SecureNow-runnable detections/mitigations live in the Detection report;
  the secret-store / cloud-IAM config & code changes live in the Code-Findings report; nothing
  security-relevant is dropped.
- The Detection report's mitigation section presents the **full toolbox** (4c §6: firewall ·
  instant-block · block [global / route / method / temporary] · rate-limit [IP / route / IP+route] ·
  challenge · auto-block · revoke · trusted · allowlist · fp · **secret-store / cloud-IAM config
  fix**), and **each modeled threat's matrix row selects specific, scoped mitigation(s) from it** —
  never a generic "block the IP"; config-fix-heavy rows lead with row 15.
- **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 (the marquee
  `ssrf.blocked`-to-metadata rule) 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/config findings by severity, gaps, framework coverage, resolved SDK version.

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