sanook-cli 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +173 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +405 -57
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +21 -7
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/models.js +22 -6
  37. package/dist/providers/registry.js +155 -1
  38. package/dist/repomap.js +93 -0
  39. package/dist/search/chunk.js +158 -0
  40. package/dist/search/embed-store.js +187 -0
  41. package/dist/search/engine.js +203 -0
  42. package/dist/search/fuse.js +35 -0
  43. package/dist/search/index-core.js +187 -0
  44. package/dist/search/indexer.js +241 -0
  45. package/dist/search/store.js +77 -0
  46. package/dist/session.js +42 -8
  47. package/dist/skill-install.js +10 -10
  48. package/dist/skills.js +12 -9
  49. package/dist/summarize.js +31 -0
  50. package/dist/tools/bash.js +21 -2
  51. package/dist/tools/diagnostics.js +41 -0
  52. package/dist/tools/edit.js +29 -7
  53. package/dist/tools/index.js +8 -1
  54. package/dist/tools/list.js +7 -2
  55. package/dist/tools/permission.js +90 -9
  56. package/dist/tools/read.js +23 -4
  57. package/dist/tools/remember.js +1 -1
  58. package/dist/tools/sandbox.js +61 -0
  59. package/dist/tools/search.js +105 -4
  60. package/dist/tools/task.js +195 -29
  61. package/dist/tools/timeout.js +35 -0
  62. package/dist/tools/util.js +10 -0
  63. package/dist/tools/write.js +6 -4
  64. package/dist/trust.js +89 -0
  65. package/dist/ui/app.js +228 -31
  66. package/dist/ui/banner.js +4 -9
  67. package/dist/ui/brain-wizard.js +2 -2
  68. package/dist/ui/history.js +30 -0
  69. package/dist/ui/mentions.js +44 -0
  70. package/dist/ui/render.js +55 -15
  71. package/dist/ui/setup.js +97 -12
  72. package/dist/ui/useEditor.js +83 -0
  73. package/dist/update.js +114 -0
  74. package/dist/worktree.js +173 -0
  75. package/package.json +11 -5
  76. package/scripts/postinstall.mjs +33 -0
  77. package/second-brain/.agents/_Index.md +30 -0
  78. package/second-brain/.agents/skills/_Index.md +30 -0
  79. package/second-brain/.agents/workflows/_Index.md +30 -0
  80. package/second-brain/AGENTS.md +4 -4
  81. package/second-brain/Acceptance/_Index.md +30 -0
  82. package/second-brain/Acceptance/golden-case-template.md +39 -0
  83. package/second-brain/Areas/_Index.md +30 -0
  84. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  85. package/second-brain/Bugs/_Index.md +30 -0
  86. package/second-brain/CLAUDE.md +4 -1
  87. package/second-brain/Checklists/_Index.md +30 -0
  88. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  89. package/second-brain/Distillations/_Index.md +30 -0
  90. package/second-brain/Entities/_Index.md +30 -0
  91. package/second-brain/Entities/entity-template.md +33 -0
  92. package/second-brain/Evals/_Index.md +30 -0
  93. package/second-brain/Evals/correction-pairs.md +24 -0
  94. package/second-brain/Evals/failure-taxonomy.md +24 -0
  95. package/second-brain/Evals/golden-set.md +25 -0
  96. package/second-brain/Evals/quality-ledger.md +23 -0
  97. package/second-brain/Evals/self-eval-rubric.md +23 -0
  98. package/second-brain/GEMINI.md +4 -4
  99. package/second-brain/Goals/_Index.md +30 -0
  100. package/second-brain/Handoffs/_Index.md +30 -0
  101. package/second-brain/Home.md +7 -0
  102. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  103. package/second-brain/Intake/_Index.md +30 -0
  104. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  105. package/second-brain/Learning/_Index.md +30 -0
  106. package/second-brain/Playbooks/_Index.md +30 -0
  107. package/second-brain/Playbooks/playbook-template.md +23 -0
  108. package/second-brain/Projects/_Index.md +30 -0
  109. package/second-brain/Prompts/_Index.md +30 -0
  110. package/second-brain/README.md +2 -1
  111. package/second-brain/Research/_Index.md +30 -0
  112. package/second-brain/Retrospectives/_Index.md +30 -0
  113. package/second-brain/Reviews/_Index.md +30 -0
  114. package/second-brain/Runbooks/_Index.md +30 -0
  115. package/second-brain/Runbooks/eval-loop.md +24 -0
  116. package/second-brain/Sessions/_Index.md +30 -0
  117. package/second-brain/Shared/AI-Context-Index.md +20 -0
  118. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  119. package/second-brain/Shared/Archive/_Index.md +30 -0
  120. package/second-brain/Shared/Assets/_Index.md +30 -0
  121. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  122. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  123. package/second-brain/Shared/Coordination/NOW.md +28 -0
  124. package/second-brain/Shared/Coordination/_Index.md +30 -0
  125. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  126. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  127. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  128. package/second-brain/Shared/Coordination/task-board.md +32 -0
  129. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  130. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  131. package/second-brain/Shared/Glossary/_Index.md +30 -0
  132. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  133. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  134. package/second-brain/Shared/Prompting/_Index.md +30 -0
  135. package/second-brain/Shared/Provenance/_Index.md +30 -0
  136. package/second-brain/Shared/Rules/_Index.md +30 -0
  137. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  138. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  139. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  140. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  141. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  142. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  143. package/second-brain/Shared/Scripts/_Index.md +30 -0
  144. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  145. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  146. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  147. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  149. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  150. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  151. package/second-brain/Shared/_Index.md +30 -0
  152. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  153. package/second-brain/Skills/_Index.md +30 -0
  154. package/second-brain/Templates/_Index.md +30 -0
  155. package/second-brain/Templates/bug.md +2 -0
  156. package/second-brain/Templates/handoff.md +2 -0
  157. package/second-brain/Templates/session.md +2 -0
  158. package/second-brain/Tools/_Index.md +30 -0
  159. package/second-brain/Traces/_Index.md +30 -0
  160. package/second-brain/Vault Structure Map.md +33 -1
  161. package/second-brain/copilot/_Index.md +30 -0
  162. package/skills/audit-license-compliance/SKILL.md +117 -0
  163. package/skills/author-codemod/SKILL.md +110 -0
  164. package/skills/build-audit-logging/SKILL.md +112 -0
  165. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  166. package/skills/build-cli-tool/SKILL.md +108 -0
  167. package/skills/build-data-table/SKILL.md +141 -0
  168. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  169. package/skills/build-offline-first-sync/SKILL.md +118 -0
  170. package/skills/build-realtime-channel/SKILL.md +122 -0
  171. package/skills/build-vector-search/SKILL.md +131 -0
  172. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  173. package/skills/configure-bundler-build/SKILL.md +166 -0
  174. package/skills/configure-dns-tls/SKILL.md +142 -0
  175. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  176. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  177. package/skills/contract-testing/SKILL.md +140 -0
  178. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  179. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  180. package/skills/debug-flaky-tests/SKILL.md +128 -0
  181. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  182. package/skills/deliver-webhooks/SKILL.md +116 -0
  183. package/skills/design-api-pagination/SKILL.md +144 -0
  184. package/skills/design-authorization-model/SKILL.md +119 -0
  185. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  186. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  187. package/skills/design-multi-tenancy/SKILL.md +100 -0
  188. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  189. package/skills/design-relational-schema/SKILL.md +129 -0
  190. package/skills/design-search-index-infra/SKILL.md +151 -0
  191. package/skills/design-state-machine/SKILL.md +108 -0
  192. package/skills/design-token-system/SKILL.md +109 -0
  193. package/skills/distributed-locks-leases/SKILL.md +120 -0
  194. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  195. package/skills/feature-flags-rollout/SKILL.md +130 -0
  196. package/skills/file-upload-object-storage/SKILL.md +107 -0
  197. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  198. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  199. package/skills/i18n-localization-setup/SKILL.md +113 -0
  200. package/skills/idempotency-keys/SKILL.md +107 -0
  201. package/skills/implement-push-notifications/SKILL.md +142 -0
  202. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  203. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  204. package/skills/load-stress-test/SKILL.md +129 -0
  205. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  206. package/skills/model-nosql-data/SKILL.md +118 -0
  207. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  208. package/skills/monitor-ml-drift/SKILL.md +109 -0
  209. package/skills/numeric-precision-units/SKILL.md +144 -0
  210. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  211. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  212. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  213. package/skills/payments-billing-integration/SKILL.md +114 -0
  214. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  215. package/skills/plan-strangler-migration/SKILL.md +95 -0
  216. package/skills/property-based-testing/SKILL.md +108 -0
  217. package/skills/publish-package-registry/SKILL.md +130 -0
  218. package/skills/recover-git-state/SKILL.md +119 -0
  219. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  220. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  221. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  222. package/skills/rewrite-git-history/SKILL.md +109 -0
  223. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  224. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  225. package/skills/send-transactional-email/SKILL.md +126 -0
  226. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  227. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  228. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  229. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  230. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  231. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  232. package/skills/structured-output-llm/SKILL.md +86 -0
  233. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  234. package/skills/test-data-factories/SKILL.md +158 -0
  235. package/skills/threat-model-stride/SKILL.md +123 -0
  236. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  237. package/skills/unicode-text-correctness/SKILL.md +109 -0
  238. package/skills/visual-regression-testing/SKILL.md +120 -0
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: encrypt-sensitive-data
3
+ description: Encrypts sensitive data at rest, in transit, and per-field using AEAD-only ciphers (AES-256-GCM or ChaCha20-Poly1305 — never ECB, never unauthenticated CBC, never raw RSA) — envelope encryption where a KMS-held KEK wraps a per-record/per-tenant DEK, per-column field encryption for PII with deterministic-vs-randomized chosen per query need, strict unique-nonce/IV discipline (random 96-bit or counter, NEVER reused under one key), AAD binding ciphertext to its context (tenant/row id), versioned keys + rotation that re-wraps DEKs without re-encrypting data, TLS 1.2+/1.3 with mTLS and modern cipher suites, and — critically — passwords are HASHED with argon2id/bcrypt, NOT encrypted. Distinct from secrets-management (stores the app secrets/keys this skill consumes) and map-privacy-data-gdpr (the legal PII/erasure obligations encryption helps satisfy).
4
+ when_to_use: You must protect sensitive data — encrypting PII/PHI/card data at rest, a per-column/field-level encryption scheme, envelope encryption with a KMS (AWS KMS/GCP KMS/Vault Transit), key rotation, choosing a cipher/mode/nonce strategy, enforcing TLS/mTLS, or hashing passwords. Distinct from secrets-management (storing and injecting the KEKs/API keys/credentials — that skill provisions the keys; this one uses them to encrypt data) and map-privacy-data-gdpr (the legal classification/erasure/residency duties that encryption and crypto-shredding help you meet).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the task is making sensitive *data* cryptographically protected — at rest, in transit, or field-by-field:
10
+
11
+ - "Encrypt SSNs / card numbers / health records / PII columns in the database"
12
+ - "Set up envelope encryption with AWS KMS / GCP KMS / Vault Transit (DEK + KEK)"
13
+ - "Rotate our encryption keys" / "we need versioned keys without re-encrypting everything"
14
+ - "Which cipher/mode — is AES-CBC okay? do we need a separate MAC? what nonce?"
15
+ - "Enforce TLS 1.3 / mutual TLS between services with modern cipher suites"
16
+ - "Are we storing passwords correctly?" (hash, don't encrypt)
17
+ - "Make a user's data unrecoverable on account deletion" (crypto-shredding)
18
+
19
+ NOT this skill:
20
+ - Storing/injecting the KEKs, API keys, DB creds, and `.env` material this skill *consumes* → secrets-management (it provisions and rotates the secrets; this skill encrypts data *with* them)
21
+ - The legal side — what counts as PII/PHI, lawful basis, right-to-erasure, data residency → map-privacy-data-gdpr (this skill is the *technical control*, e.g. crypto-shredding, that satisfies those duties)
22
+ - TLS *termination/cert issuance* at the edge proxy, ACME, SNI routing → configure-dns-tls and configure-reverse-proxy-lb (this skill covers the cipher-suite/mTLS *policy*, not cert plumbing)
23
+ - Browser security response headers (HSTS, CSP) → configure-security-headers-csp (HSTS *enforces* HTTPS; this skill is the transport crypto itself)
24
+ - Login sessions, JWT signing/verification, token rotation → auth-jwt-session (signatures/JWE are adjacent but that owns session lifecycle)
25
+ - Identifying the threats/attacker model that justify these controls → threat-model-stride
26
+ - A broad security pass over a diff → security-review (this skill is the deep crypto specialist it defers to)
27
+
28
+ ## Steps
29
+
30
+ 1. **Classify data first, then pick the protection tier — encryption is not the answer to everything.** Three distinct goals need three different tools:
31
+
32
+ | Goal | Use | NEVER |
33
+ |---|---|---|
34
+ | Verify a credential later (passwords) | **slow password hash** (argon2id) — one-way | encrypt; never decrypt a password |
35
+ | Protect data you must read back (PII, PHI, PAN, tokens) | **AEAD encryption** + KMS envelope | reversible "encoding", base64, ROT |
36
+ | Integrity/origin without secrecy | HMAC-SHA-256 / signature | "encrypt to authenticate" |
37
+ | Index/search without revealing value | HMAC-based blind index or deterministic enc | plaintext index column |
38
+
39
+ Encrypting a password is a **bug**, not a feature: anything reversible means an attacker (or insider) with the key gets every plaintext password.
40
+
41
+ 2. **Use AEAD ciphers only. Banned modes are non-negotiable.** Authenticated Encryption with Associated Data gives confidentiality *and* tamper-detection in one primitive:
42
+
43
+ | Use this | Why |
44
+ |---|---|
45
+ | **AES-256-GCM** | hardware-accelerated (AES-NI), NIST-approved, ubiquitous KMS support |
46
+ | **ChaCha20-Poly1305** | faster on CPUs without AES-NI (mobile/ARM), constant-time by design |
47
+ | **AES-256-GCM-SIV / XChaCha20-Poly1305** | nonce-misuse-resistant / 192-bit nonce — prefer when you can't guarantee unique 96-bit nonces |
48
+
49
+ | Banned | Why it's broken |
50
+ |---|---|
51
+ | **ECB** | identical plaintext blocks → identical ciphertext (the "ECB penguin"); leaks structure |
52
+ | **CBC/CTR without a MAC** | unauthenticated → padding-oracle (CBC) & bit-flipping attacks; ciphertext is malleable |
53
+ | **Raw RSA / RSA-PKCS#1v1.5 enc** | use RSA-OAEP, or better ECIES/hybrid; never "RSA the whole payload" |
54
+ | DES/3DES/RC4/MD5/SHA-1 | broken/deprecated |
55
+
56
+ Don't hand-roll "AES + separate HMAC" (encrypt-then-MAC) unless you must — get the construction order wrong and you reintroduce the oracle. Use a vetted library: **libsodium** (`crypto_aead_*` / `secretbox`), **Go** `crypto/cipher` GCM or `nacl/secretbox`, **Python** `cryptography` `AESGCM`/`ChaCha20Poly1305` (not the low-level `Cipher` API), **Java** `javax.crypto` GCM or Google **Tink**, **Rust** `aes-gcm`/`chacha20poly1305` RustCrypto crates, **Node** `crypto.createCipheriv('aes-256-gcm', …)` + `getAuthTag()`. **Tink/libsodium are the senior default** — they pick safe modes and manage nonces for you.
57
+
58
+ 3. **Nonce/IV discipline: unique per (key, message), forever. This is the #1 way AEAD fails.** GCM with a **repeated nonce under the same key is catastrophic** — it leaks the XOR of plaintexts *and* the authentication key (forgery). Rules:
59
+ - 96-bit (12-byte) nonce for GCM. Either **random** from a CSPRNG (`os.urandom`/`crypto.randomBytes`/`getrandom`) or a **monotonic counter** — never both, never `0`, never a timestamp, never reuse.
60
+ - Random 96-bit nonces are safe only up to **~2³² messages per key** (birthday bound). High-volume? Rotate the DEK sooner, or use **XChaCha20-Poly1305 (192-bit nonce)** / **AES-GCM-SIV** which tolerate accidental reuse.
61
+ - **Store the nonce alongside the ciphertext** (it's not secret) — typical record = `version ‖ nonce ‖ ciphertext ‖ tag`.
62
+ - Don't derive the nonce from the plaintext or a non-unique field. Don't reuse one nonce across a re-encrypt.
63
+
64
+ 4. **Bind ciphertext to its context with AAD (Associated Data).** AEAD lets you authenticate (not encrypt) extra context — pass the **row id / tenant id / column name / key version** as AAD. This stops an attacker from copying a valid ciphertext from row A into row B (ciphertext substitution): decryption of B fails because the AAD no longer matches. AAD must be reconstructible at decrypt time from the record's own metadata.
65
+
66
+ 5. **Envelope encryption: a KMS-held KEK wraps per-record/per-tenant DEKs. Never encrypt bulk data directly with the KMS key.** The pattern that scales and rotates cleanly:
67
+
68
+ ```
69
+ 1. KMS.GenerateDataKey(KeyId=KEK, KeySpec=AES_256)
70
+ → returns { Plaintext DEK, Encrypted DEK (wrapped by KEK) }
71
+ 2. Encrypt your data locally with the plaintext DEK (AES-256-GCM, fresh nonce)
72
+ 3. Store: encrypted_dek ‖ key_version ‖ nonce ‖ ciphertext ‖ tag
73
+ 4. ZERO the plaintext DEK from memory immediately after use
74
+ 5. Decrypt: KMS.Decrypt(encrypted_dek) → plaintext DEK → local AEAD decrypt
75
+ ```
76
+
77
+ - **KEK** lives in **AWS KMS / GCP KMS / Azure Key Vault / Vault Transit / an HSM** and *never leaves it* — KMS does the wrap/unwrap, your app never sees KEK bytes. **DEK** is short-lived in app memory, zeroed after use.
78
+ - Granularity: **per-tenant or per-record DEK** for crypto-shredding (delete the DEK → that data is gone). Per-row is most flexible; cache the unwrapped DEK briefly (e.g. LRU with TTL) to avoid a KMS call per row.
79
+ - Tools: AWS **KMS** + the **AWS Encryption SDK** (handles the envelope + nonce for you), GCP **KMS**, HashiCorp **Vault Transit** (`vault write transit/encrypt/...` — Vault holds the key, returns ciphertext), or **Tink**'s `KmsEnvelopeAead`. Prefer these over rolling your own envelope.
80
+
81
+ 6. **Per-field/column encryption for PII — choose deterministic vs randomized by query need.** Application-layer (encrypt before the DB sees it) beats trusting only DB-native TDE, because TDE protects the *disk file*, not a SQL-injection or a DBA reading rows.
82
+
83
+ | Mode | Same plaintext → | Lets you | Cost |
84
+ |---|---|---|---|
85
+ | **Randomized** (fresh nonce) | different ciphertext | only decrypt-then-use | leaks nothing; **default for PII** |
86
+ | **Deterministic** (synthetic IV / SIV) | same ciphertext | equality lookup, joins, unique constraint | leaks equality (which rows share a value) |
87
+
88
+ For *searchable* encryption use a **blind index**: store `HMAC-SHA256(key, normalize(value))` in a separate indexed column and query by that, keeping the value column randomized-encrypted. Don't reach for order-preserving/fully-homomorphic encryption (leaky / impractical) unless you truly understand the tradeoff. Postgres `pgcrypto` is fine for small cases but does *application-visible* keys in SQL logs — prefer encrypting in the app. **Don't encrypt a column you need range-query or `LIKE` on** without redesigning the access pattern first.
89
+
90
+ 7. **Passwords: hash with a memory-hard KDF, salted and parameterized — never encrypt, never plain SHA-256.** Use:
91
+
92
+ | Algorithm | Params (2025 baseline) |
93
+ |---|---|
94
+ | **argon2id** (first choice) | m=19–64 MiB, t=2–3, p=1; OWASP min m=19 MiB,t=2,p=1 |
95
+ | **scrypt** | N=2^17, r=8, p=1 (or N=2^15 for lighter) |
96
+ | **bcrypt** (legacy/compat) | cost ≥ 12; **pre-hash with SHA-256 + base64** if password may exceed 72 bytes (bcrypt silently truncates) |
97
+
98
+ - A **per-password random salt** is mandatory (the libraries generate and embed it in the encoded hash — `$argon2id$v=19$m=...`). No global "pepper-as-salt".
99
+ - **Pepper** (optional, defense-in-depth) = a secret key *not* in the DB; either HMAC the password before hashing or keep it in a KMS/HSM. Store the pepper in secrets-management, never beside the hash.
100
+ - **Never** use fast hashes (MD5, SHA-1, SHA-256, SHA-512) bare for passwords — GPUs do billions/sec. **Never** encrypt passwords (reversible = breach of all of them).
101
+ - Verify in **constant time** (the KDF's `verify`/`checkpw` does this); re-hash on login if cost params have since increased.
102
+
103
+ 8. **TLS in transit: 1.2 minimum, 1.3 preferred; modern cipher suites; mTLS for service-to-service.**
104
+ - **Versions:** disable SSLv3/TLS 1.0/1.1 entirely. Allow **TLS 1.2 + 1.3**; prefer 1.3 (1-RTT, AEAD-only, forward-secret by construction).
105
+ - **TLS 1.2 cipher suites** (AEAD + ECDHE forward secrecy only): `ECDHE-ECDSA-AES128-GCM-SHA256`, `ECDHE-RSA-AES256-GCM-SHA384`, `ECDHE-*-CHACHA20-POLY1305`. **No** CBC suites, no static RSA key exchange, no `NULL`/`RC4`/`3DES`/`EXPORT`. TLS 1.3 only offers AEAD suites, so the choice is made for you.
106
+ - Mozilla SSL Config "**Intermediate**" is the safe default; "Modern" = TLS 1.3-only. Verify with **`testssl.sh`** or SSL Labs (target **A/A+**). Enable **HSTS** at the edge (handoff to configure-security-headers-csp).
107
+ - **mTLS** for internal/service-to-service: both sides present certs; pin to your CA, short-lived certs (SPIFFE/SVID, Istio, Linkerd, or a service-mesh issuer). Validate the **full chain + SAN**, not just "a cert was presented."
108
+ - **Never disable cert verification** (`verify=False`, `rejectUnauthorized:false`, `InsecureSkipVerify:true`) outside a throwaway test — it silently turns TLS into plaintext-to-anyone.
109
+
110
+ 9. **Key rotation with versioned keys — rotate the KEK cheaply, re-wrap DEKs, lazy-re-encrypt data.** Store a **`key_version`** with every ciphertext so multiple key generations coexist:
111
+ - **KEK rotation** (cheapest, do on schedule, e.g. annually or per policy): KMS rotates the KEK; you **re-wrap each DEK** (decrypt-unwrap with old, wrap with new). Bulk data is *untouched* — that's the whole point of envelope encryption.
112
+ - **DEK rotation:** generate a new DEK, **re-encrypt the affected records lazily** (on next write, or a background backfill) and bump `key_version`. Keep old key versions readable until backfill completes, then retire.
113
+ - **On compromise:** rotate immediately and force re-encryption; **crypto-shred** by destroying a DEK to make its data permanently unrecoverable (the GDPR-erasure trick — handoff to map-privacy-data-gdpr).
114
+ - Decrypt path must **dispatch on the stored `key_version`**; never assume "current key." Keep a registry of retired versions for audit.
115
+
116
+ 10. **Operational hygiene — the parts that get forgotten.** Generate all keys/nonces/salts from a **CSPRNG** (`os.urandom`, `crypto.randomBytes`, `getrandom(2)`, `SecureRandom`) — never `Math.random`/`rand()`/`mt19937`. **Zero plaintext keys** from memory after use where the language allows (Go `defer` wipe, Rust `zeroize`, libsodium `sodium_memzero`). Don't log plaintext, keys, or full ciphertext. Encrypt **backups and replicas** too (same KMS). Use **constant-time comparison** for MACs/tags/tokens (`hmac.compare_digest`, `crypto.timingSafeEqual`, `subtle.ConstantTimeCompare`) — `==` leaks via timing. Run a **`security-review`** over the crypto diff before shipping.
117
+
118
+ ## Common Errors
119
+
120
+ - **Encrypting passwords instead of hashing.** Reversible = one key compromise dumps every password. Fix: argon2id/bcrypt, one-way (step 7).
121
+ - **Plain/fast hash for passwords** (`SHA256(password)`, unsalted MD5). GPUs crack billions/sec; rainbow tables for unsalted. Fix: memory-hard KDF with per-password salt.
122
+ - **ECB mode / unauthenticated CBC.** ECB leaks structure; CBC-without-MAC → padding oracle, malleable ciphertext. Fix: AEAD (AES-GCM/ChaCha20-Poly1305) only.
123
+ - **Nonce/IV reuse under one key (GCM).** Catastrophic — leaks plaintext XOR *and* the auth key (forgeries). Fix: unique nonce per message; XChaCha20/GCM-SIV if you can't guarantee it (step 3).
124
+ - **Hardcoded / static IV** (`iv = new byte[12]` all zeros). Same as reuse. Fix: fresh CSPRNG nonce per encryption, stored with ciphertext.
125
+ - **Encrypting bulk data directly with the KMS/KEK.** Throughput and cost explode; no clean rotation. Fix: envelope — KEK wraps per-record DEK (step 5).
126
+ - **No AAD binding.** Valid ciphertext copy-pasted between rows/tenants decrypts fine. Fix: pass row/tenant/version as AAD (step 4).
127
+ - **No key version on ciphertext.** Rotation becomes a flag-day re-encrypt-everything. Fix: store `key_version`, dispatch decryption on it (step 9).
128
+ - **Plaintext DEK left in memory / logged.** Heap dump or log leak = game over. Fix: zero after use; never log keys/plaintext/tags.
129
+ - **`Math.random()` / `rand()` for keys, nonces, or salts.** Predictable → forgeable. Fix: CSPRNG only.
130
+ - **Disabling TLS verification** (`verify=False`, `InsecureSkipVerify`, `rejectUnauthorized:false`). Silent MITM. Fix: validate chain + SAN; only bypass in isolated tests.
131
+ - **Weak TLS** (TLS 1.0/1.1, CBC suites, static RSA, RC4/3DES). Fix: TLS 1.2+/1.3, AEAD+ECDHE suites; verify with testssl.sh.
132
+ - **`==` on MACs/tags/tokens.** Timing side-channel. Fix: constant-time comparison.
133
+ - **Roll-your-own crypto / `Cipher` low-level API.** Easy to misorder encrypt-then-MAC, mishandle padding. Fix: libsodium / Tink / AWS Encryption SDK.
134
+ - **Deterministic encryption on high-cardinality PII you didn't mean to.** Leaks equality patterns. Fix: randomized by default; deterministic/blind-index only where a query needs it (step 6).
135
+
136
+ ## Verify
137
+
138
+ 1. **No banned modes/algorithms:** grep the diff for `ECB`, `AES/CBC` without an accompanying MAC, `DES`, `RC4`, `MD5`/`SHA1` on secrets, raw RSA encrypt — zero hits. All symmetric encryption is AES-GCM / ChaCha20-Poly1305 (AEAD).
139
+ 2. **Passwords are hashed, not encrypted:** grep finds argon2id/bcrypt/scrypt on the password path and **no** encrypt/decrypt of passwords; salts are per-password (encoded in the hash); cost params meet the step-7 baseline.
140
+ 3. **Nonce uniqueness:** confirm every encryption draws a fresh CSPRNG nonce (or a guaranteed-unique counter); no static/zero IV; nonce stored with ciphertext. For high volume, DEK rotation or a nonce-misuse-resistant mode is in place.
141
+ 4. **Envelope encryption holds:** bulk data is encrypted with a DEK, the DEK is wrapped by a KMS-held KEK that never leaves KMS, plaintext DEK is zeroed after use, and a `key_version` is stored per record.
142
+ 5. **AAD binds context:** moving a valid ciphertext from one row/tenant to another **fails** decryption (AAD mismatch).
143
+ 6. **Rotation works without re-encrypting everything:** rotating the KEK re-wraps DEKs only; old `key_version` ciphertext still decrypts; a DEK-destroy crypto-shreds its records (they become permanently undecryptable).
144
+ 7. **TLS posture:** `testssl.sh <host>` / SSL Labs returns **A/A+** — TLS 1.2+ only, AEAD+forward-secret suites, no CBC/RC4/3DES; mTLS validates the full chain + SAN; no `verify=False`/`InsecureSkipVerify` in non-test code.
145
+ 8. **Randomness + timing:** all keys/nonces/salts come from a CSPRNG (no `Math.random`/`rand`); MAC/tag/token comparisons are constant-time.
146
+ 9. **Tamper detection:** flipping one ciphertext byte makes decryption **fail** (auth tag rejects it) rather than returning garbage plaintext.
147
+
148
+ Done = sensitive data is encrypted with AEAD under unique nonces, bulk data uses KMS envelope encryption with versioned, rotatable keys and context-binding AAD, passwords are hashed with argon2id/bcrypt (never encrypted), PII fields are randomized-encrypted (deterministic/blind-index only where a query demands it), transport is TLS 1.2+/1.3 with modern suites and mTLS where needed, and all keys/nonces/salts come from a CSPRNG with constant-time tag checks — all proven by checks 1–9, with `security-review` run over the crypto diff.
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: feature-flags-rollout
3
+ description: Implements feature flags and progressive delivery — kill switches, percentage/targeted rollouts, sticky hashed bucketing, fail-safe evaluation, 1→10→50→100 ramps with guardrail-metric rollback, and TTL-enforced stale-flag cleanup — so changes ship decoupled from deploys and reverse in seconds.
4
+ when_to_use: Adding a flag, gating a feature, running a percentage/canary/ring rollout decoupled from deploy, building a kill switch, targeting by user/segment/plan, or paying down flag debt. Covers OpenFeature-compatible managed flag platforms, vendor SDKs, and homegrown flag tables. Distinct from deploy-release (ships the artifact; flags gate behavior inside it) and auth-jwt-session (establishes entitlement; flags must never compute it).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the request is about **decoupling a behavior change from the deploy that carries it**:
10
+
11
+ - "Put this behind a flag so we can turn it off without redeploying"
12
+ - "Roll it out to 1% / 10% / a canary ring, then ramp"
13
+ - "Add a kill switch for the new checkout / payments path"
14
+ - "Only enable for plan=enterprise / this segment / our internal allowlist"
15
+ - "Migrate from env-var booleans to a managed flag platform / OpenFeature"
16
+ - "Clean up dead flags / we have 300 flags and nobody knows which are live"
17
+
18
+ NOT this skill:
19
+ - Shipping/promoting the build, blue-green, canary *infra*, rollback of the artifact → deploy-release
20
+ - Deciding *who the user is* or whether they paid → auth-jwt-session (a flag gates rollout; it does not grant entitlement)
21
+ - Computing experiment lift / significance / metric tables from exposure logs → write-analytical-sql
22
+ - Hiding the provider SDK key / signing flag payloads → secrets-management
23
+ - Gating prompt/model changes behind a regression score → llm-eval-harness
24
+
25
+ ## Steps
26
+
27
+ 1. **Classify the flag first — type dictates lifetime, owner, and removal policy.** Do not create a flag without picking one.
28
+
29
+ | Type | Purpose | Lifetime | Owner | Removal |
30
+ |---|---|---|---|---|
31
+ | **Release** | Gate in-progress code, ramp it | days–weeks | feature author | **delete at 100% or revert** — TTL-enforced |
32
+ | **Kill switch (ops)** | Instantly disable a risky path | permanent | on-call/SRE | keep; review yearly |
33
+ | **Ops/config** | Tunables (timeouts, batch size, region) | permanent | platform | keep; document |
34
+ | **Experiment** | A/B exposure split | length of test | data/PM | delete when test concludes |
35
+ | **Permission/entitlement** | Plan/role gating | permanent | product | keep — but source of truth is auth, not the flag |
36
+
37
+ Release flags are 90% of debt. Every one gets an owner + removal date at creation (step 7). Default any new flag to **release** unless it's clearly a permanent switch.
38
+
39
+ 2. **Pick the evaluation locus — server-side by default.** Evaluate where the decision is *trusted and cheap*.
40
+
41
+ | Locus | Use for | Hard rule |
42
+ |---|---|---|
43
+ | **Server** (default) | entitlement-adjacent gating, anything secret, backend behavior | rule logic + flag values never leave the server |
44
+ | **Client** | pure UX (show new button, layout) | only flags in the **public** set; client can lie |
45
+ | **Edge/CDN** | geo/ring routing at the boundary | static rules only |
46
+
47
+ **Never evaluate an entitlement or paywall in the browser** — the user controls the client and can flip any client-side flag with devtools. Gate the *capability* server-side; the client flag only hides the UI. Server SDKs evaluate locally against a streamed ruleset (no per-request network call); client SDKs fetch a bootstrapped, scoped flag set.
48
+
49
+ 3. **Define a deterministic key + a fail-safe default.** The default is what runs when the provider is unreachable — and it *will* be unreachable.
50
+
51
+ ```ts
52
+ // ONE typed helper, the only place flags are read (step 5).
53
+ export function flag<T>(key: FlagKey, ctx: EvalContext, fallback: T): T {
54
+ try {
55
+ return client.variation(key, ctx, fallback); // local eval, no network
56
+ } catch (e) {
57
+ metrics.increment("flag.eval_error", { key });
58
+ return fallback; // FAIL-SAFE: never throw
59
+ }
60
+ }
61
+ // Release flag → fallback = OFF (old code path). Kill switch → fallback = "killed/safe".
62
+ ```
63
+
64
+ Rules: a flag read **must not throw, block, or call out per request**. Fall to **last-known-good** (SDK cache) → then the **hardcoded fallback**. For release flags the fallback is the *old* behavior (fail-off). For kill switches the fallback is the *safe* state (path disabled). Never let SDK init failure crash startup — init async with a timeout and serve fallbacks until ready.
65
+
66
+ 4. **Targeting — percentage by stable hashed bucketing, not RNG.** Bucketing must be **sticky**: the same user sees the same variant across requests, servers, and deploys.
67
+
68
+ ```ts
69
+ // Deterministic bucket 0..9999 — identical on every server, no shared state.
70
+ function bucket(flagKey: string, unitId: string): number {
71
+ const h = sha1(`${flagKey}:${unitId}`); // salt with flagKey so flags are independent
72
+ return parseInt(h.slice(0, 8), 16) % 10000;
73
+ }
74
+ const inRollout = bucket("new-checkout", user.id) < rolloutPct * 100; // 10% → <1000
75
+ ```
76
+
77
+ - **Bucketing unit** = a stable id (userId / accountId / deviceId) — **never** session id, request time, or `Math.random()` (those reshuffle users every request → broken/flickering UX and uninterpretable experiments).
78
+ - Salt the hash with the flag key so two flags at 10% don't hit the *same* 10% of users (correlated rollouts).
79
+ - **Rule order:** allowlist (force-on for QA/internal) → segment/plan rules → percentage → default. First match wins; make precedence explicit.
80
+ - Ramping the percentage must only *add* users, never reshuffle: monotonic threshold on a fixed hash guarantees a user inside 10% stays inside 50%.
81
+
82
+ 5. **Wrap every read behind the one typed helper from step 3.** No raw `client.variation(...)` or `process.env.FEATURE_X` scattered in code. Centralizing gives you: a single fallback policy, one audit point for cleanup, typed keys (no stringly-typed typos), and a place to log exposure for experiments. Key names are namespaced and stable: `team.feature.scope` (e.g. `checkout.new-flow.enabled`).
83
+
84
+ 6. **Ramp on a schedule with a guardrail metric and a one-flip rollback.** Decoupled from deploy means rollback = flip the flag, not redeploy.
85
+
86
+ | Stage | Audience | Hold | Watch (guardrail) |
87
+ |---|---|---|---|
88
+ | 0% + allowlist | internal/QA | until smoke passes | manual QA |
89
+ | 1% | canary cohort | ≥1 peak hour | error rate, p99 latency, the feature's own success metric |
90
+ | 10% | — | ≥1 business day | + downstream load, support tickets |
91
+ | 50% | — | ≥1 day | + cost / DB / queue depth |
92
+ | 100% | everyone | bake 1 week | then **delete the flag** (step 7) |
93
+
94
+ Pick the guardrail **before** ramping (e.g. "5xx rate must stay <0.5%, checkout-success must not drop >1pp"). Wire an automated trip if you can: guardrail breach → set flag to 0% (kill). A flag flip propagates in seconds; a redeploy does not — that gap is the entire point. Never jump 1%→100%.
95
+
96
+ 7. **Lifecycle = owner + removal date + CI enforcement.** A flag with no expiry is permanent debt.
97
+ - At creation, record `{ owner, type, created, removeBy }` (flag description, a registry table, or `// @flag-owner @removeBy=YYYY-MM-DD` next to the helper call).
98
+ - **CI fails the build when a `release` flag passes its `removeBy`** — grep flag metadata, exit nonzero on any overdue release flag. This is the single highest-leverage anti-debt control.
99
+ - Cleanup is a real PR: delete the flag key in the provider **and** the `flag()` call **and** the now-dead branch — keep the winning path, remove the loser. Archive the flag (don't hard-delete history) so old exposure logs stay interpretable.
100
+ - Kill switches and ops flags are exempt from TTL but get an annual review.
101
+
102
+ 8. **Test both branches; flag-off is the safe default.** Every gated change has two live code paths — both must be tested and shippable. Default the flag **off** in test config (proves old path still works), then run the suite again with it **on**. A PR that only works with the flag on is not done.
103
+
104
+ ## Common Errors
105
+
106
+ - **`Math.random()` / time / session id as the bucketing unit.** Users flicker between variants every request — broken UX and uninterpretable experiments. Hash a stable user/account id.
107
+ - **Two flags at 10% hitting the *same* users.** Forgot to salt the hash with the flag key, so rollouts are correlated. Salt = `flagKey:unitId`.
108
+ - **Reshuffling on ramp.** Changing the hash scheme/seed when going 10%→50% moves users *out* of the rollout, regressing them mid-flight. Use a monotonic threshold on one fixed hash.
109
+ - **Flag read that throws or blocks.** Provider hiccup takes down the request path. Wrap in the helper; fail to last-known-good then fallback; never network-call per request (server SDKs eval locally).
110
+ - **SDK init crashes startup.** Synchronous blocking init against an unreachable provider hangs boot. Init async with timeout, serve fallbacks until ready.
111
+ - **Entitlement evaluated client-side.** A client-side flag "unlocks" a paid feature — trivially bypassed in devtools. Gate the capability server-side; the client flag only hides UI (auth-jwt-session owns the grant).
112
+ - **Fail-open release flag.** Provider down → fallback is the *new, unfinished* path. Release fallback must be **off** (old path); only ops defaults bias to "on".
113
+ - **`process.env.FEATURE_X` booleans.** Env flags need a redeploy to flip — that's a config change, not a runtime kill switch, and defeats decoupling. Use the provider/table behind the helper.
114
+ - **Only testing the on-path.** Flag-off regressions ship silently because nobody ran the suite with the flag off. Test both states; off is the default.
115
+ - **Flag never removed.** 100% rolled out months ago, both branches still in code, the loser rotting. CI must fail past `removeBy`; cleanup deletes the dead branch.
116
+ - **Stale flag still referenced after provider deletion.** Deleting the key in the dashboard but leaving the `flag()` call → it silently serves the fallback forever (often the wrong one). Delete provider key and code in the same PR.
117
+
118
+ ## Verify
119
+
120
+ 1. **Determinism / stickiness:** Evaluate the same flag for the same user id 1000× across ≥2 processes → identical variant every time. Restart the service → still identical.
121
+ 2. **Independent rollouts:** Two flags at the same percentage do **not** select the same user set (hash is flag-salted) — compare the bucketed cohorts, overlap ≈ percentage², not 100%.
122
+ 3. **Monotonic ramp:** Take the users inside the 10% rollout; raise to 50% → every one of them is still inside (no user regresses out). Lower back → only the added tail leaves.
123
+ 4. **Fail-safe:** Block/kill the provider (firewall the SDK endpoint), send traffic → every read returns the fallback, nothing throws, requests still succeed, and `flag.eval_error` is emitted (not silent).
124
+ 5. **Kill switch latency:** Flip a kill switch to off → the gated path stops within the SDK's stream/poll interval (seconds), with **no deploy**. Time it.
125
+ 6. **Both branches green:** Full test suite passes with the flag **off** (default) and again with it **on**. CI runs at least the off state.
126
+ 7. **No raw reads:** `grep -rE "\.variation\(|process\.env\.FEATURE" src/` returns only the single helper file — every other read goes through `flag()`.
127
+ 8. **TTL enforcement:** A `release` flag with a past `removeBy` makes CI **exit nonzero**. Verify by backdating one in a throwaway branch.
128
+ 9. **Entitlement is server-trusted:** With the client-side flag forced on in devtools, the server still refuses the gated capability (403/empty), proving the browser can't unlock it.
129
+
130
+ Done = bucketing is deterministic + flag-salted + monotonic under ramp, every read goes through the fail-safe helper (provider-down test serves fallbacks without throwing), the kill switch flips in seconds with no deploy, both flag branches pass tests with off as default, no entitlement is decided client-side, and CI fails on any release flag past its removal date.
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: file-upload-object-storage
3
+ description: Implements secure file/image/video upload to object storage via short-lived presigned URLs or POST policies, with content-type + size validation, magic-byte verification, non-guessable tenant-scoped key namespacing, multipart/resumable transfer, private buckets with signed-URL access, and post-upload scan/transcode + lifecycle cleanup.
4
+ when_to_use: User is adding file/image/video upload, generating presigned/direct upload URLs, handling large/resumable/multipart transfers, validating uploads, controlling object access, or serving via signed URLs/CDN. Distinct from auth-jwt-session (who the caller is — this consumes that identity) and secrets-management (storing the bucket credentials themselves).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when bytes flow **from a client into a bucket** and back out under access control:
10
+
11
+ - "Let users upload an avatar / document / video to object storage (S3, GCS, R2, Azure Blob)"
12
+ - "Generate a presigned URL / POST policy so the browser uploads direct-to-bucket"
13
+ - "Handle large/resumable uploads, multipart with retry"
14
+ - "Validate uploaded files — block executables, cap size, check the real type"
15
+ - "Keep these files private and serve them with a time-limited signed URL / behind a CDN"
16
+ - "Scan/resize/transcode after upload; clean up orphaned objects when a record is deleted"
17
+
18
+ NOT this skill:
19
+ - Deciding *who* may request an upload URL or read an object → auth-jwt-session (this skill consumes the authenticated identity; it does not establish it)
20
+ - Where the bucket's own access keys / service-account JSON live → secrets-management
21
+ - Broad storage-class/egress cost modeling across services → cloud-cost-optimize (this covers only per-object lifecycle/tiering for uploads)
22
+ - Front-end image perf (responsive `srcset`, lazy-load, format) → optimize-core-web-vitals
23
+ - Validating the *other* form fields around the file → build-form-validation
24
+ - App-response caching to cut load → caching-strategy (CDN here is for object delivery, not API responses)
25
+
26
+ ## Steps
27
+
28
+ 1. **Default: client uploads direct-to-bucket via a short-lived presigned credential — never stream bytes through your app server.** Proxying uploads burns app memory/bandwidth and adds a hop. Your server only mints a credential and records metadata. Pick the credential type:
29
+
30
+ | Mechanism | Use when | Constrains |
31
+ |---|---|---|
32
+ | **Presigned POST policy** (S3 `create_presigned_post`, R2 same) | Browser/HTML form, single file, **want server-enforced size + type** | `Content-Type`, `content-length-range`, exact key or key prefix, expiry |
33
+ | Presigned PUT URL | Simple programmatic single-shot PUT | content-type + expiry only (no size cap pre-upload) |
34
+ | **Multipart presigned** (`create_multipart_upload` + per-part `upload_part` URLs) | File > ~100 MB, flaky networks, resumable | per-part, parallel, retryable |
35
+ | GCS resumable session (`x-goog-resumable:start`) / Azure Block Blob (`PutBlock`+`PutBlockList`) | GCS/Azure large or resumable | session URL + range |
36
+
37
+ Default for web uploads = **presigned POST policy** because it is the only one that enforces a size ceiling *before* bytes land. Expiry: **5 minutes** (`Expires=300`). Generate per-upload, single-use.
38
+
39
+ 2. **Validate on the boundary — twice. Never trust client-supplied MIME or extension.** A `.jpg` can be a polyglot HTML/JS or a zip bomb. Enforce in two places:
40
+ - **In the policy (pre-upload, hard ceiling):** size via `content-length-range` and a `Content-Type` allowlist condition. Example POST policy conditions:
41
+ ```python
42
+ fields = {"Content-Type": content_type, "x-amz-meta-owner": str(user_id)}
43
+ conditions = [
44
+ {"bucket": BUCKET},
45
+ ["starts-with", "$key", f"tenants/{tenant_id}/uploads/"],
46
+ {"Content-Type": content_type}, # must equal the one you signed
47
+ ["content-length-range", 1, 10 * 1024 * 1024], # 1 B .. 10 MiB
48
+ ]
49
+ post = s3.generate_presigned_post(BUCKET, key, Fields=fields,
50
+ Conditions=conditions, ExpiresIn=300)
51
+ ```
52
+ S3 rejects with `403 EntityTooLarge` / `AccessDenied` if the upload violates the policy — the server never sees the oversized body.
53
+ - **Server-side after upload, by magic bytes:** on the upload event (step 6), read the **first 256–512 bytes** and sniff the real type (`file --mime-type -`, `python-magic`, Go `net/http.DetectContentType`, Node `file-type`). If the sniffed type is not in your allowlist, delete the object and mark the record `rejected`. Allowlist concrete types (`image/jpeg image/png image/webp image/gif application/pdf video/mp4`) — never a denylist.
54
+
55
+ 3. **Design the key/namespace: non-guessable, tenant-scoped, no user-controlled path segments.** Pattern: `tenants/{tenant_id}/{kind}/{uuidv4}{ext}` — e.g. `tenants/9f3.../avatars/0b1e7d2a-...-.webp`.
56
+ - Use a **server-generated UUIDv4/ULID** as the object id; never the raw filename. This blocks enumeration *and* path traversal (`../../etc/passwd`, leading `/`, `%2e%2e`).
57
+ - Sanitize/extension only: strip everything but a known-good extension derived from the **validated** type, not the client name.
58
+ - Store the real filename, owner, sniffed content-type, size, and `status` in **your DB** — the bucket is a blob store, not a database. The DB row is the source of truth; the object is referenced by key.
59
+ - Same prefix discipline lets one IAM policy / lifecycle rule target `tenants/*/uploads/`.
60
+
61
+ 4. **Large files: multipart/resumable with part retry + an abort-incomplete lifecycle rule.** Parts: **8–16 MiB** each (S3 min 5 MiB except last; max 10,000 parts). Upload parts in parallel (3–4 at a time), retry a failed part by re-PUTting just that part number, then `complete_multipart_upload` with the `{PartNumber, ETag}` list. **Failed/abandoned multipart uploads keep billable orphaned parts forever** — add the lifecycle rule:
62
+ ```json
63
+ { "Rules": [{ "ID": "abort-incomplete-mpu", "Status": "Enabled",
64
+ "Filter": { "Prefix": "tenants/" },
65
+ "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 } }] }
66
+ ```
67
+ GCS resumable / Azure uncommitted blocks need the equivalent (resumable sessions expire in 7 days; Azure uncommitted blocks GC after 7 days).
68
+
69
+ 5. **Access control: private buckets by default; serve via time-limited signed URLs.** Block all public access (`PublicAccessBlockConfiguration` all-true on S3; Uniform bucket-level access + no `allUsers` on GCS). Two-bucket split:
70
+ - **`public-assets`** bucket/prefix → genuinely public, immutable, cacheable (logos, released static media) → fronted by CDN, long `Cache-Control: public, max-age=31536000, immutable`.
71
+ - **`private-data`** bucket → user docs, originals → **never public**. Read access = a signed GET URL minted **per request after an authz check**, short expiry (**60–300 s**). For many objects on one page (galleries) use **signed cookies** (CloudFront) / signed-URL prefix to avoid signing each object.
72
+ The authz check (does this user own/may-read this key?) lives in **your** code before signing — the signature only proves the URL wasn't tampered with, not that the requester is entitled. That ownership decision is auth-jwt-session territory; this skill just enforces it at sign time.
73
+
74
+ 6. **Post-upload pipeline: react to the upload event, go `pending → ready`.** Insert the DB row as `status=pending` when you mint the URL. Fire on the storage event (**S3 → EventBridge/SNS/SQS or Lambda; GCS → Pub/Sub; Azure → Event Grid**) — do **not** trust a client "I'm done" callback as the only signal. In the handler: (a) magic-byte validate (step 2), (b) AV scan (e.g. ClamAV / `clamdscan`) for any user-shared file, (c) derive — resize/strip-EXIF for images, transcode for video (`ffmpeg` → HLS/MP4), (d) on success flip `status=ready`, on failure delete object + `status=rejected`. Clients only ever see/serve `ready` objects.
75
+
76
+ 7. **Lifecycle & cost: expire temp uploads, tier cold objects, CDN the hot reads.** Separate a `tmp/` prefix for unconfirmed uploads with a **1-day expiry** lifecycle rule (the orphan from an abandoned form never lingers). Transition originals not read in 30/90 days to Infrequent-Access / Nearline / Cool. Serve hot public reads through a CDN (CloudFront/Cloudflare/Fastly) with an origin-access identity so the bucket stays private to the world but readable by the CDN.
77
+
78
+ 8. **Cleanup orphaned objects on record delete.** Deleting the DB row must enqueue a delete of its object key(s) — including derived renditions/thumbnails. Do it **transactionally-ish**: delete the row, then on commit enqueue an idempotent delete job (retry-safe; a missing key is success). A nightly **reconcile** sweep (list bucket prefix vs DB keys) catches drift in both directions — objects with no row, rows with no object.
79
+
80
+ ## Common Errors
81
+
82
+ - **Proxying upload bytes through the app server.** OOMs on large files, doubles bandwidth. Mint a presigned credential; let the client PUT/POST straight to the bucket.
83
+ - **Trusting `Content-Type`/extension from the client.** Spoofable; enables stored-XSS via a `.jpg` that's really HTML served inline. Verify magic bytes server-side and serve user files with `Content-Disposition: attachment` + `X-Content-Type-Options: nosniff`.
84
+ - **Putting the user's filename in the key.** Invites path traversal and enumeration. Key on a server UUID; keep the display name in the DB.
85
+ - **Public bucket / public ACL "just to make it work."** Leaks every object and lets anyone overwrite. Block public access; use signed URLs. A `?`-less object URL that loads in incognito is a finding.
86
+ - **Presigned URL with hours/days expiry.** A leaked long-lived URL is a permanent backdoor. Cap at minutes; mint per request.
87
+ - **No `content-length-range` in the POST policy.** Client uploads a 5 GB file and you pay for it. Always set a size ceiling in the policy, not just a client-side JS check.
88
+ - **Authz only at URL-mint time, never re-checked.** Object IDs leak in logs/referers; a stale signed URL outlives the user's permission. Keep expiry short and re-authorize on every mint.
89
+ - **Forgetting `AbortIncompleteMultipartUpload`.** Failed multipart uploads accrue invisible, billable parts indefinitely. Add the 1-day abort rule on day one.
90
+ - **Marking `ready` before the scan/transcode finishes.** Serves unscanned malware or a half-written object. Flip to `ready` only from the post-upload handler.
91
+ - **Deleting the DB row but not the object (or vice-versa).** Orphans cost money and leak data; missing objects 404 live records. Enqueue an idempotent object delete on row delete + run a reconcile sweep.
92
+ - **CORS not configured on the bucket.** Browser direct-PUT/POST fails preflight. Set `AllowedMethods` (`PUT POST`), `AllowedOrigins` (your exact origins, not `*`), and expose `ETag` for multipart.
93
+ - **Same bucket for public assets and private originals.** One misconfig exposes everything. Split public-asset and private-data buckets with different policies.
94
+
95
+ ## Verify
96
+
97
+ 1. **Direct-to-bucket works:** client obtains a presigned POST/PUT and uploads with no bytes touching the app server (confirm app logs show only the mint call, not the body).
98
+ 2. **Size ceiling enforced server-side:** upload `max+1` bytes → bucket rejects (`403 EntityTooLarge`/policy violation); the body never reaches you. A client that disables the JS size check still cannot exceed the policy.
99
+ 3. **Type spoof blocked:** upload an HTML/EXE file renamed `.png` with `Content-Type: image/png` → passes the policy but the magic-byte check deletes it and sets `status=rejected`; it is never served `ready`.
100
+ 4. **Key is non-guessable + traversal-proof:** the stored key is a server UUID under `tenants/{id}/...`; a key containing `../`, a leading `/`, or the raw filename is rejected/never produced.
101
+ 5. **Privacy:** the raw object URL (no signature) returns `403` in an incognito session; a freshly signed URL returns `200`; the **same** URL after `Expires` returns `403`.
102
+ 6. **Cross-tenant denied:** user A's signed-URL request for user B's key fails the authz check at mint time (no URL issued), not just at read time.
103
+ 7. **Resumable:** kill the network mid-multipart, resume → only missing parts re-upload, `complete` succeeds, final object bytes match the source checksum (`s3 cp` then `sha256sum`).
104
+ 8. **Orphan hygiene:** an abandoned multipart upload is gone after the abort window; a `tmp/` object expires per its 1-day rule; deleting a record removes its object + renditions (verify via `aws s3 ls`/reconcile sweep shows no drift).
105
+ 9. **Post-upload state machine:** a fresh upload is `pending`, becomes `ready` only after scan+derive complete, and a malicious/corrupt file ends `rejected` with the object deleted.
106
+
107
+ Done = uploads go direct-to-bucket under a ≤5-min single-use credential with a server-enforced size cap and magic-byte type check; objects are private, tenant-scoped, non-guessable, and readable only via short-lived signed URLs after an ownership check; large uploads resume and abandoned parts auto-abort; and every record delete (plus a reconcile sweep) leaves zero orphaned objects.
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: fuzz-dynamic-security-test
3
+ description: Sets up dynamic security testing — coverage-guided fuzzing of parsers and input handlers (libFuzzer/cargo-fuzz/AFL++/go test -fuzz/atheris) and DAST scanning of a running app (OWASP ZAP/nuclei) — wired into CI with seed corpora, crash minimization, baseline suppression, and regression-corpus commits.
4
+ when_to_use: Hardening code that parses untrusted input, or a running web app, with active runtime testing that drives real inputs to provoke crashes/vulns. Distinct from write-tests (functional-correctness tests), security-review (static code audit), remediate-web-vulnerabilities (fixing a known vuln), and load-stress-test (performance under load).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when you want to **actively drive inputs at code or a running app** to provoke crashes/vulns, not reason about them statically:
10
+
11
+ - "Fuzz this parser / deserializer / protocol decoder / image or PDF loader for crashes"
12
+ - "Set up cargo-fuzz / libFuzzer / `go test -fuzz` / atheris / AFL++ with a seed corpus and run it in CI"
13
+ - "An input crashes / hangs / OOMs — minimize it and add a regression test"
14
+ - "Run OWASP ZAP / nuclei against staging, authenticated, and triage the findings"
15
+ - "Wire short fuzz on PR + long nightly fuzz, and DAST on every staging deploy"
16
+
17
+ NOT this skill:
18
+ - Functional correctness / example-based unit tests → write-tests
19
+ - Reading code by eye for injection/authz/secret bugs (no execution) → security-review
20
+ - Fixing a *specific known* SQLi/XSS/SSRF you already found → remediate-web-vulnerabilities
21
+ - Measuring latency/throughput/breaking point under concurrency → load-stress-test
22
+ - Reviewing an authorization *design* rather than testing it at runtime → design-authorization-model
23
+
24
+ If a finding is confirmed, hand the fix to remediate-web-vulnerabilities; this skill *finds and reproduces*, it does not patch app logic.
25
+
26
+ ## Steps
27
+
28
+ 1. **Pick the right tool for the target — do not write a fuzzer by hand.** Coverage-guided engines mutate toward new code paths; random byte-spray finds nothing. Match the language:
29
+
30
+ | Target | Engine | Harness entry | Sanitizers |
31
+ |---|---|---|---|
32
+ | C/C++ | **libFuzzer** (clang `-fsanitize=fuzzer`) | `LLVMFuzzerTestOneInput(const uint8_t*, size_t)` | ASan + UBSan (+ MSan separately) |
33
+ | Rust | **cargo-fuzz** (libFuzzer under the hood) | `fuzz_target!(\|data: &[u8]\| { ... })` | ASan on by default |
34
+ | Go | **native `go test -fuzz`** | `func FuzzX(f *testing.F)` + `f.Fuzz(...)` | race + built-in checks |
35
+ | Python | **atheris** (libFuzzer bindings) | `atheris.Setup` + `TestOneInput(data)` | native-ext ASan optional |
36
+ | JS/TS | **Jazzer.js** | `module.exports.fuzz = (data) => {...}` | n/a (catches throws) |
37
+ | Out-of-process C binary | **AFL++** (`afl-fuzz -i in -o out`) | feed stdin/file | persistent mode + cmplog |
38
+
39
+ Default to the **in-process libFuzzer-family** engine for the language; reach for AFL++ only when you can't instrument the target (closed binary, weird build).
40
+
41
+ 2. **Fuzz the smallest deterministic boundary, structure-aware.** Target one pure `bytes → parsed value` function — the deserializer, the codec/protocol decode, the template/expression parser — not the whole HTTP handler. Make it deterministic (no clock/network/RNG/global state). For structured formats, decode the byte buffer into typed inputs with `arbitrary` (Rust) / `FuzzedDataProvider` (C++/atheris) so mutations stay valid-ish and reach deep logic instead of dying at the length check. Rust example:
42
+
43
+ ```rust
44
+ #![no_main]
45
+ use libfuzzer_sys::fuzz_target;
46
+ use arbitrary::Arbitrary;
47
+
48
+ #[derive(Arbitrary, Debug)]
49
+ struct Input { name: String, depth: u8, body: Vec<u8> }
50
+
51
+ fuzz_target!(|inp: Input| {
52
+ // never unwrap() inside a harness on the parser's own error path —
53
+ // a clean Err is correct, only a panic/abort is a finding.
54
+ let _ = my_parser::parse(&inp.name, inp.depth, &inp.body);
55
+ });
56
+ ```
57
+
58
+ 3. **Seed the corpus and add a dictionary — this multiplies coverage.** Drop real, valid sample files into `corpus/<target>/` (one input per file). Add a `.dict` of format tokens/magic bytes (`"PDF"`, `"\xFF\xD8"`, keywords) and pass `-dict=tokens.dict`. Without seeds the fuzzer wastes hours rediscovering the file header. Keep the corpus in the repo so CI starts warm.
59
+
60
+ 4. **Run, then minimize every crash before committing it.** Run locally first (`cargo fuzz run target -- -max_total_time=300` / `go test -fuzz=FuzzX -fuzztime=5m`). On a crash, **minimize the input** (`cargo fuzz tmin`, libFuzzer `-minimize_crash=1 -runs=100000`, AFL++ `afl-tmin`) so the repro is small and the root cause is obvious. Add `-rss_limit_mb`, `-timeout=`, and `-max_len=` so OOMs and hangs are reported as findings, not killed silently.
61
+
62
+ 5. **Commit each crash as a regression seed — this is the deliverable.** Copy the minimized input to `corpus/<target>/crash-<hash>` (or Go's `testdata/fuzz/FuzzX/`). It now re-runs on every fuzz invocation, so the bug can't silently return. This is what turns a one-off crash into a permanent test. Open a finding (step 7) linking the seed.
63
+
64
+ 6. **For a running app, run DAST against staging — never prod.** Stand up a disposable staging instance with seeded test data, then:
65
+ - **nuclei** for known-CVE/misconfig templates: `nuclei -u https://staging.app -severity medium,high,critical -rl 50`.
66
+ - **ZAP** for app-aware crawling: baseline first, then authenticated active scan. Authenticate (ZAP context + auth script, or pass a session cookie/Bearer) so the scanner reaches logged-in routes — an unauthenticated scan misses ~80% of the surface.
67
+ - **Baseline-suppress accepted findings** instead of muting the whole rule: keep a `zap-baseline.conf` / nuclei exclude list of triaged-and-accepted IDs so the gate only fails on *new* findings. Tune `-rl`/throttle so the active scan doesn't DoS staging.
68
+
69
+ 7. **Triage every finding: reproduce → severity → dedupe → file.** Re-run the exact input/request to confirm it's real (drop scanner false positives — reflected param that's actually encoded, "missing header" on an internal-only route). Rate by realistic impact (RCE/memory-corruption/authn-bypass = Critical; reflected-but-encoded = noise). Dedupe by crash stack / vuln class, not by input bytes — 500 inputs hitting one `parse()` panic are one bug. File with the minimized repro and the committed seed path.
70
+
71
+ 8. **Wire into CI in two tiers + a DAST stage.** Cheap on every PR, deep on a schedule:
72
+
73
+ ```yaml
74
+ # PR: smoke-fuzz only the changed/corpus seeds — must finish in <2 min, gates merge
75
+ pr-fuzz:
76
+ run: cargo fuzz run parser -- -runs=0 corpus/parser # replay corpus, no mutation
77
+ # Nightly: long mutation run, upload new crashes as artifacts, file on failure
78
+ nightly-fuzz:
79
+ run: cargo fuzz run parser -- -max_total_time=3600 -timeout=10 -rss_limit_mb=2048
80
+ # Per staging deploy: DAST gate
81
+ dast:
82
+ run: nuclei -u $STAGING_URL -severity high,critical -ed <accepted.txt> -ni
83
+ ```
84
+
85
+ PR job replays the corpus (deterministic, fast, catches regressions); nightly does the expensive mutation. Never put an unbounded mutation run on the PR critical path.
86
+
87
+ ## Common Errors
88
+
89
+ - **`unwrap()`/`expect()` in the harness on the parser's own error path.** Every malformed input then "crashes" — pure noise. A returned `Err` is correct behavior; only a panic/abort/sanitizer-trip in the *code under test* is a finding.
90
+ - **No seed corpus and no dictionary.** The fuzzer burns the whole budget rediscovering the file magic/header and never reaches real logic. Seed with valid samples; add a token `.dict`.
91
+ - **Non-deterministic harness.** Reading the clock, network, RNG, or mutating global state makes crashes non-reproducible and corrupts coverage feedback. The harness must be a pure function of `data`.
92
+ - **Committing the raw crashing input, not the minimized one.** A 4 MB repro hides the root cause and bloats the corpus. Always `tmin`/`-minimize_crash` first.
93
+ - **Fuzzing the whole HTTP handler instead of the parser.** Network/auth/DB setup dwarfs the parse step, so mutations rarely reach it — throughput collapses to a few execs/sec. Target the pure decode boundary in-process.
94
+ - **No `-rss_limit_mb`/`-timeout`/`-max_len`.** OOMs and infinite loops get OS-killed and look like a hung job instead of a reported memory/hang bug. Set explicit limits.
95
+ - **Sanitizers off (release build).** Use-after-free, OOB read, and integer-UB pass silently without ASan/UBSan — you only catch hard segfaults. Build the fuzz target with sanitizers on.
96
+ - **Running ZAP/nuclei against production.** Active scans send malicious payloads, mutate data, and can take the service down. Always a disposable staging instance with test data.
97
+ - **Unauthenticated DAST scan.** Misses every logged-in route — the high-value surface. Configure auth (context/script/session token) and verify the scanner is actually inside a session.
98
+ - **Muting a whole scanner rule to clear noise.** Hides future real hits of that class. Suppress the specific accepted finding ID in a baseline file so only *new* findings fail the gate.
99
+ - **Unbounded mutation fuzz on the PR job.** Blocks every merge for an hour or times out. PR replays the corpus; the long mutation run goes nightly.
100
+
101
+ ## Verify
102
+
103
+ 1. **Engine actually mutates and gains coverage:** a short run shows rising `cov:`/`ft:` and `exec/s` counters (libFuzzer) — not flat. Flat coverage means the harness rejects inputs at the door (wrong shape, missing `arbitrary`/`FuzzedDataProvider`).
104
+ 2. **Planted-bug catch:** add a deliberate `assert!`/OOB/`panic!` on a specific byte pattern (or use a target with a known-CVE-style flaw), run the fuzzer, and confirm it finds and minimizes the input within minutes. A fuzzer that can't catch a planted bug catches nothing.
105
+ 3. **Every crash yields a committed seed:** for each crash found, the minimized input lives in the corpus/`testdata` and is tracked in git. Re-running the harness over the corpus reproduces the crash deterministically.
106
+ 4. **Regression gate works:** with the seed committed but the bug *un*fixed, the PR corpus-replay job fails; after the fix it passes — proving the seed actually guards the regression.
107
+ 5. **DAST reproduced + authenticated:** at least one scanner finding is independently re-sent (curl/HTTP client) and reproduces; the scan log shows it traversed authenticated routes (logged-in paths visited), not just the login page.
108
+ 6. **Baseline suppression is scoped:** a previously-accepted finding is silenced by its specific ID, while a freshly introduced vuln of a *different* class still fails the gate (suppression didn't blanket the rule).
109
+ 7. **CI tiers honored:** PR fuzz finishes under its time cap (corpus replay only); nightly runs the bounded mutation budget and uploads any new crash as an artifact + files it.
110
+
111
+ Done = the engine provably mutates toward new coverage, a planted bug or known-CVE pattern is caught and minimized, every crash is committed as a regression seed that fails-then-passes across the fix, and DAST runs authenticated against staging with scoped baseline suppression and a two-tier CI wiring.