eslint-plugin-rulvar 1.37.0 → 1.39.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/dist/index.d.ts +23 -1
- package/dist/index.js +216 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,28 @@ interface RulvarLintDiagnostic {
|
|
|
12
12
|
}
|
|
13
13
|
declare function toJsonDiagnostics(messages: readonly Linter.LintMessage[]): RulvarLintDiagnostic[];
|
|
14
14
|
//#endregion
|
|
15
|
+
//#region src/dialect-scan.d.ts
|
|
16
|
+
/** Where a finding sits in the ORIGINAL source (line and column counted from 1). */
|
|
17
|
+
interface DialectFinding {
|
|
18
|
+
kind: "eval" | "function-constructor" | "constructor-access";
|
|
19
|
+
line: number;
|
|
20
|
+
column: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Structural scan for compileScript: every dynamic code generation form the
|
|
24
|
+
* dialect rejects, as findings positioned in the original source. Covers bare
|
|
25
|
+
* `eval`/`Function` calls and `new`, `globalThis.eval`/`globalThis.Function`,
|
|
26
|
+
* and every constructor reconstruction form the shared predicates recognize.
|
|
27
|
+
* Member access on other objects (`response.eval`, `parser.Function`) and a
|
|
28
|
+
* property NAMED constructor in an object LITERAL are not code generation and
|
|
29
|
+
* are left alone.
|
|
30
|
+
*
|
|
31
|
+
* Takes a parsed ESTree Program, typed `unknown` so a caller needs neither the
|
|
32
|
+
* estree types nor a specific parser in its own public surface; any ESTree
|
|
33
|
+
* compatible parser (espree in the lint pass, acorn in compileScript) works.
|
|
34
|
+
*/
|
|
35
|
+
declare function scanDialect(program: unknown): DialectFinding[];
|
|
36
|
+
//#endregion
|
|
15
37
|
//#region src/index.d.ts
|
|
16
38
|
declare const rules: Record<string, Rule.RuleModule>;
|
|
17
39
|
declare const plugin: ESLint.Plugin;
|
|
@@ -21,4 +43,4 @@ declare const plugin: ESLint.Plugin;
|
|
|
21
43
|
*/
|
|
22
44
|
declare const workflowsConfig: Linter.Config;
|
|
23
45
|
//#endregion
|
|
24
|
-
export { type RulvarLintDiagnostic, plugin as default, rules, toJsonDiagnostics, workflowsConfig };
|
|
46
|
+
export { type DialectFinding, type RulvarLintDiagnostic, plugin as default, rules, scanDialect, toJsonDiagnostics, workflowsConfig };
|
package/dist/index.js
CHANGED
|
@@ -151,6 +151,219 @@ const noProcessEnv = {
|
|
|
151
151
|
}
|
|
152
152
|
};
|
|
153
153
|
//#endregion
|
|
154
|
+
//#region src/dialect-scan.ts
|
|
155
|
+
function positionOf(node) {
|
|
156
|
+
const start = node.loc?.start;
|
|
157
|
+
return {
|
|
158
|
+
line: start?.line ?? 1,
|
|
159
|
+
column: (start?.column ?? 0) + 1
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Folds an expression that evaluates to a string to its constant value, or
|
|
164
|
+
* undefined when it is not statically constant. Handles string literals, templates whose
|
|
165
|
+
* every interpolation folds, and `+` concatenations of foldable operands.
|
|
166
|
+
* This is deliberately conservative: anything it cannot prove constant (a
|
|
167
|
+
* variable, a call, an index) folds to undefined and is left to the runtime.
|
|
168
|
+
*/
|
|
169
|
+
function foldStaticString(node) {
|
|
170
|
+
if (node === null || node === void 0) return;
|
|
171
|
+
if (node.type === "Literal") return typeof node.value === "string" ? node.value : void 0;
|
|
172
|
+
if (node.type === "TemplateLiteral") {
|
|
173
|
+
let out = "";
|
|
174
|
+
for (let i = 0; i < node.quasis.length; i += 1) {
|
|
175
|
+
out += node.quasis[i].value.cooked ?? node.quasis[i].value.raw;
|
|
176
|
+
if (i < node.expressions.length) {
|
|
177
|
+
const piece = foldStaticString(node.expressions[i]);
|
|
178
|
+
if (piece === void 0) return;
|
|
179
|
+
out += piece;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
185
|
+
const left = node.left.type === "PrivateIdentifier" ? void 0 : foldStaticString(node.left);
|
|
186
|
+
const right = foldStaticString(node.right);
|
|
187
|
+
return left === void 0 || right === void 0 ? void 0 : left + right;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** `fn.constructor` or `fn["constructor"]` (any statically constant key). */
|
|
191
|
+
function isConstructorMemberExpression(node) {
|
|
192
|
+
if (!node.computed) return node.property.type === "Identifier" && node.property.name === "constructor";
|
|
193
|
+
return foldStaticString(node.property) === "constructor";
|
|
194
|
+
}
|
|
195
|
+
/** A destructuring property that binds the `constructor` slot of its source. */
|
|
196
|
+
function isConstructorPatternProperty(node) {
|
|
197
|
+
if (!node.computed) {
|
|
198
|
+
if (node.key.type === "Identifier") return node.key.name === "constructor";
|
|
199
|
+
if (node.key.type === "Literal") return node.key.value === "constructor";
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return foldStaticString(node.key) === "constructor";
|
|
203
|
+
}
|
|
204
|
+
/** `Reflect.get(fn, "constructor")` with a statically constant key. */
|
|
205
|
+
function isReflectConstructorGet(node) {
|
|
206
|
+
const callee = node.callee;
|
|
207
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.object.type !== "Identifier" || callee.object.name !== "Reflect" || callee.property.type !== "Identifier" || callee.property.name !== "get") return false;
|
|
208
|
+
const keyArg = node.arguments[1];
|
|
209
|
+
return keyArg !== void 0 && keyArg.type !== "SpreadElement" && foldStaticString(keyArg) === "constructor";
|
|
210
|
+
}
|
|
211
|
+
/** A bare global `eval`/`Function` used as a call or `new` target. */
|
|
212
|
+
function bareCodegenCallee(callee) {
|
|
213
|
+
if (callee.type === "Identifier") {
|
|
214
|
+
if (callee.name === "eval") return "eval";
|
|
215
|
+
if (callee.name === "Function") return "function-constructor";
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.object.name === "globalThis" && callee.property.type === "Identifier") {
|
|
219
|
+
if (callee.property.name === "eval") return "eval";
|
|
220
|
+
if (callee.property.name === "Function") return "function-constructor";
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
225
|
+
"loc",
|
|
226
|
+
"range",
|
|
227
|
+
"start",
|
|
228
|
+
"end",
|
|
229
|
+
"parent",
|
|
230
|
+
"comments",
|
|
231
|
+
"tokens",
|
|
232
|
+
"leadingComments",
|
|
233
|
+
"trailingComments"
|
|
234
|
+
]);
|
|
235
|
+
function walk(root, visit) {
|
|
236
|
+
const stack = [root];
|
|
237
|
+
while (stack.length > 0) {
|
|
238
|
+
const current = stack.pop();
|
|
239
|
+
if (current === null || typeof current !== "object") continue;
|
|
240
|
+
if (Array.isArray(current)) {
|
|
241
|
+
for (const item of current) stack.push(item);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const node = current;
|
|
245
|
+
if (typeof node.type !== "string") continue;
|
|
246
|
+
visit(node);
|
|
247
|
+
for (const [key, value] of Object.entries(node)) if (!SKIP_KEYS.has(key)) stack.push(value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Structural scan for compileScript: every dynamic code generation form the
|
|
252
|
+
* dialect rejects, as findings positioned in the original source. Covers bare
|
|
253
|
+
* `eval`/`Function` calls and `new`, `globalThis.eval`/`globalThis.Function`,
|
|
254
|
+
* and every constructor reconstruction form the shared predicates recognize.
|
|
255
|
+
* Member access on other objects (`response.eval`, `parser.Function`) and a
|
|
256
|
+
* property NAMED constructor in an object LITERAL are not code generation and
|
|
257
|
+
* are left alone.
|
|
258
|
+
*
|
|
259
|
+
* Takes a parsed ESTree Program, typed `unknown` so a caller needs neither the
|
|
260
|
+
* estree types nor a specific parser in its own public surface; any ESTree
|
|
261
|
+
* compatible parser (espree in the lint pass, acorn in compileScript) works.
|
|
262
|
+
*/
|
|
263
|
+
function scanDialect(program) {
|
|
264
|
+
const findings = [];
|
|
265
|
+
walk(program, (node) => {
|
|
266
|
+
switch (node.type) {
|
|
267
|
+
case "CallExpression":
|
|
268
|
+
case "NewExpression": {
|
|
269
|
+
const codegen = bareCodegenCallee(node.callee);
|
|
270
|
+
if (codegen !== null) findings.push({
|
|
271
|
+
kind: codegen,
|
|
272
|
+
...positionOf(node)
|
|
273
|
+
});
|
|
274
|
+
if (node.type === "CallExpression" && isReflectConstructorGet(node)) findings.push({
|
|
275
|
+
kind: "constructor-access",
|
|
276
|
+
...positionOf(node)
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "MemberExpression":
|
|
281
|
+
if (isConstructorMemberExpression(node)) findings.push({
|
|
282
|
+
kind: "constructor-access",
|
|
283
|
+
...positionOf(node)
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
case "ObjectPattern":
|
|
287
|
+
for (const property of node.properties) if (property.type === "Property" && isConstructorPatternProperty(property)) findings.push({
|
|
288
|
+
kind: "constructor-access",
|
|
289
|
+
...positionOf(property)
|
|
290
|
+
});
|
|
291
|
+
break;
|
|
292
|
+
default: break;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return findings;
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/rules/dialect.ts
|
|
299
|
+
const noCodeGeneration = {
|
|
300
|
+
meta: {
|
|
301
|
+
type: "problem",
|
|
302
|
+
docs: { description: "ban dynamic code generation (eval, the Function constructor, and constructor reconstruction) in workflow modules; it reopens the import allowlist and the ambient capability surface the sandbox dialect closes" },
|
|
303
|
+
messages: {
|
|
304
|
+
noEval: "eval is not part of the workflow dialect; it compiles code in a fresh scope that reopens import() and ambient capability. Stay on the ctx surface.",
|
|
305
|
+
noFunctionConstructor: "the Function constructor is not part of the workflow dialect; it compiles arbitrary code that bypasses the import allowlist. Stay on the ctx surface.",
|
|
306
|
+
noConstructorAccess: "constructor access reaches the Function constructor from any function value and reopens dynamic code generation; it is not part of the workflow dialect."
|
|
307
|
+
},
|
|
308
|
+
schema: []
|
|
309
|
+
},
|
|
310
|
+
create(context) {
|
|
311
|
+
const checkCallee = (node, callee) => {
|
|
312
|
+
if (callee.type === "Identifier" && isGlobalReference(context, callee)) {
|
|
313
|
+
if (callee.name === "eval") {
|
|
314
|
+
context.report({
|
|
315
|
+
node,
|
|
316
|
+
messageId: "noEval"
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (callee.name === "Function") {
|
|
321
|
+
context.report({
|
|
322
|
+
node,
|
|
323
|
+
messageId: "noFunctionConstructor"
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (isGlobalMemberCall(context, callee, "globalThis", "eval")) {
|
|
329
|
+
context.report({
|
|
330
|
+
node,
|
|
331
|
+
messageId: "noEval"
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (isGlobalMemberCall(context, callee, "globalThis", "Function")) context.report({
|
|
336
|
+
node,
|
|
337
|
+
messageId: "noFunctionConstructor"
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
CallExpression(node) {
|
|
342
|
+
checkCallee(node, node.callee);
|
|
343
|
+
if (isReflectConstructorGet(node)) context.report({
|
|
344
|
+
node,
|
|
345
|
+
messageId: "noConstructorAccess"
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
NewExpression(node) {
|
|
349
|
+
checkCallee(node, node.callee);
|
|
350
|
+
},
|
|
351
|
+
MemberExpression(node) {
|
|
352
|
+
if (isConstructorMemberExpression(node)) context.report({
|
|
353
|
+
node,
|
|
354
|
+
messageId: "noConstructorAccess"
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
ObjectPattern(node) {
|
|
358
|
+
for (const property of node.properties) if (property.type === "Property" && isConstructorPatternProperty(property)) context.report({
|
|
359
|
+
node: property,
|
|
360
|
+
messageId: "noConstructorAccess"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
//#endregion
|
|
154
367
|
//#region src/rules/scheduling.ts
|
|
155
368
|
const PROMISE_COMBINATORS = /* @__PURE__ */ new Set([
|
|
156
369
|
"all",
|
|
@@ -245,6 +458,7 @@ const rules = {
|
|
|
245
458
|
"no-bare-random": noBareRandom,
|
|
246
459
|
"no-fetch": noFetch,
|
|
247
460
|
"no-process-env": noProcessEnv,
|
|
461
|
+
"no-code-generation": noCodeGeneration,
|
|
248
462
|
"no-promise-all-over-ctx": noPromiseAllOverCtx,
|
|
249
463
|
"duplicate-identical-call": duplicateIdenticalCall
|
|
250
464
|
};
|
|
@@ -264,10 +478,11 @@ const workflowsConfig = {
|
|
|
264
478
|
"rulvar/no-bare-random": "error",
|
|
265
479
|
"rulvar/no-fetch": "error",
|
|
266
480
|
"rulvar/no-process-env": "error",
|
|
481
|
+
"rulvar/no-code-generation": "error",
|
|
267
482
|
"rulvar/no-promise-all-over-ctx": "error",
|
|
268
483
|
"rulvar/duplicate-identical-call": "warn"
|
|
269
484
|
}
|
|
270
485
|
};
|
|
271
486
|
plugin.configs = { workflows: workflowsConfig };
|
|
272
487
|
//#endregion
|
|
273
|
-
export { plugin as default, rules, toJsonDiagnostics, workflowsConfig };
|
|
488
|
+
export { plugin as default, rules, scanDialect, toJsonDiagnostics, workflowsConfig };
|
package/package.json
CHANGED