@kernlang/review 3.1.5 → 3.1.7
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 +17 -0
- package/dist/cache.d.ts +1 -1
- package/dist/cache.js +5 -3
- package/dist/cache.js.map +1 -1
- package/dist/call-graph.d.ts +63 -0
- package/dist/call-graph.js +380 -0
- package/dist/call-graph.js.map +1 -0
- package/dist/concept-rules/boundary-mutation.d.ts +1 -1
- package/dist/concept-rules/boundary-mutation.js.map +1 -1
- package/dist/concept-rules/ignored-error.d.ts +1 -1
- package/dist/concept-rules/ignored-error.js.map +1 -1
- package/dist/concept-rules/illegal-dependency.d.ts +1 -1
- package/dist/concept-rules/illegal-dependency.js.map +1 -1
- package/dist/concept-rules/index.js +1 -6
- package/dist/concept-rules/index.js.map +1 -1
- package/dist/concept-rules/unguarded-effect.d.ts +1 -1
- package/dist/concept-rules/unguarded-effect.js.map +1 -1
- package/dist/concept-rules/unrecovered-effect.d.ts +1 -1
- package/dist/concept-rules/unrecovered-effect.js +2 -1
- package/dist/concept-rules/unrecovered-effect.js.map +1 -1
- package/dist/confidence.js +12 -8
- package/dist/confidence.js.map +1 -1
- package/dist/differ.js +3 -7
- package/dist/differ.js.map +1 -1
- package/dist/external-tools.js +5 -6
- package/dist/external-tools.js.map +1 -1
- package/dist/file-context.d.ts +21 -0
- package/dist/file-context.js +234 -0
- package/dist/file-context.js.map +1 -0
- package/dist/file-role.js +14 -7
- package/dist/file-role.js.map +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/graph.js +24 -16
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +44 -35
- package/dist/index.js +221 -68
- package/dist/index.js.map +1 -1
- package/dist/inferrer.d.ts +8 -2
- package/dist/inferrer.js +80 -47
- package/dist/inferrer.js.map +1 -1
- package/dist/kern-lint.d.ts +3 -4
- package/dist/kern-lint.js +7 -5
- package/dist/kern-lint.js.map +1 -1
- package/dist/llm-bridge.d.ts +23 -7
- package/dist/llm-bridge.js +267 -31
- package/dist/llm-bridge.js.map +1 -1
- package/dist/llm-review.d.ts +16 -2
- package/dist/llm-review.js +240 -35
- package/dist/llm-review.js.map +1 -1
- package/dist/mappers/ts-concepts.d.ts +1 -1
- package/dist/mappers/ts-concepts.js +303 -32
- package/dist/mappers/ts-concepts.js.map +1 -1
- package/dist/norm-miner.d.ts +31 -0
- package/dist/norm-miner.js +119 -0
- package/dist/norm-miner.js.map +1 -0
- package/dist/obligations.d.ts +63 -0
- package/dist/obligations.js +158 -0
- package/dist/obligations.js.map +1 -0
- package/dist/quality-rules.d.ts +3 -3
- package/dist/quality-rules.js +4 -2
- package/dist/quality-rules.js.map +1 -1
- package/dist/reporter.d.ts +7 -2
- package/dist/reporter.js +82 -51
- package/dist/reporter.js.map +1 -1
- package/dist/rule-eval.d.ts +1 -2
- package/dist/rule-eval.js +5 -9
- package/dist/rule-eval.js.map +1 -1
- package/dist/rule-loader.js +16 -14
- package/dist/rule-loader.js.map +1 -1
- package/dist/rules/base.js +153 -69
- package/dist/rules/base.js.map +1 -1
- package/dist/rules/cli.d.ts +7 -0
- package/dist/rules/cli.js +99 -0
- package/dist/rules/cli.js.map +1 -0
- package/dist/rules/confidence.d.ts +1 -1
- package/dist/rules/confidence.js +5 -5
- package/dist/rules/confidence.js.map +1 -1
- package/dist/rules/dead-code.d.ts +10 -0
- package/dist/rules/dead-code.js +75 -0
- package/dist/rules/dead-code.js.map +1 -0
- package/dist/rules/dead-logic.js +35 -31
- package/dist/rules/dead-logic.js.map +1 -1
- package/dist/rules/express.d.ts +2 -1
- package/dist/rules/express.js +380 -126
- package/dist/rules/express.js.map +1 -1
- package/dist/rules/fastapi.d.ts +10 -0
- package/dist/rules/fastapi.js +183 -0
- package/dist/rules/fastapi.js.map +1 -0
- package/dist/rules/ground-layer.js +3 -3
- package/dist/rules/ground-layer.js.map +1 -1
- package/dist/rules/index.d.ts +5 -1
- package/dist/rules/index.js +602 -84
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ink.d.ts +8 -0
- package/dist/rules/ink.js +88 -0
- package/dist/rules/ink.js.map +1 -0
- package/dist/rules/kern-source.js +202 -63
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/nextjs.js +88 -33
- package/dist/rules/nextjs.js.map +1 -1
- package/dist/rules/null-safety.js +52 -26
- package/dist/rules/null-safety.js.map +1 -1
- package/dist/rules/nuxt.js +24 -29
- package/dist/rules/nuxt.js.map +1 -1
- package/dist/rules/react.js +355 -69
- package/dist/rules/react.js.map +1 -1
- package/dist/rules/security-v2.js +71 -57
- package/dist/rules/security-v2.js.map +1 -1
- package/dist/rules/security-v3.js.map +1 -1
- package/dist/rules/security-v4.js +54 -27
- package/dist/rules/security-v4.js.map +1 -1
- package/dist/rules/security.js +35 -5
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/terminal.d.ts +8 -0
- package/dist/rules/terminal.js +139 -0
- package/dist/rules/terminal.js.map +1 -0
- package/dist/rules/vue.js +162 -107
- package/dist/rules/vue.js.map +1 -1
- package/dist/semantic-diff.d.ts +52 -0
- package/dist/semantic-diff.js +342 -0
- package/dist/semantic-diff.js.map +1 -0
- package/dist/spec-checker.js +11 -10
- package/dist/spec-checker.js.map +1 -1
- package/dist/suppression/apply-suppression.d.ts +2 -3
- package/dist/suppression/apply-suppression.js +3 -3
- package/dist/suppression/apply-suppression.js.map +1 -1
- package/dist/suppression/index.d.ts +2 -2
- package/dist/suppression/index.js +1 -1
- package/dist/suppression/index.js.map +1 -1
- package/dist/suppression/parse-directives.d.ts +1 -1
- package/dist/suppression/parse-directives.js +9 -4
- package/dist/suppression/parse-directives.js.map +1 -1
- package/dist/taint-ast.d.ts +20 -0
- package/dist/taint-ast.js +427 -0
- package/dist/taint-ast.js.map +1 -0
- package/dist/taint-crossfile.d.ts +28 -0
- package/dist/taint-crossfile.js +174 -0
- package/dist/taint-crossfile.js.map +1 -0
- package/dist/taint-findings.d.ts +17 -0
- package/dist/taint-findings.js +131 -0
- package/dist/taint-findings.js.map +1 -0
- package/dist/taint-regex.d.ts +61 -0
- package/dist/taint-regex.js +379 -0
- package/dist/taint-regex.js.map +1 -0
- package/dist/taint-types.d.ts +128 -0
- package/dist/taint-types.js +174 -0
- package/dist/taint-types.js.map +1 -0
- package/dist/taint.d.ts +13 -107
- package/dist/taint.js +16 -1067
- package/dist/taint.js.map +1 -1
- package/dist/template-detector.d.ts +2 -2
- package/dist/template-detector.js +11 -16
- package/dist/template-detector.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
package/dist/rules/index.js
CHANGED
|
@@ -3,142 +3,656 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Layers:
|
|
5
5
|
* [base] Always active — universal TS/KERN rules
|
|
6
|
-
* [react] Active when target = nextjs | tailwind | web | native
|
|
6
|
+
* [react] Active when target = nextjs | tailwind | web | native | ink
|
|
7
|
+
* [ink] Active when target = ink (on top of react)
|
|
7
8
|
* [vue] Active when target = vue | nuxt
|
|
8
9
|
* [express] Active when target = express
|
|
10
|
+
* [cli] Active when target = cli
|
|
11
|
+
* [terminal] Active when target = terminal
|
|
12
|
+
* [fastapi] Active when target = fastapi (Python concept layer)
|
|
9
13
|
* [nextjs] Active when target = nextjs (on top of react)
|
|
10
14
|
* [nuxt] Active when target = nuxt (on top of vue)
|
|
11
15
|
*/
|
|
12
16
|
import { baseRules } from './base.js';
|
|
17
|
+
import { cliRules } from './cli.js';
|
|
18
|
+
import { deadLogicRules } from './dead-logic.js';
|
|
19
|
+
import { expressRules } from './express.js';
|
|
20
|
+
import { fastapiRules } from './fastapi.js';
|
|
21
|
+
import { inkRules } from './ink.js';
|
|
22
|
+
import { nextjsRules } from './nextjs.js';
|
|
23
|
+
import { nullSafetyRules } from './null-safety.js';
|
|
24
|
+
import { nuxtRules } from './nuxt.js';
|
|
25
|
+
import { reactRules } from './react.js';
|
|
13
26
|
import { securityRules } from './security.js';
|
|
14
27
|
import { securityV2Rules } from './security-v2.js';
|
|
15
28
|
import { securityV3Rules } from './security-v3.js';
|
|
16
29
|
import { securityV4Rules } from './security-v4.js';
|
|
17
|
-
import {
|
|
18
|
-
import { reactRules } from './react.js';
|
|
30
|
+
import { terminalRules } from './terminal.js';
|
|
19
31
|
import { vueRules } from './vue.js';
|
|
20
|
-
|
|
21
|
-
import { nuxtRules } from './nuxt.js';
|
|
22
|
-
import { expressRules } from './express.js';
|
|
23
|
-
import { nullSafetyRules } from './null-safety.js';
|
|
24
|
-
const REACT_TARGETS = new Set(['nextjs', 'tailwind', 'web', 'native']);
|
|
32
|
+
const REACT_TARGETS = new Set(['nextjs', 'tailwind', 'web', 'native', 'ink']);
|
|
25
33
|
const VUE_TARGETS = new Set(['vue', 'nuxt']);
|
|
34
|
+
/** Backend targets — never load frontend-specific rules */
|
|
35
|
+
const BACKEND_TARGETS = new Set(['express', 'fastapi', 'mcp', 'cli', 'terminal']);
|
|
26
36
|
/**
|
|
27
37
|
* Get all active review rules for a given target.
|
|
28
38
|
* Base + security + dead-logic + null-safety are always active; framework rules activate by target.
|
|
29
39
|
*/
|
|
30
40
|
export function getActiveRules(target) {
|
|
31
|
-
const rules = [
|
|
32
|
-
|
|
41
|
+
const rules = [
|
|
42
|
+
...baseRules,
|
|
43
|
+
...securityRules,
|
|
44
|
+
...securityV2Rules,
|
|
45
|
+
...securityV3Rules,
|
|
46
|
+
...securityV4Rules,
|
|
47
|
+
...deadLogicRules,
|
|
48
|
+
...nullSafetyRules,
|
|
49
|
+
];
|
|
50
|
+
// Backend targets never load frontend-specific rules
|
|
51
|
+
const isBackend = target ? BACKEND_TARGETS.has(target) : false;
|
|
52
|
+
if (!isBackend && target && REACT_TARGETS.has(target)) {
|
|
33
53
|
rules.push(...reactRules);
|
|
34
54
|
}
|
|
35
|
-
if (target && VUE_TARGETS.has(target)) {
|
|
55
|
+
if (!isBackend && target && VUE_TARGETS.has(target)) {
|
|
36
56
|
rules.push(...vueRules);
|
|
37
57
|
}
|
|
38
|
-
if (target === 'nextjs') {
|
|
58
|
+
if (!isBackend && target === 'nextjs') {
|
|
39
59
|
rules.push(...nextjsRules);
|
|
40
60
|
}
|
|
41
|
-
if (target === 'nuxt') {
|
|
61
|
+
if (!isBackend && target === 'nuxt') {
|
|
42
62
|
rules.push(...nuxtRules);
|
|
43
63
|
}
|
|
44
64
|
if (target === 'express') {
|
|
45
65
|
rules.push(...expressRules);
|
|
46
66
|
}
|
|
67
|
+
if (target === 'cli') {
|
|
68
|
+
rules.push(...cliRules);
|
|
69
|
+
}
|
|
70
|
+
if (target === 'terminal') {
|
|
71
|
+
rules.push(...terminalRules);
|
|
72
|
+
}
|
|
73
|
+
if (target === 'ink') {
|
|
74
|
+
rules.push(...inkRules);
|
|
75
|
+
}
|
|
76
|
+
if (target === 'fastapi') {
|
|
77
|
+
rules.push(...fastapiRules);
|
|
78
|
+
}
|
|
47
79
|
return rules;
|
|
48
80
|
}
|
|
49
81
|
const REGISTRY = [
|
|
50
82
|
// Base (always active)
|
|
51
|
-
{
|
|
52
|
-
|
|
83
|
+
{
|
|
84
|
+
id: 'floating-promise',
|
|
85
|
+
layer: 'base',
|
|
86
|
+
severity: 'error',
|
|
87
|
+
description: 'Unresolved async operation — missing await/void/return',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'state-mutation',
|
|
91
|
+
layer: 'base',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
description: 'Illegal state mutation outside designated setter',
|
|
94
|
+
},
|
|
53
95
|
{ id: 'empty-catch', layer: 'base', severity: 'warning', description: 'Catch block swallows exception silently' },
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
96
|
+
{
|
|
97
|
+
id: 'machine-gap',
|
|
98
|
+
layer: 'base',
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
description: 'Unreachable state or missing transition in state machine',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'config-default-mismatch',
|
|
104
|
+
layer: 'base',
|
|
105
|
+
severity: 'warning',
|
|
106
|
+
description: 'Config schema default does not match type',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'event-map-mismatch',
|
|
110
|
+
layer: 'base',
|
|
111
|
+
severity: 'warning',
|
|
112
|
+
description: 'Event handler type mismatch with event map',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'non-exhaustive-switch',
|
|
116
|
+
layer: 'base',
|
|
117
|
+
severity: 'warning',
|
|
118
|
+
description: 'Switch/map missing cases for known variants',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'cognitive-complexity',
|
|
122
|
+
layer: 'base',
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
description: 'Function exceeds cognitive complexity threshold',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'template-available',
|
|
128
|
+
layer: 'base',
|
|
129
|
+
severity: 'info',
|
|
130
|
+
description: 'Pattern matches a registered KERN template',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'handler-extraction',
|
|
134
|
+
layer: 'base',
|
|
135
|
+
severity: 'info',
|
|
136
|
+
description: 'Handler-like pattern could be extracted to KERN',
|
|
137
|
+
},
|
|
61
138
|
{ id: 'memory-leak', layer: 'base', severity: 'error', description: 'Event listener added without cleanup' },
|
|
62
139
|
{ id: 'unhandled-async', layer: 'base', severity: 'warning', description: 'Async function without error handling' },
|
|
63
|
-
{
|
|
64
|
-
|
|
140
|
+
{
|
|
141
|
+
id: 'sync-in-async',
|
|
142
|
+
layer: 'base',
|
|
143
|
+
severity: 'warning',
|
|
144
|
+
description: 'Synchronous blocking call inside async function',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'bare-rethrow',
|
|
148
|
+
layer: 'base',
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
description: 'Catch rethrows error without adding context',
|
|
151
|
+
},
|
|
65
152
|
// Security
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
{
|
|
154
|
+
id: 'xss-unsafe-html',
|
|
155
|
+
layer: 'security',
|
|
156
|
+
severity: 'error',
|
|
157
|
+
description: 'innerHTML/dangerouslySetInnerHTML with untrusted data',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'hardcoded-secret',
|
|
161
|
+
layer: 'security',
|
|
162
|
+
severity: 'error',
|
|
163
|
+
description: 'API key, password, or secret in source code',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'command-injection',
|
|
167
|
+
layer: 'security',
|
|
168
|
+
severity: 'error',
|
|
169
|
+
description: 'exec/spawn with user-controlled input',
|
|
170
|
+
},
|
|
69
171
|
{ id: 'no-eval', layer: 'security', severity: 'error', description: 'eval() or Function() constructor usage' },
|
|
70
|
-
{
|
|
172
|
+
{
|
|
173
|
+
id: 'insecure-random',
|
|
174
|
+
layer: 'security',
|
|
175
|
+
severity: 'warning',
|
|
176
|
+
description: 'Math.random() used for security-sensitive operations',
|
|
177
|
+
},
|
|
71
178
|
{ id: 'cors-wildcard', layer: 'security', severity: 'warning', description: 'CORS wildcard (*) origin allowed' },
|
|
72
|
-
{
|
|
73
|
-
|
|
179
|
+
{
|
|
180
|
+
id: 'helmet-missing',
|
|
181
|
+
layer: 'security',
|
|
182
|
+
severity: 'warning',
|
|
183
|
+
description: 'Express app without helmet security headers',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'open-redirect',
|
|
187
|
+
layer: 'security',
|
|
188
|
+
severity: 'error',
|
|
189
|
+
description: 'Unvalidated redirect target from user input',
|
|
190
|
+
},
|
|
74
191
|
// Security v2
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
192
|
+
{
|
|
193
|
+
id: 'jwt-weak-verification',
|
|
194
|
+
layer: 'security-v2',
|
|
195
|
+
severity: 'warning',
|
|
196
|
+
description: 'JWT verified without algorithm restriction',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'cookie-hardening',
|
|
200
|
+
layer: 'security-v2',
|
|
201
|
+
severity: 'error',
|
|
202
|
+
description: 'Cookie missing secure/httpOnly/sameSite flags',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'csrf-detection',
|
|
206
|
+
layer: 'security-v2',
|
|
207
|
+
severity: 'error',
|
|
208
|
+
description: 'State-changing endpoint without CSRF protection',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'csp-strength',
|
|
212
|
+
layer: 'security-v2',
|
|
213
|
+
severity: 'warning',
|
|
214
|
+
description: 'Weak Content-Security-Policy headers',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'path-traversal',
|
|
218
|
+
layer: 'security-v2',
|
|
219
|
+
severity: 'error',
|
|
220
|
+
description: 'File path from user input without sanitization',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'weak-password-hashing',
|
|
224
|
+
layer: 'security-v2',
|
|
225
|
+
severity: 'error',
|
|
226
|
+
description: 'MD5/SHA1 for password hashing instead of bcrypt/argon2',
|
|
227
|
+
},
|
|
81
228
|
// Security v3 — OWASP gap closure
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
229
|
+
{
|
|
230
|
+
id: 'regex-dos',
|
|
231
|
+
layer: 'security-v3',
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
description: 'Regex vulnerable to catastrophic backtracking (ReDoS)',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'missing-input-validation',
|
|
237
|
+
layer: 'security-v3',
|
|
238
|
+
severity: 'warning',
|
|
239
|
+
description: 'User input used without validation',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'prototype-pollution',
|
|
243
|
+
layer: 'security-v3',
|
|
244
|
+
severity: 'error',
|
|
245
|
+
description: 'Object.prototype mutation via user-controlled keys',
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: 'information-exposure',
|
|
249
|
+
layer: 'security-v3',
|
|
250
|
+
severity: 'error',
|
|
251
|
+
description: 'Stack traces or internal details in error responses',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'prompt-injection',
|
|
255
|
+
layer: 'security-v3',
|
|
256
|
+
severity: 'warning',
|
|
257
|
+
description: 'User input concatenated into LLM prompts',
|
|
258
|
+
},
|
|
87
259
|
// Security v4 — LLM attack surface
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
260
|
+
{
|
|
261
|
+
id: 'indirect-prompt-injection',
|
|
262
|
+
layer: 'security-v4',
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
description: 'LLM prompt includes data from external/DB sources',
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'llm-output-execution',
|
|
268
|
+
layer: 'security-v4',
|
|
269
|
+
severity: 'error',
|
|
270
|
+
description: 'LLM-generated code executed without sandboxing',
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: 'system-prompt-leakage',
|
|
274
|
+
layer: 'security-v4',
|
|
275
|
+
severity: 'warning',
|
|
276
|
+
description: 'System prompt exposed in error paths or responses',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'rag-poisoning',
|
|
280
|
+
layer: 'security-v4',
|
|
281
|
+
severity: 'warning',
|
|
282
|
+
description: 'RAG documents injected without provenance check',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'tool-calling-manipulation',
|
|
286
|
+
layer: 'security-v4',
|
|
287
|
+
severity: 'error',
|
|
288
|
+
description: 'Tool/function call parameters from untrusted LLM output',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: 'encoding-bypass',
|
|
292
|
+
layer: 'security-v4',
|
|
293
|
+
severity: 'warning',
|
|
294
|
+
description: 'Base64/unicode encoding used to bypass prompt filters',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'delimiter-injection',
|
|
298
|
+
layer: 'security-v4',
|
|
299
|
+
severity: 'warning',
|
|
300
|
+
description: 'Prompt delimiter breakout via user input',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'unsanitized-history',
|
|
304
|
+
layer: 'security-v4',
|
|
305
|
+
severity: 'warning',
|
|
306
|
+
description: 'Chat history concatenated without sanitization',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: 'json-output-manipulation',
|
|
310
|
+
layer: 'security-v4',
|
|
311
|
+
severity: 'warning',
|
|
312
|
+
description: 'LLM JSON output used without schema validation',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: 'missing-output-validation',
|
|
316
|
+
layer: 'security-v4',
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
description: 'LLM output consumed without validation',
|
|
319
|
+
},
|
|
98
320
|
// Dead logic
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
321
|
+
{
|
|
322
|
+
id: 'identical-conditions',
|
|
323
|
+
layer: 'dead-logic',
|
|
324
|
+
severity: 'error',
|
|
325
|
+
description: 'Duplicate conditions in if/else chain',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 'identical-expressions',
|
|
329
|
+
layer: 'dead-logic',
|
|
330
|
+
severity: 'error',
|
|
331
|
+
description: 'Same expression on both sides of operator',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'all-identical-branches',
|
|
335
|
+
layer: 'dead-logic',
|
|
336
|
+
severity: 'error',
|
|
337
|
+
description: 'All branches produce identical code',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 'constant-condition',
|
|
341
|
+
layer: 'dead-logic',
|
|
342
|
+
severity: 'warning',
|
|
343
|
+
description: 'Condition is always true or always false',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: 'one-iteration-loop',
|
|
347
|
+
layer: 'dead-logic',
|
|
348
|
+
severity: 'warning',
|
|
349
|
+
description: 'Loop body always exits on first iteration',
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'unused-collection',
|
|
353
|
+
layer: 'dead-logic',
|
|
354
|
+
severity: 'warning',
|
|
355
|
+
description: 'Collection created but never read',
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: 'empty-collection-access',
|
|
359
|
+
layer: 'dead-logic',
|
|
360
|
+
severity: 'warning',
|
|
361
|
+
description: 'Accessing elements of provably empty collection',
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: 'redundant-jump',
|
|
365
|
+
layer: 'dead-logic',
|
|
366
|
+
severity: 'info',
|
|
367
|
+
description: 'Unreachable code after return/break/continue',
|
|
368
|
+
},
|
|
107
369
|
// Null safety
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
370
|
+
{
|
|
371
|
+
id: 'unchecked-find',
|
|
372
|
+
layer: 'null-safety',
|
|
373
|
+
severity: 'warning',
|
|
374
|
+
description: 'array.find() result used without null check',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: 'optional-chain-bang',
|
|
378
|
+
layer: 'null-safety',
|
|
379
|
+
severity: 'warning',
|
|
380
|
+
description: 'Optional chain (?) immediately negated by non-null assertion (!)',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'unchecked-cast',
|
|
384
|
+
layer: 'null-safety',
|
|
385
|
+
severity: 'warning',
|
|
386
|
+
description: 'Unsafe type assertion without runtime guard',
|
|
387
|
+
},
|
|
388
|
+
// React (target: nextjs, tailwind, web, native, ink)
|
|
112
389
|
{ id: 'async-effect', layer: 'react', severity: 'error', description: 'Async function passed directly to useEffect' },
|
|
113
|
-
{
|
|
114
|
-
|
|
390
|
+
{
|
|
391
|
+
id: 'render-side-effect',
|
|
392
|
+
layer: 'react',
|
|
393
|
+
severity: 'error',
|
|
394
|
+
description: 'Side effect (fetch, mutation) during render',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: 'unstable-key',
|
|
398
|
+
layer: 'react',
|
|
399
|
+
severity: 'warning',
|
|
400
|
+
description: 'Non-stable key prop (index, random, Date.now)',
|
|
401
|
+
},
|
|
115
402
|
{ id: 'stale-closure', layer: 'react', severity: 'warning', description: 'Stale variable captured in hook closure' },
|
|
116
|
-
{
|
|
403
|
+
{
|
|
404
|
+
id: 'state-explosion',
|
|
405
|
+
layer: 'react',
|
|
406
|
+
severity: 'warning',
|
|
407
|
+
description: 'Excessive useState calls — consider useReducer',
|
|
408
|
+
},
|
|
117
409
|
{ id: 'hook-order', layer: 'react', severity: 'error', description: 'React hook called inside condition or loop' },
|
|
118
|
-
{
|
|
410
|
+
{
|
|
411
|
+
id: 'effect-self-update-loop',
|
|
412
|
+
layer: 'react',
|
|
413
|
+
severity: 'error',
|
|
414
|
+
description: 'useEffect updates its own dependency — infinite loop',
|
|
415
|
+
},
|
|
416
|
+
// CLI (target: cli)
|
|
417
|
+
{
|
|
418
|
+
id: 'cli-missing-shebang',
|
|
419
|
+
layer: 'cli',
|
|
420
|
+
severity: 'warning',
|
|
421
|
+
description: 'Commander CLI entrypoint missing #!/usr/bin/env node',
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'cli-missing-parse',
|
|
425
|
+
layer: 'cli',
|
|
426
|
+
severity: 'error',
|
|
427
|
+
description: 'Command instance created without parse()/parseAsync()',
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 'cli-async-parse-sync',
|
|
431
|
+
layer: 'cli',
|
|
432
|
+
severity: 'error',
|
|
433
|
+
description: 'Async Commander action paired with parse() instead of parseAsync()',
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: 'cli-process-exit-in-action',
|
|
437
|
+
layer: 'cli',
|
|
438
|
+
severity: 'warning',
|
|
439
|
+
description: 'Commander action handler calls process.exit() directly',
|
|
440
|
+
},
|
|
119
441
|
// Vue (target: vue, nuxt)
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
442
|
+
{
|
|
443
|
+
id: 'missing-ref-value',
|
|
444
|
+
layer: 'vue',
|
|
445
|
+
severity: 'warning',
|
|
446
|
+
description: 'ref() used without .value in script setup',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'missing-onUnmounted',
|
|
450
|
+
layer: 'vue',
|
|
451
|
+
severity: 'error',
|
|
452
|
+
description: 'watch/addEventListener without onUnmounted cleanup',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: 'setup-side-effect',
|
|
456
|
+
layer: 'vue',
|
|
457
|
+
severity: 'warning',
|
|
458
|
+
description: 'Top-level await in setup without onMounted',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: 'reactive-destructure',
|
|
462
|
+
layer: 'vue',
|
|
463
|
+
severity: 'warning',
|
|
464
|
+
description: 'Destructuring reactive() loses reactivity',
|
|
465
|
+
},
|
|
466
|
+
// Terminal (target: terminal)
|
|
467
|
+
{
|
|
468
|
+
id: 'terminal-missing-tty-guard',
|
|
469
|
+
layer: 'terminal',
|
|
470
|
+
severity: 'warning',
|
|
471
|
+
description: 'Interactive terminal code runs without TTY guard',
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: 'terminal-raw-mode-no-restore',
|
|
475
|
+
layer: 'terminal',
|
|
476
|
+
severity: 'error',
|
|
477
|
+
description: 'stdin raw mode enabled without restore on exit',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: 'terminal-readline-no-close',
|
|
481
|
+
layer: 'terminal',
|
|
482
|
+
severity: 'warning',
|
|
483
|
+
description: 'Readline interface never closed — process can hang',
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: 'terminal-alt-screen-no-restore',
|
|
487
|
+
layer: 'terminal',
|
|
488
|
+
severity: 'warning',
|
|
489
|
+
description: 'Alternate screen entered without restore on exit',
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: 'terminal-missing-signal-handler',
|
|
493
|
+
layer: 'terminal',
|
|
494
|
+
severity: 'warning',
|
|
495
|
+
description: 'No SIGINT/SIGTERM handler for cleanup',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: 'terminal-cursor-not-restored',
|
|
499
|
+
layer: 'terminal',
|
|
500
|
+
severity: 'warning',
|
|
501
|
+
description: 'Cursor hidden without restore on exit',
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
id: 'terminal-unthrottled-render',
|
|
505
|
+
layer: 'terminal',
|
|
506
|
+
severity: 'warning',
|
|
507
|
+
description: 'Render loop with excessive refresh rate',
|
|
508
|
+
},
|
|
509
|
+
// Ink (target: ink, on top of React)
|
|
510
|
+
{
|
|
511
|
+
id: 'ink-console-output',
|
|
512
|
+
layer: 'ink',
|
|
513
|
+
severity: 'warning',
|
|
514
|
+
description: 'console.* output corrupts Ink terminal rendering',
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 'ink-direct-stdout',
|
|
518
|
+
layer: 'ink',
|
|
519
|
+
severity: 'error',
|
|
520
|
+
description: 'Direct stdout/stderr writes bypass Ink renderer',
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
id: 'ink-process-exit',
|
|
524
|
+
layer: 'ink',
|
|
525
|
+
severity: 'warning',
|
|
526
|
+
description: 'process.exit() used instead of useApp().exit()',
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
id: 'ink-stdin-bypass',
|
|
530
|
+
layer: 'ink',
|
|
531
|
+
severity: 'warning',
|
|
532
|
+
description: 'Raw stdin/readline listeners bypass Ink useInput()',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
id: 'ink-uncleared-interval',
|
|
536
|
+
layer: 'ink',
|
|
537
|
+
severity: 'warning',
|
|
538
|
+
description: 'setInterval without cleanup in Ink component',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
id: 'ink-missing-error-boundary',
|
|
542
|
+
layer: 'ink',
|
|
543
|
+
severity: 'warning',
|
|
544
|
+
description: 'Ink render() without error handling',
|
|
545
|
+
},
|
|
124
546
|
// Next.js (target: nextjs)
|
|
125
547
|
{ id: 'server-hook', layer: 'nextjs', severity: 'error', description: 'React hook used in Server Component' },
|
|
126
|
-
{
|
|
127
|
-
|
|
548
|
+
{
|
|
549
|
+
id: 'hydration-mismatch',
|
|
550
|
+
layer: 'nextjs',
|
|
551
|
+
severity: 'warning',
|
|
552
|
+
description: 'Nondeterministic expression causes SSR/client mismatch',
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: 'missing-use-client',
|
|
556
|
+
layer: 'nextjs',
|
|
557
|
+
severity: 'warning',
|
|
558
|
+
description: 'Event handler in Server Component — needs use client',
|
|
559
|
+
},
|
|
128
560
|
// Nuxt (target: nuxt)
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
561
|
+
{
|
|
562
|
+
id: 'missing-ssr-guard',
|
|
563
|
+
layer: 'nuxt',
|
|
564
|
+
severity: 'error',
|
|
565
|
+
description: 'Browser global accessed without SSR guard',
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: 'nuxt-direct-fetch',
|
|
569
|
+
layer: 'nuxt',
|
|
570
|
+
severity: 'warning',
|
|
571
|
+
description: 'Raw fetch() instead of $fetch/useFetch in Nuxt component',
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: 'server-route-leak',
|
|
575
|
+
layer: 'nuxt',
|
|
576
|
+
severity: 'error',
|
|
577
|
+
description: 'Server API route may expose sensitive fields',
|
|
578
|
+
},
|
|
132
579
|
// Express (target: express)
|
|
133
|
-
{
|
|
134
|
-
|
|
580
|
+
{
|
|
581
|
+
id: 'unvalidated-input',
|
|
582
|
+
layer: 'express',
|
|
583
|
+
severity: 'error',
|
|
584
|
+
description: 'req.body/params/query used without validation',
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
id: 'missing-error-middleware',
|
|
588
|
+
layer: 'express',
|
|
589
|
+
severity: 'warning',
|
|
590
|
+
description: 'Express app without error-handling middleware',
|
|
591
|
+
},
|
|
135
592
|
{ id: 'sync-in-handler', layer: 'express', severity: 'warning', description: 'Blocking I/O in request handler' },
|
|
136
|
-
{
|
|
593
|
+
{
|
|
594
|
+
id: 'double-response',
|
|
595
|
+
layer: 'express',
|
|
596
|
+
severity: 'error',
|
|
597
|
+
description: 'Response sent twice without early return',
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: 'express-missing-next',
|
|
601
|
+
layer: 'express',
|
|
602
|
+
severity: 'error',
|
|
603
|
+
description: 'Middleware accepts next but never calls it — request hangs',
|
|
604
|
+
},
|
|
605
|
+
// FastAPI (target: fastapi, concept-based Python pipeline)
|
|
606
|
+
{
|
|
607
|
+
id: 'fastapi-missing-response-model',
|
|
608
|
+
layer: 'fastapi',
|
|
609
|
+
severity: 'warning',
|
|
610
|
+
description: 'Endpoint without response_model — undocumented response',
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
id: 'fastapi-blocking-sync-route',
|
|
614
|
+
layer: 'fastapi',
|
|
615
|
+
severity: 'warning',
|
|
616
|
+
description: 'Blocking call in async route stalls event loop',
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: 'fastapi-shared-state',
|
|
620
|
+
layer: 'fastapi',
|
|
621
|
+
severity: 'error',
|
|
622
|
+
description: 'Route mutates global/module state — race condition',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
id: 'fastapi-broad-except',
|
|
626
|
+
layer: 'fastapi',
|
|
627
|
+
severity: 'warning',
|
|
628
|
+
description: 'Broad except without re-raising HTTPException',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
id: 'fastapi-broad-cors',
|
|
632
|
+
layer: 'fastapi',
|
|
633
|
+
severity: 'warning',
|
|
634
|
+
description: 'CORSMiddleware with allow_origins=["*"] — overly permissive',
|
|
635
|
+
},
|
|
137
636
|
// Concept rules (always active, language-agnostic)
|
|
138
|
-
{
|
|
637
|
+
{
|
|
638
|
+
id: 'boundary-mutation',
|
|
639
|
+
layer: 'concept',
|
|
640
|
+
severity: 'warning',
|
|
641
|
+
description: 'Global/shared state mutation across boundaries',
|
|
642
|
+
},
|
|
139
643
|
{ id: 'ignored-error', layer: 'concept', severity: 'warning', description: 'Caught exception silently ignored' },
|
|
140
|
-
{
|
|
141
|
-
|
|
644
|
+
{
|
|
645
|
+
id: 'unguarded-effect',
|
|
646
|
+
layer: 'concept',
|
|
647
|
+
severity: 'warning',
|
|
648
|
+
description: 'Network/DB effect without auth/validation guard',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: 'unrecovered-effect',
|
|
652
|
+
layer: 'concept',
|
|
653
|
+
severity: 'warning',
|
|
654
|
+
description: 'Network/DB effect without error recovery',
|
|
655
|
+
},
|
|
142
656
|
];
|
|
143
657
|
/** Layer → target mapping for filtering */
|
|
144
658
|
const LAYER_TARGET_MAP = {
|
|
@@ -150,11 +664,15 @@ const LAYER_TARGET_MAP = {
|
|
|
150
664
|
'dead-logic': null,
|
|
151
665
|
'null-safety': null,
|
|
152
666
|
concept: null,
|
|
153
|
-
react: ['nextjs', 'tailwind', 'web', 'native'],
|
|
667
|
+
react: ['nextjs', 'tailwind', 'web', 'native', 'ink'],
|
|
668
|
+
cli: ['cli'],
|
|
154
669
|
vue: ['vue', 'nuxt'],
|
|
670
|
+
ink: ['ink'],
|
|
671
|
+
terminal: ['terminal'],
|
|
155
672
|
nextjs: ['nextjs'],
|
|
156
673
|
nuxt: ['nuxt'],
|
|
157
674
|
express: ['express'],
|
|
675
|
+
fastapi: ['fastapi'],
|
|
158
676
|
};
|
|
159
677
|
/**
|
|
160
678
|
* Get the rule registry, optionally filtered by target.
|
|
@@ -163,7 +681,7 @@ const LAYER_TARGET_MAP = {
|
|
|
163
681
|
export function getRuleRegistry(target) {
|
|
164
682
|
if (!target)
|
|
165
683
|
return [...REGISTRY];
|
|
166
|
-
return REGISTRY.filter(r => {
|
|
684
|
+
return REGISTRY.filter((r) => {
|
|
167
685
|
const targets = LAYER_TARGET_MAP[r.layer];
|
|
168
686
|
return targets === null || targets.includes(target);
|
|
169
687
|
});
|