@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.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/USAT.md +596 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +438 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -0
- package/dist/detect/index.d.ts +58 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +329 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/engine/audit.d.ts +21 -0
- package/dist/engine/audit.d.ts.map +1 -0
- package/dist/engine/audit.js +295 -0
- package/dist/engine/audit.js.map +1 -0
- package/dist/engine/diff.d.ts +24 -0
- package/dist/engine/diff.d.ts.map +1 -0
- package/dist/engine/diff.js +136 -0
- package/dist/engine/diff.js.map +1 -0
- package/dist/engine/evaluate.d.ts +19 -0
- package/dist/engine/evaluate.d.ts.map +1 -0
- package/dist/engine/evaluate.js +377 -0
- package/dist/engine/evaluate.js.map +1 -0
- package/dist/engine/gate.d.ts +19 -0
- package/dist/engine/gate.d.ts.map +1 -0
- package/dist/engine/gate.js +47 -0
- package/dist/engine/gate.js.map +1 -0
- package/dist/engine/loader.d.ts +22 -0
- package/dist/engine/loader.d.ts.map +1 -0
- package/dist/engine/loader.js +360 -0
- package/dist/engine/loader.js.map +1 -0
- package/dist/engine/maturity.d.ts +20 -0
- package/dist/engine/maturity.d.ts.map +1 -0
- package/dist/engine/maturity.js +157 -0
- package/dist/engine/maturity.js.map +1 -0
- package/dist/engine/score.d.ts +19 -0
- package/dist/engine/score.d.ts.map +1 -0
- package/dist/engine/score.js +141 -0
- package/dist/engine/score.js.map +1 -0
- package/dist/engine/sections.d.ts +14 -0
- package/dist/engine/sections.d.ts.map +1 -0
- package/dist/engine/sections.js +50 -0
- package/dist/engine/sections.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/report/markdown.d.ts +16 -0
- package/dist/report/markdown.d.ts.map +1 -0
- package/dist/report/markdown.js +488 -0
- package/dist/report/markdown.js.map +1 -0
- package/dist/types.d.ts +282 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/util/glob.d.ts +11 -0
- package/dist/util/glob.d.ts.map +1 -0
- package/dist/util/glob.js +86 -0
- package/dist/util/glob.js.map +1 -0
- package/dist/util/project.d.ts +72 -0
- package/dist/util/project.d.ts.map +1 -0
- package/dist/util/project.js +586 -0
- package/dist/util/project.js.map +1 -0
- package/dist/util/yaml.d.ts +28 -0
- package/dist/util/yaml.d.ts.map +1 -0
- package/dist/util/yaml.js +48 -0
- package/dist/util/yaml.js.map +1 -0
- package/docs/ARCHITECTURE.md +119 -0
- package/docs/README.md +27 -0
- package/docs/adr/0001-record-architecture-decisions.md +35 -0
- package/docs/adr/0002-typescript-engine-over-python.md +49 -0
- package/docs/adr/0003-rules-are-data-not-code.md +50 -0
- package/docs/adr/0004-markdown-only-output.md +41 -0
- package/docs/adr/0005-maturity-dampens-severity.md +56 -0
- package/docs/adr/0006-severity-status-two-axes.md +48 -0
- package/docs/adr/0007-suppressions-are-visible-and-expiring.md +46 -0
- package/docs/adr/0008-report-trailer-and-diff.md +48 -0
- package/docs/adr/0009-fail-closed-on-malformed-input.md +56 -0
- package/docs/adr/0010-detector-design.md +54 -0
- package/docs/adr/0011-coexist-with-deep-scanners.md +52 -0
- package/docs/adr/README.md +32 -0
- package/docs/agent-integration.md +129 -0
- package/docs/ci-integration.md +163 -0
- package/docs/concepts.md +196 -0
- package/docs/configuration.md +105 -0
- package/docs/detectors.md +186 -0
- package/docs/getting-started.md +99 -0
- package/docs/maturity-profiles.md +156 -0
- package/docs/reference/api.md +193 -0
- package/docs/reference/cli.md +412 -0
- package/docs/rule-packs.md +278 -0
- package/docs/standards-mapping.md +171 -0
- package/package.json +96 -0
- package/rules/core/architecture.yaml +190 -0
- package/rules/core/cicd.yaml +212 -0
- package/rules/core/code-quality.yaml +261 -0
- package/rules/core/dependencies.yaml +132 -0
- package/rules/core/documentation.yaml +119 -0
- package/rules/core/future-readiness.yaml +91 -0
- package/rules/core/release.yaml +131 -0
- package/rules/core/repo.yaml +303 -0
- package/rules/core/security.yaml +494 -0
- package/rules/core/supply-chain.yaml +357 -0
- package/rules/core/testing.yaml +260 -0
- package/rules/detectors.yaml +2255 -0
- package/rules/index.yaml +41 -0
- package/rules/profiles/maturity.yaml +135 -0
- package/rules/stacks/ai-era.yaml +193 -0
- package/rules/stacks/api-backend.yaml +121 -0
- package/rules/stacks/cli.yaml +105 -0
- package/rules/stacks/compliance.yaml +157 -0
- package/rules/stacks/containers.yaml +124 -0
- package/rules/stacks/data.yaml +165 -0
- package/rules/stacks/go.yaml +134 -0
- package/rules/stacks/iac.yaml +128 -0
- package/rules/stacks/jvm.yaml +150 -0
- package/rules/stacks/ml-ai.yaml +140 -0
- package/rules/stacks/mobile.yaml +121 -0
- package/rules/stacks/node-typescript.yaml +166 -0
- package/rules/stacks/python.yaml +175 -0
- package/rules/stacks/rust.yaml +129 -0
- package/rules/stacks/solidity.yaml +139 -0
- package/rules/stacks/web-frontend.yaml +159 -0
- package/templates/AGENTS.audit.md +135 -0
- 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']
|