@cosmicdrift/kumiko-guards 0.355.0 → 0.357.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/package.json +2 -2
- package/src/_lib/bun-lock-packages.ts +5 -5
- package/src/_lib/guard-kit.ts +180 -131
- package/src/_lib/qn.ts +9 -9
- package/src/_lib/roots.ts +4 -3
- package/src/_lib/security-baseline.ts +193 -116
- package/src/_lib/sql-inventory.ts +34 -32
- package/src/changes.json +6 -0
- package/src/check-as-casts.ts +196 -180
- package/src/check-complexity.ts +68 -69
- package/src/check-predicates.ts +93 -67
- package/src/check-secret-literals.ts +49 -38
- package/src/check-single-runtime-instance.ts +1 -1
- package/src/cli.ts +30 -17
- package/src/guard-access-denied-test.ts +49 -23
- package/src/guard-app-feature-structure.ts +92 -73
- package/src/guard-broker-subscribe.ts +17 -17
- package/src/guard-comment-lang.ts +126 -54
- package/src/guard-direct-entity-writes.ts +58 -45
- package/src/guard-escape-hatch-declared.ts +26 -23
- package/src/guard-html-escape.ts +112 -86
- package/src/guard-i18n-keys.ts +39 -36
- package/src/guard-i18n-locale-mount.ts +47 -40
- package/src/guard-i18n-ui-strings.ts +108 -92
- package/src/guard-lib-test-coverage.ts +37 -37
- package/src/guard-loadall-events.ts +17 -17
- package/src/guard-no-framed-extension-sections.ts +4 -4
- package/src/guard-no-temporal-polyfill-import.ts +19 -1
- package/src/guard-pii-annotations.ts +16 -16
- package/src/guard-primitives-discipline.ts +61 -65
- package/src/guard-screen-conventions.ts +2 -0
- package/src/guard-table-ddl.ts +56 -62
- package/src/guard-tenant-escalation.ts +42 -34
- package/src/guard-test-stack-drift.ts +6 -6
- package/src/guard-test-timeouts.ts +5 -4
- package/src/guard-text-field-stance.ts +22 -22
- package/src/guard-thin-wrappers.ts +42 -45
- package/src/guard-write-handler-qns.ts +29 -29
- package/src/runtime-isolation-classify.ts +4 -3
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type ArrowFunction,
|
|
33
33
|
type FunctionDeclaration,
|
|
34
34
|
type FunctionExpression,
|
|
35
|
+
type Node,
|
|
35
36
|
Project,
|
|
36
37
|
type SourceFile,
|
|
37
38
|
SyntaxKind,
|
|
@@ -230,6 +231,38 @@ function getFnName(node: FnNode): string | undefined {
|
|
|
230
231
|
return undefined;
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
function collectCalleeNames(node: FnNode): Set<string> {
|
|
235
|
+
const callees = new Set<string>();
|
|
236
|
+
node.forEachDescendant((n) => {
|
|
237
|
+
if (n.isKind(SyntaxKind.CallExpression)) {
|
|
238
|
+
const name = extractCalleeName(n);
|
|
239
|
+
if (name) callees.add(name);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return callees;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function wrapperCalleeName(callExpr: Node): string | null {
|
|
246
|
+
const name = extractCalleeName(callExpr);
|
|
247
|
+
if (!name) return null;
|
|
248
|
+
if (BUILTIN_NAMES.has(name)) return null;
|
|
249
|
+
return name;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The single call expression a one-statement body consists of, if it has that shape.
|
|
253
|
+
function singleStatementCall(single: Node): Node | null {
|
|
254
|
+
if (single.isKind(SyntaxKind.ReturnStatement)) {
|
|
255
|
+
const expr = single.getExpression();
|
|
256
|
+
// `return () => ...` is a factory, not a delegation
|
|
257
|
+
return expr?.isKind(SyntaxKind.CallExpression) ? expr : null;
|
|
258
|
+
}
|
|
259
|
+
if (single.isKind(SyntaxKind.ExpressionStatement)) {
|
|
260
|
+
const expr = single.getExpression();
|
|
261
|
+
return expr.isKind(SyntaxKind.CallExpression) ? expr : null;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
233
266
|
function getSingleDirectCallee(node: FnNode): string | null {
|
|
234
267
|
const rawBody =
|
|
235
268
|
node.isKind(SyntaxKind.ArrowFunction) && !node.getBody().isKind(SyntaxKind.Block)
|
|
@@ -241,59 +274,21 @@ function getSingleDirectCallee(node: FnNode): string | null {
|
|
|
241
274
|
const body = node.getBody();
|
|
242
275
|
if (!body?.isKind(SyntaxKind.CallExpression)) return null;
|
|
243
276
|
// Method chains (A.from(x).toMethod(y)) have 2 call expressions — not a thin wrapper
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!n.isKind(SyntaxKind.CallExpression)) return;
|
|
247
|
-
const cName = extractCalleeName(n);
|
|
248
|
-
if (cName) conciseCallees.add(cName);
|
|
249
|
-
});
|
|
250
|
-
if (conciseCallees.size !== 1) return null;
|
|
251
|
-
const name = extractCalleeName(body);
|
|
252
|
-
if (!name || BUILTIN_NAMES.has(name)) return null;
|
|
253
|
-
return name;
|
|
277
|
+
if (collectCalleeNames(node).size !== 1) return null;
|
|
278
|
+
return wrapperCalleeName(body);
|
|
254
279
|
}
|
|
255
280
|
|
|
256
281
|
const stmts = rawBody.getStatements();
|
|
257
282
|
if (stmts.length !== 1) return null;
|
|
258
283
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (single.isKind(SyntaxKind.SwitchStatement)) return null;
|
|
262
|
-
|
|
263
|
-
let callExpr = null;
|
|
264
|
-
|
|
265
|
-
if (single.isKind(SyntaxKind.ReturnStatement)) {
|
|
266
|
-
const expr = single.getExpression();
|
|
267
|
-
if (!expr) return null;
|
|
268
|
-
// factory: return () => ... → not a wrapper
|
|
269
|
-
if (expr.isKind(SyntaxKind.ArrowFunction) || expr.isKind(SyntaxKind.FunctionExpression))
|
|
270
|
-
return null;
|
|
271
|
-
// direct delegation: return someCall(...)
|
|
272
|
-
if (!expr.isKind(SyntaxKind.CallExpression)) return null;
|
|
273
|
-
callExpr = expr;
|
|
274
|
-
} else if (single.isKind(SyntaxKind.ExpressionStatement)) {
|
|
275
|
-
const expr = single.getExpression();
|
|
276
|
-
if (!expr.isKind(SyntaxKind.CallExpression)) return null;
|
|
277
|
-
callExpr = expr;
|
|
278
|
-
} else {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
284
|
+
const callExpr = singleStatementCall(stmts.reduce((only) => only));
|
|
285
|
+
if (!callExpr) return null;
|
|
281
286
|
|
|
282
287
|
// Ensure the call isn't part of a larger nested-call tree: a direct call
|
|
283
288
|
// has exactly 1 unique callee name across the whole body.
|
|
284
|
-
|
|
285
|
-
node.forEachDescendant((n) => {
|
|
286
|
-
if (!n.isKind(SyntaxKind.CallExpression)) return;
|
|
287
|
-
const name = extractCalleeName(n);
|
|
288
|
-
if (name) allCallees.add(name);
|
|
289
|
-
});
|
|
290
|
-
if (allCallees.size !== 1) return null;
|
|
291
|
-
|
|
292
|
-
const name = extractCalleeName(callExpr);
|
|
293
|
-
if (!name) return null;
|
|
294
|
-
if (BUILTIN_NAMES.has(name)) return null;
|
|
289
|
+
if (collectCalleeNames(node).size !== 1) return null;
|
|
295
290
|
|
|
296
|
-
return
|
|
291
|
+
return wrapperCalleeName(callExpr);
|
|
297
292
|
}
|
|
298
293
|
|
|
299
294
|
function extractCalleeName(callNode: ReturnType<FnNode["getBody"]> | undefined): string | null {
|
|
@@ -349,11 +344,13 @@ export function collectFindings(sf: SourceFile): Finding[] {
|
|
|
349
344
|
|
|
350
345
|
const check = (node: FnNode) => {
|
|
351
346
|
const fnName = getFnName(node);
|
|
347
|
+
// skip: anonymous functions cannot be reported by name
|
|
352
348
|
if (!fnName) return;
|
|
353
349
|
|
|
354
350
|
const callee = getSingleDirectCallee(node);
|
|
351
|
+
// skip: not a single-callee wrapper
|
|
355
352
|
if (!callee) return;
|
|
356
|
-
//
|
|
353
|
+
// skip: self-recursion / same-name delegation is a false positive
|
|
357
354
|
if (callee === fnName) return;
|
|
358
355
|
|
|
359
356
|
const markerReason = extractMarkerReason(node);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Guard:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Guard: checks that `dispatcher.write("qn:literal:...")` calls in
|
|
4
|
+
* custom screens reference valid write-handler QNs. Typos
|
|
5
|
+
* in the QN would otherwise only surface at runtime as a 404.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* 1. **
|
|
9
|
-
*
|
|
10
|
-
* 2. **
|
|
11
|
-
*
|
|
7
|
+
* Validation in two stages:
|
|
8
|
+
* 1. **Structural**: every QN must contain `:write:` — catches
|
|
9
|
+
* obvious typos ("feautre:write:create").
|
|
10
|
+
* 2. **Against manifest**: if `feature-manifest.json` is in the repo root
|
|
11
|
+
* and contains `writeHandlers`, the guard matches against it.
|
|
12
12
|
*
|
|
13
13
|
* Usage:
|
|
14
14
|
* bun infra/guards/guard-write-handler-qns.ts
|
|
@@ -94,11 +94,11 @@ function findKnownQns(filePath: string, entries: ReadonlyArray<ManifestEntry>):
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
97
|
+
* Extracts the literal value from a string literal, a backtick literal
|
|
98
|
+
* without interpolation (`NoSubstitutionTemplateLiteral`) or a
|
|
99
|
+
* `<literal> as T` cast. Backtick constants (`const QN = \`x:write:y\``)
|
|
100
|
+
* used to be recognized only as a direct argument, not as a resolved
|
|
101
|
+
* declaration.
|
|
102
102
|
*/
|
|
103
103
|
function literalValueOf(node: Node): string | undefined {
|
|
104
104
|
if (
|
|
@@ -137,8 +137,8 @@ export function resolveWriteQnFromArg(node: Node): string | undefined {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
140
|
+
* Scans a SourceFile for `dispatcher.write(<stringLiteral>, ...)`
|
|
141
|
+
* or `<expr>.write(<stringLiteral>, ...)` calls.
|
|
142
142
|
*/
|
|
143
143
|
export function scanDispatcherWriteCalls(
|
|
144
144
|
sf: SourceFile,
|
|
@@ -148,14 +148,14 @@ export function scanDispatcherWriteCalls(
|
|
|
148
148
|
const expr = call.getExpression();
|
|
149
149
|
const exprText = expr.getText();
|
|
150
150
|
|
|
151
|
-
// `dispatcher.write(...)`
|
|
152
|
-
//
|
|
151
|
+
// `dispatcher.write(...)` or any `<obj>.write(...)` (an aliased
|
|
152
|
+
// dispatcher is not necessarily named "dispatcher").
|
|
153
153
|
const isDispatcherCall = exprText === "dispatcher.write";
|
|
154
154
|
if (!isDispatcherCall && !exprText.endsWith(".write")) continue;
|
|
155
155
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
156
|
+
// The first parameter must be a string literal — dynamic QNs (handler
|
|
157
|
+
// constants, template literals) are not validated (they are either
|
|
158
|
+
// type-safe or not checkable).
|
|
159
159
|
const args = call.getArguments();
|
|
160
160
|
const first = args[0];
|
|
161
161
|
if (!first) continue;
|
|
@@ -163,11 +163,11 @@ export function scanDispatcherWriteCalls(
|
|
|
163
163
|
const qn = resolveWriteQnFromArg(first);
|
|
164
164
|
if (qn === undefined) continue;
|
|
165
165
|
|
|
166
|
-
// `.endsWith(".write")`
|
|
166
|
+
// `.endsWith(".write")` also matches non-dispatcher writes:
|
|
167
167
|
// `res.write(...)`, `stream.write(...)`, SSE `res.write("data: …")`.
|
|
168
|
-
//
|
|
169
|
-
// false
|
|
170
|
-
//
|
|
168
|
+
// Their argument is not a QN → it would otherwise be reported as an
|
|
169
|
+
// "invalid QN format" false positive. Outside an explicit `dispatcher.write`
|
|
170
|
+
// only check when the literal is a write QN (contains ":write:").
|
|
171
171
|
if (!isDispatcherCall && !qn.includes(":write:")) continue;
|
|
172
172
|
|
|
173
173
|
hits.push({
|
|
@@ -212,9 +212,9 @@ export const guard: AstGuard = {
|
|
|
212
212
|
continue;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
//
|
|
216
|
-
// QN
|
|
217
|
-
// camelCase
|
|
215
|
+
// Stage 2: match against the manifest (if present).
|
|
216
|
+
// The QN is normalized to kebab-case before the match, so that
|
|
217
|
+
// camelCase and kebab-case inputs are treated the same.
|
|
218
218
|
const normalizedQn = toKebab(hit.qn);
|
|
219
219
|
if (knownQns.size > 0 && !knownQns.has(normalizedQn)) {
|
|
220
220
|
violations.push({
|
|
@@ -226,8 +226,8 @@ export const guard: AstGuard = {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// Info
|
|
230
|
-
//
|
|
229
|
+
// Info log when no manifest was loaded — not an error, but
|
|
230
|
+
// the guard then runs with the structural check only.
|
|
231
231
|
if (knownQnsByRepo.size === 0 && violations.length === 0) {
|
|
232
232
|
console.warn(
|
|
233
233
|
" [INFO] No feature-manifest.json with writeHandlers found — structural check only.",
|
|
@@ -421,9 +421,10 @@ export function findRuntimeIsolationViolations(
|
|
|
421
421
|
const outsideRoot: string[] = [];
|
|
422
422
|
const seenOutside = new Set<string>();
|
|
423
423
|
const noteOutside = (fp: string) => {
|
|
424
|
-
if (seenOutside.has(fp))
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
if (!seenOutside.has(fp)) {
|
|
425
|
+
seenOutside.add(fp);
|
|
426
|
+
outsideRoot.push(fp);
|
|
427
|
+
}
|
|
427
428
|
};
|
|
428
429
|
const stats: Record<Runtime, number> = {
|
|
429
430
|
runtime: 0,
|