eslint-plugin-rulvar 1.38.0 → 1.40.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 +158 -12
- 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,15 +151,159 @@ 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
|
|
154
298
|
//#region src/rules/dialect.ts
|
|
155
299
|
const noCodeGeneration = {
|
|
156
300
|
meta: {
|
|
157
301
|
type: "problem",
|
|
158
|
-
docs: { description: "ban dynamic code generation (eval, the Function constructor, and
|
|
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" },
|
|
159
303
|
messages: {
|
|
160
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.",
|
|
161
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.",
|
|
162
|
-
noConstructorAccess: "
|
|
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."
|
|
163
307
|
},
|
|
164
308
|
schema: []
|
|
165
309
|
},
|
|
@@ -196,23 +340,25 @@ const noCodeGeneration = {
|
|
|
196
340
|
return {
|
|
197
341
|
CallExpression(node) {
|
|
198
342
|
checkCallee(node, node.callee);
|
|
343
|
+
if (isReflectConstructorGet(node)) context.report({
|
|
344
|
+
node,
|
|
345
|
+
messageId: "noConstructorAccess"
|
|
346
|
+
});
|
|
199
347
|
},
|
|
200
348
|
NewExpression(node) {
|
|
201
349
|
checkCallee(node, node.callee);
|
|
202
350
|
},
|
|
203
351
|
MemberExpression(node) {
|
|
204
|
-
|
|
205
|
-
if (!computed && property.type === "Identifier" && property.name === "constructor") {
|
|
206
|
-
context.report({
|
|
207
|
-
node,
|
|
208
|
-
messageId: "noConstructorAccess"
|
|
209
|
-
});
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (computed && property.type === "Literal" && property.value === "constructor") context.report({
|
|
352
|
+
if (isConstructorMemberExpression(node)) context.report({
|
|
213
353
|
node,
|
|
214
354
|
messageId: "noConstructorAccess"
|
|
215
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
|
+
});
|
|
216
362
|
}
|
|
217
363
|
};
|
|
218
364
|
}
|
|
@@ -339,4 +485,4 @@ const workflowsConfig = {
|
|
|
339
485
|
};
|
|
340
486
|
plugin.configs = { workflows: workflowsConfig };
|
|
341
487
|
//#endregion
|
|
342
|
-
export { plugin as default, rules, toJsonDiagnostics, workflowsConfig };
|
|
488
|
+
export { plugin as default, rules, scanDialect, toJsonDiagnostics, workflowsConfig };
|
package/package.json
CHANGED