@xenos1996/usat 1.0.0

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/USAT.md +596 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +438 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +72 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/detect/index.d.ts +58 -0
  13. package/dist/detect/index.d.ts.map +1 -0
  14. package/dist/detect/index.js +329 -0
  15. package/dist/detect/index.js.map +1 -0
  16. package/dist/engine/audit.d.ts +21 -0
  17. package/dist/engine/audit.d.ts.map +1 -0
  18. package/dist/engine/audit.js +295 -0
  19. package/dist/engine/audit.js.map +1 -0
  20. package/dist/engine/diff.d.ts +24 -0
  21. package/dist/engine/diff.d.ts.map +1 -0
  22. package/dist/engine/diff.js +136 -0
  23. package/dist/engine/diff.js.map +1 -0
  24. package/dist/engine/evaluate.d.ts +19 -0
  25. package/dist/engine/evaluate.d.ts.map +1 -0
  26. package/dist/engine/evaluate.js +377 -0
  27. package/dist/engine/evaluate.js.map +1 -0
  28. package/dist/engine/gate.d.ts +19 -0
  29. package/dist/engine/gate.d.ts.map +1 -0
  30. package/dist/engine/gate.js +47 -0
  31. package/dist/engine/gate.js.map +1 -0
  32. package/dist/engine/loader.d.ts +22 -0
  33. package/dist/engine/loader.d.ts.map +1 -0
  34. package/dist/engine/loader.js +360 -0
  35. package/dist/engine/loader.js.map +1 -0
  36. package/dist/engine/maturity.d.ts +20 -0
  37. package/dist/engine/maturity.d.ts.map +1 -0
  38. package/dist/engine/maturity.js +157 -0
  39. package/dist/engine/maturity.js.map +1 -0
  40. package/dist/engine/score.d.ts +19 -0
  41. package/dist/engine/score.d.ts.map +1 -0
  42. package/dist/engine/score.js +141 -0
  43. package/dist/engine/score.js.map +1 -0
  44. package/dist/engine/sections.d.ts +14 -0
  45. package/dist/engine/sections.d.ts.map +1 -0
  46. package/dist/engine/sections.js +50 -0
  47. package/dist/engine/sections.js.map +1 -0
  48. package/dist/index.d.ts +23 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +17 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/report/markdown.d.ts +16 -0
  53. package/dist/report/markdown.d.ts.map +1 -0
  54. package/dist/report/markdown.js +488 -0
  55. package/dist/report/markdown.js.map +1 -0
  56. package/dist/types.d.ts +282 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +16 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/util/glob.d.ts +11 -0
  61. package/dist/util/glob.d.ts.map +1 -0
  62. package/dist/util/glob.js +86 -0
  63. package/dist/util/glob.js.map +1 -0
  64. package/dist/util/project.d.ts +72 -0
  65. package/dist/util/project.d.ts.map +1 -0
  66. package/dist/util/project.js +586 -0
  67. package/dist/util/project.js.map +1 -0
  68. package/dist/util/yaml.d.ts +28 -0
  69. package/dist/util/yaml.d.ts.map +1 -0
  70. package/dist/util/yaml.js +48 -0
  71. package/dist/util/yaml.js.map +1 -0
  72. package/docs/ARCHITECTURE.md +119 -0
  73. package/docs/README.md +27 -0
  74. package/docs/adr/0001-record-architecture-decisions.md +35 -0
  75. package/docs/adr/0002-typescript-engine-over-python.md +49 -0
  76. package/docs/adr/0003-rules-are-data-not-code.md +50 -0
  77. package/docs/adr/0004-markdown-only-output.md +41 -0
  78. package/docs/adr/0005-maturity-dampens-severity.md +56 -0
  79. package/docs/adr/0006-severity-status-two-axes.md +48 -0
  80. package/docs/adr/0007-suppressions-are-visible-and-expiring.md +46 -0
  81. package/docs/adr/0008-report-trailer-and-diff.md +48 -0
  82. package/docs/adr/0009-fail-closed-on-malformed-input.md +56 -0
  83. package/docs/adr/0010-detector-design.md +54 -0
  84. package/docs/adr/0011-coexist-with-deep-scanners.md +52 -0
  85. package/docs/adr/README.md +32 -0
  86. package/docs/agent-integration.md +129 -0
  87. package/docs/ci-integration.md +163 -0
  88. package/docs/concepts.md +196 -0
  89. package/docs/configuration.md +105 -0
  90. package/docs/detectors.md +186 -0
  91. package/docs/getting-started.md +99 -0
  92. package/docs/maturity-profiles.md +156 -0
  93. package/docs/reference/api.md +193 -0
  94. package/docs/reference/cli.md +412 -0
  95. package/docs/rule-packs.md +278 -0
  96. package/docs/standards-mapping.md +171 -0
  97. package/package.json +96 -0
  98. package/rules/core/architecture.yaml +190 -0
  99. package/rules/core/cicd.yaml +212 -0
  100. package/rules/core/code-quality.yaml +261 -0
  101. package/rules/core/dependencies.yaml +132 -0
  102. package/rules/core/documentation.yaml +119 -0
  103. package/rules/core/future-readiness.yaml +91 -0
  104. package/rules/core/release.yaml +131 -0
  105. package/rules/core/repo.yaml +303 -0
  106. package/rules/core/security.yaml +494 -0
  107. package/rules/core/supply-chain.yaml +357 -0
  108. package/rules/core/testing.yaml +260 -0
  109. package/rules/detectors.yaml +2255 -0
  110. package/rules/index.yaml +41 -0
  111. package/rules/profiles/maturity.yaml +135 -0
  112. package/rules/stacks/ai-era.yaml +193 -0
  113. package/rules/stacks/api-backend.yaml +121 -0
  114. package/rules/stacks/cli.yaml +105 -0
  115. package/rules/stacks/compliance.yaml +157 -0
  116. package/rules/stacks/containers.yaml +124 -0
  117. package/rules/stacks/data.yaml +165 -0
  118. package/rules/stacks/go.yaml +134 -0
  119. package/rules/stacks/iac.yaml +128 -0
  120. package/rules/stacks/jvm.yaml +150 -0
  121. package/rules/stacks/ml-ai.yaml +140 -0
  122. package/rules/stacks/mobile.yaml +121 -0
  123. package/rules/stacks/node-typescript.yaml +166 -0
  124. package/rules/stacks/python.yaml +175 -0
  125. package/rules/stacks/rust.yaml +129 -0
  126. package/rules/stacks/solidity.yaml +139 -0
  127. package/rules/stacks/web-frontend.yaml +159 -0
  128. package/templates/AGENTS.audit.md +135 -0
  129. package/templates/AUDIT_REPORT.md +155 -0
@@ -0,0 +1,494 @@
1
+ id: core/security
2
+ title: Security
3
+ section: S2
4
+ section_title: Security
5
+ description: >
6
+ Universal security baseline. Severity is never dampened here below CRITICAL,
7
+ and security-class rules carry the heaviest multiplier in the score.
8
+ USAT is not a penetration test — it finds the doors that were left open.
9
+ version: '1.0'
10
+
11
+ rules:
12
+ # ---------------------------------------------------------------- secrets
13
+ - id: SEC-001
14
+ title: No hardcoded credentials in source
15
+ section: S2
16
+ section_title: Security
17
+ severity: CRITICAL
18
+ class: security
19
+ check:
20
+ kind: grep_absent
21
+ pattern: '(api[_-]?key|apikey|secret|password|passwd|access[_-]?token|private[_-]?key|client[_-]?secret)\s*[:=]\s*[''"`][A-Za-z0-9._/+_-]{12,}[''"`]'
22
+ include:
23
+ [
24
+ '**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,swift,rb,php,c,cpp,cs,scala,ex,exs,dart,sh,yml,yaml,toml,ini,conf,env}',
25
+ ]
26
+ exclude:
27
+ [
28
+ '**/*.test.*',
29
+ '**/*.spec.*',
30
+ '**/test/**',
31
+ '**/tests/**',
32
+ '**/fixtures/**',
33
+ '**/*.example',
34
+ '**/*.md',
35
+ '**/docs/**',
36
+ ]
37
+ flags: i
38
+ why: 'Hardcoded credentials leak through every clone, fork, and CI log. Rotation after exposure is mandatory, not optional.'
39
+ remediation: 'Move to environment variables backed by a secret manager; rotate anything that was ever committed.'
40
+ references: ['CWE-798', 'ASVS-2.10.4', 'OWASP-A02:2021']
41
+
42
+ - id: SEC-002
43
+ title: Sensitive data is not written to logs
44
+ section: S2
45
+ section_title: Security
46
+ severity: HIGH
47
+ class: security
48
+ check:
49
+ kind: grep_wrong
50
+ # The sensitive word must be followed by a separator and a *value* —
51
+ # `token:`, `password=`, `api_key: "${k}"`. Matching the bare word flags
52
+ # every log line that merely talks about secrets, including the honest
53
+ # ones ("No unexpected secrets found.").
54
+ pattern: '(console\.(log|info|debug|warn|error)|print|logger\.(info|debug|warn|error)|log\.(Info|Debug|Warn|Error))\s*\(.*(password|passwd|secret|token|api[_-]?key|authorization|ssn|credit[_-]?card)\b\s*[''"`]?\s*[:=]'
55
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php}']
56
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/test/**', '**/tests/**']
57
+ flags: i
58
+ why: 'Logs are replicated, shipped, indexed, and retained longer than the data itself. A password in a log is a password in five systems.'
59
+ remediation: 'Log identifiers, not values. Add a redaction filter on the logging pipeline for known sensitive keys.'
60
+ references: ['CWE-532', 'ASVS-7.1.1', 'OWASP-A09:2021']
61
+
62
+ # ------------------------------------------------------------ transport
63
+ - id: SEC-003
64
+ title: No plaintext HTTP endpoints in configuration
65
+ section: S2
66
+ section_title: Security
67
+ severity: HIGH
68
+ class: security
69
+ applies_when:
70
+ any:
71
+ - { fact: 'platform:server' }
72
+ - { fact: 'platform:web' }
73
+ - { fact: 'has:containers' }
74
+ check:
75
+ kind: grep_absent
76
+ # NOTE: `http://` only — an earlier revision used `https?://` and flagged
77
+ # every compliant https:// URL (including this repo's own package.json)
78
+ # as a plaintext-HTTP FAIL. The `s?` must never come back.
79
+ pattern: 'http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])[a-z0-9.-]+'
80
+ include:
81
+ ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php,yml,yaml,json,toml,env,conf,nginx}']
82
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/README*', '**/*.md', '**/docs/**']
83
+ flags: i
84
+ why: 'An http:// URL to a real host means credentials and user data cross the network in the clear.'
85
+ remediation: 'Move every non-localhost URL to https://. Terminate TLS at the edge and keep it on internally too.'
86
+ references: ['CWE-319', 'ASVS-9.1.1']
87
+
88
+ - id: SEC-004
89
+ title: TLS verification is not disabled
90
+ section: S2
91
+ section_title: Security
92
+ severity: HIGH
93
+ class: security
94
+ check:
95
+ kind: grep_absent
96
+ pattern: '(rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*[''"]?0|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true|curl\s+.*-k\b|GIT_SSL_NO_VERIFY)'
97
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php,sh,yml,yaml,json}']
98
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/test/**', '**/tests/**']
99
+ flags: i
100
+ why: 'Disabling certificate verification turns every TLS connection into plaintext with extra steps.'
101
+ remediation: 'Fix the certificate chain instead. If a proxy needs a custom CA, install that CA — do not disable verification.'
102
+ references: ['CWE-295', 'ASVS-9.2.3']
103
+
104
+ # -------------------------------------------------------------- injection
105
+ - id: SEC-005
106
+ title: SQL is not built by string concatenation
107
+ section: S2
108
+ section_title: Security
109
+ severity: CRITICAL
110
+ class: security
111
+ applies_when: { fact: 'has:database' }
112
+ check:
113
+ kind: grep_wrong
114
+ # NOTE: the printf alternative matches the `%` *operator* (`"..." % x`),
115
+ # never a bare `%s` placeholder: `%s` inside a string is the safe DB-API
116
+ # parameter style (`execute("... WHERE id = %s", (id,))`), and flagging
117
+ # it failed every correctly parameterised query. Bare `%s` must never
118
+ # come back as an alternative.
119
+ pattern: '(SELECT|INSERT INTO|UPDATE|DELETE FROM)\s+.*(\+\s*\w+|\$\{[^}]*\}|"\s*%\s*[^''"\s]|"\s*\+|\.format\(|f[''"])'
120
+ include: ['**/*.{ts,tsx,js,jsx,py,go,rs,java,kt,rb,php}']
121
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/migrations/**', '**/*.sql']
122
+ flags: i
123
+ why: 'String-built SQL is the oldest and most reliable way to lose a database. Parameterise or use a query builder.'
124
+ remediation: 'Use parameterised queries ($1, ?, :name) or the ORM query API. Never interpolate user input into SQL text.'
125
+ references: ['CWE-89', 'ASVS-5.3.4', 'OWASP-A03:2021']
126
+
127
+ - id: SEC-006
128
+ title: No unsafe dynamic code execution
129
+ section: S2
130
+ section_title: Security
131
+ severity: HIGH
132
+ class: security
133
+ check:
134
+ kind: grep_wrong
135
+ # `exec(` is only dangerous as child_process.exec with user input. The
136
+ # bare alternative also matched `regex.exec(body)`, and a bare `body`
137
+ # matched document.body / res.body — both flagged ordinary code. The
138
+ # lookbehind drops the method-call form; req./params/input keep signal.
139
+ pattern: '\b(eval\s*\(|new Function\s*\(|execSync\s*\(\s*[^\n)]*\$\{|(?<!\.)exec\s*\(\s*[^\n)]*(req\.|params|input)|setTimeout\s*\(\s*[''"`])'
140
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,php,rs}']
141
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/test/**', '**/tests/**', '**/scripts/**']
142
+ why: 'eval and friends turn untrusted strings into executable code. Almost every RCE starts here.'
143
+ remediation: 'Replace with explicit parsing, a lookup table, or a sandboxed expression evaluator with a capability allowlist.'
144
+ references: ['CWE-95', 'ASVS-5.2.4']
145
+
146
+ - id: SEC-007
147
+ title: Shell commands are not built from user input
148
+ section: S2
149
+ section_title: Security
150
+ severity: CRITICAL
151
+ class: security
152
+ check:
153
+ kind: grep_wrong
154
+ pattern: '(execSync|exec|spawn|system|subprocess\.(run|call|Popen)|os\.system|Runtime\.getRuntime\(\)\.exec)\s*\(.*(\$\{|%s|\+\s*\w+|f[''"]|\.format\()'
155
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php,sh}']
156
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/scripts/**']
157
+ why: 'Command injection with user-controlled input is full remote code execution, usually unauthenticated.'
158
+ remediation: 'Use execFile with an argument array (no shell), or a library call. If unavoidable, allowlist the inputs.'
159
+ references: ['CWE-78', 'ASVS-5.3.8']
160
+
161
+ - id: SEC-008
162
+ title: User input is validated at a trust boundary
163
+ section: S2
164
+ section_title: Security
165
+ severity: HIGH
166
+ class: security
167
+ check:
168
+ kind: grep_present
169
+ pattern: '(zod|joi|yup|valibot|superstruct|pydantic|marshmallow|cerberus|class-validator|go-playground/validator|validator\.js|ajv|jakarta\.validation|javax\.validation|FluentValidation|HibernateValidator|express-validator|celebrate|attrs|msgspec)'
170
+ include: ['**/*.{ts,tsx,js,jsx,py,go,rs,java,kt}']
171
+ exclude: ['**/*.test.*', '**/*.spec.*']
172
+ flags: i
173
+ why: 'Validation that lives in the UI is a suggestion. Server-side schema validation is the actual control.'
174
+ remediation: 'Define schemas at the edge (HTTP handler, queue consumer, CLI arg parser) and parse before use.'
175
+ references: ['CWE-20', 'ASVS-5.1.1', 'OWASP-A03:2021']
176
+
177
+ - id: SEC-009
178
+ title: Output is encoded rather than interpolated as HTML
179
+ section: S2
180
+ section_title: Security
181
+ severity: HIGH
182
+ class: security
183
+ applies_when:
184
+ any:
185
+ - { fact: 'platform:web' }
186
+ - { fact: 'fw:react' }
187
+ - { fact: 'fw:vue' }
188
+ - { fact: 'fw:svelte' }
189
+ - { fact: 'fw:next' }
190
+ check:
191
+ kind: grep_absent
192
+ pattern: '(dangerouslySetInnerHTML|innerHTML\s*=|v-html|\$\{.*\}\s*.*<script|document\.write\s*\()'
193
+ include: ['**/*.{ts,tsx,js,jsx,vue,svelte,html,php,rb}']
194
+ exclude: ['**/*.test.*', '**/*.spec.*']
195
+ why: 'Rendering untrusted strings as HTML is stored XSS. Frameworks do not save you if you opt out.'
196
+ remediation: 'Render as text. If HTML is genuinely required, sanitise with DOMPurify/server-side sanitiser first.'
197
+ references: ['CWE-79', 'ASVS-5.3.1', 'OWASP-A03:2021']
198
+
199
+ - id: SEC-010
200
+ title: Path traversal is prevented on filesystem access
201
+ section: S2
202
+ section_title: Security
203
+ severity: HIGH
204
+ class: security
205
+ check:
206
+ kind: grep_wrong
207
+ pattern: '(fs\.(read|write|createReadStream|createWriteStream)|open\(|os\.path\.join|path\.join)\s*\(.*(req\.|params\.|query\.|body\.|input|\$\{)'
208
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php}']
209
+ exclude: ['**/*.test.*', '**/*.spec.*', '**/scripts/**', '**/tools/**']
210
+ why: '`../../etc/passwd` is still the single most reliable bug class in file-serving code.'
211
+ remediation: 'Resolve the path, then verify it is inside the intended base directory before touching the disk.'
212
+ references: ['CWE-22', 'ASVS-12.5.1']
213
+
214
+ # ------------------------------------------------------------------ auth
215
+ - id: SEC-011
216
+ title: Passwords use a modern adaptive hash
217
+ section: S2
218
+ section_title: Security
219
+ severity: CRITICAL
220
+ class: security
221
+ applies_when: { fact: 'has:crypto' }
222
+ check:
223
+ kind: grep_absent
224
+ pattern: '(createHash\s*\(\s*[''"]?(md5|sha1|sha256)|hashlib\.(md5|sha1|sha256)\s*\(|MessageDigest\.getInstance\(\s*[''"]?(MD5|SHA-1|SHA-256)|Digest::(MD5|SHA1)\b)'
225
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php}']
226
+ exclude: ['**/*.test.*', '**/*.spec.*']
227
+ flags: i
228
+ why: 'A fast hash is a slow password check and a fast offline cracking job. GPUs do billions of SHA-256 per second.'
229
+ remediation: 'Use bcrypt (cost ≥12), scrypt, or argon2id. Never a plain digest, never reversible encryption.'
230
+ references: ['CWE-916', 'ASVS-2.4.1', 'OWASP-Password-Storage-Cheat-Sheet']
231
+
232
+ - id: SEC-012
233
+ title: Tokens and sessions expire
234
+ section: S2
235
+ section_title: Security
236
+ severity: MEDIUM
237
+ class: security
238
+ applies_when:
239
+ any:
240
+ - { fact: 'auth:jwt' }
241
+ - { fact: 'auth:session' }
242
+ check:
243
+ kind: grep_present
244
+ pattern: '(expiresIn|expire|maxAge|ttl|EXPIR|expires_at|refresh[_-]?token)'
245
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt}']
246
+ exclude: ['**/*.test.*', '**/*.spec.*']
247
+ flags: i
248
+ why: 'A token that never expires is a permanent credential the moment it leaks.'
249
+ remediation: 'Short-lived access tokens (≤15 min) plus refresh tokens with rotation and server-side revocation.'
250
+ references: ['ASVS-3.3.1', 'ASVS-3.3.4']
251
+
252
+ - id: SEC-013
253
+ title: JWTs are not accepted with alg:none or unverified
254
+ section: S2
255
+ section_title: Security
256
+ severity: HIGH
257
+ class: security
258
+ applies_when: { fact: 'auth:jwt' }
259
+ check:
260
+ kind: grep_absent
261
+ # NOTE: the final alternative requires the quote after the comma: it
262
+ # flags `verify(token, "hardcoded-secret")` but must NOT flag
263
+ # `verify(token, SECRET)` — an earlier revision had `[''"]?` (optional)
264
+ # and failed every correct two-argument verify call. The `?` must never
265
+ # come back.
266
+ pattern: '(algorithms?\s*:\s*\[?\s*[''"]none|alg\s*:\s*[''"]none|jwt\.decode\s*\((?!.*verify)|verify\s*:\s*false|jwt\.verify\s*\(\s*[^,)]+\s*,\s*[''"])'
267
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt}']
268
+ exclude: ['**/*.test.*', '**/*.spec.*']
269
+ flags: i
270
+ why: 'alg:none and unverified decode let anyone mint a token for any user.'
271
+ remediation: 'Always verify with an explicit algorithm allowlist: jwt.verify(token, secret, { algorithms: ["RS256"] }).'
272
+ references: ['CWE-347', 'ASVS-3.2.3']
273
+
274
+ - id: SEC-014
275
+ title: Failed authentication is rate limited
276
+ section: S2
277
+ section_title: Security
278
+ severity: HIGH
279
+ class: security
280
+ applies_when:
281
+ any:
282
+ - { fact: 'auth:jwt' }
283
+ - { fact: 'auth:session' }
284
+ - { fact: 'auth:oauth' }
285
+ - { fact: 'auth:apikey' }
286
+ check:
287
+ kind: grep_present
288
+ pattern: '(rate[_-]?limit|rateLimit|throttle|slowapi|express-rate-limit|@nestjs/throttler|tower.*limit|429)'
289
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,yml,yaml,conf,nginx}']
290
+ exclude: ['**/*.test.*', '**/*.spec.*']
291
+ flags: i
292
+ why: 'Without throttling, credential stuffing is just a for-loop with a wordlist.'
293
+ remediation: 'Rate-limit by IP and by account on all auth endpoints; add progressive backoff and lockout after N failures.'
294
+ references: ['CWE-307', 'ASVS-2.2.1']
295
+
296
+ - id: SEC-015
297
+ title: Authorization is enforced per request, not per route
298
+ section: S2
299
+ section_title: Security
300
+ severity: HIGH
301
+ class: security
302
+ applies_when:
303
+ any:
304
+ - { fact: 'auth:jwt' }
305
+ - { fact: 'auth:session' }
306
+ - { fact: 'auth:oauth' }
307
+ - { fact: 'has:database' }
308
+ check: { kind: manual }
309
+ why: >
310
+ Checking "is logged in" is not checking "may access THIS record". IDOR is
311
+ the most common authorization bug and no linter finds it.
312
+ evidence: 'For each data endpoint: file:line showing the ownership/permission check on the requested resource ID.'
313
+ remediation: "Centralise authorisation in middleware or a policy layer; test with a second user attempting to read the first user's records."
314
+ references: ['CWE-639', 'ASVS-4.2.1', 'OWASP-A01:2021']
315
+
316
+ - id: SEC-016
317
+ title: Secrets come from a manager in production
318
+ section: S2
319
+ section_title: Security
320
+ severity: HIGH
321
+ class: security
322
+ applies_when: { fact: 'maturity:production' }
323
+ check: { kind: manual }
324
+ why: >-
325
+ Env files and CI variables are readable by everyone who can deploy. A secret manager adds rotation, audit, and revocation.
326
+ evidence: 'Deployment configuration showing secret injection (Vault, AWS/GCP Secrets Manager, Doppler, platform secrets).'
327
+ remediation: 'Move runtime secrets out of env files into a managed secret store with audit logging and rotation.'
328
+ references: ['ASVS-14.3.3', 'NIST-SSDF-PO.3']
329
+
330
+ # ------------------------------------------------------------- transports
331
+ - id: SEC-017
332
+ title: CORS is not a wildcard
333
+ section: S2
334
+ section_title: Security
335
+ severity: HIGH
336
+ class: security
337
+ applies_when:
338
+ any:
339
+ - { fact: 'project:api' }
340
+ - { fact: 'platform:server' }
341
+ check:
342
+ kind: grep_absent
343
+ pattern: '(?:"|\s)(Access-Control-Allow-Origin|origin)\s*[:=]\s*[''"]?\*[''"]?|cors\s*\(\s*\{\s*origin\s*:\s*[''"]\*'
344
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,yml,yaml,json,conf,nginx}']
345
+ exclude: ['**/*.test.*', '**/*.spec.*']
346
+ flags: i
347
+ why: '`Access-Control-Allow-Origin: *` lets any site read authenticated responses from your API.'
348
+ remediation: 'Allowlist exact origins; only use * for genuinely public, unauthenticated assets.'
349
+ references: ['CWE-942', 'ASVS-14.5.3']
350
+
351
+ - id: SEC-018
352
+ title: Security headers are set
353
+ section: S2
354
+ section_title: Security
355
+ severity: MEDIUM
356
+ class: security
357
+ applies_when:
358
+ any:
359
+ - { fact: 'platform:web' }
360
+ - { fact: 'platform:server' }
361
+ check:
362
+ kind: grep_present
363
+ pattern: '(helmet|Content-Security-Policy|Strict-Transport-Security|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy)'
364
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,yml,yaml,json,conf,nginx,toml}']
365
+ flags: i
366
+ why: 'Four headers turn whole classes of browser attack off. They cost about six lines.'
367
+ remediation: 'Use helmet (Node) or equivalent; add HSTS (max-age ≥31536000; includeSubDomains), X-Content-Type-Options: nosniff, Referrer-Policy, and a CSP.'
368
+ references: ['ASVS-14.4.3', 'OWASP-Secure-Headers']
369
+
370
+ - id: SEC-019
371
+ title: CSRF protection for cookie-based sessions
372
+ section: S2
373
+ section_title: Security
374
+ severity: MEDIUM
375
+ class: security
376
+ applies_when: { fact: 'auth:session' }
377
+ check: { kind: manual }
378
+ why: >-
379
+ Without SameSite and a token, any site on the internet can submit forms as your logged-in users.
380
+ evidence: 'SameSite cookie attribute plus a CSRF token or double-submit check on every state-changing request.'
381
+ remediation: 'Set cookies SameSite=Lax (or Strict) and require a CSRF token on all non-GET requests.'
382
+ references: ['CWE-352', 'ASVS-4.2.2']
383
+
384
+ - id: SEC-020
385
+ title: Errors do not leak internals to clients
386
+ section: S2
387
+ section_title: Security
388
+ severity: MEDIUM
389
+ class: security
390
+ applies_when: { fact: 'platform:server' }
391
+ check:
392
+ kind: grep_wrong
393
+ pattern: '(res\.(send|json|status)\s*\(.*\berr\.(stack|message)|\.stack\b.*(res|response)\.(send|json)|app\.use\(\s*\(err, req, res.*res\.send\(\s*err\.stack)'
394
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt}']
395
+ exclude: ['**/*.test.*', '**/*.spec.*']
396
+ flags: i
397
+ why: 'Stack traces hand an attacker your file layout, framework versions, and sometimes connection strings.'
398
+ remediation: 'Return a generic error plus a correlation ID; log the detail server-side only.'
399
+ references: ['CWE-209', 'ASVS-7.4.1']
400
+
401
+ - id: SEC-021
402
+ title: Cryptographic randomness for tokens and IDs
403
+ section: S2
404
+ section_title: Security
405
+ severity: MEDIUM
406
+ class: security
407
+ check:
408
+ kind: grep_wrong
409
+ pattern: 'Math\.random\s*\(\s*\)|random\.random\s*\(\s*\)|rand\.Intn\s*\(|new Random\s*\(\s*\)'
410
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt}']
411
+ exclude:
412
+ [
413
+ '**/*.test.*',
414
+ '**/*.spec.*',
415
+ '**/test/**',
416
+ '**/tests/**',
417
+ '**/benchmarks/**',
418
+ '**/__mocks__/**',
419
+ ]
420
+ why: 'Math.random and friends are predictable. Session tokens built from them are guessable.'
421
+ remediation: 'Use crypto.randomBytes / secrets.token_urlsafe / crypto/rand / SecureRandom for anything security-relevant.'
422
+ references: ['CWE-338', 'ASVS-6.3.1']
423
+
424
+ - id: SEC-022
425
+ title: Uploads are validated for type, size, and content
426
+ section: S2
427
+ section_title: Security
428
+ severity: HIGH
429
+ class: security
430
+ applies_when: { fact: 'has:file-upload' }
431
+ check: { kind: manual }
432
+ why: >-
433
+ An upload endpoint that trusts the client's Content-Type is a remote code execution endpoint with extra steps.
434
+ evidence: 'Upload handler: allowed MIME allowlist, max size, and where the file is written (outside the web root).'
435
+ remediation: 'Allowlist extensions AND sniff content; cap size; store outside the served directory; serve from a separate origin.'
436
+ references: ['CWE-434', 'ASVS-12.5.2']
437
+
438
+ - id: SEC-023
439
+ title: PII is identified and protected at rest
440
+ section: S2
441
+ section_title: Security
442
+ severity: HIGH
443
+ class: compliance
444
+ applies_when: { fact: 'has:pii' }
445
+ check: { kind: manual }
446
+ why: >-
447
+ You cannot protect data you have not inventoried. And regulators start from the inventory, not the intent.
448
+ evidence: 'Data inventory listing PII fields, their storage location, encryption status, and retention period.'
449
+ remediation: 'Encrypt PII columns/tables at rest, restrict access by role, and set a retention/erasure schedule.'
450
+ references: ['GDPR-Art.32', 'ASVS-8.1.1', 'CWE-311']
451
+
452
+ - id: SEC-024
453
+ title: Secrets and PII are redacted in logs and telemetry
454
+ section: S2
455
+ section_title: Security
456
+ severity: MEDIUM
457
+ class: compliance
458
+ applies_when:
459
+ any:
460
+ - { fact: 'has:pii' }
461
+ - { fact: 'auth:jwt' }
462
+ check: { kind: manual }
463
+ why: >-
464
+ Logs outlive the systems that wrote them, get shipped to vendors, and are usually the least protected copy of your data.
465
+ evidence: 'Sample log lines showing that tokens, emails, and identifiers are masked or hashed.'
466
+ remediation: 'Add a serialiser that redacts a denylist of keys before writing; never log whole request bodies.'
467
+ references: ['CWE-532', 'GDPR-Art.5']
468
+
469
+ - id: SEC-025
470
+ title: Adaptive password hashing is present where auth exists
471
+ section: S2
472
+ section_title: Security
473
+ severity: HIGH
474
+ class: security
475
+ applies_when:
476
+ any:
477
+ - { fact: 'auth:jwt' }
478
+ - { fact: 'auth:session' }
479
+ - { fact: 'auth:oauth' }
480
+ - { fact: 'auth:apikey' }
481
+ check:
482
+ kind: grep_present
483
+ # NOTE: companion to SEC-011, which only proves weak hashes are absent.
484
+ # Absence of md5 is not presence of bcrypt: plaintext password storage
485
+ # passed SEC-011 cleanly. This rule demands positive evidence of an
486
+ # adaptive function. HIGH, not CRITICAL: presence-grep cannot prove the
487
+ # hash actually guards the password path (that is judgement-queue work).
488
+ pattern: '(bcrypt|argon2(id)?|scrypt|pbkdf2|password_hash|BCryptPasswordEncoder|passlib|generate_password_hash|hashpw)'
489
+ include: ['**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,kt,rb,php}']
490
+ exclude: ['**/*.test.*', '**/*.spec.*']
491
+ flags: i
492
+ why: 'No adaptive hash found anywhere auth exists. Either passwords are stored weakly (or plainly), or hashing lives outside the scanned tree — both need a human to confirm.'
493
+ remediation: 'Hash passwords with bcrypt (cost ≥12), scrypt, or argon2id at registration and verify on login. Never a plain digest, never reversible encryption.'
494
+ references: ['CWE-916', 'ASVS-2.4.1', 'OWASP-Password-Storage-Cheat-Sheet']