@cosmicdrift/kumiko-guards 0.382.0 → 0.383.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 +3 -3
- package/src/changes.json +7 -0
- package/src/guard-error-reasons.ts +1 -1
- package/src/guard-screen-conventions.ts +4 -40
- package/src/run-guards.ts +0 -18
- package/src/guard-api-object-form.ts +0 -211
- package/src/guard-broker-subscribe.ts +0 -99
- package/src/guard-escape-hatch-declared.ts +0 -743
- package/src/guard-loadall-events.ts +0 -139
- package/src/guard-open-to-all-reason.ts +0 -119
- package/src/guard-pre-es-patterns.ts +0 -206
- package/src/guard-section-fields-raw.ts +0 -300
- package/src/guard-table-ddl.ts +0 -155
- package/src/guard-text-field-stance.ts +0 -248
|
@@ -1,743 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Guard: escape-hatch declarations for the cross-tenant/system-identity
|
|
4
|
-
* surface (ctx.db.raw / ctx.systemDb.unsafeRaw / ctx.queryAs|writeAs(system)).
|
|
5
|
-
*
|
|
6
|
-
* R1 raw-outside-system-scope: a TenantDb `.raw` escape used outside a
|
|
7
|
-
* `r.systemScope()` feature.
|
|
8
|
-
* R2 unsafe-raw-outside-system-scope: `ctx.systemDb.unsafeRaw(...)` outside
|
|
9
|
-
* systemScope, a job scope, an explicit `withUnsafeRawGrant(...)`, a
|
|
10
|
-
* handler/hook that lexically declares `escapeHatch`, or a standalone
|
|
11
|
-
* function whose direct body declares `declareEscapeHatch({ reason: "..." })`.
|
|
12
|
-
* R3 system-identity-outside-declared-scope: `queryAs`/`writeAs` called
|
|
13
|
-
* with a system identity outside systemScope, `.job.ts`, an `r.job(...)`
|
|
14
|
-
* call, a `*Job` function, a handler/hook that lexically declares
|
|
15
|
-
* `escapeHatch`, or a standalone function whose direct body declares
|
|
16
|
-
* `declareEscapeHatch({ reason: "..." })`.
|
|
17
|
-
* R4 generic-reason: `acknowledgeCrossTenant`/`unsafeRaw`, a declared
|
|
18
|
-
* `escapeHatch: { reason }`/`unsafeAllTenants: { reason }`, or a
|
|
19
|
-
* `declareEscapeHatch({ reason })` call, given a placeholder reason
|
|
20
|
-
* literal — hard-fails everywhere, not baselined.
|
|
21
|
-
* R5 unsafe-all-tenants-outside-declared-scope: an `unsafeAllTenants: true`
|
|
22
|
-
* or `unsafeAllTenants: { reason: "..." }` option, passed directly as a
|
|
23
|
-
* call argument, outside systemScope, `.job.ts`, an `r.job(...)` call,
|
|
24
|
-
* a `*Job` function, or a scope that already declares `escapeHatch`.
|
|
25
|
-
*
|
|
26
|
-
* escapeHatch (R2/R3) is recognized only as a direct, literal `escapeHatch`
|
|
27
|
-
* property (object literal, or ternary of two object literals) either in the
|
|
28
|
-
* same object literal as any inline function property (ArrowFunction or
|
|
29
|
-
* FunctionExpression, under any key name — e.g. `handler`, `export`,
|
|
30
|
-
* `delete`), or in an options object passed to
|
|
31
|
-
* `r.hook`/`writeHandler`/`queryHandler`/`streamHandler`/`useExtension`
|
|
32
|
-
* alongside the handler function argument. A standalone function (arrow
|
|
33
|
-
* function, function expression, method declaration, or function
|
|
34
|
-
* declaration) is additionally recognized when a statement in its own direct
|
|
35
|
-
* body — not a nested function's, not inside an `if` — calls the bare
|
|
36
|
-
* identifier `declareEscapeHatch` with exactly one object-literal argument
|
|
37
|
-
* carrying a non-placeholder `reason` (from
|
|
38
|
-
* `@cosmicdrift/kumiko-framework/engine`'s `declareEscapeHatch`: a helper
|
|
39
|
-
* that escalates on a `HandlerContext` handed to it by its caller, rather
|
|
40
|
-
* than a `HandlerContext` from its own registration). `reason` may be a
|
|
41
|
-
* string/template literal or an Identifier resolving to a module-local
|
|
42
|
-
* `const` with such an initializer (`_lib/generic-reason.ts`'s
|
|
43
|
-
* `resolveReasonText`, shared by R2/R3/R4's four call sites) — so one shared
|
|
44
|
-
* constant covers several declarations without repeating the literal text.
|
|
45
|
-
* An import, a function call, or a template with substitutions stays
|
|
46
|
-
* unresolved and the R2/R3 finding names that explicitly. The declaration
|
|
47
|
-
* does not propagate upward: it covers escalations inside that function's
|
|
48
|
-
* own body, not the function it is nested inside. This detection is purely
|
|
49
|
-
* lexical except for origin: `declareEscapeHatch` (and `withUnsafeRawGrant`
|
|
50
|
-
* for R2) must resolve to an import from `@cosmicdrift/kumiko-framework`
|
|
51
|
-
* (or, inside the framework package, a relative import into its `src`), so a
|
|
52
|
-
* same-named local function does not clear. `escapeHatch:` itself is a plain
|
|
53
|
-
* property and is never checked for origin. Referenced-by-variable functions, spread options, computed/string
|
|
54
|
-
* keys, and a non-literal, non-module-local-const `escapeHatch`/`reason`
|
|
55
|
-
* value are conservatively not recognized (miss, don't falsely clear).
|
|
56
|
-
*
|
|
57
|
-
* Empty reasons, `openToAll.personalData` and PII are the framework boot validator's
|
|
58
|
-
* job (access-declarations.ts), not this guard's — except a `declareEscapeHatch`
|
|
59
|
-
* reason, which has no boot validator behind it: an empty or placeholder
|
|
60
|
-
* reason there is never recognized as a valid declaration (see R4). Known
|
|
61
|
-
* false-negatives: multi-hop aliasing, `ctx["db"]` through an intermediate
|
|
62
|
-
* variable, and a TenantDb/system-identity handed to another function across
|
|
63
|
-
* file boundaries with no `declareEscapeHatch` call at the escalation site
|
|
64
|
-
* (declarable now, so no longer a blanket false-negative) — all conservative
|
|
65
|
-
* (miss, don't falsely flag). R5 additionally misses `unsafeAllTenants`
|
|
66
|
-
* given via an identifier, a ternary, `false`, or `undefined`, and an
|
|
67
|
-
* options object passed by variable reference or spread rather than as a
|
|
68
|
-
* literal call argument — all conservative (miss, don't falsely flag).
|
|
69
|
-
*
|
|
70
|
-
* Usage:
|
|
71
|
-
* bun guards/guard-escape-hatch-declared.ts
|
|
72
|
-
* Baseline: kumiko-guards guards --write-security-baseline
|
|
73
|
-
*/
|
|
74
|
-
import * as path from "node:path";
|
|
75
|
-
import {
|
|
76
|
-
type BindingElement,
|
|
77
|
-
type Node,
|
|
78
|
-
type ObjectLiteralExpression,
|
|
79
|
-
type PropertyAssignment,
|
|
80
|
-
type SourceFile,
|
|
81
|
-
SyntaxKind,
|
|
82
|
-
type VariableDeclaration,
|
|
83
|
-
VariableDeclarationKind,
|
|
84
|
-
} from "ts-morph";
|
|
85
|
-
import { isFrameworkImportOf } from "./_lib/framework-import";
|
|
86
|
-
import { isGenericReason, resolveReasonText } from "./_lib/generic-reason";
|
|
87
|
-
import {
|
|
88
|
-
ALL_REPO_KINDS,
|
|
89
|
-
type AstGuard,
|
|
90
|
-
type GuardViolation,
|
|
91
|
-
runStandalone,
|
|
92
|
-
type ScanSpec,
|
|
93
|
-
} from "./_lib/guard-kit";
|
|
94
|
-
|
|
95
|
-
const SCAN: ScanSpec = {
|
|
96
|
-
kinds: ALL_REPO_KINDS,
|
|
97
|
-
scope: "source",
|
|
98
|
-
extensions: ["ts"],
|
|
99
|
-
frameworkWithin: ["packages/*/src/**", "samples/**"],
|
|
100
|
-
};
|
|
101
|
-
const EXCLUDE = /(__tests__\/|\.test\.tsx?$|\.d\.ts$)/;
|
|
102
|
-
|
|
103
|
-
const DB_RECEIVER_NAMES = new Set(["db", "dbOutsideTransaction"]);
|
|
104
|
-
const SYSTEM_IDENTITY_RE = /^(SYSTEM(_[A-Z0-9_]+)?|system(User|Actor)?)$/;
|
|
105
|
-
const CTX_IDENTIFIER_NAMES = new Set(["ctx", "context", "handlerCtx", "handlerContext"]);
|
|
106
|
-
|
|
107
|
-
export type EscapeHatchFinding = {
|
|
108
|
-
file: string;
|
|
109
|
-
line: number;
|
|
110
|
-
rule:
|
|
111
|
-
| "raw-outside-system-scope"
|
|
112
|
-
| "unsafe-raw-outside-system-scope"
|
|
113
|
-
| "system-identity-outside-declared-scope"
|
|
114
|
-
| "unsafe-all-tenants-outside-declared-scope";
|
|
115
|
-
message: string;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export type GenericReasonFinding = {
|
|
119
|
-
file: string;
|
|
120
|
-
line: number;
|
|
121
|
-
message: string;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
function scannableFiles(files: readonly SourceFile[]): SourceFile[] {
|
|
125
|
-
return files.filter((sf) => !EXCLUDE.test(sf.getFilePath()));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function systemScopeDirs(files: readonly SourceFile[]): string[] {
|
|
129
|
-
const dirs: string[] = [];
|
|
130
|
-
for (const sf of scannableFiles(files)) {
|
|
131
|
-
if (!sf.getFullText().includes("systemScope")) continue;
|
|
132
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
133
|
-
const expr = call.getExpression();
|
|
134
|
-
if (
|
|
135
|
-
expr.isKind(SyntaxKind.PropertyAccessExpression) &&
|
|
136
|
-
expr.getName() === "systemScope" &&
|
|
137
|
-
call.getArguments().length === 0
|
|
138
|
-
) {
|
|
139
|
-
dirs.push(path.dirname(sf.getFilePath()));
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return dirs;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function isInSystemScope(filePath: string, dirs: readonly string[]): boolean {
|
|
148
|
-
return dirs.some((dir) => filePath.startsWith(`${dir}/`));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function isDirectTenantDbAccess(node: Node): boolean {
|
|
152
|
-
if (node.isKind(SyntaxKind.PropertyAccessExpression)) {
|
|
153
|
-
return DB_RECEIVER_NAMES.has(node.getName());
|
|
154
|
-
}
|
|
155
|
-
if (node.isKind(SyntaxKind.ElementAccessExpression)) {
|
|
156
|
-
const arg = node.getArgumentExpression();
|
|
157
|
-
if (arg?.isKind(SyntaxKind.StringLiteral)) {
|
|
158
|
-
return DB_RECEIVER_NAMES.has(arg.getLiteralValue());
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isTenantDbVariable(decl: VariableDeclaration): boolean {
|
|
165
|
-
if (decl.getTypeNode()?.getText() === "TenantDb") return true;
|
|
166
|
-
const init = decl.getInitializer();
|
|
167
|
-
return init !== undefined && isDirectTenantDbAccess(init);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function isCtxDbBindingElement(decl: BindingElement): boolean {
|
|
171
|
-
const propName = decl.getPropertyNameNode()?.getText() ?? decl.getName();
|
|
172
|
-
if (!DB_RECEIVER_NAMES.has(propName)) return false;
|
|
173
|
-
// Only `const { db } = ctx` — parameter destructuring (`({ db }) =>`)
|
|
174
|
-
// stays a deliberate false-negative, not worth the ambiguity.
|
|
175
|
-
const owner = decl.getParent()?.getParent();
|
|
176
|
-
const init = owner?.isKind(SyntaxKind.VariableDeclaration) ? owner.getInitializer() : undefined;
|
|
177
|
-
return init?.isKind(SyntaxKind.Identifier) === true && CTX_IDENTIFIER_NAMES.has(init.getText());
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function isTenantDbDeclaration(decl: Node): boolean {
|
|
181
|
-
if (decl.isKind(SyntaxKind.VariableDeclaration)) return isTenantDbVariable(decl);
|
|
182
|
-
if (decl.isKind(SyntaxKind.BindingElement)) return isCtxDbBindingElement(decl);
|
|
183
|
-
if (decl.isKind(SyntaxKind.Parameter)) return decl.getTypeNode()?.getText() === "TenantDb";
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isTenantDbExpression(node: Node): boolean {
|
|
188
|
-
if (isDirectTenantDbAccess(node)) return true;
|
|
189
|
-
if (!node.isKind(SyntaxKind.Identifier)) return false;
|
|
190
|
-
return (node.getSymbol()?.getDeclarations() ?? []).some(isTenantDbDeclaration);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Text prefilters: a node walk per rule per file is the guard's dominant cost, and
|
|
194
|
-
// a rule's anchor name must appear verbatim in any file it can fire on.
|
|
195
|
-
const RAW_ACCESS = /\.\s*raw\b/;
|
|
196
|
-
|
|
197
|
-
function findRawFindings(
|
|
198
|
-
sf: SourceFile,
|
|
199
|
-
root: string,
|
|
200
|
-
systemDirs: readonly string[],
|
|
201
|
-
): EscapeHatchFinding[] {
|
|
202
|
-
if (isInSystemScope(sf.getFilePath(), systemDirs)) return [];
|
|
203
|
-
const out: EscapeHatchFinding[] = [];
|
|
204
|
-
if (!RAW_ACCESS.test(sf.getFullText())) return out;
|
|
205
|
-
for (const pa of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
|
|
206
|
-
if (pa.getName() !== "raw") continue;
|
|
207
|
-
if (!isTenantDbExpression(pa.getExpression())) continue;
|
|
208
|
-
out.push({
|
|
209
|
-
file: path.relative(root, sf.getFilePath()),
|
|
210
|
-
line: pa.getStartLineNumber(),
|
|
211
|
-
rule: "raw-outside-system-scope",
|
|
212
|
-
message:
|
|
213
|
-
"ctx.db.raw used outside a systemScope feature — a tenant-scoped raw escape needs a systemScope feature, or route the actual query through the framework's tenant filtering.",
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function findUnsafeRawFindings(
|
|
220
|
-
sf: SourceFile,
|
|
221
|
-
root: string,
|
|
222
|
-
systemDirs: readonly string[],
|
|
223
|
-
): EscapeHatchFinding[] {
|
|
224
|
-
if (isInSystemScope(sf.getFilePath(), systemDirs)) return [];
|
|
225
|
-
const out: EscapeHatchFinding[] = [];
|
|
226
|
-
if (!sf.getFullText().includes("unsafeRaw")) return out;
|
|
227
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
228
|
-
const expr = call.getExpression();
|
|
229
|
-
if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) continue;
|
|
230
|
-
if (expr.getName() !== "unsafeRaw") continue;
|
|
231
|
-
if (isAllowedEscapeHatchCall(call, sf, systemDirs) || isExplicitUnsafeRawGrant(call)) continue;
|
|
232
|
-
out.push({
|
|
233
|
-
file: path.relative(root, sf.getFilePath()),
|
|
234
|
-
line: call.getStartLineNumber(),
|
|
235
|
-
rule: "unsafe-raw-outside-system-scope",
|
|
236
|
-
message:
|
|
237
|
-
'unsafeRaw(...) used outside a systemScope feature and outside a handler/hook declaring escapeHatch — declare { escapeHatch: { reason: "..." } } on the handler or hook, declare the feature systemScope, call declareEscapeHatch({ reason: "..." }) as a direct body statement of a named hook, or use ctx.systemDb.acknowledgeCrossTenant(reason) for a scoped read.' +
|
|
238
|
-
unresolvableDeclareEscapeHatchHint(call),
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
return out;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function isExplicitUnsafeRawGrant(call: Node): boolean {
|
|
245
|
-
if (!call.isKind(SyntaxKind.CallExpression)) return false;
|
|
246
|
-
const expr = call.getExpression();
|
|
247
|
-
if (!expr.isKind(SyntaxKind.PropertyAccessExpression) || expr.getName() !== "unsafeRaw") {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
const receiver = expr.getExpression();
|
|
251
|
-
return (
|
|
252
|
-
receiver.isKind(SyntaxKind.CallExpression) &&
|
|
253
|
-
isFrameworkImportOf(receiver.getExpression(), "withUnsafeRawGrant")
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function isSystemIdentityExpression(node: Node): boolean {
|
|
258
|
-
if (node.isKind(SyntaxKind.CallExpression)) {
|
|
259
|
-
const calleeText = node.getExpression().getText();
|
|
260
|
-
return calleeText.endsWith("createSystemUser") || calleeText.endsWith("systemUserOf");
|
|
261
|
-
}
|
|
262
|
-
if (!node.isKind(SyntaxKind.Identifier)) return false;
|
|
263
|
-
if (SYSTEM_IDENTITY_RE.test(node.getText())) return true;
|
|
264
|
-
const decls = node.getSymbol()?.getDeclarations() ?? [];
|
|
265
|
-
for (const decl of decls) {
|
|
266
|
-
if (!decl.isKind(SyntaxKind.VariableDeclaration)) continue;
|
|
267
|
-
const init = decl.getInitializer();
|
|
268
|
-
if (init && isSystemIdentityExpression(init)) return true;
|
|
269
|
-
}
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function hasJobScopeAncestor(call: Node): boolean {
|
|
274
|
-
let ancestor: Node | undefined = call.getParent();
|
|
275
|
-
while (ancestor) {
|
|
276
|
-
if (ancestor.isKind(SyntaxKind.CallExpression)) {
|
|
277
|
-
const calleeExpr = ancestor.getExpression();
|
|
278
|
-
if (
|
|
279
|
-
calleeExpr.isKind(SyntaxKind.PropertyAccessExpression) &&
|
|
280
|
-
calleeExpr.getName() === "job"
|
|
281
|
-
) {
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (
|
|
286
|
-
ancestor.isKind(SyntaxKind.FunctionDeclaration) ||
|
|
287
|
-
ancestor.isKind(SyntaxKind.MethodDeclaration)
|
|
288
|
-
) {
|
|
289
|
-
if (ancestor.getName()?.endsWith("Job")) return true;
|
|
290
|
-
}
|
|
291
|
-
if (ancestor.isKind(SyntaxKind.ArrowFunction)) {
|
|
292
|
-
const parent = ancestor.getParent();
|
|
293
|
-
if (parent?.isKind(SyntaxKind.VariableDeclaration) && parent.getName().endsWith("Job")) {
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
ancestor = ancestor.getParent();
|
|
298
|
-
}
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function unwrapParenthesized(node: Node): Node {
|
|
303
|
-
let current = node;
|
|
304
|
-
while (current.isKind(SyntaxKind.ParenthesizedExpression)) {
|
|
305
|
-
current = current.getExpression();
|
|
306
|
-
}
|
|
307
|
-
return current;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function isEscapeHatchDeclarationValue(node: Node | undefined): boolean {
|
|
311
|
-
if (!node) return false;
|
|
312
|
-
const value = unwrapParenthesized(node);
|
|
313
|
-
if (value.isKind(SyntaxKind.ObjectLiteralExpression)) return true;
|
|
314
|
-
if (value.isKind(SyntaxKind.ConditionalExpression)) {
|
|
315
|
-
const whenTrue = unwrapParenthesized(value.getWhenTrue());
|
|
316
|
-
const whenFalse = unwrapParenthesized(value.getWhenFalse());
|
|
317
|
-
return (
|
|
318
|
-
whenTrue.isKind(SyntaxKind.ObjectLiteralExpression) &&
|
|
319
|
-
whenFalse.isKind(SyntaxKind.ObjectLiteralExpression)
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function objectDeclaresEscapeHatch(obj: ObjectLiteralExpression): boolean {
|
|
326
|
-
return obj.getProperties().some((prop) => {
|
|
327
|
-
if (!prop.isKind(SyntaxKind.PropertyAssignment)) return false;
|
|
328
|
-
const nameNode = prop.getNameNode();
|
|
329
|
-
if (!nameNode.isKind(SyntaxKind.Identifier) || nameNode.getText() !== "escapeHatch") {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
return isEscapeHatchDeclarationValue(prop.getInitializer());
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const ESCAPE_HATCH_CALL_METHODS = new Set([
|
|
337
|
-
"hook",
|
|
338
|
-
"writeHandler",
|
|
339
|
-
"queryHandler",
|
|
340
|
-
"streamHandler",
|
|
341
|
-
"useExtension",
|
|
342
|
-
]);
|
|
343
|
-
|
|
344
|
-
function isEscapeHatchDeclaredFunction(fn: Node): boolean {
|
|
345
|
-
const parent = fn.getParent();
|
|
346
|
-
if (!parent) return false;
|
|
347
|
-
|
|
348
|
-
if (fn.isKind(SyntaxKind.MethodDeclaration)) {
|
|
349
|
-
return parent.isKind(SyntaxKind.ObjectLiteralExpression) && objectDeclaresEscapeHatch(parent);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!fn.isKind(SyntaxKind.ArrowFunction) && !fn.isKind(SyntaxKind.FunctionExpression)) {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (parent.isKind(SyntaxKind.PropertyAssignment)) {
|
|
357
|
-
const obj = parent.getParent();
|
|
358
|
-
return (
|
|
359
|
-
parent.getInitializer() === fn &&
|
|
360
|
-
obj.isKind(SyntaxKind.ObjectLiteralExpression) &&
|
|
361
|
-
objectDeclaresEscapeHatch(obj)
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (parent.isKind(SyntaxKind.CallExpression)) {
|
|
366
|
-
const args = parent.getArguments();
|
|
367
|
-
if (!args.includes(fn)) return false;
|
|
368
|
-
const callee = parent.getExpression();
|
|
369
|
-
if (
|
|
370
|
-
!callee.isKind(SyntaxKind.PropertyAccessExpression) ||
|
|
371
|
-
!ESCAPE_HATCH_CALL_METHODS.has(callee.getName())
|
|
372
|
-
) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
return args.some(
|
|
376
|
-
(arg) =>
|
|
377
|
-
arg !== fn &&
|
|
378
|
-
arg.isKind(SyntaxKind.ObjectLiteralExpression) &&
|
|
379
|
-
objectDeclaresEscapeHatch(arg),
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// The bare-identifier `reason` PropertyAssignment on an object literal —
|
|
387
|
-
// shared between the declareEscapeHatch statement check below and its R4
|
|
388
|
-
// generic-reason collector, so both agree on what counts as the reason.
|
|
389
|
-
function findReasonPropertyAssignment(
|
|
390
|
-
obj: ObjectLiteralExpression,
|
|
391
|
-
): PropertyAssignment | undefined {
|
|
392
|
-
return obj
|
|
393
|
-
.getProperties()
|
|
394
|
-
.find(
|
|
395
|
-
(prop): prop is PropertyAssignment =>
|
|
396
|
-
prop.isKind(SyntaxKind.PropertyAssignment) &&
|
|
397
|
-
prop.getNameNode().isKind(SyntaxKind.Identifier) &&
|
|
398
|
-
prop.getNameNode().getText() === "reason",
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Syntactic extraction only — shared by the clearance check below and by
|
|
403
|
-
// the "why" hint on R2/R3, which needs the reason node even when it turns
|
|
404
|
-
// out not to resolve.
|
|
405
|
-
function declareEscapeHatchReasonNode(stmt: Node): Node | undefined {
|
|
406
|
-
if (!stmt.isKind(SyntaxKind.ExpressionStatement)) return undefined;
|
|
407
|
-
const expr = stmt.getExpression();
|
|
408
|
-
if (!expr.isKind(SyntaxKind.CallExpression)) return undefined;
|
|
409
|
-
const callee = expr.getExpression();
|
|
410
|
-
if (!isFrameworkImportOf(callee, "declareEscapeHatch")) return undefined;
|
|
411
|
-
const args = expr.getArguments();
|
|
412
|
-
const arg = args[0];
|
|
413
|
-
if (args.length !== 1 || !arg?.isKind(SyntaxKind.ObjectLiteralExpression)) return undefined;
|
|
414
|
-
return findReasonPropertyAssignment(arg)?.getInitializer();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// declareEscapeHatch({ reason: "..." }) as a direct-body statement of a
|
|
418
|
-
// standalone function. No boot validator backs this form (unlike the
|
|
419
|
-
// escapeHatch: {...} property, which access-declarations.ts checks at boot),
|
|
420
|
-
// so an empty/placeholder reason is rejected here rather than left to it.
|
|
421
|
-
function isValidDeclareEscapeHatchCall(stmt: Node): boolean {
|
|
422
|
-
const reasonNode = declareEscapeHatchReasonNode(stmt);
|
|
423
|
-
if (!reasonNode) return false;
|
|
424
|
-
const reasonText = resolveReasonText(reasonNode);
|
|
425
|
-
return reasonText !== undefined && !isGenericReason(reasonText);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// The three reason shapes the guard can name a concrete cause for: an
|
|
429
|
-
// import, a function call, or a template with substitutions — none of
|
|
430
|
-
// those are statically judgeable. An ambient/uninitialized identifier
|
|
431
|
-
// (e.g. a `declare const` parameter) stays silently unresolved instead;
|
|
432
|
-
// there is nothing more specific to say about it.
|
|
433
|
-
function isExplicitlyUnresolvableReason(node: Node): boolean {
|
|
434
|
-
if (node.isKind(SyntaxKind.CallExpression)) return true;
|
|
435
|
-
if (node.isKind(SyntaxKind.TemplateExpression)) return true;
|
|
436
|
-
if (!node.isKind(SyntaxKind.Identifier)) return false;
|
|
437
|
-
const decls = node.getSymbol()?.getDeclarations() ?? [];
|
|
438
|
-
return decls.some(
|
|
439
|
-
(decl) =>
|
|
440
|
-
decl.isKind(SyntaxKind.ImportSpecifier) ||
|
|
441
|
-
decl.isKind(SyntaxKind.ImportClause) ||
|
|
442
|
-
decl.isKind(SyntaxKind.NamespaceImport) ||
|
|
443
|
-
isNonConstVariableDeclaration(decl),
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function isNonConstVariableDeclaration(decl: Node): boolean {
|
|
448
|
-
if (!decl.isKind(SyntaxKind.VariableDeclaration)) return false;
|
|
449
|
-
const list = decl.getParent();
|
|
450
|
-
return (
|
|
451
|
-
list.isKind(SyntaxKind.VariableDeclarationList) &&
|
|
452
|
-
list.getDeclarationKind() !== VariableDeclarationKind.Const
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const UNRESOLVABLE_REASON_HINT =
|
|
457
|
-
" A declareEscapeHatch({ reason }) call was found here, but its reason is an import, a function call, or a template with substitutions (or a `let`/non-`const` binding) — none of those can be statically judged, so declareEscapeHatch needs a string literal or a module-local const instead.";
|
|
458
|
-
|
|
459
|
-
// Walks the same ancestor chain as isInsideEscapeHatchDeclaredFunction, but
|
|
460
|
-
// looks for a declareEscapeHatch statement whose reason is one of the three
|
|
461
|
-
// explicitly-unresolvable shapes above, to explain a still-firing R2/R3
|
|
462
|
-
// finding rather than leave the reader to guess why a visible
|
|
463
|
-
// declareEscapeHatch call didn't clear it.
|
|
464
|
-
function unresolvableDeclareEscapeHatchHint(node: Node): string {
|
|
465
|
-
let ancestor: Node | undefined = node.getParent();
|
|
466
|
-
while (ancestor) {
|
|
467
|
-
if (
|
|
468
|
-
ancestor.isKind(SyntaxKind.ArrowFunction) ||
|
|
469
|
-
ancestor.isKind(SyntaxKind.FunctionExpression) ||
|
|
470
|
-
ancestor.isKind(SyntaxKind.MethodDeclaration) ||
|
|
471
|
-
ancestor.isKind(SyntaxKind.FunctionDeclaration)
|
|
472
|
-
) {
|
|
473
|
-
const body = ancestor.getBody();
|
|
474
|
-
if (body?.isKind(SyntaxKind.Block)) {
|
|
475
|
-
for (const stmt of body.getStatements()) {
|
|
476
|
-
const reasonNode = declareEscapeHatchReasonNode(stmt);
|
|
477
|
-
if (reasonNode && isExplicitlyUnresolvableReason(reasonNode)) {
|
|
478
|
-
return UNRESOLVABLE_REASON_HINT;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
ancestor = ancestor.getParent();
|
|
484
|
-
}
|
|
485
|
-
return "";
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Only the function's own direct body — not a nested function's, not an
|
|
489
|
-
// `if`'s — so a declareEscapeHatch call does not cover the function it is
|
|
490
|
-
// itself nested inside (miss, don't falsely clear).
|
|
491
|
-
function hasDeclaredEscapeHatchStatement(fn: Node): boolean {
|
|
492
|
-
let body: Node | undefined;
|
|
493
|
-
if (
|
|
494
|
-
fn.isKind(SyntaxKind.ArrowFunction) ||
|
|
495
|
-
fn.isKind(SyntaxKind.FunctionExpression) ||
|
|
496
|
-
fn.isKind(SyntaxKind.MethodDeclaration) ||
|
|
497
|
-
fn.isKind(SyntaxKind.FunctionDeclaration)
|
|
498
|
-
) {
|
|
499
|
-
body = fn.getBody();
|
|
500
|
-
}
|
|
501
|
-
if (!body?.isKind(SyntaxKind.Block)) return false;
|
|
502
|
-
const firstStatement = body.getStatements()[0];
|
|
503
|
-
return firstStatement !== undefined && isValidDeclareEscapeHatchCall(firstStatement);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function isInsideEscapeHatchDeclaredFunction(node: Node): boolean {
|
|
507
|
-
let ancestor: Node | undefined = node.getParent();
|
|
508
|
-
while (ancestor) {
|
|
509
|
-
if (
|
|
510
|
-
ancestor.isKind(SyntaxKind.ArrowFunction) ||
|
|
511
|
-
ancestor.isKind(SyntaxKind.FunctionExpression) ||
|
|
512
|
-
ancestor.isKind(SyntaxKind.MethodDeclaration) ||
|
|
513
|
-
ancestor.isKind(SyntaxKind.FunctionDeclaration)
|
|
514
|
-
) {
|
|
515
|
-
if (isEscapeHatchDeclaredFunction(ancestor) || hasDeclaredEscapeHatchStatement(ancestor)) {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
ancestor = ancestor.getParent();
|
|
520
|
-
}
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function isAllowedEscapeHatchCall(
|
|
525
|
-
call: Node,
|
|
526
|
-
sf: SourceFile,
|
|
527
|
-
systemDirs: readonly string[],
|
|
528
|
-
): boolean {
|
|
529
|
-
const filePath = sf.getFilePath();
|
|
530
|
-
if (isInSystemScope(filePath, systemDirs)) return true;
|
|
531
|
-
if (filePath.endsWith(".job.ts")) return true;
|
|
532
|
-
return hasJobScopeAncestor(call) || isInsideEscapeHatchDeclaredFunction(call);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function findSystemIdentityFindings(
|
|
536
|
-
sf: SourceFile,
|
|
537
|
-
root: string,
|
|
538
|
-
systemDirs: readonly string[],
|
|
539
|
-
): EscapeHatchFinding[] {
|
|
540
|
-
const out: EscapeHatchFinding[] = [];
|
|
541
|
-
const text = sf.getFullText();
|
|
542
|
-
if (!text.includes("queryAs") && !text.includes("writeAs")) return out;
|
|
543
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
544
|
-
const expr = call.getExpression();
|
|
545
|
-
if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) continue;
|
|
546
|
-
const methodName = expr.getName();
|
|
547
|
-
if (methodName !== "queryAs" && methodName !== "writeAs") continue;
|
|
548
|
-
const firstArg = call.getArguments()[0];
|
|
549
|
-
if (!firstArg || !isSystemIdentityExpression(firstArg)) continue;
|
|
550
|
-
if (isAllowedEscapeHatchCall(call, sf, systemDirs)) continue;
|
|
551
|
-
out.push({
|
|
552
|
-
file: path.relative(root, sf.getFilePath()),
|
|
553
|
-
line: call.getStartLineNumber(),
|
|
554
|
-
rule: "system-identity-outside-declared-scope",
|
|
555
|
-
message:
|
|
556
|
-
`${methodName}(...) called with a system identity outside a declared scope — restrict to a systemScope feature, a .job.ts / r.job(...) job, declare { escapeHatch: { reason: "..." } } on the handler or hook, or call declareEscapeHatch({ reason: "..." }) as a direct body statement of a named hook.` +
|
|
557
|
-
unresolvableDeclareEscapeHatchHint(call),
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
return out;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function isUnsafeAllTenantsDeclarationValue(node: Node): boolean {
|
|
564
|
-
if (node.isKind(SyntaxKind.TrueKeyword)) return true;
|
|
565
|
-
return node.isKind(SyntaxKind.ObjectLiteralExpression);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function findUnsafeAllTenantsFindings(
|
|
569
|
-
sf: SourceFile,
|
|
570
|
-
root: string,
|
|
571
|
-
systemDirs: readonly string[],
|
|
572
|
-
): EscapeHatchFinding[] {
|
|
573
|
-
const out: EscapeHatchFinding[] = [];
|
|
574
|
-
if (!sf.getFullText().includes("unsafeAllTenants")) return out;
|
|
575
|
-
for (const prop of sf.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
576
|
-
const nameNode = prop.getNameNode();
|
|
577
|
-
if (!nameNode.isKind(SyntaxKind.Identifier) || nameNode.getText() !== "unsafeAllTenants") {
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
const initializer = prop.getInitializer();
|
|
581
|
-
if (!initializer) continue;
|
|
582
|
-
const value = unwrapParenthesized(initializer);
|
|
583
|
-
if (!isUnsafeAllTenantsDeclarationValue(value)) continue;
|
|
584
|
-
const obj = prop.getParent();
|
|
585
|
-
if (!obj.isKind(SyntaxKind.ObjectLiteralExpression)) continue;
|
|
586
|
-
const call = obj.getParent();
|
|
587
|
-
if (!call?.isKind(SyntaxKind.CallExpression)) continue;
|
|
588
|
-
if (!call.getArguments().includes(obj)) continue;
|
|
589
|
-
if (objectDeclaresEscapeHatch(obj)) continue;
|
|
590
|
-
if (isAllowedEscapeHatchCall(call, sf, systemDirs)) continue;
|
|
591
|
-
out.push({
|
|
592
|
-
file: path.relative(root, sf.getFilePath()),
|
|
593
|
-
line: prop.getStartLineNumber(),
|
|
594
|
-
rule: "unsafe-all-tenants-outside-declared-scope",
|
|
595
|
-
message:
|
|
596
|
-
'unsafeAllTenants used outside a declared scope — restrict to a systemScope feature, a .job.ts / r.job(...) job, declare { escapeHatch: { reason: "..." } } alongside it, or call declareEscapeHatch({ reason: "..." }) as a direct body statement of a named hook.' +
|
|
597
|
-
unresolvableDeclareEscapeHatchHint(call),
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
return out;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function findEscapeHatchFindings(
|
|
604
|
-
files: readonly SourceFile[],
|
|
605
|
-
root: string,
|
|
606
|
-
): EscapeHatchFinding[] {
|
|
607
|
-
const systemDirs = systemScopeDirs(files);
|
|
608
|
-
const out: EscapeHatchFinding[] = [];
|
|
609
|
-
for (const sf of scannableFiles(files)) {
|
|
610
|
-
out.push(...findRawFindings(sf, root, systemDirs));
|
|
611
|
-
out.push(...findUnsafeRawFindings(sf, root, systemDirs));
|
|
612
|
-
out.push(...findSystemIdentityFindings(sf, root, systemDirs));
|
|
613
|
-
out.push(...findUnsafeAllTenantsFindings(sf, root, systemDirs));
|
|
614
|
-
}
|
|
615
|
-
return out;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const GENERIC_REASON_METHODS = new Set(["acknowledgeCrossTenant", "unsafeRaw"]);
|
|
619
|
-
|
|
620
|
-
function findGenericReasonMethodCalls(sf: SourceFile, root: string): GenericReasonFinding[] {
|
|
621
|
-
const out: GenericReasonFinding[] = [];
|
|
622
|
-
const text = sf.getFullText();
|
|
623
|
-
if (![...GENERIC_REASON_METHODS].some((method) => text.includes(method))) return out;
|
|
624
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
625
|
-
const expr = call.getExpression();
|
|
626
|
-
if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) continue;
|
|
627
|
-
const methodName = expr.getName();
|
|
628
|
-
if (!GENERIC_REASON_METHODS.has(methodName)) continue;
|
|
629
|
-
const reasonText = resolveReasonText(call.getArguments()[0]);
|
|
630
|
-
if (reasonText === undefined || !isGenericReason(reasonText)) continue;
|
|
631
|
-
out.push({
|
|
632
|
-
file: path.relative(root, sf.getFilePath()),
|
|
633
|
-
line: call.getStartLineNumber(),
|
|
634
|
-
message: `${methodName}("${reasonText}") uses a placeholder reason — give a concrete, reviewable justification for this cross-tenant/unsafe access.`,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
return out;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// The declareEscapeHatch({ reason }) form falls through both existing R4
|
|
641
|
-
// collectors: its callee is a bare identifier, not a PropertyAccessExpression
|
|
642
|
-
// (unlike acknowledgeCrossTenant/unsafeRaw), and its reason sits directly in
|
|
643
|
-
// the call argument, not under an escapeHatch:/unsafeAllTenants: property.
|
|
644
|
-
function findGenericReasonDeclareEscapeHatchCalls(
|
|
645
|
-
sf: SourceFile,
|
|
646
|
-
root: string,
|
|
647
|
-
): GenericReasonFinding[] {
|
|
648
|
-
const out: GenericReasonFinding[] = [];
|
|
649
|
-
if (!sf.getFullText().includes("declareEscapeHatch")) return out;
|
|
650
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
651
|
-
const callee = call.getExpression();
|
|
652
|
-
if (!callee.isKind(SyntaxKind.Identifier) || callee.getText() !== "declareEscapeHatch") {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
const arg = call.getArguments()[0];
|
|
656
|
-
if (!arg?.isKind(SyntaxKind.ObjectLiteralExpression)) continue;
|
|
657
|
-
const reasonProp = findReasonPropertyAssignment(arg);
|
|
658
|
-
if (!reasonProp) continue;
|
|
659
|
-
const reasonText = resolveReasonText(reasonProp.getInitializer());
|
|
660
|
-
if (reasonText === undefined || !isGenericReason(reasonText)) continue;
|
|
661
|
-
out.push({
|
|
662
|
-
file: path.relative(root, sf.getFilePath()),
|
|
663
|
-
line: call.getStartLineNumber(),
|
|
664
|
-
message: `declareEscapeHatch({ reason: "${reasonText}" }) uses a placeholder reason — give a concrete, reviewable justification for this cross-tenant/unsafe access.`,
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
return out;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const REASON_OBJECT_PROPERTY_NAMES = ["escapeHatch", "unsafeAllTenants"] as const;
|
|
671
|
-
|
|
672
|
-
function findGenericReasonObjectProperty(
|
|
673
|
-
sf: SourceFile,
|
|
674
|
-
root: string,
|
|
675
|
-
propertyName: string,
|
|
676
|
-
): GenericReasonFinding[] {
|
|
677
|
-
const out: GenericReasonFinding[] = [];
|
|
678
|
-
if (!sf.getFullText().includes(propertyName)) return out;
|
|
679
|
-
for (const pa of sf.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
680
|
-
if (pa.getName() !== propertyName) continue;
|
|
681
|
-
const init = pa.getInitializer();
|
|
682
|
-
if (!init?.isKind(SyntaxKind.ObjectLiteralExpression)) continue;
|
|
683
|
-
const reasonProp = init.getProperty("reason");
|
|
684
|
-
if (!reasonProp?.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
685
|
-
const reasonText = resolveReasonText(reasonProp.getInitializer());
|
|
686
|
-
if (reasonText === undefined) continue;
|
|
687
|
-
// Empty/whitespace is the boot validator's job — no double-check.
|
|
688
|
-
if (reasonText.trim() === "") continue;
|
|
689
|
-
if (!isGenericReason(reasonText)) continue;
|
|
690
|
-
out.push({
|
|
691
|
-
file: path.relative(root, sf.getFilePath()),
|
|
692
|
-
line: pa.getStartLineNumber(),
|
|
693
|
-
message: `${propertyName}: { reason: "${reasonText}" } uses a placeholder reason — give a concrete, reviewable justification for this cross-tenant/unsafe access.`,
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
return out;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
export function findGenericReasonCalls(
|
|
700
|
-
files: readonly SourceFile[],
|
|
701
|
-
root: string,
|
|
702
|
-
): GenericReasonFinding[] {
|
|
703
|
-
const out: GenericReasonFinding[] = [];
|
|
704
|
-
for (const sf of scannableFiles(files)) {
|
|
705
|
-
out.push(...findGenericReasonMethodCalls(sf, root));
|
|
706
|
-
out.push(...findGenericReasonDeclareEscapeHatchCalls(sf, root));
|
|
707
|
-
for (const propertyName of REASON_OBJECT_PROPERTY_NAMES) {
|
|
708
|
-
out.push(...findGenericReasonObjectProperty(sf, root, propertyName));
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
return out;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
export function createEscapeHatchGuard(opts: { root: string }): AstGuard {
|
|
715
|
-
return {
|
|
716
|
-
name: "Escape-Hatch-Declared Guard",
|
|
717
|
-
scan: SCAN,
|
|
718
|
-
security: true,
|
|
719
|
-
hint: 'Declare an escape hatch (r.systemScope() on the feature definition, .job.ts/r.job(...) for jobs, or { escapeHatch: { reason: "..." } } on the handler or hook), or remove the ctx.db.raw/unsafeRaw/queryAs|writeAs(system)/unsafeAllTenants access. Baseline after a deliberate reduction: `kumiko-guards guards --write-security-baseline`',
|
|
720
|
-
run(files) {
|
|
721
|
-
const violations: GuardViolation[] = [
|
|
722
|
-
...findGenericReasonCalls(files, opts.root).map((f) => ({
|
|
723
|
-
file: f.file,
|
|
724
|
-
line: f.line,
|
|
725
|
-
message: f.message,
|
|
726
|
-
neverFrozen: true,
|
|
727
|
-
})),
|
|
728
|
-
...findEscapeHatchFindings(files, opts.root).map((f) => ({
|
|
729
|
-
file: f.file,
|
|
730
|
-
line: f.line,
|
|
731
|
-
message: f.message,
|
|
732
|
-
})),
|
|
733
|
-
];
|
|
734
|
-
return { violations };
|
|
735
|
-
},
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
export const guard = createEscapeHatchGuard({ root: process.cwd() });
|
|
740
|
-
|
|
741
|
-
if (import.meta.main) {
|
|
742
|
-
runStandalone(guard);
|
|
743
|
-
}
|