@cosmicdrift/kumiko-guards 0.1.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 +57 -0
- package/README.md +16 -0
- package/package.json +40 -0
- package/src/_lib/baseline-compare.ts +56 -0
- package/src/_lib/generic-reason.ts +39 -0
- package/src/_lib/guard-kit.ts +534 -0
- package/src/_lib/handler-name-forms.ts +29 -0
- package/src/_lib/ignore-tag.ts +24 -0
- package/src/_lib/primitives-access.ts +19 -0
- package/src/_lib/roots.ts +304 -0
- package/src/_lib/scan-lines.ts +25 -0
- package/src/_lib/scan-scope.ts +152 -0
- package/src/_lib/security-baseline-cli.ts +54 -0
- package/src/_lib/security-baseline.ts +325 -0
- package/src/_lib/sql-inventory.ts +267 -0
- package/src/guard-access-denied-test.ts +135 -0
- package/src/guard-admin-api.ts +134 -0
- package/src/guard-cross-feature-imports.ts +244 -0
- package/src/guard-direct-entity-writes.ts +387 -0
- package/src/guard-direct-fetch.ts +154 -0
- package/src/guard-escape-hatch-declared.ts +520 -0
- package/src/guard-fake-tests.ts +137 -0
- package/src/guard-html-escape.ts +345 -0
- package/src/guard-no-custom-primitives.ts +196 -0
- package/src/guard-no-date-api.ts +186 -0
- package/src/guard-no-direct-fs.ts +232 -0
- package/src/guard-no-direct-process-env.ts +126 -0
- package/src/guard-no-inline-styles.ts +58 -0
- package/src/guard-no-logic-in-views.ts +147 -0
- package/src/guard-no-raw-hooks.ts +76 -0
- package/src/guard-open-to-all-reason.ts +112 -0
- package/src/guard-pre-es-patterns.ts +199 -0
- package/src/guard-primitives-discipline.ts +330 -0
- package/src/guard-raw-classname.ts +111 -0
- package/src/guard-raw-interactive-elements.ts +154 -0
- package/src/guard-raw-sql.ts +89 -0
- package/src/guard-renderer-boundaries.ts +157 -0
- package/src/guard-restricted-symbols.ts +138 -0
- package/src/guard-silent-skip.ts +186 -0
- package/src/guard-tailwind-scan-surface.ts +588 -0
- package/src/guard-tenant-escalation.ts +312 -0
- package/src/guard-thin-wrappers.ts +422 -0
- package/src/guard-unsafe-json-parse.ts +86 -0
- package/src/index.ts +29 -0
- package/src/run-guards.ts +78 -0
- package/src/run-repo-checks.ts +22 -0
- package/src/run-ui-guards.ts +25 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Thin-Wrapper Guard (warning, never fails).
|
|
4
|
+
*
|
|
5
|
+
* Finds functions that only call another function (1 statement, direct
|
|
6
|
+
* delegation) and are not marked as a deliberate wrapper.
|
|
7
|
+
*
|
|
8
|
+
* Such wrappers often appear as compatibility shims after refactorings —
|
|
9
|
+
* this guard keeps the list visible so they can be deliberately removed.
|
|
10
|
+
*
|
|
11
|
+
* === Setting a marker ===
|
|
12
|
+
*
|
|
13
|
+
* When a wrapper is deliberate and permanent, right above the function:
|
|
14
|
+
*
|
|
15
|
+
* // @wrapper-known semantic-alias
|
|
16
|
+
* export function signDeletionToken(dek: Buffer): string {
|
|
17
|
+
* return signToken(dek);
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Known reasons:
|
|
21
|
+
* semantic-alias — deliberate rename for domain clarity
|
|
22
|
+
* error-helper — named error constructor (delegates to writeFailure or similar)
|
|
23
|
+
* uuid-domain — domain-specific UUID generator
|
|
24
|
+
* test-helper — test-utility alias
|
|
25
|
+
* monitoring — monitoring/metrics alias
|
|
26
|
+
* health-check — health-check wrapper
|
|
27
|
+
* entry-point — entry-point-specific configuration
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import {
|
|
32
|
+
type ArrowFunction,
|
|
33
|
+
type FunctionDeclaration,
|
|
34
|
+
type FunctionExpression,
|
|
35
|
+
Project,
|
|
36
|
+
type SourceFile,
|
|
37
|
+
SyntaxKind,
|
|
38
|
+
type VariableDeclaration,
|
|
39
|
+
} from "ts-morph";
|
|
40
|
+
import { type RepoCheck, reportResults, runRepoChecks } from "./_lib/guard-kit";
|
|
41
|
+
import { frameworkTsConfigPath } from "./_lib/roots";
|
|
42
|
+
import { type ScanSpec, scanFiles } from "./_lib/scan-scope";
|
|
43
|
+
|
|
44
|
+
const ROOT = process.cwd();
|
|
45
|
+
|
|
46
|
+
const EXCLUDE =
|
|
47
|
+
/(__tests__|\.test\.(ts|tsx)$|\.integration\.(ts|tsx)$|\.d\.(ts|tsx)$|\.spec\.(ts|tsx)$|node_modules)/;
|
|
48
|
+
|
|
49
|
+
// ── Known reasons ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export const KNOWN_WRAPPER_REASONS = [
|
|
52
|
+
"semantic-alias", // deliberate rename for domain clarity
|
|
53
|
+
"error-helper", // named error constructor
|
|
54
|
+
"uuid-domain", // domain-specific UUID generator
|
|
55
|
+
"test-helper", // test-utility alias
|
|
56
|
+
"monitoring", // monitoring/metrics alias
|
|
57
|
+
"health-check", // health-check wrapper
|
|
58
|
+
"entry-point", // entry-point-specific configuration
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export type WrapperReason = (typeof KNOWN_WRAPPER_REASONS)[number];
|
|
62
|
+
|
|
63
|
+
function isKnownReason(r: string): r is WrapperReason {
|
|
64
|
+
return (KNOWN_WRAPPER_REASONS as readonly string[]).includes(r);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Built-in filter ─────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const BUILTIN_NAMES = new Set([
|
|
70
|
+
"push",
|
|
71
|
+
"pop",
|
|
72
|
+
"shift",
|
|
73
|
+
"unshift",
|
|
74
|
+
"map",
|
|
75
|
+
"filter",
|
|
76
|
+
"reduce",
|
|
77
|
+
"reduceRight",
|
|
78
|
+
"forEach",
|
|
79
|
+
"find",
|
|
80
|
+
"findIndex",
|
|
81
|
+
"findLast",
|
|
82
|
+
"findLastIndex",
|
|
83
|
+
"some",
|
|
84
|
+
"every",
|
|
85
|
+
"includes",
|
|
86
|
+
"indexOf",
|
|
87
|
+
"lastIndexOf",
|
|
88
|
+
"slice",
|
|
89
|
+
"splice",
|
|
90
|
+
"concat",
|
|
91
|
+
"flat",
|
|
92
|
+
"flatMap",
|
|
93
|
+
"join",
|
|
94
|
+
"sort",
|
|
95
|
+
"reverse",
|
|
96
|
+
"fill",
|
|
97
|
+
"copyWithin",
|
|
98
|
+
"at",
|
|
99
|
+
"entries",
|
|
100
|
+
"keys",
|
|
101
|
+
"values",
|
|
102
|
+
"from",
|
|
103
|
+
"isArray",
|
|
104
|
+
"of",
|
|
105
|
+
"assign",
|
|
106
|
+
"create",
|
|
107
|
+
"fromEntries",
|
|
108
|
+
"defineProperty",
|
|
109
|
+
"getOwnPropertyNames",
|
|
110
|
+
"getOwnPropertyDescriptor",
|
|
111
|
+
"hasOwn",
|
|
112
|
+
"freeze",
|
|
113
|
+
"seal",
|
|
114
|
+
"replace",
|
|
115
|
+
"replaceAll",
|
|
116
|
+
"split",
|
|
117
|
+
"trim",
|
|
118
|
+
"trimStart",
|
|
119
|
+
"trimEnd",
|
|
120
|
+
"trimLeft",
|
|
121
|
+
"trimRight",
|
|
122
|
+
"startsWith",
|
|
123
|
+
"endsWith",
|
|
124
|
+
"substring",
|
|
125
|
+
"padStart",
|
|
126
|
+
"padEnd",
|
|
127
|
+
"repeat",
|
|
128
|
+
"match",
|
|
129
|
+
"matchAll",
|
|
130
|
+
"search",
|
|
131
|
+
"charAt",
|
|
132
|
+
"charCodeAt",
|
|
133
|
+
"toUpperCase",
|
|
134
|
+
"toLowerCase",
|
|
135
|
+
"normalize",
|
|
136
|
+
"toString",
|
|
137
|
+
"valueOf",
|
|
138
|
+
"stringify",
|
|
139
|
+
"parse",
|
|
140
|
+
"log",
|
|
141
|
+
"error",
|
|
142
|
+
"warn",
|
|
143
|
+
"info",
|
|
144
|
+
"debug",
|
|
145
|
+
"trace",
|
|
146
|
+
"table",
|
|
147
|
+
"group",
|
|
148
|
+
"groupEnd",
|
|
149
|
+
"time",
|
|
150
|
+
"timeEnd",
|
|
151
|
+
"min",
|
|
152
|
+
"max",
|
|
153
|
+
"floor",
|
|
154
|
+
"ceil",
|
|
155
|
+
"round",
|
|
156
|
+
"abs",
|
|
157
|
+
"sqrt",
|
|
158
|
+
"pow",
|
|
159
|
+
"random",
|
|
160
|
+
"sign",
|
|
161
|
+
"trunc",
|
|
162
|
+
"then",
|
|
163
|
+
"catch",
|
|
164
|
+
"finally",
|
|
165
|
+
"resolve",
|
|
166
|
+
"reject",
|
|
167
|
+
"all",
|
|
168
|
+
"allSettled",
|
|
169
|
+
"race",
|
|
170
|
+
"any",
|
|
171
|
+
"get",
|
|
172
|
+
"set",
|
|
173
|
+
"has",
|
|
174
|
+
"delete",
|
|
175
|
+
"clear",
|
|
176
|
+
"call",
|
|
177
|
+
"apply",
|
|
178
|
+
"bind",
|
|
179
|
+
"setTimeout",
|
|
180
|
+
"clearTimeout",
|
|
181
|
+
"setInterval",
|
|
182
|
+
"clearInterval",
|
|
183
|
+
"String",
|
|
184
|
+
"Number",
|
|
185
|
+
"Boolean",
|
|
186
|
+
"Symbol",
|
|
187
|
+
"BigInt",
|
|
188
|
+
"Array",
|
|
189
|
+
"Object",
|
|
190
|
+
"Promise",
|
|
191
|
+
"Map",
|
|
192
|
+
"Set",
|
|
193
|
+
"send",
|
|
194
|
+
"emit",
|
|
195
|
+
"on",
|
|
196
|
+
"off",
|
|
197
|
+
"once",
|
|
198
|
+
"next",
|
|
199
|
+
"done",
|
|
200
|
+
"throw",
|
|
201
|
+
"return",
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
// ── Types ─────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
type FnNode = FunctionDeclaration | ArrowFunction | FunctionExpression;
|
|
207
|
+
|
|
208
|
+
export interface Finding {
|
|
209
|
+
file: string;
|
|
210
|
+
line: number;
|
|
211
|
+
fnName: string;
|
|
212
|
+
callee: string;
|
|
213
|
+
markerReason: string | null; // null = no marker, string = reason (unknown reasons included)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── AST helpers ───────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
function getFnName(node: FnNode): string | undefined {
|
|
219
|
+
if (node.isKind(SyntaxKind.FunctionDeclaration)) return node.getName() ?? undefined;
|
|
220
|
+
const parent = node.getParent();
|
|
221
|
+
if (parent?.isKind(SyntaxKind.VariableDeclaration)) {
|
|
222
|
+
return (parent as VariableDeclaration).getName();
|
|
223
|
+
}
|
|
224
|
+
if (parent?.isKind(SyntaxKind.PropertyAssignment)) {
|
|
225
|
+
return parent.getFirstChild()?.getText();
|
|
226
|
+
}
|
|
227
|
+
// ponytail: MethodDeclaration dropped — getFirstChild() returns the modifier keyword, not the name.
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getSingleDirectCallee(node: FnNode): string | null {
|
|
232
|
+
const rawBody =
|
|
233
|
+
node.isKind(SyntaxKind.ArrowFunction) && !node.getBody().isKind(SyntaxKind.Block)
|
|
234
|
+
? null
|
|
235
|
+
: node.getBody();
|
|
236
|
+
|
|
237
|
+
// Concise arrow: `x => someCall(x)` — body IS the expression
|
|
238
|
+
if (!rawBody || !rawBody.isKind(SyntaxKind.Block)) {
|
|
239
|
+
const body = node.getBody();
|
|
240
|
+
if (!body || !body.isKind(SyntaxKind.CallExpression)) return null;
|
|
241
|
+
// Method chains (A.from(x).toMethod(y)) have 2 call expressions — not a thin wrapper
|
|
242
|
+
const conciseCallees = new Set<string>();
|
|
243
|
+
node.forEachDescendant((n) => {
|
|
244
|
+
if (!n.isKind(SyntaxKind.CallExpression)) return;
|
|
245
|
+
const cName = extractCalleeName(n);
|
|
246
|
+
if (cName) conciseCallees.add(cName);
|
|
247
|
+
});
|
|
248
|
+
if (conciseCallees.size !== 1) return null;
|
|
249
|
+
const name = extractCalleeName(body);
|
|
250
|
+
if (!name || BUILTIN_NAMES.has(name)) return null;
|
|
251
|
+
return name;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const stmts = rawBody.getStatements();
|
|
255
|
+
if (stmts.length !== 1) return null;
|
|
256
|
+
|
|
257
|
+
const single = stmts.reduce((only) => only);
|
|
258
|
+
// switch → not a wrapper
|
|
259
|
+
if (single.isKind(SyntaxKind.SwitchStatement)) return null;
|
|
260
|
+
|
|
261
|
+
let callExpr = null;
|
|
262
|
+
|
|
263
|
+
if (single.isKind(SyntaxKind.ReturnStatement)) {
|
|
264
|
+
const expr = single.getExpression();
|
|
265
|
+
if (!expr) return null;
|
|
266
|
+
// factory: return () => ... → not a wrapper
|
|
267
|
+
if (expr.isKind(SyntaxKind.ArrowFunction) || expr.isKind(SyntaxKind.FunctionExpression))
|
|
268
|
+
return null;
|
|
269
|
+
// direct delegation: return someCall(...)
|
|
270
|
+
if (!expr.isKind(SyntaxKind.CallExpression)) return null;
|
|
271
|
+
callExpr = expr;
|
|
272
|
+
} else if (single.isKind(SyntaxKind.ExpressionStatement)) {
|
|
273
|
+
const expr = single.getExpression();
|
|
274
|
+
if (!expr.isKind(SyntaxKind.CallExpression)) return null;
|
|
275
|
+
callExpr = expr;
|
|
276
|
+
} else {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Ensure the call isn't part of a larger nested-call tree: a direct call
|
|
281
|
+
// has exactly 1 unique callee name across the whole body.
|
|
282
|
+
const allCallees = new Set<string>();
|
|
283
|
+
node.forEachDescendant((n) => {
|
|
284
|
+
if (!n.isKind(SyntaxKind.CallExpression)) return;
|
|
285
|
+
const name = extractCalleeName(n);
|
|
286
|
+
if (name) allCallees.add(name);
|
|
287
|
+
});
|
|
288
|
+
if (allCallees.size !== 1) return null;
|
|
289
|
+
|
|
290
|
+
const name = extractCalleeName(callExpr);
|
|
291
|
+
if (!name) return null;
|
|
292
|
+
if (BUILTIN_NAMES.has(name)) return null;
|
|
293
|
+
|
|
294
|
+
return name;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractCalleeName(callNode: ReturnType<FnNode["getBody"]> | undefined): string | null {
|
|
298
|
+
if (!callNode) return null;
|
|
299
|
+
const expr = callNode.isKind(SyntaxKind.CallExpression) ? callNode.getExpression() : null;
|
|
300
|
+
if (!expr) return null;
|
|
301
|
+
if (expr.isKind(SyntaxKind.Identifier)) return expr.getText();
|
|
302
|
+
if (expr.isKind(SyntaxKind.PropertyAccessExpression)) return expr.getName();
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Marker detection ──────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
const MARKER_RE = /\/[/*]\s*@wrapper-known(?:\s+([\w-]+))?/;
|
|
309
|
+
|
|
310
|
+
function extractMarkerReason(node: FnNode): string | null {
|
|
311
|
+
const fullText = node.getSourceFile().getFullText();
|
|
312
|
+
|
|
313
|
+
// Anchor node for leading comments:
|
|
314
|
+
// VariableStatement → const foo = () => ...
|
|
315
|
+
// PropertyAssignment → { foo: () => ... } (marker above the property)
|
|
316
|
+
// FunctionDeclaration → function foo() { ... }
|
|
317
|
+
const parentNode = node.getParent();
|
|
318
|
+
const anchor =
|
|
319
|
+
node.getFirstAncestorByKind(SyntaxKind.VariableStatement) ??
|
|
320
|
+
(parentNode?.isKind(SyntaxKind.PropertyAssignment) ? parentNode : null) ??
|
|
321
|
+
(node.isKind(SyntaxKind.FunctionDeclaration) ? node : null);
|
|
322
|
+
|
|
323
|
+
if (anchor) {
|
|
324
|
+
for (const r of anchor.getLeadingCommentRanges()) {
|
|
325
|
+
const m = MARKER_RE.exec(r.getText());
|
|
326
|
+
if (m) return m[1] ?? "";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Trailing comment on the same line
|
|
331
|
+
const end = node.getEnd();
|
|
332
|
+
const eol = fullText.indexOf("\n", end);
|
|
333
|
+
const trailing = fullText.slice(end, eol === -1 ? fullText.length : eol);
|
|
334
|
+
const m = MARKER_RE.exec(trailing);
|
|
335
|
+
if (m) return m[1] ?? "";
|
|
336
|
+
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
export function collectFindings(sf: SourceFile): Finding[] {
|
|
343
|
+
const file = path.relative(ROOT, sf.getFilePath());
|
|
344
|
+
const findings: Finding[] = [];
|
|
345
|
+
|
|
346
|
+
const check = (node: FnNode) => {
|
|
347
|
+
const fnName = getFnName(node);
|
|
348
|
+
if (!fnName) return;
|
|
349
|
+
|
|
350
|
+
const callee = getSingleDirectCallee(node);
|
|
351
|
+
if (!callee) return;
|
|
352
|
+
// Self-recursion / delegation false-positive (same name)
|
|
353
|
+
if (callee === fnName) return;
|
|
354
|
+
|
|
355
|
+
const markerReason = extractMarkerReason(node);
|
|
356
|
+
findings.push({
|
|
357
|
+
file,
|
|
358
|
+
line: node.getStartLineNumber(),
|
|
359
|
+
fnName,
|
|
360
|
+
callee,
|
|
361
|
+
markerReason,
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
sf.getFunctions().forEach(check);
|
|
366
|
+
sf.getDescendantsOfKind(SyntaxKind.ArrowFunction).forEach(check);
|
|
367
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionExpression).forEach(check);
|
|
368
|
+
|
|
369
|
+
return findings;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── RepoCheck ─────────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
const SCAN: ScanSpec = {
|
|
375
|
+
scope: "source",
|
|
376
|
+
extensions: ["ts"],
|
|
377
|
+
frameworkWithin: ["packages/*/src/**"],
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export const check: RepoCheck = {
|
|
381
|
+
name: "Thin-Wrappers Guard",
|
|
382
|
+
run(roots) {
|
|
383
|
+
const project = new Project({
|
|
384
|
+
tsConfigFilePath: frameworkTsConfigPath(),
|
|
385
|
+
skipAddingFilesFromTsConfig: true,
|
|
386
|
+
skipFileDependencyResolution: true,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const paths = scanFiles(SCAN, roots);
|
|
390
|
+
for (const p of paths) project.addSourceFileAtPath(p);
|
|
391
|
+
|
|
392
|
+
let scanned = 0;
|
|
393
|
+
const warnings: { file: string; line: number; message: string }[] = [];
|
|
394
|
+
for (const sf of project.getSourceFiles()) {
|
|
395
|
+
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
396
|
+
scanned++;
|
|
397
|
+
for (const f of collectFindings(sf)) {
|
|
398
|
+
if (f.markerReason === null) {
|
|
399
|
+
warnings.push({ file: f.file, line: f.line, message: `${f.fnName} → ${f.callee}` });
|
|
400
|
+
} else if (!isKnownReason(f.markerReason)) {
|
|
401
|
+
warnings.push({
|
|
402
|
+
file: f.file,
|
|
403
|
+
line: f.line,
|
|
404
|
+
message: `${f.fnName} → ${f.callee} (reason="${f.markerReason}")`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
violations: [],
|
|
412
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
413
|
+
matchedFiles: scanned,
|
|
414
|
+
notApplicable: roots.length === 0,
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (import.meta.main) {
|
|
420
|
+
const failed = reportResults(await runRepoChecks([check]));
|
|
421
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
422
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Guard: JSON.parse calls must be inside a try/catch OR routed through the
|
|
4
|
+
* safe-json helpers (parseJsonSafe / parseJsonOrThrow). Bare JSON.parse on
|
|
5
|
+
* data from external systems (Redis, DB, HTTP) crashes the pipeline silently
|
|
6
|
+
* with a SyntaxError when input is corrupt.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun guards/guard-unsafe-json-parse.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { type Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
14
|
+
import { type AstGuard, runStandalone, type ScanSpec } from "./_lib/guard-kit";
|
|
15
|
+
|
|
16
|
+
const ROOT = process.cwd();
|
|
17
|
+
|
|
18
|
+
const SCAN: ScanSpec = {
|
|
19
|
+
scope: "source",
|
|
20
|
+
extensions: ["ts"],
|
|
21
|
+
frameworkWithin: ["packages/*/src/**"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$|safe-json\.ts$)/;
|
|
25
|
+
|
|
26
|
+
interface UnsafeSite {
|
|
27
|
+
file: string;
|
|
28
|
+
line: number;
|
|
29
|
+
snippet: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isInsideTry(node: Node): boolean {
|
|
33
|
+
let cur: Node | undefined = node.getParent();
|
|
34
|
+
while (cur) {
|
|
35
|
+
if (cur.isKind(SyntaxKind.TryStatement)) {
|
|
36
|
+
const tryBlock = cur.asKindOrThrow(SyntaxKind.TryStatement).getTryBlock();
|
|
37
|
+
if (
|
|
38
|
+
tryBlock &&
|
|
39
|
+
node.getStart() >= tryBlock.getStart() &&
|
|
40
|
+
node.getEnd() <= tryBlock.getEnd()
|
|
41
|
+
) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
cur = cur.getParent();
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scanFile(sf: SourceFile): UnsafeSite[] {
|
|
51
|
+
const sites: UnsafeSite[] = [];
|
|
52
|
+
const calls = sf.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
53
|
+
for (const call of calls) {
|
|
54
|
+
const expr = call.getExpression();
|
|
55
|
+
if (expr.getText() !== "JSON.parse") continue;
|
|
56
|
+
if (isInsideTry(call)) continue;
|
|
57
|
+
sites.push({
|
|
58
|
+
file: path.relative(ROOT, sf.getFilePath()),
|
|
59
|
+
line: call.getStartLineNumber(),
|
|
60
|
+
snippet: call.getText().slice(0, 80),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return sites;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const guard: AstGuard = {
|
|
67
|
+
name: "Unsafe-JSON-Parse Guard",
|
|
68
|
+
scan: SCAN,
|
|
69
|
+
hint: "Nutze parseJsonSafe (Cache-Semantik) oder parseJsonOrThrow (Boundary-Semantik) aus utils/safe-json.",
|
|
70
|
+
run(files) {
|
|
71
|
+
const violations: Array<{ file: string; line: number; message: string }> = [];
|
|
72
|
+
for (const sf of files) {
|
|
73
|
+
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
74
|
+
for (const site of scanFile(sf)) {
|
|
75
|
+
violations.push({
|
|
76
|
+
file: site.file,
|
|
77
|
+
line: site.line,
|
|
78
|
+
message: `unguarded JSON.parse — ${site.snippet}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { violations };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (import.meta.main) runStandalone(guard);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AstGuard,
|
|
3
|
+
buildSharedProject,
|
|
4
|
+
explainGuards,
|
|
5
|
+
filesForGuard,
|
|
6
|
+
type GuardOutcome,
|
|
7
|
+
type GuardViolation,
|
|
8
|
+
isSecurityGuard,
|
|
9
|
+
type RepoCheck,
|
|
10
|
+
type RepoCheckOutcome,
|
|
11
|
+
type RunGuardsDeps,
|
|
12
|
+
type RunResult,
|
|
13
|
+
reportResults,
|
|
14
|
+
runGuards,
|
|
15
|
+
runRepoChecks,
|
|
16
|
+
type ScanSpec,
|
|
17
|
+
} from "./_lib/guard-kit";
|
|
18
|
+
export { findLocalRepo, type RepoRoot, resolveRepoRoots } from "./_lib/roots";
|
|
19
|
+
export {
|
|
20
|
+
loadSecurityBaseline,
|
|
21
|
+
parseSecurityBaseline,
|
|
22
|
+
SECURITY_BASELINE_FILE,
|
|
23
|
+
type SecurityBaseline,
|
|
24
|
+
type SecurityBaselineLoad,
|
|
25
|
+
} from "./_lib/security-baseline";
|
|
26
|
+
export { writeSecurityBaselines } from "./_lib/security-baseline-cli";
|
|
27
|
+
export { GUARDS } from "./run-guards";
|
|
28
|
+
export { REPO_CHECKS } from "./run-repo-checks";
|
|
29
|
+
export { UI_GUARDS } from "./run-ui-guards";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Shared-project guard runner. Loads ONE ts-morph project and runs all
|
|
3
|
+
// registered AST guards serially in-process over it — instead of N
|
|
4
|
+
// subprocesses each with their own project (memory thrashing at pool=6).
|
|
5
|
+
//
|
|
6
|
+
// `--explain` prints the root, scan scope, and file count per guard without checking.
|
|
7
|
+
// `--strict-security-baseline` also fails on security-baseline headroom; only for
|
|
8
|
+
// the maintenance run that rewrites the committed baseline. It runs only the
|
|
9
|
+
// security guards.
|
|
10
|
+
import {
|
|
11
|
+
buildSharedProject,
|
|
12
|
+
explainGuards,
|
|
13
|
+
isSecurityGuard,
|
|
14
|
+
reportResults,
|
|
15
|
+
runGuards,
|
|
16
|
+
} from "./_lib/guard-kit";
|
|
17
|
+
import { writeSecurityBaselines } from "./_lib/security-baseline-cli";
|
|
18
|
+
import { guard as accessDeniedTest } from "./guard-access-denied-test";
|
|
19
|
+
import { guard as adminApi } from "./guard-admin-api";
|
|
20
|
+
import { guard as crossFeatureImports } from "./guard-cross-feature-imports";
|
|
21
|
+
import { guard as directEntityWrites } from "./guard-direct-entity-writes";
|
|
22
|
+
import { guard as directFetch } from "./guard-direct-fetch";
|
|
23
|
+
import { guard as escapeHatchDeclared } from "./guard-escape-hatch-declared";
|
|
24
|
+
import { guard as fakeTests } from "./guard-fake-tests";
|
|
25
|
+
import { guard as htmlEscape } from "./guard-html-escape";
|
|
26
|
+
import { guard as noDateApi } from "./guard-no-date-api";
|
|
27
|
+
import { guard as noDirectFs } from "./guard-no-direct-fs";
|
|
28
|
+
import { guard as noLogicInViews } from "./guard-no-logic-in-views";
|
|
29
|
+
import { guard as openToAllReason } from "./guard-open-to-all-reason";
|
|
30
|
+
import { guard as preEsPatterns } from "./guard-pre-es-patterns";
|
|
31
|
+
import { guard as restrictedSymbols } from "./guard-restricted-symbols";
|
|
32
|
+
import { guard as silentSkip } from "./guard-silent-skip";
|
|
33
|
+
import { guard as tenantEscalation } from "./guard-tenant-escalation";
|
|
34
|
+
import { guard as unsafeJsonParse } from "./guard-unsafe-json-parse";
|
|
35
|
+
|
|
36
|
+
export const GUARDS = [
|
|
37
|
+
accessDeniedTest,
|
|
38
|
+
adminApi,
|
|
39
|
+
directEntityWrites,
|
|
40
|
+
directFetch,
|
|
41
|
+
escapeHatchDeclared,
|
|
42
|
+
noDirectFs,
|
|
43
|
+
openToAllReason,
|
|
44
|
+
tenantEscalation,
|
|
45
|
+
preEsPatterns,
|
|
46
|
+
unsafeJsonParse,
|
|
47
|
+
silentSkip,
|
|
48
|
+
htmlEscape,
|
|
49
|
+
crossFeatureImports,
|
|
50
|
+
noDateApi,
|
|
51
|
+
restrictedSymbols,
|
|
52
|
+
fakeTests,
|
|
53
|
+
noLogicInViews,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Only run on direct invocation — otherwise `import { GUARDS }` would kick
|
|
57
|
+
// off the whole suite and the list wouldn't be testable.
|
|
58
|
+
if (import.meta.main) {
|
|
59
|
+
if (process.argv.includes("--explain")) {
|
|
60
|
+
for (const line of explainGuards(GUARDS, buildSharedProject(GUARDS))) {
|
|
61
|
+
console.log(line);
|
|
62
|
+
}
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
if (process.argv.includes("--write-security-baseline")) {
|
|
66
|
+
writeSecurityBaselines(
|
|
67
|
+
GUARDS.filter(isSecurityGuard),
|
|
68
|
+
buildSharedProject(GUARDS.filter(isSecurityGuard)),
|
|
69
|
+
);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
const strictSecurityBaseline = process.argv.includes("--strict-security-baseline");
|
|
73
|
+
const guards = strictSecurityBaseline ? GUARDS.filter(isSecurityGuard) : GUARDS;
|
|
74
|
+
const failed = reportResults(
|
|
75
|
+
runGuards(guards, buildSharedProject(guards), { strictSecurityBaseline }),
|
|
76
|
+
);
|
|
77
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Standalone-`main()` guards ported as RepoCheck — run in-process, no
|
|
3
|
+
// per-guard subprocess/project.
|
|
4
|
+
import { reportResults, runRepoChecks } from "./_lib/guard-kit";
|
|
5
|
+
import { check as noDirectProcessEnv } from "./guard-no-direct-process-env";
|
|
6
|
+
import { check as primitivesDiscipline } from "./guard-primitives-discipline";
|
|
7
|
+
import { check as rawSql } from "./guard-raw-sql";
|
|
8
|
+
import { check as rendererBoundaries } from "./guard-renderer-boundaries";
|
|
9
|
+
import { check as thinWrappers } from "./guard-thin-wrappers";
|
|
10
|
+
|
|
11
|
+
export const REPO_CHECKS = [
|
|
12
|
+
rawSql,
|
|
13
|
+
noDirectProcessEnv,
|
|
14
|
+
rendererBoundaries,
|
|
15
|
+
primitivesDiscipline,
|
|
16
|
+
thinWrappers,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
if (import.meta.main) {
|
|
20
|
+
const failed = reportResults(await runRepoChecks(REPO_CHECKS));
|
|
21
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// UI-guard bundle (App-Mounting 2.0, infra#208): one process, shared ts-morph
|
|
3
|
+
// Project over the UI enforcement guards.
|
|
4
|
+
import { reportResults, runGuards } from "./_lib/guard-kit";
|
|
5
|
+
import { guard as noCustomPrimitives } from "./guard-no-custom-primitives";
|
|
6
|
+
import { guard as noInlineStyles } from "./guard-no-inline-styles";
|
|
7
|
+
import { guard as noRawHooks } from "./guard-no-raw-hooks";
|
|
8
|
+
import { guard as rawClassname } from "./guard-raw-classname";
|
|
9
|
+
import { guard as rawInteractiveElements } from "./guard-raw-interactive-elements";
|
|
10
|
+
import { guard as tailwindScanSurface } from "./guard-tailwind-scan-surface";
|
|
11
|
+
|
|
12
|
+
export const UI_GUARDS = [
|
|
13
|
+
rawClassname,
|
|
14
|
+
noInlineStyles,
|
|
15
|
+
noCustomPrimitives,
|
|
16
|
+
noRawHooks,
|
|
17
|
+
tailwindScanSurface,
|
|
18
|
+
rawInteractiveElements,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Same as run-guards.ts: only run on direct invocation.
|
|
22
|
+
if (import.meta.main) {
|
|
23
|
+
const failed = reportResults(runGuards(UI_GUARDS));
|
|
24
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
25
|
+
}
|