@undercurrentai/eslint-plugin-ai-guard 2.0.0-beta.3
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 +21 -0
- package/README.md +252 -0
- package/dist/cli/index.js +2293 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +3679 -0
- package/dist/index.mjs +3652 -0
- package/package.json +104 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3679 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
default: () => index_default
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/rules/error-handling/no-empty-catch.ts
|
|
28
|
+
var import_utils = require("@typescript-eslint/utils");
|
|
29
|
+
var createRule = import_utils.ESLintUtils.RuleCreator(
|
|
30
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
31
|
+
);
|
|
32
|
+
var noEmptyCatch = createRule({
|
|
33
|
+
name: "no-empty-catch",
|
|
34
|
+
meta: {
|
|
35
|
+
type: "problem",
|
|
36
|
+
docs: {
|
|
37
|
+
description: "Disallow empty catch blocks. AI tools frequently generate try/catch with empty catch bodies that silently swallow errors, hiding failures in production."
|
|
38
|
+
},
|
|
39
|
+
fixable: void 0,
|
|
40
|
+
hasSuggestions: true,
|
|
41
|
+
schema: [],
|
|
42
|
+
messages: {
|
|
43
|
+
emptyCatch: "Catch block is empty. AI tools frequently generate empty catch blocks that silently swallow errors. Handle the error (log, rethrow, or recover), or add a comment explaining why it is intentionally empty.",
|
|
44
|
+
addTodoHandler: "Add a minimal placeholder so this catch block is not silently swallowing errors."
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
defaultOptions: [],
|
|
48
|
+
create(context) {
|
|
49
|
+
return {
|
|
50
|
+
CatchClause(node) {
|
|
51
|
+
if (node.body.body.length === 0) {
|
|
52
|
+
const sourceCode = context.sourceCode;
|
|
53
|
+
const comments = sourceCode.getCommentsInside(node.body);
|
|
54
|
+
if (comments.length > 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
context.report({
|
|
58
|
+
node,
|
|
59
|
+
messageId: "emptyCatch",
|
|
60
|
+
suggest: [
|
|
61
|
+
{
|
|
62
|
+
messageId: "addTodoHandler",
|
|
63
|
+
fix: (fixer) => fixer.replaceText(node.body, "{ /* TODO: handle error */ }")
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/rules/error-handling/no-broad-exception.ts
|
|
74
|
+
var import_utils2 = require("@typescript-eslint/utils");
|
|
75
|
+
var createRule2 = import_utils2.ESLintUtils.RuleCreator(
|
|
76
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
77
|
+
);
|
|
78
|
+
var noBroadException = createRule2({
|
|
79
|
+
name: "no-broad-exception",
|
|
80
|
+
meta: {
|
|
81
|
+
type: "suggestion",
|
|
82
|
+
deprecated: true,
|
|
83
|
+
replacedBy: ["@typescript-eslint/no-explicit-any"],
|
|
84
|
+
docs: {
|
|
85
|
+
description: "[DEPRECATED \u2014 partial coverage by `@typescript-eslint/no-explicit-any` plus TypeScript `useUnknownInCatchVariables: true`] Disallow `catch (e: any)` or `catch (e: unknown)` without narrowing. Kept for backwards-compatibility in v2.x; removed in v3.0."
|
|
86
|
+
},
|
|
87
|
+
fixable: void 0,
|
|
88
|
+
schema: [],
|
|
89
|
+
messages: {
|
|
90
|
+
broadException: "[ai-guard deprecated \u2014 use `@typescript-eslint/no-explicit-any`] Catch parameter has an overly broad type annotation `{{type}}`. Use a specific error type or narrow with `instanceof` checks."
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
defaultOptions: [],
|
|
94
|
+
create(context) {
|
|
95
|
+
return {
|
|
96
|
+
CatchClause(node) {
|
|
97
|
+
const param = node.param;
|
|
98
|
+
if (!param) return;
|
|
99
|
+
if (param.type === import_utils2.AST_NODE_TYPES.Identifier && param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
|
|
100
|
+
const typeAnnotation = param.typeAnnotation.typeAnnotation;
|
|
101
|
+
if (typeAnnotation.type === import_utils2.AST_NODE_TYPES.TSAnyKeyword) {
|
|
102
|
+
context.report({
|
|
103
|
+
node: param,
|
|
104
|
+
messageId: "broadException",
|
|
105
|
+
data: { type: "any" }
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (typeAnnotation.type === import_utils2.AST_NODE_TYPES.TSUnknownKeyword) {
|
|
110
|
+
const hasNarrowing = checkForNarrowing(node, param.name);
|
|
111
|
+
if (!hasNarrowing) {
|
|
112
|
+
context.report({
|
|
113
|
+
node: param,
|
|
114
|
+
messageId: "broadException",
|
|
115
|
+
data: { type: "unknown" }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
function checkForNarrowing(catchClause, paramName) {
|
|
125
|
+
const bodyStatements = catchClause.body.body;
|
|
126
|
+
for (const stmt of bodyStatements) {
|
|
127
|
+
if (containsInstanceofCheck(stmt, paramName)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function containsInstanceofCheck(node, paramName) {
|
|
134
|
+
if (!node || typeof node !== "object") return false;
|
|
135
|
+
if (node.type === import_utils2.AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.left?.type === import_utils2.AST_NODE_TYPES.Identifier && node.left.name === paramName) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
for (const key of Object.keys(node)) {
|
|
139
|
+
if (key === "parent") continue;
|
|
140
|
+
const child = node[key];
|
|
141
|
+
if (Array.isArray(child)) {
|
|
142
|
+
for (const item of child) {
|
|
143
|
+
if (containsInstanceofCheck(item, paramName)) return true;
|
|
144
|
+
}
|
|
145
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
146
|
+
if (containsInstanceofCheck(child, paramName)) return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/rules/error-handling/no-catch-log-rethrow.ts
|
|
153
|
+
var import_utils3 = require("@typescript-eslint/utils");
|
|
154
|
+
var createRule3 = import_utils3.ESLintUtils.RuleCreator(
|
|
155
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
156
|
+
);
|
|
157
|
+
function isConsoleCallStatement(statement) {
|
|
158
|
+
if (statement.type !== import_utils3.AST_NODE_TYPES.ExpressionStatement) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const expression = statement.expression;
|
|
162
|
+
if (expression.type !== import_utils3.AST_NODE_TYPES.CallExpression) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (expression.callee.type !== import_utils3.AST_NODE_TYPES.MemberExpression) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return expression.callee.object.type === import_utils3.AST_NODE_TYPES.Identifier && expression.callee.object.name === "console" && expression.callee.property.type === import_utils3.AST_NODE_TYPES.Identifier && ["log", "error", "warn", "info", "debug"].includes(expression.callee.property.name);
|
|
169
|
+
}
|
|
170
|
+
function isRethrowOfCatchParam(statement, catchParamName) {
|
|
171
|
+
return statement.type === import_utils3.AST_NODE_TYPES.ThrowStatement && statement.argument !== null && statement.argument.type === import_utils3.AST_NODE_TYPES.Identifier && statement.argument.name === catchParamName;
|
|
172
|
+
}
|
|
173
|
+
var noCatchLogRethrow = createRule3({
|
|
174
|
+
name: "no-catch-log-rethrow",
|
|
175
|
+
meta: {
|
|
176
|
+
type: "suggestion",
|
|
177
|
+
docs: {
|
|
178
|
+
description: "Disallow catch blocks that only log and rethrow the same error. AI tools frequently generate catch-log-rethrow patterns that add noise without recovery or context."
|
|
179
|
+
},
|
|
180
|
+
fixable: void 0,
|
|
181
|
+
schema: [],
|
|
182
|
+
messages: {
|
|
183
|
+
catchLogRethrow: "This catch block only logs and rethrows the same error. AI tools frequently generate this noisy pattern without adding recovery. Either remove the catch block or add meaningful handling/context."
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
defaultOptions: [],
|
|
187
|
+
create(context) {
|
|
188
|
+
return {
|
|
189
|
+
CatchClause(node) {
|
|
190
|
+
if (!node.param || node.param.type !== import_utils3.AST_NODE_TYPES.Identifier) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const bodyStatements = node.body.body;
|
|
194
|
+
if (bodyStatements.length < 2) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const lastStatement = bodyStatements[bodyStatements.length - 1];
|
|
198
|
+
if (!isRethrowOfCatchParam(lastStatement, node.param.name)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const leadingStatements = bodyStatements.slice(0, -1);
|
|
202
|
+
const allConsoleCalls = leadingStatements.every(isConsoleCallStatement);
|
|
203
|
+
if (allConsoleCalls) {
|
|
204
|
+
context.report({
|
|
205
|
+
node,
|
|
206
|
+
messageId: "catchLogRethrow"
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// src/rules/error-handling/no-catch-without-use.ts
|
|
215
|
+
var import_utils4 = require("@typescript-eslint/utils");
|
|
216
|
+
var createRule4 = import_utils4.ESLintUtils.RuleCreator(
|
|
217
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
218
|
+
);
|
|
219
|
+
var noCatchWithoutUse = createRule4({
|
|
220
|
+
name: "no-catch-without-use",
|
|
221
|
+
meta: {
|
|
222
|
+
type: "suggestion",
|
|
223
|
+
deprecated: true,
|
|
224
|
+
replacedBy: ["@typescript-eslint/no-unused-vars"],
|
|
225
|
+
docs: {
|
|
226
|
+
description: '[DEPRECATED \u2014 use `@typescript-eslint/no-unused-vars` with `caughtErrors: "all"`] Disallow catch parameters that are never used. Kept for backwards-compatibility in v2.x; removed in v3.0.'
|
|
227
|
+
},
|
|
228
|
+
fixable: void 0,
|
|
229
|
+
schema: [],
|
|
230
|
+
messages: {
|
|
231
|
+
unusedCatchParam: '[ai-guard deprecated \u2014 use `@typescript-eslint/no-unused-vars` with `caughtErrors: "all"`] Catch parameter `{{name}}` is never used.'
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
defaultOptions: [],
|
|
235
|
+
create(context) {
|
|
236
|
+
return {
|
|
237
|
+
CatchClause(node) {
|
|
238
|
+
if (!node.param || node.param.type !== import_utils4.AST_NODE_TYPES.Identifier) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const catchParamName = node.param.name;
|
|
242
|
+
if (catchParamName.startsWith("_")) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const tokens = context.sourceCode.getTokens(node.body);
|
|
246
|
+
const hasUsage = tokens.some(
|
|
247
|
+
(token) => token.type === "Identifier" && token.value === catchParamName
|
|
248
|
+
);
|
|
249
|
+
if (!hasUsage) {
|
|
250
|
+
context.report({
|
|
251
|
+
node: node.param,
|
|
252
|
+
messageId: "unusedCatchParam",
|
|
253
|
+
data: { name: catchParamName }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// src/rules/async/no-async-array-callback.ts
|
|
262
|
+
var import_utils6 = require("@typescript-eslint/utils");
|
|
263
|
+
|
|
264
|
+
// src/utils/async-scope.ts
|
|
265
|
+
var import_utils5 = require("@typescript-eslint/utils");
|
|
266
|
+
function isAstNode(value) {
|
|
267
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
268
|
+
}
|
|
269
|
+
var FUNCTION_SCOPE_BOUNDARY_TYPES = /* @__PURE__ */ new Set([
|
|
270
|
+
import_utils5.AST_NODE_TYPES.FunctionDeclaration,
|
|
271
|
+
import_utils5.AST_NODE_TYPES.FunctionExpression,
|
|
272
|
+
import_utils5.AST_NODE_TYPES.ArrowFunctionExpression
|
|
273
|
+
]);
|
|
274
|
+
function nodeHasCatchClause(node) {
|
|
275
|
+
if (node.type === import_utils5.AST_NODE_TYPES.TryStatement && !!node.handler) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
for (const [key, value] of Object.entries(node)) {
|
|
279
|
+
if (key === "parent") continue;
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
for (const item of value) {
|
|
282
|
+
if (isAstNode(item) && !FUNCTION_SCOPE_BOUNDARY_TYPES.has(item.type) && nodeHasCatchClause(item)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (isAstNode(value) && !FUNCTION_SCOPE_BOUNDARY_TYPES.has(value.type) && nodeHasCatchClause(value)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
function isAsyncFunctionLike(node) {
|
|
295
|
+
if (!node) return false;
|
|
296
|
+
if (node.type === import_utils5.AST_NODE_TYPES.FunctionDeclaration) {
|
|
297
|
+
return node.async;
|
|
298
|
+
}
|
|
299
|
+
if (node.type === import_utils5.AST_NODE_TYPES.FunctionExpression || node.type === import_utils5.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
300
|
+
return node.async;
|
|
301
|
+
}
|
|
302
|
+
if (node.type === import_utils5.AST_NODE_TYPES.VariableDeclarator && node.init && (node.init.type === import_utils5.AST_NODE_TYPES.FunctionExpression || node.init.type === import_utils5.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
303
|
+
return node.init.async;
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
function getAsyncBindingInfo(node) {
|
|
308
|
+
if (!node) {
|
|
309
|
+
return { isAsync: false, hasInternalErrorHandling: false };
|
|
310
|
+
}
|
|
311
|
+
if (node.type === import_utils5.AST_NODE_TYPES.FunctionDeclaration) {
|
|
312
|
+
return {
|
|
313
|
+
isAsync: node.async,
|
|
314
|
+
hasInternalErrorHandling: node.body ? nodeHasCatchClause(node.body) : false
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (node.type === import_utils5.AST_NODE_TYPES.FunctionExpression || node.type === import_utils5.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
318
|
+
return {
|
|
319
|
+
isAsync: node.async,
|
|
320
|
+
hasInternalErrorHandling: node.body.type === import_utils5.AST_NODE_TYPES.BlockStatement ? nodeHasCatchClause(node.body) : false
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (node.type === import_utils5.AST_NODE_TYPES.VariableDeclarator && node.init && (node.init.type === import_utils5.AST_NODE_TYPES.FunctionExpression || node.init.type === import_utils5.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
324
|
+
return {
|
|
325
|
+
isAsync: node.init.async,
|
|
326
|
+
hasInternalErrorHandling: node.init.body.type === import_utils5.AST_NODE_TYPES.BlockStatement ? nodeHasCatchClause(node.init.body) : false
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return { isAsync: false, hasInternalErrorHandling: false };
|
|
330
|
+
}
|
|
331
|
+
function findVariableInScope(scope, name) {
|
|
332
|
+
const fromMap = scope.set?.get(name);
|
|
333
|
+
if (fromMap) {
|
|
334
|
+
return fromMap;
|
|
335
|
+
}
|
|
336
|
+
if (scope.variables) {
|
|
337
|
+
const fromArray = scope.variables.find((v) => v.name === name);
|
|
338
|
+
if (fromArray) {
|
|
339
|
+
return fromArray;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function isIdentifierBoundToAsyncFunction(identifier, context) {
|
|
345
|
+
let scope = context.sourceCode.getScope(identifier);
|
|
346
|
+
while (scope) {
|
|
347
|
+
const variable = findVariableInScope(scope, identifier.name);
|
|
348
|
+
if (variable) {
|
|
349
|
+
const defs = variable.defs ?? [];
|
|
350
|
+
for (const def of defs) {
|
|
351
|
+
const info = getAsyncBindingInfo(def.node);
|
|
352
|
+
if (info.isAsync) {
|
|
353
|
+
return info;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (defs.length === 0 && isAsyncFunctionLike(variable)) {
|
|
357
|
+
return getAsyncBindingInfo(variable);
|
|
358
|
+
}
|
|
359
|
+
return { isAsync: false, hasInternalErrorHandling: false };
|
|
360
|
+
}
|
|
361
|
+
scope = scope.upper;
|
|
362
|
+
}
|
|
363
|
+
return { isAsync: false, hasInternalErrorHandling: false };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/rules/async/no-async-array-callback.ts
|
|
367
|
+
var createRule5 = import_utils6.ESLintUtils.RuleCreator(
|
|
368
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
369
|
+
);
|
|
370
|
+
var ARRAY_CALLBACK_METHODS = ["map", "filter", "forEach", "reduce", "flatMap", "find", "findIndex", "some", "every"];
|
|
371
|
+
var PROMISE_COMBINATORS = ["all", "allSettled", "race", "any"];
|
|
372
|
+
var METHODS_ALLOWING_PROMISE_COLLECTION = /* @__PURE__ */ new Set(["map", "flatMap"]);
|
|
373
|
+
function isNode(value) {
|
|
374
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
375
|
+
}
|
|
376
|
+
function getChildNodes(node) {
|
|
377
|
+
const children = [];
|
|
378
|
+
for (const [key, value] of Object.entries(node)) {
|
|
379
|
+
if (key === "parent") continue;
|
|
380
|
+
if (!value) continue;
|
|
381
|
+
if (Array.isArray(value)) {
|
|
382
|
+
for (const item of value) {
|
|
383
|
+
if (isNode(item)) {
|
|
384
|
+
children.push(item);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (isNode(value)) {
|
|
390
|
+
children.push(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return children;
|
|
394
|
+
}
|
|
395
|
+
function isPromiseCombinatorCall(node) {
|
|
396
|
+
return node.callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils6.AST_NODE_TYPES.Identifier && node.callee.object.name === "Promise" && node.callee.property.type === import_utils6.AST_NODE_TYPES.Identifier && PROMISE_COMBINATORS.includes(
|
|
397
|
+
node.callee.property.name
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
function isWrappedInPromiseCombinator(node, context) {
|
|
401
|
+
const ancestors = context.sourceCode.getAncestors(node);
|
|
402
|
+
const directParent = node.parent;
|
|
403
|
+
if (directParent && directParent.type === import_utils6.AST_NODE_TYPES.CallExpression && isPromiseCombinatorCall(directParent)) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
407
|
+
const ancestor = ancestors[i];
|
|
408
|
+
if (ancestor.type === import_utils6.AST_NODE_TYPES.CallExpression && isPromiseCombinatorCall(ancestor)) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (ancestor.type === import_utils6.AST_NODE_TYPES.CallExpression) {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
function isIdentifierConsumedByPromiseCombinator(node, identifierName) {
|
|
418
|
+
let found = false;
|
|
419
|
+
const visit = (current) => {
|
|
420
|
+
if (found) return;
|
|
421
|
+
if (current.type === import_utils6.AST_NODE_TYPES.CallExpression && isPromiseCombinatorCall(current)) {
|
|
422
|
+
for (const arg of current.arguments) {
|
|
423
|
+
if (arg.type === import_utils6.AST_NODE_TYPES.Identifier && arg.name === identifierName) {
|
|
424
|
+
found = true;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
for (const child of getChildNodes(current)) {
|
|
430
|
+
visit(child);
|
|
431
|
+
if (found) return;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
visit(node);
|
|
435
|
+
return found;
|
|
436
|
+
}
|
|
437
|
+
function isAssignedAndConsumedByPromiseCombinator(node) {
|
|
438
|
+
if (!node.parent || node.parent.type !== import_utils6.AST_NODE_TYPES.VariableDeclarator || node.parent.id.type !== import_utils6.AST_NODE_TYPES.Identifier) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
const variableName = node.parent.id.name;
|
|
442
|
+
const declaration = node.parent.parent;
|
|
443
|
+
if (!declaration || declaration.type !== import_utils6.AST_NODE_TYPES.VariableDeclaration) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
let container = declaration.parent;
|
|
447
|
+
let anchor = declaration;
|
|
448
|
+
if (container && container.type === import_utils6.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
449
|
+
anchor = container;
|
|
450
|
+
container = container.parent;
|
|
451
|
+
}
|
|
452
|
+
if (!container || container.type !== import_utils6.AST_NODE_TYPES.Program && container.type !== import_utils6.AST_NODE_TYPES.BlockStatement) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const body = container.body;
|
|
456
|
+
const declarationIndex = body.findIndex((statement) => statement === anchor);
|
|
457
|
+
if (declarationIndex === -1) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
for (let i = declarationIndex + 1; i < body.length; i++) {
|
|
461
|
+
const statement = body[i];
|
|
462
|
+
if (isIdentifierConsumedByPromiseCombinator(statement, variableName)) {
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
function isAsyncCallback(node, context) {
|
|
469
|
+
if (node.type === import_utils6.AST_NODE_TYPES.Identifier) {
|
|
470
|
+
return isIdentifierBoundToAsyncFunction(node, context).isAsync;
|
|
471
|
+
}
|
|
472
|
+
return (node.type === import_utils6.AST_NODE_TYPES.FunctionExpression || node.type === import_utils6.AST_NODE_TYPES.ArrowFunctionExpression) && node.async;
|
|
473
|
+
}
|
|
474
|
+
var noAsyncArrayCallback = createRule5({
|
|
475
|
+
name: "no-async-array-callback",
|
|
476
|
+
meta: {
|
|
477
|
+
type: "problem",
|
|
478
|
+
docs: {
|
|
479
|
+
description: "Disallow async callbacks in array iteration methods (map, filter, forEach, reduce). AI tools generate `array.map(async ...)` which returns `Promise[]` instead of resolved values \u2014 a silent bug."
|
|
480
|
+
},
|
|
481
|
+
fixable: void 0,
|
|
482
|
+
schema: [],
|
|
483
|
+
messages: {
|
|
484
|
+
asyncArrayCallback: "Async callback passed to Array.{{method}}(). This returns an array of Promises, not resolved values. AI tools frequently generate this pattern. Wrap with `await Promise.all(array.{{method}}(...))` or use a for...of loop."
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
defaultOptions: [],
|
|
488
|
+
create(context) {
|
|
489
|
+
return {
|
|
490
|
+
CallExpression(node) {
|
|
491
|
+
if (node.callee.type !== import_utils6.AST_NODE_TYPES.MemberExpression || node.callee.property.type !== import_utils6.AST_NODE_TYPES.Identifier) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const methodName = node.callee.property.name;
|
|
495
|
+
if (!ARRAY_CALLBACK_METHODS.includes(
|
|
496
|
+
methodName
|
|
497
|
+
)) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const callback = node.arguments[0];
|
|
501
|
+
if (!callback) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (isAsyncCallback(callback, context)) {
|
|
505
|
+
if (isWrappedInPromiseCombinator(node, context)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (METHODS_ALLOWING_PROMISE_COLLECTION.has(methodName) && isAssignedAndConsumedByPromiseCombinator(node)) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
context.report({
|
|
512
|
+
node: callback,
|
|
513
|
+
messageId: "asyncArrayCallback",
|
|
514
|
+
data: {
|
|
515
|
+
method: methodName
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// src/rules/async/no-floating-promise.ts
|
|
525
|
+
var import_utils7 = require("@typescript-eslint/utils");
|
|
526
|
+
var createRule6 = import_utils7.ESLintUtils.RuleCreator(
|
|
527
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
528
|
+
);
|
|
529
|
+
var KNOWN_PROMISE_FACTORIES = /* @__PURE__ */ new Set([
|
|
530
|
+
"fetch"
|
|
531
|
+
]);
|
|
532
|
+
var PROMISE_STATIC_METHODS = /* @__PURE__ */ new Set([
|
|
533
|
+
"resolve",
|
|
534
|
+
"reject",
|
|
535
|
+
"all",
|
|
536
|
+
"allSettled",
|
|
537
|
+
"race",
|
|
538
|
+
"any"
|
|
539
|
+
]);
|
|
540
|
+
function isLocallyAsyncCallee(node, context) {
|
|
541
|
+
if (node.callee.type === import_utils7.AST_NODE_TYPES.Identifier) {
|
|
542
|
+
const info = isIdentifierBoundToAsyncFunction(node.callee, context);
|
|
543
|
+
if (info.isAsync) {
|
|
544
|
+
return info;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if ((node.callee.type === import_utils7.AST_NODE_TYPES.FunctionExpression || node.callee.type === import_utils7.AST_NODE_TYPES.ArrowFunctionExpression) && node.callee.async) {
|
|
548
|
+
return {
|
|
549
|
+
isAsync: true,
|
|
550
|
+
hasInternalErrorHandling: node.callee.body.type === import_utils7.AST_NODE_TYPES.BlockStatement ? nodeHasCatchClause(node.callee.body) : false
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { isAsync: false, hasInternalErrorHandling: false };
|
|
554
|
+
}
|
|
555
|
+
function isKnownPromiseFactoryCall(node) {
|
|
556
|
+
if (node.callee.type === import_utils7.AST_NODE_TYPES.Identifier) {
|
|
557
|
+
return KNOWN_PROMISE_FACTORIES.has(node.callee.name);
|
|
558
|
+
}
|
|
559
|
+
if (node.callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils7.AST_NODE_TYPES.Identifier && node.callee.object.name === "Promise" && node.callee.property.type === import_utils7.AST_NODE_TYPES.Identifier) {
|
|
560
|
+
return PROMISE_STATIC_METHODS.has(node.callee.property.name);
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
function getParserServices(context) {
|
|
565
|
+
const services = context.sourceCode.parserServices;
|
|
566
|
+
if (!services || typeof services !== "object") {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
return services;
|
|
570
|
+
}
|
|
571
|
+
function isPromiseLikeByTypeInfo(node, context) {
|
|
572
|
+
const services = getParserServices(context);
|
|
573
|
+
if (!services?.program || !services.esTreeNodeToTSNodeMap) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
578
|
+
if (!tsNode) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const checker = services.program.getTypeChecker();
|
|
582
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
583
|
+
if (typeof checker.getPromisedTypeOfPromise === "function") {
|
|
584
|
+
if (checker.getPromisedTypeOfPromise(type)) {
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const text = checker.typeToString(type).toLowerCase();
|
|
589
|
+
return text === "promise" || text.includes("promise<") || text.includes("promiselike<") || text.includes("thenable");
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function getCallExpression(expression) {
|
|
595
|
+
if (expression.type === import_utils7.AST_NODE_TYPES.CallExpression) {
|
|
596
|
+
return expression;
|
|
597
|
+
}
|
|
598
|
+
if (expression.type === import_utils7.AST_NODE_TYPES.ChainExpression && expression.expression.type === import_utils7.AST_NODE_TYPES.CallExpression) {
|
|
599
|
+
return expression.expression;
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
function isExpressionHandled(node) {
|
|
604
|
+
const expr = node.expression;
|
|
605
|
+
const callExpr = getCallExpression(expr);
|
|
606
|
+
if (callExpr) {
|
|
607
|
+
if (callExpr.callee.type === import_utils7.AST_NODE_TYPES.MemberExpression) {
|
|
608
|
+
const prop = callExpr.callee.property;
|
|
609
|
+
if (prop.type === import_utils7.AST_NODE_TYPES.Identifier) {
|
|
610
|
+
if (prop.name === "then" || prop.name === "catch" || prop.name === "finally") {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (expr.type === import_utils7.AST_NODE_TYPES.UnaryExpression && expr.operator === "void") {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
var noFloatingPromise = createRule6({
|
|
622
|
+
name: "no-floating-promise",
|
|
623
|
+
meta: {
|
|
624
|
+
type: "problem",
|
|
625
|
+
docs: {
|
|
626
|
+
description: "Disallow calling an async function or Promise-returning function without awaiting or handling the result. AI tools frequently generate floating promises where errors disappear silently."
|
|
627
|
+
},
|
|
628
|
+
fixable: void 0,
|
|
629
|
+
schema: [],
|
|
630
|
+
messages: {
|
|
631
|
+
floatingPromise: "This async call is not awaited, returned, or error-handled (.catch). AI tools frequently generate floating promises, causing errors to be silently lost. Add `await`, `.catch()`, or assign the result."
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
defaultOptions: [],
|
|
635
|
+
create(context) {
|
|
636
|
+
return {
|
|
637
|
+
// Check ExpressionStatements — standalone call expressions
|
|
638
|
+
ExpressionStatement(node) {
|
|
639
|
+
if (node.expression.type === import_utils7.AST_NODE_TYPES.NewExpression && node.expression.callee.type === import_utils7.AST_NODE_TYPES.Identifier && node.expression.callee.name === "Promise") {
|
|
640
|
+
context.report({
|
|
641
|
+
node,
|
|
642
|
+
messageId: "floatingPromise"
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (isExpressionHandled(node)) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const callExpr = getCallExpression(node.expression);
|
|
650
|
+
if (!callExpr) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const localAsyncInfo = isLocallyAsyncCallee(callExpr, context);
|
|
654
|
+
if (localAsyncInfo.isAsync && localAsyncInfo.hasInternalErrorHandling) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (localAsyncInfo.isAsync || isKnownPromiseFactoryCall(callExpr) || isPromiseLikeByTypeInfo(callExpr, context)) {
|
|
658
|
+
context.report({
|
|
659
|
+
node,
|
|
660
|
+
messageId: "floatingPromise"
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// src/rules/async/no-await-in-loop.ts
|
|
669
|
+
var import_utils8 = require("@typescript-eslint/utils");
|
|
670
|
+
var createRule7 = import_utils8.ESLintUtils.RuleCreator(
|
|
671
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
672
|
+
);
|
|
673
|
+
var LOOP_TYPES = /* @__PURE__ */ new Set([
|
|
674
|
+
import_utils8.AST_NODE_TYPES.ForStatement,
|
|
675
|
+
import_utils8.AST_NODE_TYPES.ForInStatement,
|
|
676
|
+
import_utils8.AST_NODE_TYPES.ForOfStatement,
|
|
677
|
+
import_utils8.AST_NODE_TYPES.WhileStatement,
|
|
678
|
+
import_utils8.AST_NODE_TYPES.DoWhileStatement
|
|
679
|
+
]);
|
|
680
|
+
var SUPPRESSION_REGEX = /ai-guard-disable\s+no-await-in-loop\b/i;
|
|
681
|
+
var RETRY_NAME_REGEX = /(retry|retries|attempt|attempts|fallback|tryagain|recovery)/i;
|
|
682
|
+
var SEQUENTIAL_DEPENDENCY_NAME_REGEX = /(previous|prev|last|carry|accumulator|stateful)/i;
|
|
683
|
+
var ERROR_CODE_HINTS = [
|
|
684
|
+
"access-denied",
|
|
685
|
+
"timeout",
|
|
686
|
+
"rate-limit",
|
|
687
|
+
"not-found"
|
|
688
|
+
];
|
|
689
|
+
var CONTROL_AWAIT_HINTS = [
|
|
690
|
+
"sleep",
|
|
691
|
+
"delay",
|
|
692
|
+
"wait",
|
|
693
|
+
"throttle",
|
|
694
|
+
"ratelimit",
|
|
695
|
+
"backoff"
|
|
696
|
+
];
|
|
697
|
+
var MUTATION_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
698
|
+
"push",
|
|
699
|
+
"pop",
|
|
700
|
+
"shift",
|
|
701
|
+
"unshift",
|
|
702
|
+
"splice",
|
|
703
|
+
"set",
|
|
704
|
+
"add",
|
|
705
|
+
"delete",
|
|
706
|
+
"clear"
|
|
707
|
+
]);
|
|
708
|
+
function isFunctionNode(node) {
|
|
709
|
+
return node.type === import_utils8.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils8.AST_NODE_TYPES.FunctionExpression || node.type === import_utils8.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
710
|
+
}
|
|
711
|
+
function isNodeLike(value) {
|
|
712
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
713
|
+
}
|
|
714
|
+
function walkNode(node, visitor, skipNestedFunctions, isRoot = true) {
|
|
715
|
+
visitor(node);
|
|
716
|
+
for (const [key, value] of Object.entries(node)) {
|
|
717
|
+
if (key === "parent" || !value) continue;
|
|
718
|
+
if (Array.isArray(value)) {
|
|
719
|
+
for (const item of value) {
|
|
720
|
+
if (!isNodeLike(item)) continue;
|
|
721
|
+
if (skipNestedFunctions && !isRoot && isFunctionNode(item)) continue;
|
|
722
|
+
walkNode(item, visitor, skipNestedFunctions, false);
|
|
723
|
+
}
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (!isNodeLike(value)) continue;
|
|
727
|
+
if (skipNestedFunctions && !isRoot && isFunctionNode(value)) continue;
|
|
728
|
+
walkNode(value, visitor, skipNestedFunctions, false);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function collectPatternNames(pattern, bucket) {
|
|
732
|
+
if (pattern.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
733
|
+
bucket.add(pattern.name);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (pattern.type === import_utils8.AST_NODE_TYPES.AssignmentPattern) {
|
|
737
|
+
collectPatternNames(pattern.left, bucket);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (pattern.type === import_utils8.AST_NODE_TYPES.RestElement) {
|
|
741
|
+
collectPatternNames(pattern.argument, bucket);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (pattern.type === import_utils8.AST_NODE_TYPES.ArrayPattern) {
|
|
745
|
+
for (const element of pattern.elements) {
|
|
746
|
+
if (!element) continue;
|
|
747
|
+
if (element.type === import_utils8.AST_NODE_TYPES.RestElement) {
|
|
748
|
+
collectPatternNames(element.argument, bucket);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
collectPatternNames(element, bucket);
|
|
752
|
+
}
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (pattern.type === import_utils8.AST_NODE_TYPES.ObjectPattern) {
|
|
756
|
+
for (const prop of pattern.properties) {
|
|
757
|
+
if (prop.type === import_utils8.AST_NODE_TYPES.RestElement) {
|
|
758
|
+
collectPatternNames(prop.argument, bucket);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
collectPatternNames(prop.value, bucket);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function getLoopNodeName(type) {
|
|
766
|
+
switch (type) {
|
|
767
|
+
case import_utils8.AST_NODE_TYPES.ForStatement:
|
|
768
|
+
return "for loop";
|
|
769
|
+
case import_utils8.AST_NODE_TYPES.ForInStatement:
|
|
770
|
+
return "for...in loop";
|
|
771
|
+
case import_utils8.AST_NODE_TYPES.ForOfStatement:
|
|
772
|
+
return "for...of loop";
|
|
773
|
+
case import_utils8.AST_NODE_TYPES.WhileStatement:
|
|
774
|
+
return "while loop";
|
|
775
|
+
case import_utils8.AST_NODE_TYPES.DoWhileStatement:
|
|
776
|
+
return "do...while loop";
|
|
777
|
+
default:
|
|
778
|
+
return "loop";
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function hasSuppressionComment(comments) {
|
|
782
|
+
return comments.some((comment) => SUPPRESSION_REGEX.test(comment.value));
|
|
783
|
+
}
|
|
784
|
+
function hasFileSuppression(sourceCode) {
|
|
785
|
+
return hasSuppressionComment(sourceCode.getAllComments());
|
|
786
|
+
}
|
|
787
|
+
function hasLoopSuppression(loopNode, sourceCode) {
|
|
788
|
+
const commentsBefore = sourceCode.getCommentsBefore(loopNode);
|
|
789
|
+
if (hasSuppressionComment(commentsBefore)) {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
const allComments = sourceCode.getAllComments();
|
|
793
|
+
const nearComments = allComments.filter((comment) => {
|
|
794
|
+
if (!comment.range || !loopNode.range) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
const [, commentEnd] = comment.range;
|
|
798
|
+
const [loopStart] = loopNode.range;
|
|
799
|
+
return commentEnd <= loopStart && loopStart - commentEnd < 160;
|
|
800
|
+
});
|
|
801
|
+
return hasSuppressionComment(nearComments);
|
|
802
|
+
}
|
|
803
|
+
function getCalleeName(callee) {
|
|
804
|
+
if (callee.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
805
|
+
return callee.name;
|
|
806
|
+
}
|
|
807
|
+
if (callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
808
|
+
return callee.property.name;
|
|
809
|
+
}
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
function getRootIdentifierName(node) {
|
|
813
|
+
if (node.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
814
|
+
return node.name;
|
|
815
|
+
}
|
|
816
|
+
if (node.type === import_utils8.AST_NODE_TYPES.MemberExpression) {
|
|
817
|
+
return getRootIdentifierName(node.object);
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function collectLocalBindings(loopNode) {
|
|
822
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
823
|
+
if (loopNode.type === import_utils8.AST_NODE_TYPES.ForStatement && loopNode.init?.type === import_utils8.AST_NODE_TYPES.VariableDeclaration) {
|
|
824
|
+
for (const decl of loopNode.init.declarations) {
|
|
825
|
+
collectPatternNames(decl.id, bindings);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if ((loopNode.type === import_utils8.AST_NODE_TYPES.ForOfStatement || loopNode.type === import_utils8.AST_NODE_TYPES.ForInStatement) && loopNode.left.type === import_utils8.AST_NODE_TYPES.VariableDeclaration) {
|
|
829
|
+
for (const decl of loopNode.left.declarations) {
|
|
830
|
+
collectPatternNames(decl.id, bindings);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
walkNode(
|
|
834
|
+
loopNode.body,
|
|
835
|
+
(node) => {
|
|
836
|
+
if (node.type === import_utils8.AST_NODE_TYPES.VariableDeclarator) {
|
|
837
|
+
collectPatternNames(node.id, bindings);
|
|
838
|
+
}
|
|
839
|
+
if (node.type === import_utils8.AST_NODE_TYPES.FunctionDeclaration && node.id) {
|
|
840
|
+
bindings.add(node.id.name);
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
true
|
|
844
|
+
);
|
|
845
|
+
return bindings;
|
|
846
|
+
}
|
|
847
|
+
function collectSiblingStatements(loopNode) {
|
|
848
|
+
if (!loopNode.parent || loopNode.parent.type !== import_utils8.AST_NODE_TYPES.BlockStatement) {
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
const siblings = loopNode.parent.body;
|
|
852
|
+
const idx = siblings.findIndex((stmt) => stmt === loopNode);
|
|
853
|
+
if (idx === -1) return [];
|
|
854
|
+
const result = [];
|
|
855
|
+
if (idx > 0) result.push(siblings[idx - 1]);
|
|
856
|
+
if (idx < siblings.length - 1) result.push(siblings[idx + 1]);
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
function nodeContainsErrorCodeHint(node) {
|
|
860
|
+
let found = false;
|
|
861
|
+
walkNode(
|
|
862
|
+
node,
|
|
863
|
+
(current) => {
|
|
864
|
+
if (found) return;
|
|
865
|
+
if (current.type !== import_utils8.AST_NODE_TYPES.Literal || typeof current.value !== "string") {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const lower = current.value.toLowerCase();
|
|
869
|
+
if (ERROR_CODE_HINTS.some((hint) => lower.includes(hint))) {
|
|
870
|
+
found = true;
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
true
|
|
874
|
+
);
|
|
875
|
+
return found;
|
|
876
|
+
}
|
|
877
|
+
function analyzeIntent(loopNode) {
|
|
878
|
+
if (loopNode.type === import_utils8.AST_NODE_TYPES.ForOfStatement && loopNode.await) {
|
|
879
|
+
return { isIndependent: false };
|
|
880
|
+
}
|
|
881
|
+
const localBindings = collectLocalBindings(loopNode);
|
|
882
|
+
const siblingStatements = collectSiblingStatements(loopNode);
|
|
883
|
+
let hasRetryNameHint = false;
|
|
884
|
+
let hasCounterIncrement = false;
|
|
885
|
+
let hasEarlyExit = false;
|
|
886
|
+
let hasCatchContinue = false;
|
|
887
|
+
let hasErrorCodeHint = false;
|
|
888
|
+
let hasSequentialDependency = false;
|
|
889
|
+
let hasControlAwait = false;
|
|
890
|
+
const checkIdentifierName = (name) => {
|
|
891
|
+
if (RETRY_NAME_REGEX.test(name)) {
|
|
892
|
+
hasRetryNameHint = true;
|
|
893
|
+
}
|
|
894
|
+
if (SEQUENTIAL_DEPENDENCY_NAME_REGEX.test(name)) {
|
|
895
|
+
hasSequentialDependency = true;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
walkNode(
|
|
899
|
+
loopNode,
|
|
900
|
+
(node) => {
|
|
901
|
+
if (node.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
902
|
+
checkIdentifierName(node.name);
|
|
903
|
+
}
|
|
904
|
+
if (node.type === import_utils8.AST_NODE_TYPES.AwaitExpression && node.argument.type === import_utils8.AST_NODE_TYPES.CallExpression) {
|
|
905
|
+
const calleeName = getCalleeName(node.argument.callee);
|
|
906
|
+
if (calleeName) {
|
|
907
|
+
const lower = calleeName.toLowerCase();
|
|
908
|
+
if (CONTROL_AWAIT_HINTS.some((hint) => lower.includes(hint))) {
|
|
909
|
+
hasControlAwait = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (node.type === import_utils8.AST_NODE_TYPES.UpdateExpression) {
|
|
914
|
+
const target = node.argument;
|
|
915
|
+
if (target.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
916
|
+
checkIdentifierName(target.name);
|
|
917
|
+
if (RETRY_NAME_REGEX.test(target.name)) {
|
|
918
|
+
hasCounterIncrement = true;
|
|
919
|
+
}
|
|
920
|
+
if (!localBindings.has(target.name)) {
|
|
921
|
+
hasSequentialDependency = true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (node.type === import_utils8.AST_NODE_TYPES.AssignmentExpression) {
|
|
926
|
+
if (node.left.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
927
|
+
checkIdentifierName(node.left.name);
|
|
928
|
+
if (RETRY_NAME_REGEX.test(node.left.name)) {
|
|
929
|
+
hasCounterIncrement = true;
|
|
930
|
+
}
|
|
931
|
+
if (!localBindings.has(node.left.name)) {
|
|
932
|
+
hasSequentialDependency = true;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (node.left.type === import_utils8.AST_NODE_TYPES.MemberExpression) {
|
|
936
|
+
const root = getRootIdentifierName(node.left.object);
|
|
937
|
+
if (root && !localBindings.has(root)) {
|
|
938
|
+
hasSequentialDependency = true;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (node.type === import_utils8.AST_NODE_TYPES.ReturnStatement || node.type === import_utils8.AST_NODE_TYPES.BreakStatement || node.type === import_utils8.AST_NODE_TYPES.ContinueStatement) {
|
|
943
|
+
hasEarlyExit = true;
|
|
944
|
+
}
|
|
945
|
+
if (node.type === import_utils8.AST_NODE_TYPES.TryStatement && node.handler) {
|
|
946
|
+
let catchHasContinue = false;
|
|
947
|
+
walkNode(
|
|
948
|
+
node.handler.body,
|
|
949
|
+
(catchNode) => {
|
|
950
|
+
if (catchNode.type === import_utils8.AST_NODE_TYPES.ContinueStatement) {
|
|
951
|
+
catchHasContinue = true;
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
true
|
|
955
|
+
);
|
|
956
|
+
if (catchHasContinue) {
|
|
957
|
+
hasCatchContinue = true;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (node.type === import_utils8.AST_NODE_TYPES.CallExpression) {
|
|
961
|
+
if (node.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
962
|
+
const method = node.callee.property.name.toLowerCase();
|
|
963
|
+
if (MUTATION_METHOD_NAMES.has(method)) {
|
|
964
|
+
const root = getRootIdentifierName(node.callee.object);
|
|
965
|
+
if (root && !localBindings.has(root)) {
|
|
966
|
+
hasSequentialDependency = true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (node.type === import_utils8.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
972
|
+
const lower = node.value.toLowerCase();
|
|
973
|
+
if (ERROR_CODE_HINTS.some((hint) => lower.includes(hint))) {
|
|
974
|
+
hasErrorCodeHint = true;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
true
|
|
979
|
+
);
|
|
980
|
+
if (!hasRetryNameHint && siblingStatements.length > 0) {
|
|
981
|
+
for (const sibling of siblingStatements) {
|
|
982
|
+
if (nodeContainsErrorCodeHint(sibling)) {
|
|
983
|
+
hasErrorCodeHint = true;
|
|
984
|
+
}
|
|
985
|
+
walkNode(
|
|
986
|
+
sibling,
|
|
987
|
+
(node) => {
|
|
988
|
+
if (node.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
989
|
+
checkIdentifierName(node.name);
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
true
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
const hasRetryOrFallbackIntent = hasRetryNameHint || hasCounterIncrement || hasEarlyExit || hasCatchContinue || hasErrorCodeHint || hasSequentialDependency || hasControlAwait;
|
|
997
|
+
return {
|
|
998
|
+
isIndependent: !hasRetryOrFallbackIntent
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function getLoopBodyStatements(loopNode) {
|
|
1002
|
+
return loopNode.body.type === import_utils8.AST_NODE_TYPES.BlockStatement ? loopNode.body.body : [loopNode.body];
|
|
1003
|
+
}
|
|
1004
|
+
function getForOfParamName(loopNode) {
|
|
1005
|
+
if (loopNode.left.type === import_utils8.AST_NODE_TYPES.Identifier) {
|
|
1006
|
+
return loopNode.left.name;
|
|
1007
|
+
}
|
|
1008
|
+
if (loopNode.left.type === import_utils8.AST_NODE_TYPES.VariableDeclaration) {
|
|
1009
|
+
if (loopNode.left.declarations.length !== 1) return null;
|
|
1010
|
+
const id = loopNode.left.declarations[0].id;
|
|
1011
|
+
if (id.type !== import_utils8.AST_NODE_TYPES.Identifier) return null;
|
|
1012
|
+
return id.name;
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
function buildSafeAutofix(loopNode, awaitNode, sourceCode) {
|
|
1017
|
+
if (!loopNode.parent || loopNode.parent.type !== import_utils8.AST_NODE_TYPES.BlockStatement || !loopNode.parent.parent || !isFunctionNode(loopNode.parent.parent)) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
if (loopNode.type !== import_utils8.AST_NODE_TYPES.ForOfStatement || loopNode.await) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
const loopStatements = getLoopBodyStatements(loopNode);
|
|
1024
|
+
if (loopStatements.length !== 1) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
const onlyStatement = loopStatements[0];
|
|
1028
|
+
if (onlyStatement.type !== import_utils8.AST_NODE_TYPES.ExpressionStatement || onlyStatement.expression.type !== import_utils8.AST_NODE_TYPES.AwaitExpression || onlyStatement.expression !== awaitNode || onlyStatement.expression.argument.type !== import_utils8.AST_NODE_TYPES.CallExpression) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
const paramName = getForOfParamName(loopNode);
|
|
1032
|
+
if (!paramName) {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
const iterableText = sourceCode.getText(loopNode.right);
|
|
1036
|
+
const awaitedCallText = sourceCode.getText(onlyStatement.expression.argument);
|
|
1037
|
+
return `const results = await Promise.all(${iterableText}.map(async (${paramName}) => await ${awaitedCallText}));`;
|
|
1038
|
+
}
|
|
1039
|
+
var noAwaitInLoop = createRule7({
|
|
1040
|
+
name: "no-await-in-loop",
|
|
1041
|
+
meta: {
|
|
1042
|
+
type: "suggestion",
|
|
1043
|
+
deprecated: true,
|
|
1044
|
+
replacedBy: ["no-await-in-loop"],
|
|
1045
|
+
docs: {
|
|
1046
|
+
description: "[DEPRECATED \u2014 use ESLint core `no-await-in-loop`] Disallow independent `await` usage inside loops. Kept for backwards-compatibility in v2.x; removed in v3.0."
|
|
1047
|
+
},
|
|
1048
|
+
fixable: "code",
|
|
1049
|
+
schema: [],
|
|
1050
|
+
messages: {
|
|
1051
|
+
awaitInLoop: "[ai-guard deprecated \u2014 use ESLint core `no-await-in-loop`] Unexpected `await` inside a {{loopType}}. AI tools frequently generate sequential awaits in loops, causing O(n) latency. Consider `Promise.all()` for parallel execution."
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
defaultOptions: [],
|
|
1055
|
+
create(context) {
|
|
1056
|
+
if (hasFileSuppression(context.sourceCode)) {
|
|
1057
|
+
return {};
|
|
1058
|
+
}
|
|
1059
|
+
const loopIntentCache = /* @__PURE__ */ new WeakMap();
|
|
1060
|
+
const reportedLoops = /* @__PURE__ */ new WeakSet();
|
|
1061
|
+
const functionBoundary = [];
|
|
1062
|
+
function enterFunction(node) {
|
|
1063
|
+
functionBoundary.push(node);
|
|
1064
|
+
}
|
|
1065
|
+
function exitFunction() {
|
|
1066
|
+
functionBoundary.pop();
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
FunctionDeclaration: enterFunction,
|
|
1070
|
+
FunctionExpression: enterFunction,
|
|
1071
|
+
ArrowFunctionExpression: enterFunction,
|
|
1072
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
1073
|
+
"FunctionExpression:exit": exitFunction,
|
|
1074
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
1075
|
+
AwaitExpression(node) {
|
|
1076
|
+
const ancestors = context.sourceCode.getAncestors(node);
|
|
1077
|
+
const currentFunction = functionBoundary[functionBoundary.length - 1];
|
|
1078
|
+
let enclosingLoop = null;
|
|
1079
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
1080
|
+
const ancestor = ancestors[i];
|
|
1081
|
+
if (ancestor === currentFunction) {
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
if (LOOP_TYPES.has(ancestor.type)) {
|
|
1085
|
+
enclosingLoop = ancestor;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!enclosingLoop) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (reportedLoops.has(enclosingLoop)) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (hasLoopSuppression(enclosingLoop, context.sourceCode)) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const intent = loopIntentCache.get(enclosingLoop) ?? analyzeIntent(enclosingLoop);
|
|
1099
|
+
loopIntentCache.set(enclosingLoop, intent);
|
|
1100
|
+
if (!intent.isIndependent) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const fixText = buildSafeAutofix(enclosingLoop, node, context.sourceCode);
|
|
1104
|
+
const loopName = getLoopNodeName(enclosingLoop.type);
|
|
1105
|
+
context.report({
|
|
1106
|
+
node,
|
|
1107
|
+
messageId: "awaitInLoop",
|
|
1108
|
+
data: { loopType: loopName },
|
|
1109
|
+
fix: fixText === null ? void 0 : (fixer) => fixer.replaceText(enclosingLoop, fixText)
|
|
1110
|
+
});
|
|
1111
|
+
reportedLoops.add(enclosingLoop);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// src/rules/async/no-async-without-await.ts
|
|
1118
|
+
var import_utils9 = require("@typescript-eslint/utils");
|
|
1119
|
+
var createRule8 = import_utils9.ESLintUtils.RuleCreator(
|
|
1120
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1121
|
+
);
|
|
1122
|
+
function containsAwaitExpression(node) {
|
|
1123
|
+
if (node.type === import_utils9.AST_NODE_TYPES.AwaitExpression) {
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
if (node.type === import_utils9.AST_NODE_TYPES.ForOfStatement && node.await) {
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
if (node.type === import_utils9.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils9.AST_NODE_TYPES.FunctionExpression || node.type === import_utils9.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
const entries = Object.entries(node);
|
|
1133
|
+
for (const [key, value] of entries) {
|
|
1134
|
+
if (key === "parent") {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (Array.isArray(value)) {
|
|
1138
|
+
for (const child of value) {
|
|
1139
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
1140
|
+
if (containsAwaitExpression(child)) {
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (value && typeof value === "object" && "type" in value) {
|
|
1148
|
+
if (containsAwaitExpression(value)) {
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
var noAsyncWithoutAwait = createRule8({
|
|
1156
|
+
name: "no-async-without-await",
|
|
1157
|
+
meta: {
|
|
1158
|
+
type: "suggestion",
|
|
1159
|
+
deprecated: true,
|
|
1160
|
+
replacedBy: ["@typescript-eslint/require-await"],
|
|
1161
|
+
docs: {
|
|
1162
|
+
description: "[DEPRECATED \u2014 use `@typescript-eslint/require-await`] Disallow async functions that never use await. Kept for backwards-compatibility in v2.x; removed in v3.0."
|
|
1163
|
+
},
|
|
1164
|
+
fixable: void 0,
|
|
1165
|
+
schema: [],
|
|
1166
|
+
messages: {
|
|
1167
|
+
asyncWithoutAwait: "[ai-guard deprecated \u2014 use `@typescript-eslint/require-await`] Async function does not contain `await`. Remove `async` or add proper await logic."
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
defaultOptions: [],
|
|
1171
|
+
create(context) {
|
|
1172
|
+
function reportIfNeeded(node) {
|
|
1173
|
+
if (!node.async) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (node.body.type !== import_utils9.AST_NODE_TYPES.BlockStatement) {
|
|
1177
|
+
if (node.body.type !== import_utils9.AST_NODE_TYPES.AwaitExpression) {
|
|
1178
|
+
context.report({
|
|
1179
|
+
node,
|
|
1180
|
+
messageId: "asyncWithoutAwait"
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (!containsAwaitExpression(node.body)) {
|
|
1186
|
+
context.report({
|
|
1187
|
+
node,
|
|
1188
|
+
messageId: "asyncWithoutAwait"
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
FunctionDeclaration: reportIfNeeded,
|
|
1194
|
+
FunctionExpression: reportIfNeeded,
|
|
1195
|
+
ArrowFunctionExpression: reportIfNeeded
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// src/rules/async/no-redundant-await.ts
|
|
1201
|
+
var import_utils10 = require("@typescript-eslint/utils");
|
|
1202
|
+
var createRule9 = import_utils10.ESLintUtils.RuleCreator(
|
|
1203
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1204
|
+
);
|
|
1205
|
+
function isInsideTryLikeBlock(returnNode, context) {
|
|
1206
|
+
const ancestors = context.sourceCode.getAncestors(returnNode);
|
|
1207
|
+
for (let i = ancestors.length - 1; i >= 0; i -= 1) {
|
|
1208
|
+
const ancestor = ancestors[i];
|
|
1209
|
+
if (ancestor.type === import_utils10.AST_NODE_TYPES.FunctionDeclaration || ancestor.type === import_utils10.AST_NODE_TYPES.FunctionExpression || ancestor.type === import_utils10.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
if (ancestor.type === import_utils10.AST_NODE_TYPES.TryStatement) {
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
function isInsideAsyncFunction(returnNode, context) {
|
|
1219
|
+
const ancestors = context.sourceCode.getAncestors(returnNode);
|
|
1220
|
+
for (let i = ancestors.length - 1; i >= 0; i -= 1) {
|
|
1221
|
+
const ancestor = ancestors[i];
|
|
1222
|
+
if (ancestor.type === import_utils10.AST_NODE_TYPES.FunctionDeclaration || ancestor.type === import_utils10.AST_NODE_TYPES.FunctionExpression || ancestor.type === import_utils10.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
1223
|
+
return ancestor.async;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
var noRedundantAwait = createRule9({
|
|
1229
|
+
name: "no-redundant-await",
|
|
1230
|
+
meta: {
|
|
1231
|
+
type: "suggestion",
|
|
1232
|
+
deprecated: true,
|
|
1233
|
+
replacedBy: ["@typescript-eslint/return-await"],
|
|
1234
|
+
docs: {
|
|
1235
|
+
description: "[DEPRECATED \u2014 use `@typescript-eslint/return-await`] Disallow `return await` in async functions when not inside try/catch/finally. Kept for backwards-compatibility in v2.x; removed in v3.0."
|
|
1236
|
+
},
|
|
1237
|
+
fixable: void 0,
|
|
1238
|
+
schema: [],
|
|
1239
|
+
messages: {
|
|
1240
|
+
redundantAwait: "[ai-guard deprecated \u2014 use `@typescript-eslint/return-await`] Redundant `return await` detected. Return the Promise directly unless you need await for try/catch behavior."
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
defaultOptions: [],
|
|
1244
|
+
create(context) {
|
|
1245
|
+
return {
|
|
1246
|
+
ReturnStatement(node) {
|
|
1247
|
+
if (!node.argument || node.argument.type !== import_utils10.AST_NODE_TYPES.AwaitExpression) {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (!isInsideAsyncFunction(node, context)) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (isInsideTryLikeBlock(node, context)) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
context.report({
|
|
1257
|
+
node: node.argument,
|
|
1258
|
+
messageId: "redundantAwait"
|
|
1259
|
+
});
|
|
1260
|
+
},
|
|
1261
|
+
ArrowFunctionExpression(node) {
|
|
1262
|
+
if (!node.async || node.body.type !== import_utils10.AST_NODE_TYPES.AwaitExpression) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
context.report({
|
|
1266
|
+
node: node.body,
|
|
1267
|
+
messageId: "redundantAwait"
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// src/rules/security/no-hardcoded-secret.ts
|
|
1275
|
+
var import_utils11 = require("@typescript-eslint/utils");
|
|
1276
|
+
var createRule10 = import_utils11.ESLintUtils.RuleCreator(
|
|
1277
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1278
|
+
);
|
|
1279
|
+
var SECRET_TOKENS = /* @__PURE__ */ new Set([
|
|
1280
|
+
"secret",
|
|
1281
|
+
"password",
|
|
1282
|
+
"passwd",
|
|
1283
|
+
"apikey",
|
|
1284
|
+
"authtoken",
|
|
1285
|
+
"accesstoken",
|
|
1286
|
+
"privatekey",
|
|
1287
|
+
"clientsecret",
|
|
1288
|
+
"jwtsecret",
|
|
1289
|
+
"encryptionkey",
|
|
1290
|
+
"signingkey"
|
|
1291
|
+
]);
|
|
1292
|
+
var SECRET_TOKEN_PAIRS = [
|
|
1293
|
+
["api", "key"],
|
|
1294
|
+
["auth", "token"],
|
|
1295
|
+
["access", "token"],
|
|
1296
|
+
["private", "key"],
|
|
1297
|
+
["client", "secret"],
|
|
1298
|
+
["jwt", "secret"],
|
|
1299
|
+
["encryption", "key"],
|
|
1300
|
+
["signing", "key"]
|
|
1301
|
+
];
|
|
1302
|
+
function tokenizeIdentifier(name) {
|
|
1303
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").split(/[_\-]+/).map((t) => t.toLowerCase()).filter((t) => t.length > 0);
|
|
1304
|
+
}
|
|
1305
|
+
function isSecretName(name) {
|
|
1306
|
+
const tokens = tokenizeIdentifier(name);
|
|
1307
|
+
for (const t of tokens) {
|
|
1308
|
+
if (SECRET_TOKENS.has(t)) return true;
|
|
1309
|
+
}
|
|
1310
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
1311
|
+
for (const [a, b] of SECRET_TOKEN_PAIRS) {
|
|
1312
|
+
if (tokens[i] === a && tokens[i + 1] === b) return true;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
var FALSE_POSITIVE_VALUES = /* @__PURE__ */ new Set([
|
|
1318
|
+
"password",
|
|
1319
|
+
"secret",
|
|
1320
|
+
"token",
|
|
1321
|
+
"key",
|
|
1322
|
+
"api_key",
|
|
1323
|
+
"apikey",
|
|
1324
|
+
"",
|
|
1325
|
+
"undefined",
|
|
1326
|
+
"null",
|
|
1327
|
+
"test",
|
|
1328
|
+
"example",
|
|
1329
|
+
"placeholder",
|
|
1330
|
+
"changeme",
|
|
1331
|
+
"your-api-key",
|
|
1332
|
+
"your-secret",
|
|
1333
|
+
"YOUR_API_KEY",
|
|
1334
|
+
"YOUR_SECRET",
|
|
1335
|
+
"xxx",
|
|
1336
|
+
"TODO"
|
|
1337
|
+
]);
|
|
1338
|
+
var noHardcodedSecret = createRule10({
|
|
1339
|
+
name: "no-hardcoded-secret",
|
|
1340
|
+
meta: {
|
|
1341
|
+
type: "problem",
|
|
1342
|
+
docs: {
|
|
1343
|
+
description: "Disallow hardcoded secrets, API keys, passwords, and tokens in source code. AI tools frequently generate placeholder credentials that get committed to version control, creating security vulnerabilities."
|
|
1344
|
+
},
|
|
1345
|
+
fixable: void 0,
|
|
1346
|
+
schema: [],
|
|
1347
|
+
messages: {
|
|
1348
|
+
hardcodedSecret: "Possible hardcoded secret in variable `{{name}}`. AI tools frequently generate placeholder credentials that get committed to version control. Use environment variables (e.g., `process.env.{{envName}}`) instead."
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
defaultOptions: [],
|
|
1352
|
+
create(context) {
|
|
1353
|
+
return {
|
|
1354
|
+
VariableDeclarator(node) {
|
|
1355
|
+
if (!node.init) return;
|
|
1356
|
+
if (!node.id || node.id.type !== import_utils11.AST_NODE_TYPES.Identifier) return;
|
|
1357
|
+
const varName = node.id.name;
|
|
1358
|
+
if (!isSecretName(varName)) return;
|
|
1359
|
+
const value = getStringValue(node.init);
|
|
1360
|
+
if (value === null) return;
|
|
1361
|
+
if (FALSE_POSITIVE_VALUES.has(value)) return;
|
|
1362
|
+
if (value.length < 8) return;
|
|
1363
|
+
if (isProcessEnvAccess(node.init)) return;
|
|
1364
|
+
context.report({
|
|
1365
|
+
node: node.init,
|
|
1366
|
+
messageId: "hardcodedSecret",
|
|
1367
|
+
data: {
|
|
1368
|
+
name: varName,
|
|
1369
|
+
envName: toEnvVarName(varName)
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
},
|
|
1373
|
+
// Also check: obj.secret = 'literal_value' (including quoted / bracket
|
|
1374
|
+
// forms like obj['secret'] = '...', obj["apiKey"] = '...').
|
|
1375
|
+
AssignmentExpression(node) {
|
|
1376
|
+
if (node.left.type !== import_utils11.AST_NODE_TYPES.MemberExpression) return;
|
|
1377
|
+
const propName = getStaticMemberPropertyName(node.left);
|
|
1378
|
+
if (!propName) return;
|
|
1379
|
+
if (!isSecretName(propName)) return;
|
|
1380
|
+
const value = getStringValue(node.right);
|
|
1381
|
+
if (value === null) return;
|
|
1382
|
+
if (FALSE_POSITIVE_VALUES.has(value)) return;
|
|
1383
|
+
if (value.length < 8) return;
|
|
1384
|
+
if (isProcessEnvAccess(node.right)) return;
|
|
1385
|
+
context.report({
|
|
1386
|
+
node: node.right,
|
|
1387
|
+
messageId: "hardcodedSecret",
|
|
1388
|
+
data: {
|
|
1389
|
+
name: propName,
|
|
1390
|
+
envName: toEnvVarName(propName)
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
},
|
|
1394
|
+
// Check property assignments in object literals. Accept both bare
|
|
1395
|
+
// Identifier keys `{ secret: 'value' }` and string-Literal keys
|
|
1396
|
+
// `{ 'apiKey': 'sk-...' }` / `{ ["apiKey"]: '...' }`. Prettier / JSON-to-
|
|
1397
|
+
// config codegen routinely quotes keys, and skipping that form lets
|
|
1398
|
+
// placeholder credentials slip into version control.
|
|
1399
|
+
Property(node) {
|
|
1400
|
+
const propName = getStaticObjectPropertyKey(node);
|
|
1401
|
+
if (!propName) return;
|
|
1402
|
+
if (isRuleMetaMessagesProperty(node)) return;
|
|
1403
|
+
if (!isSecretName(propName)) return;
|
|
1404
|
+
const value = getStringValue(node.value);
|
|
1405
|
+
if (value === null) return;
|
|
1406
|
+
if (FALSE_POSITIVE_VALUES.has(value)) return;
|
|
1407
|
+
if (value.length < 8) return;
|
|
1408
|
+
if (isProcessEnvAccess(node.value)) return;
|
|
1409
|
+
context.report({
|
|
1410
|
+
node: node.value,
|
|
1411
|
+
messageId: "hardcodedSecret",
|
|
1412
|
+
data: {
|
|
1413
|
+
name: propName,
|
|
1414
|
+
envName: toEnvVarName(propName)
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
function getStaticObjectPropertyKey(node) {
|
|
1422
|
+
if (!node.computed && node.key.type === import_utils11.AST_NODE_TYPES.Identifier) {
|
|
1423
|
+
return node.key.name;
|
|
1424
|
+
}
|
|
1425
|
+
if (node.key.type === import_utils11.AST_NODE_TYPES.Literal && typeof node.key.value === "string") {
|
|
1426
|
+
return node.key.value;
|
|
1427
|
+
}
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
function getStaticMemberPropertyName(node) {
|
|
1431
|
+
if (!node.computed && node.property.type === import_utils11.AST_NODE_TYPES.Identifier) {
|
|
1432
|
+
return node.property.name;
|
|
1433
|
+
}
|
|
1434
|
+
if (node.property.type === import_utils11.AST_NODE_TYPES.Literal && typeof node.property.value === "string") {
|
|
1435
|
+
return node.property.value;
|
|
1436
|
+
}
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
function getStringValue(node) {
|
|
1440
|
+
if (node.type === import_utils11.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
1441
|
+
return node.value;
|
|
1442
|
+
}
|
|
1443
|
+
if (node.type === import_utils11.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
1444
|
+
return node.quasis[0].value.cooked ?? null;
|
|
1445
|
+
}
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
function isRuleMetaMessagesProperty(node) {
|
|
1449
|
+
if (!node.parent || node.parent.type !== import_utils11.AST_NODE_TYPES.ObjectExpression) {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
const parentProperty = node.parent.parent;
|
|
1453
|
+
if (!parentProperty || parentProperty.type !== import_utils11.AST_NODE_TYPES.Property) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
return parentProperty.key.type === import_utils11.AST_NODE_TYPES.Identifier && parentProperty.key.name === "messages";
|
|
1457
|
+
}
|
|
1458
|
+
function isProcessEnvAccess(node) {
|
|
1459
|
+
return node.type === import_utils11.AST_NODE_TYPES.MemberExpression && node.object.type === import_utils11.AST_NODE_TYPES.MemberExpression && node.object.object.type === import_utils11.AST_NODE_TYPES.Identifier && node.object.object.name === "process" && node.object.property.type === import_utils11.AST_NODE_TYPES.Identifier && node.object.property.name === "env";
|
|
1460
|
+
}
|
|
1461
|
+
function toEnvVarName(name) {
|
|
1462
|
+
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]/g, "_").toUpperCase();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/rules/security/no-eval-dynamic.ts
|
|
1466
|
+
var import_utils12 = require("@typescript-eslint/utils");
|
|
1467
|
+
var createRule11 = import_utils12.ESLintUtils.RuleCreator(
|
|
1468
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1469
|
+
);
|
|
1470
|
+
var noEvalDynamic = createRule11({
|
|
1471
|
+
name: "no-eval-dynamic",
|
|
1472
|
+
meta: {
|
|
1473
|
+
type: "problem",
|
|
1474
|
+
docs: {
|
|
1475
|
+
description: "Disallow `eval()` and `new Function()` with non-literal arguments. AI tools frequently generate dynamic code evaluation patterns that create serious security vulnerabilities including code injection and XSS."
|
|
1476
|
+
},
|
|
1477
|
+
fixable: void 0,
|
|
1478
|
+
schema: [],
|
|
1479
|
+
messages: {
|
|
1480
|
+
evalDynamic: "Dangerous use of `{{callee}}` with a dynamic argument. AI tools frequently generate `eval()` or `new Function()` patterns that enable code injection attacks. Use safer alternatives like JSON.parse(), a lookup table, or a sandboxed evaluator.",
|
|
1481
|
+
newFunctionDynamic: "Dangerous use of `new Function()` with a dynamic argument. AI tools frequently generate this pattern which enables arbitrary code execution. Use safer alternatives like a lookup table or pre-defined functions."
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
defaultOptions: [],
|
|
1485
|
+
create(context) {
|
|
1486
|
+
return {
|
|
1487
|
+
CallExpression(node) {
|
|
1488
|
+
if (node.callee.type === import_utils12.AST_NODE_TYPES.Identifier && node.callee.name === "eval") {
|
|
1489
|
+
if (node.arguments.length === 0) return;
|
|
1490
|
+
const firstArg = node.arguments[0];
|
|
1491
|
+
if (isLiteral(firstArg)) return;
|
|
1492
|
+
context.report({
|
|
1493
|
+
node,
|
|
1494
|
+
messageId: "evalDynamic",
|
|
1495
|
+
data: { callee: "eval()" }
|
|
1496
|
+
});
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (node.callee.type === import_utils12.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils12.AST_NODE_TYPES.Identifier && node.callee.property.name === "eval" && node.callee.object.type === import_utils12.AST_NODE_TYPES.Identifier && (node.callee.object.name === "window" || node.callee.object.name === "globalThis")) {
|
|
1500
|
+
if (node.arguments.length === 0) return;
|
|
1501
|
+
const firstArg = node.arguments[0];
|
|
1502
|
+
if (isLiteral(firstArg)) return;
|
|
1503
|
+
context.report({
|
|
1504
|
+
node,
|
|
1505
|
+
messageId: "evalDynamic",
|
|
1506
|
+
data: { callee: `${node.callee.object.name}.eval()` }
|
|
1507
|
+
});
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (isFunctionConstructorCallee(node.callee)) {
|
|
1511
|
+
if (node.arguments.length === 0) return;
|
|
1512
|
+
const allLiteral = node.arguments.every(isLiteral);
|
|
1513
|
+
if (allLiteral) return;
|
|
1514
|
+
context.report({
|
|
1515
|
+
node,
|
|
1516
|
+
messageId: "newFunctionDynamic"
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
NewExpression(node) {
|
|
1521
|
+
if (isFunctionConstructorCallee(node.callee)) {
|
|
1522
|
+
if (node.arguments.length === 0) return;
|
|
1523
|
+
const allLiteral = node.arguments.every(isLiteral);
|
|
1524
|
+
if (allLiteral) return;
|
|
1525
|
+
context.report({
|
|
1526
|
+
node,
|
|
1527
|
+
messageId: "newFunctionDynamic"
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
function isFunctionConstructorCallee(callee) {
|
|
1535
|
+
if (callee.type === import_utils12.AST_NODE_TYPES.Identifier && callee.name === "Function") {
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
if (callee.type === import_utils12.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils12.AST_NODE_TYPES.Identifier && callee.property.name === "Function" && callee.object.type === import_utils12.AST_NODE_TYPES.Identifier && (callee.object.name === "window" || callee.object.name === "globalThis")) {
|
|
1539
|
+
return true;
|
|
1540
|
+
}
|
|
1541
|
+
return false;
|
|
1542
|
+
}
|
|
1543
|
+
function isLiteral(node) {
|
|
1544
|
+
if (node.type === import_utils12.AST_NODE_TYPES.Literal) return true;
|
|
1545
|
+
if (node.type === import_utils12.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/rules/security/no-sql-string-concat.ts
|
|
1552
|
+
var import_utils13 = require("@typescript-eslint/utils");
|
|
1553
|
+
var createRule12 = import_utils13.ESLintUtils.RuleCreator(
|
|
1554
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1555
|
+
);
|
|
1556
|
+
var SQL_STATEMENT_PATTERN = /\b(select\s+[\s\S]*\s+from|insert\s+into|update\s+\S+\s+set|delete\s+from|drop\s+table|create\s+(table|database)|alter\s+table|truncate\s+table|exec(?:ute)?\s+\S+|union\s+select)\b/i;
|
|
1557
|
+
var SQL_SINK_METHODS = /* @__PURE__ */ new Set([
|
|
1558
|
+
"query",
|
|
1559
|
+
"execute",
|
|
1560
|
+
"queryraw",
|
|
1561
|
+
"queryrawunsafe",
|
|
1562
|
+
"executeraw",
|
|
1563
|
+
"executerawunsafe",
|
|
1564
|
+
"raw",
|
|
1565
|
+
"run",
|
|
1566
|
+
"all",
|
|
1567
|
+
"get",
|
|
1568
|
+
"prepare"
|
|
1569
|
+
]);
|
|
1570
|
+
var SQL_SINK_FUNCTIONS = /* @__PURE__ */ new Set(["query", "execute"]);
|
|
1571
|
+
function getIdentifierName(node) {
|
|
1572
|
+
if (node.type === import_utils13.AST_NODE_TYPES.Identifier) {
|
|
1573
|
+
return node.name;
|
|
1574
|
+
}
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
function isSqlSinkCall(node) {
|
|
1578
|
+
if (node.callee.type === import_utils13.AST_NODE_TYPES.Identifier) {
|
|
1579
|
+
return SQL_SINK_FUNCTIONS.has(node.callee.name.toLowerCase());
|
|
1580
|
+
}
|
|
1581
|
+
if (node.callee.type === import_utils13.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils13.AST_NODE_TYPES.Identifier) {
|
|
1582
|
+
const methodName = node.callee.property.name.toLowerCase().replace(/^\$/, "");
|
|
1583
|
+
return SQL_SINK_METHODS.has(methodName);
|
|
1584
|
+
}
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
function resolveExpression(node, variableMap) {
|
|
1588
|
+
const identifier = getIdentifierName(node);
|
|
1589
|
+
if (identifier && variableMap.has(identifier)) {
|
|
1590
|
+
return variableMap.get(identifier);
|
|
1591
|
+
}
|
|
1592
|
+
return node;
|
|
1593
|
+
}
|
|
1594
|
+
function isDynamicSqlExpression(node) {
|
|
1595
|
+
if (node.type === import_utils13.AST_NODE_TYPES.TemplateLiteral) {
|
|
1596
|
+
if (node.expressions.length === 0) {
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
const staticText = node.quasis.map((q) => q.value.raw).join(" ");
|
|
1600
|
+
return SQL_STATEMENT_PATTERN.test(staticText);
|
|
1601
|
+
}
|
|
1602
|
+
if (node.type === import_utils13.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
|
|
1603
|
+
const staticText = collectStaticText(node);
|
|
1604
|
+
if (!SQL_STATEMENT_PATTERN.test(staticText)) {
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
return hasDynamicParts(node);
|
|
1608
|
+
}
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
var noSqlStringConcat = createRule12({
|
|
1612
|
+
name: "no-sql-string-concat",
|
|
1613
|
+
meta: {
|
|
1614
|
+
type: "problem",
|
|
1615
|
+
docs: {
|
|
1616
|
+
description: "Disallow string concatenation or interpolation with variables in SQL query contexts. AI tools frequently generate SQL queries using template literals or string concatenation with user input, creating SQL injection vulnerabilities."
|
|
1617
|
+
},
|
|
1618
|
+
fixable: void 0,
|
|
1619
|
+
schema: [],
|
|
1620
|
+
messages: {
|
|
1621
|
+
sqlStringConcat: 'Potential SQL injection: string concatenation or interpolation detected in a SQL query. AI tools frequently generate this pattern. Use parameterized queries (e.g., `db.query("SELECT * FROM users WHERE id = $1", [id])`) instead.'
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
defaultOptions: [],
|
|
1625
|
+
create(context) {
|
|
1626
|
+
const variableMap = /* @__PURE__ */ new Map();
|
|
1627
|
+
return {
|
|
1628
|
+
VariableDeclarator(node) {
|
|
1629
|
+
if (node.id.type === import_utils13.AST_NODE_TYPES.Identifier && node.init && node.init.type !== import_utils13.AST_NODE_TYPES.AwaitExpression) {
|
|
1630
|
+
variableMap.set(node.id.name, node.init);
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
AssignmentExpression(node) {
|
|
1634
|
+
if (node.left.type === import_utils13.AST_NODE_TYPES.Identifier && node.right.type !== import_utils13.AST_NODE_TYPES.AwaitExpression) {
|
|
1635
|
+
variableMap.set(node.left.name, node.right);
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
CallExpression(node) {
|
|
1639
|
+
if (!isSqlSinkCall(node)) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
const firstArg = node.arguments[0];
|
|
1643
|
+
if (!firstArg || firstArg.type === import_utils13.AST_NODE_TYPES.SpreadElement) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const resolved = resolveExpression(firstArg, variableMap);
|
|
1647
|
+
if (!isDynamicSqlExpression(resolved)) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
context.report({
|
|
1651
|
+
node: firstArg,
|
|
1652
|
+
messageId: "sqlStringConcat"
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
function collectStaticText(node) {
|
|
1659
|
+
if (node.type === import_utils13.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
|
|
1660
|
+
return `${collectStaticText(node.left)} ${collectStaticText(node.right)}`;
|
|
1661
|
+
}
|
|
1662
|
+
if (node.type === import_utils13.AST_NODE_TYPES.TemplateLiteral) {
|
|
1663
|
+
return node.quasis.map((q) => q.value.raw).join(" ");
|
|
1664
|
+
}
|
|
1665
|
+
const literalValue = getStringLiteralValue(node);
|
|
1666
|
+
return literalValue ?? "";
|
|
1667
|
+
}
|
|
1668
|
+
function hasDynamicParts(node) {
|
|
1669
|
+
if (node.type === import_utils13.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
|
|
1670
|
+
return hasDynamicParts(node.left) || hasDynamicParts(node.right);
|
|
1671
|
+
}
|
|
1672
|
+
return !isStaticString(node);
|
|
1673
|
+
}
|
|
1674
|
+
function getStringLiteralValue(node) {
|
|
1675
|
+
if (node.type === import_utils13.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
1676
|
+
return node.value;
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
function isStaticString(node) {
|
|
1681
|
+
if (node.type === import_utils13.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
if (node.type === import_utils13.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/rules/security/no-unsafe-deserialize.ts
|
|
1691
|
+
var import_utils14 = require("@typescript-eslint/utils");
|
|
1692
|
+
var createRule13 = import_utils14.ESLintUtils.RuleCreator(
|
|
1693
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1694
|
+
);
|
|
1695
|
+
var UNTRUSTED_IDENTIFIER_NAMES = [
|
|
1696
|
+
"input",
|
|
1697
|
+
"userInput",
|
|
1698
|
+
"rawBody",
|
|
1699
|
+
"payload",
|
|
1700
|
+
"body",
|
|
1701
|
+
"query",
|
|
1702
|
+
"param",
|
|
1703
|
+
"params",
|
|
1704
|
+
"data",
|
|
1705
|
+
"requestBody"
|
|
1706
|
+
];
|
|
1707
|
+
var UNTRUSTED_REQUEST_OBJECTS = ["req", "request"];
|
|
1708
|
+
var UNTRUSTED_REQUEST_PROPERTIES = ["body", "query", "params", "param"];
|
|
1709
|
+
var UNTRUSTED_LOCATION_PROPERTIES = ["hash", "search", "href"];
|
|
1710
|
+
var UNTRUSTED_DOCUMENT_PROPERTIES = ["URL", "referrer"];
|
|
1711
|
+
function includesName(list, name) {
|
|
1712
|
+
return list.includes(name);
|
|
1713
|
+
}
|
|
1714
|
+
function isJsonParseCall(node) {
|
|
1715
|
+
return node.callee.type === import_utils14.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils14.AST_NODE_TYPES.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === import_utils14.AST_NODE_TYPES.Identifier && node.callee.property.name === "parse";
|
|
1716
|
+
}
|
|
1717
|
+
function unwrapCoercion(node) {
|
|
1718
|
+
let current = node;
|
|
1719
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
1720
|
+
if (current.type !== import_utils14.AST_NODE_TYPES.CallExpression) {
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
if (current.callee.type === import_utils14.AST_NODE_TYPES.Identifier && current.callee.name === "String" && current.arguments.length === 1 && current.arguments[0].type !== import_utils14.AST_NODE_TYPES.SpreadElement) {
|
|
1724
|
+
current = current.arguments[0];
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (current.callee.type === import_utils14.AST_NODE_TYPES.MemberExpression && current.callee.property.type === import_utils14.AST_NODE_TYPES.Identifier && current.callee.property.name === "toString" && current.arguments.length === 0) {
|
|
1728
|
+
current = current.callee.object;
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
return current;
|
|
1734
|
+
}
|
|
1735
|
+
function isUntrustedMemberExpression(node) {
|
|
1736
|
+
if (node.object.type === import_utils14.AST_NODE_TYPES.Identifier && includesName(UNTRUSTED_REQUEST_OBJECTS, node.object.name) && node.property.type === import_utils14.AST_NODE_TYPES.Identifier && includesName(UNTRUSTED_REQUEST_PROPERTIES, node.property.name)) {
|
|
1737
|
+
return true;
|
|
1738
|
+
}
|
|
1739
|
+
if (node.object.type === import_utils14.AST_NODE_TYPES.Identifier && node.object.name === "location" && node.property.type === import_utils14.AST_NODE_TYPES.Identifier && includesName(UNTRUSTED_LOCATION_PROPERTIES, node.property.name)) {
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
if (node.object.type === import_utils14.AST_NODE_TYPES.MemberExpression && node.object.object.type === import_utils14.AST_NODE_TYPES.Identifier && node.object.object.name === "window" && node.object.property.type === import_utils14.AST_NODE_TYPES.Identifier && node.object.property.name === "location" && node.property.type === import_utils14.AST_NODE_TYPES.Identifier && includesName(UNTRUSTED_LOCATION_PROPERTIES, node.property.name)) {
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
if (node.object.type === import_utils14.AST_NODE_TYPES.Identifier && node.object.name === "document" && node.property.type === import_utils14.AST_NODE_TYPES.Identifier && includesName(UNTRUSTED_DOCUMENT_PROPERTIES, node.property.name)) {
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
if (isLikelyUntrustedSource(node.object)) {
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
function isLikelyUntrustedSource(node) {
|
|
1754
|
+
if (node.type === import_utils14.AST_NODE_TYPES.Identifier) {
|
|
1755
|
+
return includesName(UNTRUSTED_IDENTIFIER_NAMES, node.name);
|
|
1756
|
+
}
|
|
1757
|
+
if (node.type === import_utils14.AST_NODE_TYPES.MemberExpression) {
|
|
1758
|
+
return isUntrustedMemberExpression(node);
|
|
1759
|
+
}
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
var noUnsafeDeserialize = createRule13({
|
|
1763
|
+
name: "no-unsafe-deserialize",
|
|
1764
|
+
meta: {
|
|
1765
|
+
type: "suggestion",
|
|
1766
|
+
docs: {
|
|
1767
|
+
description: "Disallow JSON.parse() on likely untrusted input without visible validation. AI tools frequently parse request/user payloads directly, which can introduce unsafe deserialization and downstream injection risks."
|
|
1768
|
+
},
|
|
1769
|
+
fixable: void 0,
|
|
1770
|
+
schema: [],
|
|
1771
|
+
messages: {
|
|
1772
|
+
unsafeDeserialize: "Potential unsafe deserialization: JSON.parse() is used on likely untrusted input without visible schema validation. AI tools frequently generate this shortcut. Validate input before parsing."
|
|
1773
|
+
}
|
|
1774
|
+
},
|
|
1775
|
+
defaultOptions: [],
|
|
1776
|
+
create(context) {
|
|
1777
|
+
function resolvesToUntrustedConst(idNode) {
|
|
1778
|
+
let scope = context.sourceCode.getScope(idNode);
|
|
1779
|
+
while (scope) {
|
|
1780
|
+
const variable = scope.variables.find((v) => v.name === idNode.name);
|
|
1781
|
+
if (variable) {
|
|
1782
|
+
if (variable.defs.length !== 1) {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
const declNode = variable.defs[0].node;
|
|
1786
|
+
if (declNode.type === import_utils14.AST_NODE_TYPES.VariableDeclarator && declNode.init !== null && declNode.parent.type === import_utils14.AST_NODE_TYPES.VariableDeclaration && declNode.parent.kind === "const") {
|
|
1787
|
+
return isLikelyUntrustedSource(declNode.init);
|
|
1788
|
+
}
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
scope = scope.upper;
|
|
1792
|
+
}
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
CallExpression(node) {
|
|
1797
|
+
if (!isJsonParseCall(node)) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const rawArg = node.arguments[0];
|
|
1801
|
+
if (!rawArg || rawArg.type === import_utils14.AST_NODE_TYPES.SpreadElement) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const inputArg = unwrapCoercion(rawArg);
|
|
1805
|
+
const flagged = isLikelyUntrustedSource(inputArg) || inputArg.type === import_utils14.AST_NODE_TYPES.Identifier && resolvesToUntrustedConst(inputArg);
|
|
1806
|
+
if (flagged) {
|
|
1807
|
+
context.report({
|
|
1808
|
+
node,
|
|
1809
|
+
messageId: "unsafeDeserialize"
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
// src/rules/security/require-auth-middleware.ts
|
|
1818
|
+
var import_utils15 = require("@typescript-eslint/utils");
|
|
1819
|
+
var createRule14 = import_utils15.ESLintUtils.RuleCreator(
|
|
1820
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
1821
|
+
);
|
|
1822
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
1823
|
+
"get",
|
|
1824
|
+
"post",
|
|
1825
|
+
"put",
|
|
1826
|
+
"patch",
|
|
1827
|
+
"delete",
|
|
1828
|
+
"all",
|
|
1829
|
+
"options",
|
|
1830
|
+
"head"
|
|
1831
|
+
]);
|
|
1832
|
+
var DEFAULT_AUTH_MIDDLEWARE_NAMES = /* @__PURE__ */ new Set([
|
|
1833
|
+
"authenticate",
|
|
1834
|
+
"requireAuth",
|
|
1835
|
+
"isAuthenticated",
|
|
1836
|
+
"verifyToken",
|
|
1837
|
+
"protect",
|
|
1838
|
+
"authorized",
|
|
1839
|
+
"authorize",
|
|
1840
|
+
"isAdmin",
|
|
1841
|
+
"ensureAuthenticated",
|
|
1842
|
+
"ensureLoggedIn",
|
|
1843
|
+
"auth",
|
|
1844
|
+
"authMiddleware",
|
|
1845
|
+
"requireLogin",
|
|
1846
|
+
"checkAuth",
|
|
1847
|
+
"validateToken",
|
|
1848
|
+
"passport.authenticate",
|
|
1849
|
+
"jwt",
|
|
1850
|
+
"requireSession"
|
|
1851
|
+
]);
|
|
1852
|
+
var PUBLIC_ROUTE_PATTERNS = [
|
|
1853
|
+
/^\/?$/,
|
|
1854
|
+
// '/' root
|
|
1855
|
+
/^\*$/,
|
|
1856
|
+
// '*' SPA fallback
|
|
1857
|
+
/^\/\*$/,
|
|
1858
|
+
// '/*' SPA fallback
|
|
1859
|
+
/^\/health/,
|
|
1860
|
+
// health checks
|
|
1861
|
+
/^\/ping/,
|
|
1862
|
+
// ping
|
|
1863
|
+
/^\/status/,
|
|
1864
|
+
// status
|
|
1865
|
+
/^\/api\/v\d+\/auth/,
|
|
1866
|
+
// auth routes
|
|
1867
|
+
/^\/auth/,
|
|
1868
|
+
// auth routes
|
|
1869
|
+
/^\/login/,
|
|
1870
|
+
// login
|
|
1871
|
+
/^\/register/,
|
|
1872
|
+
// register
|
|
1873
|
+
/^\/signup/,
|
|
1874
|
+
// signup
|
|
1875
|
+
/^\/forgot/,
|
|
1876
|
+
// forgot password
|
|
1877
|
+
/^\/reset/,
|
|
1878
|
+
// reset password
|
|
1879
|
+
/^\/webhook/,
|
|
1880
|
+
// webhooks
|
|
1881
|
+
/^\/public/,
|
|
1882
|
+
// explicitly public
|
|
1883
|
+
/^\/assets/,
|
|
1884
|
+
// static assets
|
|
1885
|
+
/^\/static/,
|
|
1886
|
+
// static files
|
|
1887
|
+
/^\/favicon/,
|
|
1888
|
+
// favicon
|
|
1889
|
+
/^\/robots/,
|
|
1890
|
+
// robots.txt
|
|
1891
|
+
/^\/sitemap/
|
|
1892
|
+
// sitemap
|
|
1893
|
+
];
|
|
1894
|
+
var requireAuthMiddleware = createRule14({
|
|
1895
|
+
name: "require-auth-middleware",
|
|
1896
|
+
meta: {
|
|
1897
|
+
type: "suggestion",
|
|
1898
|
+
deprecated: true,
|
|
1899
|
+
replacedBy: ["require-framework-auth"],
|
|
1900
|
+
docs: {
|
|
1901
|
+
description: "Require authentication middleware on Express/Fastify route definitions. AI tools frequently generate route handlers without auth middleware, creating unprotected endpoints that expose sensitive data or operations."
|
|
1902
|
+
},
|
|
1903
|
+
fixable: void 0,
|
|
1904
|
+
schema: [
|
|
1905
|
+
{
|
|
1906
|
+
type: "object",
|
|
1907
|
+
properties: {
|
|
1908
|
+
authMiddlewareNames: {
|
|
1909
|
+
type: "array",
|
|
1910
|
+
items: { type: "string" },
|
|
1911
|
+
description: "Additional custom middleware names to recognize as authentication middleware."
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
additionalProperties: false
|
|
1915
|
+
}
|
|
1916
|
+
],
|
|
1917
|
+
messages: {
|
|
1918
|
+
missingAuth: "[ai-guard deprecated \u2014 use require-framework-auth] Route `{{method}} {{path}}` appears to have no authentication middleware. Add authentication middleware (e.g., `protect`, `authenticate`) to the handler chain."
|
|
1919
|
+
}
|
|
1920
|
+
},
|
|
1921
|
+
defaultOptions: [{}],
|
|
1922
|
+
create(context, [options]) {
|
|
1923
|
+
const customNames = new Set(options.authMiddlewareNames ?? []);
|
|
1924
|
+
const allAuthNames = /* @__PURE__ */ new Set([...DEFAULT_AUTH_MIDDLEWARE_NAMES, ...customNames]);
|
|
1925
|
+
let hasRouterUseAuth = false;
|
|
1926
|
+
return {
|
|
1927
|
+
CallExpression(node) {
|
|
1928
|
+
if (isRouterUseAuth(node, allAuthNames)) {
|
|
1929
|
+
hasRouterUseAuth = true;
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
if (!isRouteDefinition(node)) return;
|
|
1933
|
+
if (hasRouterUseAuth) return;
|
|
1934
|
+
const callee = node.callee;
|
|
1935
|
+
const method = callee.property.name;
|
|
1936
|
+
const args = node.arguments;
|
|
1937
|
+
if (args.length < 2) return;
|
|
1938
|
+
const pathArg = args[0];
|
|
1939
|
+
const pathStr = getPathString(pathArg);
|
|
1940
|
+
if (pathStr && isPublicRoute(pathStr)) return;
|
|
1941
|
+
const middlewareArgs = args.slice(1, -1);
|
|
1942
|
+
if (middlewareArgs.length === 0 && args.length === 2) {
|
|
1943
|
+
context.report({
|
|
1944
|
+
node,
|
|
1945
|
+
messageId: "missingAuth",
|
|
1946
|
+
data: {
|
|
1947
|
+
method: method.toUpperCase(),
|
|
1948
|
+
path: pathStr || "<dynamic>"
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const hasAuth = middlewareArgs.some((arg) => isAuthMiddleware(arg, allAuthNames));
|
|
1954
|
+
if (!hasAuth) {
|
|
1955
|
+
context.report({
|
|
1956
|
+
node,
|
|
1957
|
+
messageId: "missingAuth",
|
|
1958
|
+
data: {
|
|
1959
|
+
method: method.toUpperCase(),
|
|
1960
|
+
path: pathStr || "<dynamic>"
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
function isRouteDefinition(node) {
|
|
1969
|
+
if (node.callee.type !== import_utils15.AST_NODE_TYPES.MemberExpression) return false;
|
|
1970
|
+
if (node.callee.property.type !== import_utils15.AST_NODE_TYPES.Identifier) return false;
|
|
1971
|
+
const methodName = node.callee.property.name;
|
|
1972
|
+
if (!HTTP_METHODS.has(methodName)) return false;
|
|
1973
|
+
const obj = node.callee.object;
|
|
1974
|
+
if (obj.type === import_utils15.AST_NODE_TYPES.Identifier) {
|
|
1975
|
+
const name = obj.name.toLowerCase();
|
|
1976
|
+
return name === "router" || name === "app" || name.includes("router");
|
|
1977
|
+
}
|
|
1978
|
+
if (obj.type === import_utils15.AST_NODE_TYPES.CallExpression) return true;
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
function isRouterUseAuth(node, authNames) {
|
|
1982
|
+
if (node.callee.type !== import_utils15.AST_NODE_TYPES.MemberExpression) return false;
|
|
1983
|
+
if (node.callee.property.type !== import_utils15.AST_NODE_TYPES.Identifier) return false;
|
|
1984
|
+
if (node.callee.property.name !== "use") return false;
|
|
1985
|
+
return node.arguments.some((arg) => isAuthMiddleware(arg, authNames));
|
|
1986
|
+
}
|
|
1987
|
+
function isAuthMiddleware(node, authNames) {
|
|
1988
|
+
if (node.type === import_utils15.AST_NODE_TYPES.Identifier) {
|
|
1989
|
+
return authNames.has(node.name);
|
|
1990
|
+
}
|
|
1991
|
+
if (node.type === import_utils15.AST_NODE_TYPES.CallExpression) {
|
|
1992
|
+
if (node.callee.type === import_utils15.AST_NODE_TYPES.Identifier) {
|
|
1993
|
+
return authNames.has(node.callee.name);
|
|
1994
|
+
}
|
|
1995
|
+
if (node.callee.type === import_utils15.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils15.AST_NODE_TYPES.Identifier && node.callee.property.type === import_utils15.AST_NODE_TYPES.Identifier) {
|
|
1996
|
+
const fullName = `${node.callee.object.name}.${node.callee.property.name}`;
|
|
1997
|
+
return authNames.has(fullName) || authNames.has(node.callee.property.name);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
2002
|
+
function getPathString(node) {
|
|
2003
|
+
if (node.type === import_utils15.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
2004
|
+
return node.value;
|
|
2005
|
+
}
|
|
2006
|
+
if (node.type === import_utils15.AST_NODE_TYPES.TemplateLiteral) {
|
|
2007
|
+
if (node.expressions.length === 0 && node.quasis.length === 1) {
|
|
2008
|
+
return node.quasis[0].value.cooked ?? null;
|
|
2009
|
+
}
|
|
2010
|
+
if (node.quasis.length > 0) {
|
|
2011
|
+
return node.quasis[0].value.cooked ?? null;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
function isPublicRoute(pathStr) {
|
|
2017
|
+
return PUBLIC_ROUTE_PATTERNS.some((pattern) => pattern.test(pathStr));
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/rules/security/require-authz-check.ts
|
|
2021
|
+
var import_utils16 = require("@typescript-eslint/utils");
|
|
2022
|
+
var createRule15 = import_utils16.ESLintUtils.RuleCreator(
|
|
2023
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
2024
|
+
);
|
|
2025
|
+
var ROUTE_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
2026
|
+
var AUTHZ_HELPER_NAMES = [
|
|
2027
|
+
"authorize",
|
|
2028
|
+
"authorise",
|
|
2029
|
+
"checkOwnership",
|
|
2030
|
+
"ensureOwner",
|
|
2031
|
+
"isOwner",
|
|
2032
|
+
"canAccess",
|
|
2033
|
+
"canModify",
|
|
2034
|
+
"hasAccess"
|
|
2035
|
+
];
|
|
2036
|
+
function isRouteRegistrationCall(node) {
|
|
2037
|
+
if (node.callee.type !== import_utils16.AST_NODE_TYPES.MemberExpression || node.callee.property.type !== import_utils16.AST_NODE_TYPES.Identifier) {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
if (!ROUTE_METHODS.includes(node.callee.property.name)) {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
const firstArg = node.arguments[0];
|
|
2044
|
+
if (!firstArg) {
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
return firstArg.type === import_utils16.AST_NODE_TYPES.Literal && typeof firstArg.value === "string" || firstArg.type === import_utils16.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0;
|
|
2048
|
+
}
|
|
2049
|
+
function getStaticPathArg(node) {
|
|
2050
|
+
const firstArg = node.arguments[0];
|
|
2051
|
+
if (!firstArg) return null;
|
|
2052
|
+
if (firstArg.type === import_utils16.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") {
|
|
2053
|
+
return firstArg.value;
|
|
2054
|
+
}
|
|
2055
|
+
if (firstArg.type === import_utils16.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0) {
|
|
2056
|
+
return firstArg.quasis[0]?.value.cooked ?? null;
|
|
2057
|
+
}
|
|
2058
|
+
return null;
|
|
2059
|
+
}
|
|
2060
|
+
function getMemberPath(node) {
|
|
2061
|
+
if (node.type === import_utils16.AST_NODE_TYPES.Identifier) {
|
|
2062
|
+
return [node.name];
|
|
2063
|
+
}
|
|
2064
|
+
if (node.type !== import_utils16.AST_NODE_TYPES.MemberExpression || node.computed) {
|
|
2065
|
+
return null;
|
|
2066
|
+
}
|
|
2067
|
+
const objectPath = getMemberPath(node.object);
|
|
2068
|
+
if (!objectPath) {
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
if (node.property.type !== import_utils16.AST_NODE_TYPES.Identifier) {
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2074
|
+
return [...objectPath, node.property.name];
|
|
2075
|
+
}
|
|
2076
|
+
function hasPathPrefix(path, prefix) {
|
|
2077
|
+
if (!path || path.length < prefix.length) {
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
for (let i = 0; i < prefix.length; i += 1) {
|
|
2081
|
+
if (path[i] !== prefix[i]) {
|
|
2082
|
+
return false;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
function isLikelyResourceIdPath(path) {
|
|
2088
|
+
if (!path || path.length < 3) {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
if (!hasPathPrefix(path, ["req", "params"]) && !hasPathPrefix(path, ["req", "body"]) && !hasPathPrefix(path, ["req", "query"])) {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
const last = path[path.length - 1].toLowerCase();
|
|
2095
|
+
return last === "id" || last.endsWith("id");
|
|
2096
|
+
}
|
|
2097
|
+
function isReqUserPath(path) {
|
|
2098
|
+
return hasPathPrefix(path, ["req", "user"]);
|
|
2099
|
+
}
|
|
2100
|
+
function containsAuthorizationHelper(node) {
|
|
2101
|
+
if (node.type === import_utils16.AST_NODE_TYPES.CallExpression && (node.callee.type === import_utils16.AST_NODE_TYPES.Identifier && AUTHZ_HELPER_NAMES.includes(node.callee.name) || node.callee.type === import_utils16.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils16.AST_NODE_TYPES.Identifier && AUTHZ_HELPER_NAMES.includes(node.callee.property.name))) {
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
const entries = Object.entries(node);
|
|
2105
|
+
for (const [key, value] of entries) {
|
|
2106
|
+
if (key === "parent") continue;
|
|
2107
|
+
if (Array.isArray(value)) {
|
|
2108
|
+
for (const child of value) {
|
|
2109
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
2110
|
+
if (containsAuthorizationHelper(child)) return true;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (value && typeof value === "object" && "type" in value) {
|
|
2116
|
+
if (containsAuthorizationHelper(value)) return true;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2121
|
+
function collectBodySignals(node) {
|
|
2122
|
+
let hasResourceIdAccess = false;
|
|
2123
|
+
let hasOwnershipCheck = false;
|
|
2124
|
+
const walk = (current) => {
|
|
2125
|
+
if (current.type === import_utils16.AST_NODE_TYPES.BinaryExpression && ["===", "==", "!==", "!="].includes(current.operator)) {
|
|
2126
|
+
const leftPath = getMemberPath(current.left);
|
|
2127
|
+
const rightPath = getMemberPath(current.right);
|
|
2128
|
+
const leftIsUser = isReqUserPath(leftPath);
|
|
2129
|
+
const rightIsUser = isReqUserPath(rightPath);
|
|
2130
|
+
const leftIsResource = isLikelyResourceIdPath(leftPath);
|
|
2131
|
+
const rightIsResource = isLikelyResourceIdPath(rightPath);
|
|
2132
|
+
if (leftIsUser && rightIsResource || rightIsUser && leftIsResource) {
|
|
2133
|
+
hasOwnershipCheck = true;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
if (current.type === import_utils16.AST_NODE_TYPES.MemberExpression) {
|
|
2137
|
+
const path = getMemberPath(current);
|
|
2138
|
+
if (isLikelyResourceIdPath(path)) {
|
|
2139
|
+
hasResourceIdAccess = true;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (containsAuthorizationHelper(current)) {
|
|
2143
|
+
hasOwnershipCheck = true;
|
|
2144
|
+
}
|
|
2145
|
+
const entries = Object.entries(current);
|
|
2146
|
+
for (const [key, value] of entries) {
|
|
2147
|
+
if (key === "parent") continue;
|
|
2148
|
+
if (Array.isArray(value)) {
|
|
2149
|
+
for (const child of value) {
|
|
2150
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
2151
|
+
walk(child);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
if (value && typeof value === "object" && "type" in value) {
|
|
2157
|
+
walk(value);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
walk(node);
|
|
2162
|
+
return { hasResourceIdAccess, hasOwnershipCheck };
|
|
2163
|
+
}
|
|
2164
|
+
var requireAuthzCheck = createRule15({
|
|
2165
|
+
name: "require-authz-check",
|
|
2166
|
+
meta: {
|
|
2167
|
+
type: "suggestion",
|
|
2168
|
+
deprecated: true,
|
|
2169
|
+
replacedBy: ["require-framework-authz"],
|
|
2170
|
+
docs: {
|
|
2171
|
+
description: "Require a visible authorization/ownership check when route handlers access resource identifiers (e.g., req.params.id). AI tools often add auth middleware but forget per-resource authorization checks."
|
|
2172
|
+
},
|
|
2173
|
+
fixable: void 0,
|
|
2174
|
+
schema: [],
|
|
2175
|
+
messages: {
|
|
2176
|
+
missingAuthz: "[ai-guard deprecated \u2014 use require-framework-authz] Potential missing authorization check. This handler uses resource identifiers (like req.params.id) but no visible ownership/authorization guard was found."
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
defaultOptions: [],
|
|
2180
|
+
create(context) {
|
|
2181
|
+
return {
|
|
2182
|
+
CallExpression(node) {
|
|
2183
|
+
if (!isRouteRegistrationCall(node)) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const routePath = getStaticPathArg(node);
|
|
2187
|
+
const hasRouteIdParam = typeof routePath === "string" && routePath.includes(":");
|
|
2188
|
+
for (const arg of node.arguments) {
|
|
2189
|
+
if (arg.type !== import_utils16.AST_NODE_TYPES.FunctionExpression && arg.type !== import_utils16.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
if (arg.body.type !== import_utils16.AST_NODE_TYPES.BlockStatement) {
|
|
2193
|
+
continue;
|
|
2194
|
+
}
|
|
2195
|
+
const signals = collectBodySignals(arg.body);
|
|
2196
|
+
const isSensitive = hasRouteIdParam && signals.hasResourceIdAccess;
|
|
2197
|
+
if (isSensitive && !signals.hasOwnershipCheck) {
|
|
2198
|
+
context.report({
|
|
2199
|
+
node: arg,
|
|
2200
|
+
messageId: "missingAuthz"
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
// src/rules/security/require-framework-auth.ts
|
|
2210
|
+
var import_utils18 = require("@typescript-eslint/utils");
|
|
2211
|
+
|
|
2212
|
+
// src/utils/framework-detectors.ts
|
|
2213
|
+
var import_utils17 = require("@typescript-eslint/utils");
|
|
2214
|
+
var FRAMEWORK_MODULES = {
|
|
2215
|
+
express: "express",
|
|
2216
|
+
fastify: "fastify",
|
|
2217
|
+
hono: "hono",
|
|
2218
|
+
"@nestjs/common": "nestjs",
|
|
2219
|
+
"@nestjs/core": "nestjs"
|
|
2220
|
+
};
|
|
2221
|
+
var NEXTJS_ROUTE_PATTERN = /[/\\]app[/\\](.*[/\\])?route\.(ts|tsx|js|jsx)$/;
|
|
2222
|
+
function buildImportMap(sourceCode) {
|
|
2223
|
+
const modules = /* @__PURE__ */ new Map();
|
|
2224
|
+
const locals = /* @__PURE__ */ new Map();
|
|
2225
|
+
for (const node of sourceCode.ast.body) {
|
|
2226
|
+
if (node.type !== import_utils17.AST_NODE_TYPES.ImportDeclaration) continue;
|
|
2227
|
+
const mod = node.source.value;
|
|
2228
|
+
if (!modules.has(mod)) {
|
|
2229
|
+
modules.set(mod, /* @__PURE__ */ new Set());
|
|
2230
|
+
}
|
|
2231
|
+
const names = modules.get(mod);
|
|
2232
|
+
for (const spec of node.specifiers) {
|
|
2233
|
+
switch (spec.type) {
|
|
2234
|
+
case import_utils17.AST_NODE_TYPES.ImportDefaultSpecifier:
|
|
2235
|
+
names.add("default");
|
|
2236
|
+
locals.set(spec.local.name, mod);
|
|
2237
|
+
break;
|
|
2238
|
+
case import_utils17.AST_NODE_TYPES.ImportSpecifier:
|
|
2239
|
+
names.add(spec.imported.type === import_utils17.AST_NODE_TYPES.Identifier ? spec.imported.name : spec.imported.value);
|
|
2240
|
+
locals.set(spec.local.name, mod);
|
|
2241
|
+
break;
|
|
2242
|
+
case import_utils17.AST_NODE_TYPES.ImportNamespaceSpecifier:
|
|
2243
|
+
names.add("*");
|
|
2244
|
+
locals.set(spec.local.name, mod);
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return { modules, locals };
|
|
2250
|
+
}
|
|
2251
|
+
function detectFramework(importMap, filename) {
|
|
2252
|
+
for (const [mod, kind] of Object.entries(FRAMEWORK_MODULES)) {
|
|
2253
|
+
if (importMap.modules.has(mod)) return kind;
|
|
2254
|
+
}
|
|
2255
|
+
if (isNextjsRouteHandler(filename)) return "nextjs-app-router";
|
|
2256
|
+
return "unknown";
|
|
2257
|
+
}
|
|
2258
|
+
function isNextjsRouteHandler(filename) {
|
|
2259
|
+
return NEXTJS_ROUTE_PATTERN.test(filename);
|
|
2260
|
+
}
|
|
2261
|
+
function hasImport(importMap, moduleName) {
|
|
2262
|
+
return importMap.modules.has(moduleName);
|
|
2263
|
+
}
|
|
2264
|
+
function localComesFrom(importMap, localName, moduleName) {
|
|
2265
|
+
return importMap.locals.get(localName) === moduleName;
|
|
2266
|
+
}
|
|
2267
|
+
function hasDecoratorNamed(node, names) {
|
|
2268
|
+
const decorators = node.decorators;
|
|
2269
|
+
if (!decorators) return false;
|
|
2270
|
+
return decorators.some((d) => {
|
|
2271
|
+
const name = getDecoratorCallName(d);
|
|
2272
|
+
return name !== null && names.has(name);
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
function getDecoratorCallName(decorator) {
|
|
2276
|
+
const expr = decorator.expression;
|
|
2277
|
+
if (expr.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2278
|
+
return expr.name;
|
|
2279
|
+
}
|
|
2280
|
+
if (expr.type === import_utils17.AST_NODE_TYPES.MemberExpression && expr.property.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2281
|
+
return expr.property.name;
|
|
2282
|
+
}
|
|
2283
|
+
if (expr.type === import_utils17.AST_NODE_TYPES.CallExpression) {
|
|
2284
|
+
if (expr.callee.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2285
|
+
return expr.callee.name;
|
|
2286
|
+
}
|
|
2287
|
+
if (expr.callee.type === import_utils17.AST_NODE_TYPES.MemberExpression && expr.callee.property.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2288
|
+
return expr.callee.property.name;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
function getMemberPath2(node) {
|
|
2294
|
+
const unwrapped = unwrapTSExpression(node);
|
|
2295
|
+
if (unwrapped.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2296
|
+
return [unwrapped.name];
|
|
2297
|
+
}
|
|
2298
|
+
if (unwrapped.type !== import_utils17.AST_NODE_TYPES.MemberExpression || unwrapped.computed) {
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2301
|
+
const objectPath = getMemberPath2(unwrapped.object);
|
|
2302
|
+
if (!objectPath) return null;
|
|
2303
|
+
if (unwrapped.property.type !== import_utils17.AST_NODE_TYPES.Identifier) return null;
|
|
2304
|
+
return [...objectPath, unwrapped.property.name];
|
|
2305
|
+
}
|
|
2306
|
+
function getStaticPropKey(prop) {
|
|
2307
|
+
if (prop.type !== import_utils17.AST_NODE_TYPES.Property) return null;
|
|
2308
|
+
if (prop.key.type === import_utils17.AST_NODE_TYPES.Identifier) return prop.key.name;
|
|
2309
|
+
if (prop.key.type === import_utils17.AST_NODE_TYPES.Literal && typeof prop.key.value === "string") {
|
|
2310
|
+
return prop.key.value;
|
|
2311
|
+
}
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
function getPathString2(node) {
|
|
2315
|
+
if (node.type === import_utils17.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
2316
|
+
return node.value;
|
|
2317
|
+
}
|
|
2318
|
+
if (node.type === import_utils17.AST_NODE_TYPES.TemplateLiteral) {
|
|
2319
|
+
if (node.expressions.length === 0 && node.quasis.length === 1) {
|
|
2320
|
+
return node.quasis[0].value.cooked ?? null;
|
|
2321
|
+
}
|
|
2322
|
+
if (node.quasis.length > 0) {
|
|
2323
|
+
const first = node.quasis[0].value.cooked ?? null;
|
|
2324
|
+
if (first === "" || first === "/") return null;
|
|
2325
|
+
return first;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
function isCallToName(node, names) {
|
|
2331
|
+
if (node.callee.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2332
|
+
return names.has(node.callee.name);
|
|
2333
|
+
}
|
|
2334
|
+
if (node.callee.type === import_utils17.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2335
|
+
if (names.has(node.callee.property.name)) return true;
|
|
2336
|
+
if (node.callee.object.type === import_utils17.AST_NODE_TYPES.Identifier) {
|
|
2337
|
+
return names.has(`${node.callee.object.name}.${node.callee.property.name}`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
return false;
|
|
2341
|
+
}
|
|
2342
|
+
function bodyContainsCallTo(body, names) {
|
|
2343
|
+
return walkForCall(body, names);
|
|
2344
|
+
}
|
|
2345
|
+
var AST_SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
2346
|
+
"parent",
|
|
2347
|
+
"loc",
|
|
2348
|
+
"range",
|
|
2349
|
+
"typeAnnotation",
|
|
2350
|
+
"returnType",
|
|
2351
|
+
"typeParameters",
|
|
2352
|
+
"typeArguments"
|
|
2353
|
+
]);
|
|
2354
|
+
var STOP_DESCENT_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
2355
|
+
import_utils17.AST_NODE_TYPES.FunctionDeclaration,
|
|
2356
|
+
import_utils17.AST_NODE_TYPES.ClassDeclaration,
|
|
2357
|
+
import_utils17.AST_NODE_TYPES.ClassExpression,
|
|
2358
|
+
import_utils17.AST_NODE_TYPES.MethodDefinition
|
|
2359
|
+
]);
|
|
2360
|
+
function walkForCall(node, names, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2361
|
+
if (seen.has(node)) return false;
|
|
2362
|
+
seen.add(node);
|
|
2363
|
+
if (node.type === import_utils17.AST_NODE_TYPES.CallExpression && isCallToName(node, names)) {
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2366
|
+
for (const key of Object.keys(node)) {
|
|
2367
|
+
if (AST_SKIP_KEYS.has(key)) continue;
|
|
2368
|
+
const value = node[key];
|
|
2369
|
+
if (Array.isArray(value)) {
|
|
2370
|
+
for (const child of value) {
|
|
2371
|
+
if (isASTNode(child) && !STOP_DESCENT_NODE_TYPES.has(child.type)) {
|
|
2372
|
+
if (walkForCall(child, names, seen)) return true;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
} else if (isASTNode(value) && !STOP_DESCENT_NODE_TYPES.has(value.type)) {
|
|
2376
|
+
if (walkForCall(value, names, seen)) return true;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
function safeCompileRegex(pattern, maxLength = 200) {
|
|
2382
|
+
if (pattern.length > maxLength) return null;
|
|
2383
|
+
if (/\([^)]*[+*]\)[+*?{]/.test(pattern)) return null;
|
|
2384
|
+
if (/\(([^|)]+)\|\1\)[+*]/.test(pattern)) return null;
|
|
2385
|
+
try {
|
|
2386
|
+
return new RegExp(pattern);
|
|
2387
|
+
} catch {
|
|
2388
|
+
return null;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
function unwrapTSExpression(node) {
|
|
2392
|
+
let current = node;
|
|
2393
|
+
while (current.type === import_utils17.AST_NODE_TYPES.TSAsExpression || current.type === import_utils17.AST_NODE_TYPES.TSTypeAssertion || current.type === import_utils17.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils17.AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
2394
|
+
current = current.expression;
|
|
2395
|
+
}
|
|
2396
|
+
return current;
|
|
2397
|
+
}
|
|
2398
|
+
function isASTNode(value) {
|
|
2399
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/rules/security/require-framework-auth.ts
|
|
2403
|
+
var createRule16 = import_utils18.ESLintUtils.RuleCreator(
|
|
2404
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
2405
|
+
);
|
|
2406
|
+
var HTTP_METHODS2 = /* @__PURE__ */ new Set([
|
|
2407
|
+
"get",
|
|
2408
|
+
"post",
|
|
2409
|
+
"put",
|
|
2410
|
+
"patch",
|
|
2411
|
+
"delete",
|
|
2412
|
+
"all",
|
|
2413
|
+
"options",
|
|
2414
|
+
"head"
|
|
2415
|
+
]);
|
|
2416
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
2417
|
+
var NEXTJS_EXPORT_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE", "GET"]);
|
|
2418
|
+
var DEFAULT_AUTH_NAMES = /* @__PURE__ */ new Set([
|
|
2419
|
+
"authenticate",
|
|
2420
|
+
"requireAuth",
|
|
2421
|
+
"isAuthenticated",
|
|
2422
|
+
"verifyToken",
|
|
2423
|
+
"protect",
|
|
2424
|
+
"authorized",
|
|
2425
|
+
"authorize",
|
|
2426
|
+
"isAdmin",
|
|
2427
|
+
"ensureAuthenticated",
|
|
2428
|
+
"ensureLoggedIn",
|
|
2429
|
+
"auth",
|
|
2430
|
+
"authMiddleware",
|
|
2431
|
+
"requireLogin",
|
|
2432
|
+
"checkAuth",
|
|
2433
|
+
"validateToken",
|
|
2434
|
+
"passport.authenticate",
|
|
2435
|
+
"jwt",
|
|
2436
|
+
"requireSession",
|
|
2437
|
+
"clerkMiddleware",
|
|
2438
|
+
"withAuth",
|
|
2439
|
+
"requireAuthentication"
|
|
2440
|
+
]);
|
|
2441
|
+
var DEFAULT_NEXTJS_AUTH_CALLERS = /* @__PURE__ */ new Set([
|
|
2442
|
+
"auth",
|
|
2443
|
+
"auth.protect",
|
|
2444
|
+
"currentUser",
|
|
2445
|
+
"getServerSession",
|
|
2446
|
+
"getToken",
|
|
2447
|
+
"verifySession",
|
|
2448
|
+
"getUser",
|
|
2449
|
+
"supabase.auth.getUser"
|
|
2450
|
+
]);
|
|
2451
|
+
var DEFAULT_SKIP_DECORATORS = /* @__PURE__ */ new Set([
|
|
2452
|
+
"Public",
|
|
2453
|
+
"SkipAuth",
|
|
2454
|
+
"AllowAnonymous",
|
|
2455
|
+
"NoAuth"
|
|
2456
|
+
]);
|
|
2457
|
+
var NESTJS_HTTP_DECORATORS = /* @__PURE__ */ new Set([
|
|
2458
|
+
"Get",
|
|
2459
|
+
"Post",
|
|
2460
|
+
"Put",
|
|
2461
|
+
"Patch",
|
|
2462
|
+
"Delete",
|
|
2463
|
+
"All",
|
|
2464
|
+
"Options",
|
|
2465
|
+
"Head"
|
|
2466
|
+
]);
|
|
2467
|
+
var NESTJS_MUTATING_DECORATORS = /* @__PURE__ */ new Set(["Post", "Put", "Patch", "Delete"]);
|
|
2468
|
+
var DEFAULT_PUBLIC_ROUTE_PATTERNS = [
|
|
2469
|
+
/^\/?$/,
|
|
2470
|
+
/^\*$/,
|
|
2471
|
+
/^\/\*$/,
|
|
2472
|
+
/^\/health(\/|$)/,
|
|
2473
|
+
/^\/healthz(\/|$)/,
|
|
2474
|
+
/^\/ping(\/|$)/,
|
|
2475
|
+
/^\/status(\/|$)/,
|
|
2476
|
+
/^\/api\/v\d+\/auth(\/|$)/,
|
|
2477
|
+
/^\/auth(\/|$)/,
|
|
2478
|
+
/^\/login(\/|$)/,
|
|
2479
|
+
/^\/register(\/|$)/,
|
|
2480
|
+
/^\/signup(\/|$)/,
|
|
2481
|
+
/^\/forgot(\/|$)/,
|
|
2482
|
+
/^\/reset(\/|$)/,
|
|
2483
|
+
/^\/public(\/|$)/,
|
|
2484
|
+
/^\/assets(\/|$)/,
|
|
2485
|
+
/^\/static(\/|$)/,
|
|
2486
|
+
/^\/favicon(\/|$|\.ico$)/,
|
|
2487
|
+
/^\/robots(\/|$|\.txt$)/,
|
|
2488
|
+
/^\/sitemap(\/|$|\.xml$)/,
|
|
2489
|
+
/^\/\.well-known(\/|$)/
|
|
2490
|
+
];
|
|
2491
|
+
var HONO_AUTH_MODULES = /* @__PURE__ */ new Set([
|
|
2492
|
+
"hono/basic-auth",
|
|
2493
|
+
"hono/bearer-auth",
|
|
2494
|
+
"hono/jwt"
|
|
2495
|
+
]);
|
|
2496
|
+
var requireFrameworkAuth = createRule16({
|
|
2497
|
+
name: "require-framework-auth",
|
|
2498
|
+
meta: {
|
|
2499
|
+
type: "suggestion",
|
|
2500
|
+
docs: {
|
|
2501
|
+
description: "Require authentication on route handlers across Express, Fastify, Hono, NestJS, and Next.js App Router. Detects missing auth middleware, guards, or imperative auth calls."
|
|
2502
|
+
},
|
|
2503
|
+
fixable: void 0,
|
|
2504
|
+
schema: [
|
|
2505
|
+
{
|
|
2506
|
+
type: "object",
|
|
2507
|
+
properties: {
|
|
2508
|
+
knownAuthCallers: {
|
|
2509
|
+
type: "array",
|
|
2510
|
+
items: { type: "string" },
|
|
2511
|
+
description: "Additional function/middleware names recognized as auth checks."
|
|
2512
|
+
},
|
|
2513
|
+
publicRoutePatterns: {
|
|
2514
|
+
type: "array",
|
|
2515
|
+
items: { type: "string" },
|
|
2516
|
+
description: "Regex patterns for routes that should not require auth."
|
|
2517
|
+
},
|
|
2518
|
+
skipDecorators: {
|
|
2519
|
+
type: "array",
|
|
2520
|
+
items: { type: "string" },
|
|
2521
|
+
description: "NestJS decorator names that mark a handler as public."
|
|
2522
|
+
},
|
|
2523
|
+
assumeGlobalAuth: {
|
|
2524
|
+
type: "boolean",
|
|
2525
|
+
description: "If true, suppresses reports when global auth may exist."
|
|
2526
|
+
},
|
|
2527
|
+
mutatingOnly: {
|
|
2528
|
+
type: "boolean",
|
|
2529
|
+
description: "If true, only check POST/PUT/PATCH/DELETE routes."
|
|
2530
|
+
}
|
|
2531
|
+
},
|
|
2532
|
+
additionalProperties: false
|
|
2533
|
+
}
|
|
2534
|
+
],
|
|
2535
|
+
messages: {
|
|
2536
|
+
missingAuth: "Route handler `{{method}} {{path}}` has no visible authentication check. Add auth middleware or configure `knownAuthCallers`.",
|
|
2537
|
+
missingAuthNextjs: "Exported `{{method}}` handler in App Router route file has no visible auth call. Add an auth check (e.g., `auth()`, `getServerSession()`) or configure `knownAuthCallers`.",
|
|
2538
|
+
missingAuthNestjs: "Method `{{method}}` in `@Controller` class has no `@UseGuards` decorator. Add `@UseGuards(AuthGuard)` at method or class level, or mark with `@Public()`."
|
|
2539
|
+
}
|
|
2540
|
+
},
|
|
2541
|
+
defaultOptions: [{}],
|
|
2542
|
+
create(context, [options]) {
|
|
2543
|
+
const customAuthNames = new Set(options.knownAuthCallers ?? []);
|
|
2544
|
+
const allAuthNames = /* @__PURE__ */ new Set([...DEFAULT_AUTH_NAMES, ...customAuthNames]);
|
|
2545
|
+
const allNextjsCallers = /* @__PURE__ */ new Set([...DEFAULT_NEXTJS_AUTH_CALLERS, ...customAuthNames]);
|
|
2546
|
+
const skipDecoratorNames = /* @__PURE__ */ new Set([
|
|
2547
|
+
...DEFAULT_SKIP_DECORATORS,
|
|
2548
|
+
...options.skipDecorators ?? []
|
|
2549
|
+
]);
|
|
2550
|
+
const assumeGlobalAuth = options.assumeGlobalAuth ?? false;
|
|
2551
|
+
const mutatingOnly = options.mutatingOnly ?? false;
|
|
2552
|
+
const userPublicPatterns = (options.publicRoutePatterns ?? []).map((p) => safeCompileRegex(p)).filter((r) => r !== null);
|
|
2553
|
+
const allPublicPatterns = [...DEFAULT_PUBLIC_ROUTE_PATTERNS, ...userPublicPatterns];
|
|
2554
|
+
let importMap = null;
|
|
2555
|
+
let framework2 = null;
|
|
2556
|
+
let hasBlanketAuth = false;
|
|
2557
|
+
let hasHonoAuthImport = false;
|
|
2558
|
+
function getImportMap() {
|
|
2559
|
+
if (!importMap) {
|
|
2560
|
+
importMap = buildImportMap(context.sourceCode);
|
|
2561
|
+
framework2 = detectFramework(importMap, context.filename);
|
|
2562
|
+
for (const mod of HONO_AUTH_MODULES) {
|
|
2563
|
+
if (hasImport(importMap, mod)) {
|
|
2564
|
+
hasHonoAuthImport = true;
|
|
2565
|
+
break;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
return importMap;
|
|
2570
|
+
}
|
|
2571
|
+
function isPublicRoute2(path) {
|
|
2572
|
+
return allPublicPatterns.some((p) => p.test(path));
|
|
2573
|
+
}
|
|
2574
|
+
function isAuthMiddleware2(node) {
|
|
2575
|
+
if (node.type === import_utils18.AST_NODE_TYPES.Identifier) {
|
|
2576
|
+
return allAuthNames.has(node.name);
|
|
2577
|
+
}
|
|
2578
|
+
if (node.type === import_utils18.AST_NODE_TYPES.CallExpression) {
|
|
2579
|
+
return isCallToName(node, allAuthNames);
|
|
2580
|
+
}
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
function isFastifyRouteReceiver(objNode) {
|
|
2584
|
+
const obj = unwrapTSExpression(objNode);
|
|
2585
|
+
if (obj.type === import_utils18.AST_NODE_TYPES.Identifier) {
|
|
2586
|
+
const name = obj.name.toLowerCase();
|
|
2587
|
+
if (name === "router" || name === "app" || name === "fastify" || name === "server" || name.includes("router")) {
|
|
2588
|
+
return true;
|
|
2589
|
+
}
|
|
2590
|
+
if (importMap) {
|
|
2591
|
+
const sourceMod = importMap.locals.get(obj.name);
|
|
2592
|
+
if (sourceMod === "fastify") return true;
|
|
2593
|
+
}
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
return obj.type === import_utils18.AST_NODE_TYPES.CallExpression;
|
|
2597
|
+
}
|
|
2598
|
+
function isRouteDefinition2(node) {
|
|
2599
|
+
if (node.callee.type !== import_utils18.AST_NODE_TYPES.MemberExpression) return false;
|
|
2600
|
+
if (node.callee.property.type !== import_utils18.AST_NODE_TYPES.Identifier) return false;
|
|
2601
|
+
const methodName = node.callee.property.name;
|
|
2602
|
+
if (!HTTP_METHODS2.has(methodName)) return false;
|
|
2603
|
+
if (mutatingOnly && !MUTATING_METHODS.has(methodName)) return false;
|
|
2604
|
+
const obj = unwrapTSExpression(node.callee.object);
|
|
2605
|
+
if (obj.type === import_utils18.AST_NODE_TYPES.Identifier) {
|
|
2606
|
+
const name = obj.name.toLowerCase();
|
|
2607
|
+
if (name === "router" || name === "app" || name === "fastify" || name === "server" || name.includes("router")) {
|
|
2608
|
+
return true;
|
|
2609
|
+
}
|
|
2610
|
+
if (importMap) {
|
|
2611
|
+
const sourceMod = importMap.locals.get(obj.name);
|
|
2612
|
+
if (sourceMod && (sourceMod === "express" || sourceMod === "fastify" || sourceMod === "hono")) {
|
|
2613
|
+
return true;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
if (obj.type === import_utils18.AST_NODE_TYPES.CallExpression) return true;
|
|
2619
|
+
return false;
|
|
2620
|
+
}
|
|
2621
|
+
function checkBlanketAuth(node) {
|
|
2622
|
+
if (node.callee.type !== import_utils18.AST_NODE_TYPES.MemberExpression) return;
|
|
2623
|
+
if (node.callee.property.type !== import_utils18.AST_NODE_TYPES.Identifier) return;
|
|
2624
|
+
const propName = node.callee.property.name;
|
|
2625
|
+
if (propName === "use") {
|
|
2626
|
+
if (node.arguments.some((arg) => isAuthMiddleware2(arg))) {
|
|
2627
|
+
hasBlanketAuth = true;
|
|
2628
|
+
}
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (propName === "addHook" && node.arguments.length >= 2) {
|
|
2632
|
+
const hookName = node.arguments[0];
|
|
2633
|
+
if (hookName.type === import_utils18.AST_NODE_TYPES.Literal && typeof hookName.value === "string" && (hookName.value === "preHandler" || hookName.value === "onRequest")) {
|
|
2634
|
+
if (node.arguments.slice(1).some((arg) => isAuthMiddleware2(arg))) {
|
|
2635
|
+
hasBlanketAuth = true;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
function checkFastifyRouteOptions(node, _methodName) {
|
|
2641
|
+
for (const arg of node.arguments) {
|
|
2642
|
+
if (arg.type !== import_utils18.AST_NODE_TYPES.ObjectExpression) continue;
|
|
2643
|
+
for (const prop of arg.properties) {
|
|
2644
|
+
const keyName = getStaticPropKey(prop);
|
|
2645
|
+
if (keyName !== "preHandler" && keyName !== "onRequest") continue;
|
|
2646
|
+
const propValue = prop.value;
|
|
2647
|
+
if (propValue.type === import_utils18.AST_NODE_TYPES.ArrayExpression) {
|
|
2648
|
+
if (propValue.elements.some((el) => el && isAuthMiddleware2(el))) {
|
|
2649
|
+
return true;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
if (isAuthMiddleware2(propValue)) return true;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
return false;
|
|
2656
|
+
}
|
|
2657
|
+
function checkHonoOnRoute(node) {
|
|
2658
|
+
const args = node.arguments;
|
|
2659
|
+
if (args.length < 3) return;
|
|
2660
|
+
const methodArg = args[0];
|
|
2661
|
+
let methodLabel = "";
|
|
2662
|
+
let isMutating = false;
|
|
2663
|
+
const recordMethod = (s) => {
|
|
2664
|
+
const lower = s.toLowerCase();
|
|
2665
|
+
if (MUTATING_METHODS.has(lower)) isMutating = true;
|
|
2666
|
+
methodLabel = methodLabel ? `${methodLabel}|${s.toUpperCase()}` : s.toUpperCase();
|
|
2667
|
+
};
|
|
2668
|
+
if (methodArg.type === import_utils18.AST_NODE_TYPES.Literal && typeof methodArg.value === "string") {
|
|
2669
|
+
recordMethod(methodArg.value);
|
|
2670
|
+
} else if (methodArg.type === import_utils18.AST_NODE_TYPES.ArrayExpression) {
|
|
2671
|
+
if (methodArg.elements.length === 0) return;
|
|
2672
|
+
let hasUnknownMethod = false;
|
|
2673
|
+
for (const el of methodArg.elements) {
|
|
2674
|
+
if (el && el.type === import_utils18.AST_NODE_TYPES.Literal && typeof el.value === "string") {
|
|
2675
|
+
recordMethod(el.value);
|
|
2676
|
+
} else if (el) {
|
|
2677
|
+
hasUnknownMethod = true;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (hasUnknownMethod) {
|
|
2681
|
+
isMutating = true;
|
|
2682
|
+
methodLabel = methodLabel ? `${methodLabel}|<dynamic>` : "<dynamic>";
|
|
2683
|
+
}
|
|
2684
|
+
} else {
|
|
2685
|
+
isMutating = true;
|
|
2686
|
+
methodLabel = "<dynamic>";
|
|
2687
|
+
}
|
|
2688
|
+
if (mutatingOnly && !isMutating) return;
|
|
2689
|
+
const pathStr = getPathString2(args[1]);
|
|
2690
|
+
if (pathStr && isPublicRoute2(pathStr)) return;
|
|
2691
|
+
const middlewareArgs = args.slice(2, -1);
|
|
2692
|
+
if (middlewareArgs.some((arg) => isAuthMiddleware2(arg))) return;
|
|
2693
|
+
context.report({
|
|
2694
|
+
node,
|
|
2695
|
+
messageId: "missingAuth",
|
|
2696
|
+
data: { method: methodLabel || "ON", path: pathStr || "<dynamic>" }
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
function checkExpressHonoRoute(node) {
|
|
2700
|
+
const callee = node.callee;
|
|
2701
|
+
const method = callee.property.name;
|
|
2702
|
+
const args = node.arguments;
|
|
2703
|
+
function getRoutePathFromChain(start) {
|
|
2704
|
+
let current = unwrapTSExpression(start);
|
|
2705
|
+
while (current.type === import_utils18.AST_NODE_TYPES.CallExpression) {
|
|
2706
|
+
if (current.callee.type !== import_utils18.AST_NODE_TYPES.MemberExpression || current.callee.property.type !== import_utils18.AST_NODE_TYPES.Identifier) {
|
|
2707
|
+
return void 0;
|
|
2708
|
+
}
|
|
2709
|
+
const propName = current.callee.property.name;
|
|
2710
|
+
if (propName === "route") {
|
|
2711
|
+
return current.arguments[0] ? getPathString2(current.arguments[0]) : null;
|
|
2712
|
+
}
|
|
2713
|
+
if (!HTTP_METHODS2.has(propName)) return void 0;
|
|
2714
|
+
current = unwrapTSExpression(current.callee.object);
|
|
2715
|
+
}
|
|
2716
|
+
return void 0;
|
|
2717
|
+
}
|
|
2718
|
+
const inheritedPath = getRoutePathFromChain(callee.object);
|
|
2719
|
+
if (inheritedPath !== void 0) {
|
|
2720
|
+
if (inheritedPath && isPublicRoute2(inheritedPath)) return;
|
|
2721
|
+
const middlewareArgs2 = args.slice(0, -1);
|
|
2722
|
+
if (args.length === 0) return;
|
|
2723
|
+
if (args.length === 1) {
|
|
2724
|
+
context.report({
|
|
2725
|
+
node,
|
|
2726
|
+
messageId: "missingAuth",
|
|
2727
|
+
data: { method: method.toUpperCase(), path: inheritedPath || "<dynamic>" }
|
|
2728
|
+
});
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (!middlewareArgs2.some((arg) => isAuthMiddleware2(arg))) {
|
|
2732
|
+
context.report({
|
|
2733
|
+
node,
|
|
2734
|
+
messageId: "missingAuth",
|
|
2735
|
+
data: { method: method.toUpperCase(), path: inheritedPath || "<dynamic>" }
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
if (args.length < 2) return;
|
|
2741
|
+
const pathStr = getPathString2(args[0]);
|
|
2742
|
+
if (pathStr && isPublicRoute2(pathStr)) return;
|
|
2743
|
+
const middlewareArgs = args.slice(1, -1);
|
|
2744
|
+
if (middlewareArgs.length === 0 && args.length === 2) {
|
|
2745
|
+
if (hasHonoAuthImport) return;
|
|
2746
|
+
context.report({
|
|
2747
|
+
node,
|
|
2748
|
+
messageId: "missingAuth",
|
|
2749
|
+
data: { method: method.toUpperCase(), path: pathStr || "<dynamic>" }
|
|
2750
|
+
});
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
if (!middlewareArgs.some((arg) => isAuthMiddleware2(arg))) {
|
|
2754
|
+
context.report({
|
|
2755
|
+
node,
|
|
2756
|
+
messageId: "missingAuth",
|
|
2757
|
+
data: { method: method.toUpperCase(), path: pathStr || "<dynamic>" }
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
function checkFastifyRoute(node) {
|
|
2762
|
+
const callee = node.callee;
|
|
2763
|
+
const method = callee.property.name;
|
|
2764
|
+
const args = node.arguments;
|
|
2765
|
+
if (args.length < 1) return;
|
|
2766
|
+
if (method === "route") {
|
|
2767
|
+
const optsArg = args[0];
|
|
2768
|
+
if (optsArg.type === import_utils18.AST_NODE_TYPES.ObjectExpression) {
|
|
2769
|
+
let routeMethod = "";
|
|
2770
|
+
let routeUrl = "";
|
|
2771
|
+
for (const prop of optsArg.properties) {
|
|
2772
|
+
const keyName = getStaticPropKey(prop);
|
|
2773
|
+
if (keyName === null) continue;
|
|
2774
|
+
const propValue = prop.value;
|
|
2775
|
+
if (keyName === "method" && propValue.type === import_utils18.AST_NODE_TYPES.Literal) {
|
|
2776
|
+
routeMethod = String(propValue.value).toUpperCase();
|
|
2777
|
+
}
|
|
2778
|
+
if (keyName === "url") {
|
|
2779
|
+
routeUrl = getPathString2(propValue) || "<dynamic>";
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
if (routeUrl && isPublicRoute2(routeUrl)) return;
|
|
2783
|
+
if (mutatingOnly && !MUTATING_METHODS.has(routeMethod.toLowerCase())) return;
|
|
2784
|
+
if (!checkFastifyRouteOptions(node, routeMethod)) {
|
|
2785
|
+
context.report({
|
|
2786
|
+
node,
|
|
2787
|
+
messageId: "missingAuth",
|
|
2788
|
+
data: { method: routeMethod || "ROUTE", path: routeUrl || "<dynamic>" }
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
if (args.length < 2) return;
|
|
2795
|
+
const pathStr = getPathString2(args[0]);
|
|
2796
|
+
if (pathStr && isPublicRoute2(pathStr)) return;
|
|
2797
|
+
if (checkFastifyRouteOptions(node, method)) return;
|
|
2798
|
+
const middlewareArgs = args.slice(1, -1);
|
|
2799
|
+
if (middlewareArgs.some((arg) => isAuthMiddleware2(arg))) return;
|
|
2800
|
+
context.report({
|
|
2801
|
+
node,
|
|
2802
|
+
messageId: "missingAuth",
|
|
2803
|
+
data: { method: method.toUpperCase(), path: pathStr || "<dynamic>" }
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
return {
|
|
2807
|
+
CallExpression(node) {
|
|
2808
|
+
getImportMap();
|
|
2809
|
+
if (assumeGlobalAuth) return;
|
|
2810
|
+
if (framework2 === "nestjs" || framework2 === "nextjs-app-router") return;
|
|
2811
|
+
checkBlanketAuth(node);
|
|
2812
|
+
if (hasBlanketAuth) return;
|
|
2813
|
+
if (framework2 === "hono" && node.callee.type === import_utils18.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils18.AST_NODE_TYPES.Identifier && node.callee.property.name === "on") {
|
|
2814
|
+
checkHonoOnRoute(node);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
if (framework2 === "fastify" && node.callee.type === import_utils18.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils18.AST_NODE_TYPES.Identifier && node.callee.property.name === "route" && isFastifyRouteReceiver(node.callee.object)) {
|
|
2818
|
+
checkFastifyRoute(node);
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
if (!isRouteDefinition2(node)) return;
|
|
2822
|
+
if (framework2 === "fastify") {
|
|
2823
|
+
checkFastifyRoute(node);
|
|
2824
|
+
} else {
|
|
2825
|
+
checkExpressHonoRoute(node);
|
|
2826
|
+
}
|
|
2827
|
+
},
|
|
2828
|
+
ClassDeclaration(node) {
|
|
2829
|
+
getImportMap();
|
|
2830
|
+
if (framework2 !== "nestjs") return;
|
|
2831
|
+
if (assumeGlobalAuth) return;
|
|
2832
|
+
if (!hasDecoratorNamed(node, /* @__PURE__ */ new Set(["Controller"]))) return;
|
|
2833
|
+
const classHasGuards = hasDecoratorNamed(node, /* @__PURE__ */ new Set(["UseGuards"]));
|
|
2834
|
+
for (const member of node.body.body) {
|
|
2835
|
+
if (member.type !== import_utils18.AST_NODE_TYPES.MethodDefinition) continue;
|
|
2836
|
+
if (member.kind !== "method") continue;
|
|
2837
|
+
if (member.static) continue;
|
|
2838
|
+
const httpDecorators = member.decorators?.filter((d) => {
|
|
2839
|
+
const name = getDecoratorCallName(d);
|
|
2840
|
+
return name !== null && NESTJS_HTTP_DECORATORS.has(name);
|
|
2841
|
+
}) ?? [];
|
|
2842
|
+
if (httpDecorators.length === 0) continue;
|
|
2843
|
+
const httpDecName = getDecoratorCallName(httpDecorators[0]) ?? "unknown";
|
|
2844
|
+
if (mutatingOnly && !NESTJS_MUTATING_DECORATORS.has(httpDecName)) continue;
|
|
2845
|
+
if (hasDecoratorNamed(member, skipDecoratorNames)) continue;
|
|
2846
|
+
if (classHasGuards) continue;
|
|
2847
|
+
if (hasDecoratorNamed(member, /* @__PURE__ */ new Set(["UseGuards"]))) continue;
|
|
2848
|
+
const methodName = member.key.type === import_utils18.AST_NODE_TYPES.Identifier ? member.key.name : "<computed>";
|
|
2849
|
+
context.report({
|
|
2850
|
+
node: member,
|
|
2851
|
+
messageId: "missingAuthNestjs",
|
|
2852
|
+
data: { method: methodName }
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
},
|
|
2856
|
+
ExportNamedDeclaration(node) {
|
|
2857
|
+
getImportMap();
|
|
2858
|
+
if (framework2 !== "nextjs-app-router") return;
|
|
2859
|
+
if (assumeGlobalAuth) return;
|
|
2860
|
+
const decl = node.declaration;
|
|
2861
|
+
if (!decl) return;
|
|
2862
|
+
if (decl.type === import_utils18.AST_NODE_TYPES.FunctionDeclaration && decl.id && NEXTJS_EXPORT_METHODS.has(decl.id.name)) {
|
|
2863
|
+
const method = decl.id.name;
|
|
2864
|
+
if (mutatingOnly && method === "GET") return;
|
|
2865
|
+
if (decl.body && !bodyContainsCallTo(decl.body, allNextjsCallers)) {
|
|
2866
|
+
context.report({
|
|
2867
|
+
node: decl,
|
|
2868
|
+
messageId: "missingAuthNextjs",
|
|
2869
|
+
data: { method }
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
if (decl.type === import_utils18.AST_NODE_TYPES.VariableDeclaration) {
|
|
2874
|
+
for (const declarator of decl.declarations) {
|
|
2875
|
+
if (declarator.id.type === import_utils18.AST_NODE_TYPES.Identifier && NEXTJS_EXPORT_METHODS.has(declarator.id.name) && declarator.init) {
|
|
2876
|
+
const method = declarator.id.name;
|
|
2877
|
+
if (mutatingOnly && method === "GET") continue;
|
|
2878
|
+
const init = declarator.init;
|
|
2879
|
+
if (init.type === import_utils18.AST_NODE_TYPES.ArrowFunctionExpression || init.type === import_utils18.AST_NODE_TYPES.FunctionExpression) {
|
|
2880
|
+
if (!bodyContainsCallTo(init.body, allNextjsCallers)) {
|
|
2881
|
+
context.report({
|
|
2882
|
+
node: declarator,
|
|
2883
|
+
messageId: "missingAuthNextjs",
|
|
2884
|
+
data: { method }
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
|
|
2896
|
+
// src/rules/security/require-framework-authz.ts
|
|
2897
|
+
var import_utils19 = require("@typescript-eslint/utils");
|
|
2898
|
+
var createRule17 = import_utils19.ESLintUtils.RuleCreator(
|
|
2899
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
2900
|
+
);
|
|
2901
|
+
var ROUTE_METHODS2 = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete"]);
|
|
2902
|
+
var DEFAULT_AUTHZ_HELPERS = /* @__PURE__ */ new Set([
|
|
2903
|
+
"authorize",
|
|
2904
|
+
"authorise",
|
|
2905
|
+
"checkOwnership",
|
|
2906
|
+
"ensureOwner",
|
|
2907
|
+
"isOwner",
|
|
2908
|
+
"canAccess",
|
|
2909
|
+
"canModify",
|
|
2910
|
+
"hasAccess",
|
|
2911
|
+
"checkPermission",
|
|
2912
|
+
"checkPermissions"
|
|
2913
|
+
]);
|
|
2914
|
+
var CASL_METHODS = /* @__PURE__ */ new Set(["can", "cannot", "throwUnlessCan"]);
|
|
2915
|
+
var CASL_MODULES = /* @__PURE__ */ new Set(["@casl/ability"]);
|
|
2916
|
+
var CASBIN_METHODS = /* @__PURE__ */ new Set(["enforce", "enforceSync"]);
|
|
2917
|
+
var CASBIN_MODULES = /* @__PURE__ */ new Set(["casbin"]);
|
|
2918
|
+
var CERBOS_METHODS = /* @__PURE__ */ new Set(["checkResource", "checkResources", "isAllowed"]);
|
|
2919
|
+
var CERBOS_MODULES = /* @__PURE__ */ new Set(["@cerbos/grpc", "@cerbos/http", "@cerbos/core"]);
|
|
2920
|
+
var PERMIT_METHODS = /* @__PURE__ */ new Set(["check"]);
|
|
2921
|
+
var PERMIT_MODULES = /* @__PURE__ */ new Set(["permitio"]);
|
|
2922
|
+
function isRouteRegistration(node) {
|
|
2923
|
+
if (node.callee.type !== import_utils19.AST_NODE_TYPES.MemberExpression) return false;
|
|
2924
|
+
if (node.callee.property.type !== import_utils19.AST_NODE_TYPES.Identifier) return false;
|
|
2925
|
+
if (!ROUTE_METHODS2.has(node.callee.property.name)) return false;
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
function hasPathPrefix2(path, prefix) {
|
|
2929
|
+
if (!path || path.length < prefix.length) return false;
|
|
2930
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
2931
|
+
if (path[i] !== prefix[i]) return false;
|
|
2932
|
+
}
|
|
2933
|
+
return true;
|
|
2934
|
+
}
|
|
2935
|
+
var REQUEST_NAMES = ["req", "request"];
|
|
2936
|
+
var RESOURCE_FIELDS = ["params", "body", "query"];
|
|
2937
|
+
function isLikelyResourceIdPath2(path) {
|
|
2938
|
+
if (!path || path.length < 3) return false;
|
|
2939
|
+
let matched = false;
|
|
2940
|
+
for (const reqName of REQUEST_NAMES) {
|
|
2941
|
+
for (const field of RESOURCE_FIELDS) {
|
|
2942
|
+
if (hasPathPrefix2(path, [reqName, field])) {
|
|
2943
|
+
matched = true;
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
if (matched) break;
|
|
2948
|
+
}
|
|
2949
|
+
if (!matched) return false;
|
|
2950
|
+
const last = path[path.length - 1].toLowerCase();
|
|
2951
|
+
return last === "id" || last.endsWith("id");
|
|
2952
|
+
}
|
|
2953
|
+
function isReqUserPath2(path) {
|
|
2954
|
+
return hasPathPrefix2(path, ["req", "user"]) || hasPathPrefix2(path, ["request", "user"]);
|
|
2955
|
+
}
|
|
2956
|
+
var requireFrameworkAuthz = createRule17({
|
|
2957
|
+
name: "require-framework-authz",
|
|
2958
|
+
meta: {
|
|
2959
|
+
type: "suggestion",
|
|
2960
|
+
docs: {
|
|
2961
|
+
description: "Require a visible authorization/ownership check when route handlers access resource identifiers. Supports CASL, Casbin, Cerbos, Permit.io, and custom authz helpers."
|
|
2962
|
+
},
|
|
2963
|
+
fixable: void 0,
|
|
2964
|
+
schema: [
|
|
2965
|
+
{
|
|
2966
|
+
type: "object",
|
|
2967
|
+
properties: {
|
|
2968
|
+
authzHelperNames: {
|
|
2969
|
+
type: "array",
|
|
2970
|
+
items: { type: "string" },
|
|
2971
|
+
description: "Additional function names recognized as authorization checks."
|
|
2972
|
+
}
|
|
2973
|
+
},
|
|
2974
|
+
additionalProperties: false
|
|
2975
|
+
}
|
|
2976
|
+
],
|
|
2977
|
+
messages: {
|
|
2978
|
+
missingAuthz: "This handler accesses resource identifiers (`{{access}}`) but has no visible authorization check. Add an ownership guard, policy check (CASL, Casbin, Cerbos), or configure `authzHelperNames`."
|
|
2979
|
+
}
|
|
2980
|
+
},
|
|
2981
|
+
defaultOptions: [{}],
|
|
2982
|
+
create(context, [options]) {
|
|
2983
|
+
const customHelpers = new Set(options.authzHelperNames ?? []);
|
|
2984
|
+
const allHelpers = /* @__PURE__ */ new Set([...DEFAULT_AUTHZ_HELPERS, ...customHelpers]);
|
|
2985
|
+
let importMap = null;
|
|
2986
|
+
let hasCasl = false;
|
|
2987
|
+
let hasCasbin = false;
|
|
2988
|
+
let hasCerbos = false;
|
|
2989
|
+
let hasPermit = false;
|
|
2990
|
+
function getImports() {
|
|
2991
|
+
if (importMap) return;
|
|
2992
|
+
importMap = buildImportMap(context.sourceCode);
|
|
2993
|
+
for (const mod of CASL_MODULES) {
|
|
2994
|
+
if (hasImport(importMap, mod)) hasCasl = true;
|
|
2995
|
+
}
|
|
2996
|
+
for (const mod of CASBIN_MODULES) {
|
|
2997
|
+
if (hasImport(importMap, mod)) hasCasbin = true;
|
|
2998
|
+
}
|
|
2999
|
+
for (const mod of CERBOS_MODULES) {
|
|
3000
|
+
if (hasImport(importMap, mod)) hasCerbos = true;
|
|
3001
|
+
}
|
|
3002
|
+
for (const mod of PERMIT_MODULES) {
|
|
3003
|
+
if (hasImport(importMap, mod)) hasPermit = true;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
function hasAuthzCall(body) {
|
|
3007
|
+
return walkForAuthz(body, /* @__PURE__ */ new WeakSet());
|
|
3008
|
+
}
|
|
3009
|
+
function walkForAuthz(node, seen) {
|
|
3010
|
+
if (seen.has(node)) return false;
|
|
3011
|
+
seen.add(node);
|
|
3012
|
+
if (node.type === import_utils19.AST_NODE_TYPES.CallExpression) {
|
|
3013
|
+
if (node.callee.type === import_utils19.AST_NODE_TYPES.Identifier && allHelpers.has(node.callee.name)) {
|
|
3014
|
+
return true;
|
|
3015
|
+
}
|
|
3016
|
+
if (node.callee.type === import_utils19.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils19.AST_NODE_TYPES.Identifier) {
|
|
3017
|
+
const method = node.callee.property.name;
|
|
3018
|
+
if (allHelpers.has(method)) return true;
|
|
3019
|
+
if (hasCasl && CASL_METHODS.has(method)) return true;
|
|
3020
|
+
if (hasCasbin && CASBIN_METHODS.has(method)) return true;
|
|
3021
|
+
if (hasCerbos && CERBOS_METHODS.has(method)) return true;
|
|
3022
|
+
if (hasPermit && PERMIT_METHODS.has(method)) return true;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
if (node.type === import_utils19.AST_NODE_TYPES.BinaryExpression && ["===", "==", "!==", "!="].includes(node.operator)) {
|
|
3026
|
+
const leftPath = getMemberPath2(node.left);
|
|
3027
|
+
const rightPath = getMemberPath2(node.right);
|
|
3028
|
+
const leftIsUser = isReqUserPath2(leftPath);
|
|
3029
|
+
const rightIsUser = isReqUserPath2(rightPath);
|
|
3030
|
+
const leftIsResource = isLikelyResourceIdPath2(leftPath);
|
|
3031
|
+
const rightIsResource = isLikelyResourceIdPath2(rightPath);
|
|
3032
|
+
if (leftIsUser && rightIsResource || rightIsUser && leftIsResource) {
|
|
3033
|
+
return true;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
for (const key of Object.keys(node)) {
|
|
3037
|
+
if (AST_SKIP_KEYS.has(key)) continue;
|
|
3038
|
+
const value = node[key];
|
|
3039
|
+
if (Array.isArray(value)) {
|
|
3040
|
+
for (const child of value) {
|
|
3041
|
+
if (isASTNode(child) && !STOP_DESCENT_NODE_TYPES.has(child.type)) {
|
|
3042
|
+
if (walkForAuthz(child, seen)) return true;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
} else if (isASTNode(value) && !STOP_DESCENT_NODE_TYPES.has(value.type)) {
|
|
3046
|
+
if (walkForAuthz(value, seen)) return true;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return false;
|
|
3050
|
+
}
|
|
3051
|
+
function findResourceAccess(body) {
|
|
3052
|
+
return walkForResourceAccess(body, /* @__PURE__ */ new WeakSet());
|
|
3053
|
+
}
|
|
3054
|
+
function walkForResourceAccess(node, seen) {
|
|
3055
|
+
if (seen.has(node)) return null;
|
|
3056
|
+
seen.add(node);
|
|
3057
|
+
if (node.type === import_utils19.AST_NODE_TYPES.MemberExpression) {
|
|
3058
|
+
const path = getMemberPath2(node);
|
|
3059
|
+
if (isLikelyResourceIdPath2(path)) {
|
|
3060
|
+
return path.join(".");
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
if (node.type === import_utils19.AST_NODE_TYPES.VariableDeclarator && node.id.type === import_utils19.AST_NODE_TYPES.ObjectPattern && node.init) {
|
|
3064
|
+
const initPath = getMemberPath2(node.init);
|
|
3065
|
+
if (initPath && initPath.length === 2) {
|
|
3066
|
+
const sourceMatches = REQUEST_NAMES.includes(initPath[0]) && RESOURCE_FIELDS.includes(initPath[1]);
|
|
3067
|
+
if (sourceMatches) {
|
|
3068
|
+
for (const prop of node.id.properties) {
|
|
3069
|
+
if (prop.type !== import_utils19.AST_NODE_TYPES.Property) continue;
|
|
3070
|
+
let sourceKey = null;
|
|
3071
|
+
if (prop.key.type === import_utils19.AST_NODE_TYPES.Identifier) {
|
|
3072
|
+
sourceKey = prop.key.name;
|
|
3073
|
+
} else if (prop.key.type === import_utils19.AST_NODE_TYPES.Literal && typeof prop.key.value === "string") {
|
|
3074
|
+
sourceKey = prop.key.value;
|
|
3075
|
+
}
|
|
3076
|
+
let bindingName = null;
|
|
3077
|
+
if (prop.value.type === import_utils19.AST_NODE_TYPES.Identifier) {
|
|
3078
|
+
bindingName = prop.value.name;
|
|
3079
|
+
} else if (prop.value.type === import_utils19.AST_NODE_TYPES.AssignmentPattern && prop.value.left.type === import_utils19.AST_NODE_TYPES.Identifier) {
|
|
3080
|
+
bindingName = prop.value.left.name;
|
|
3081
|
+
}
|
|
3082
|
+
const isIdLike = (n) => n !== null && (n.toLowerCase() === "id" || n.toLowerCase().endsWith("id"));
|
|
3083
|
+
if (isIdLike(sourceKey) || isIdLike(bindingName)) {
|
|
3084
|
+
const reportKey = sourceKey ?? bindingName ?? "<unknown>";
|
|
3085
|
+
return `${initPath[0]}.${initPath[1]}.${reportKey}`;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
for (const key of Object.keys(node)) {
|
|
3092
|
+
if (AST_SKIP_KEYS.has(key)) continue;
|
|
3093
|
+
const value = node[key];
|
|
3094
|
+
if (Array.isArray(value)) {
|
|
3095
|
+
for (const child of value) {
|
|
3096
|
+
if (isASTNode(child) && !STOP_DESCENT_NODE_TYPES.has(child.type)) {
|
|
3097
|
+
const found = walkForResourceAccess(child, seen);
|
|
3098
|
+
if (found) return found;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
} else if (isASTNode(value) && !STOP_DESCENT_NODE_TYPES.has(value.type)) {
|
|
3102
|
+
const found = walkForResourceAccess(value, seen);
|
|
3103
|
+
if (found) return found;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return null;
|
|
3107
|
+
}
|
|
3108
|
+
return {
|
|
3109
|
+
CallExpression(node) {
|
|
3110
|
+
getImports();
|
|
3111
|
+
if (!isRouteRegistration(node)) return;
|
|
3112
|
+
const routePath = node.arguments[0] ? getPathString2(node.arguments[0]) : null;
|
|
3113
|
+
const hasRouteIdParam = typeof routePath === "string" && routePath.includes(":");
|
|
3114
|
+
for (const arg of node.arguments) {
|
|
3115
|
+
if (arg.type !== import_utils19.AST_NODE_TYPES.FunctionExpression && arg.type !== import_utils19.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
const resourceAccess = findResourceAccess(arg.body);
|
|
3119
|
+
const isSensitive = hasRouteIdParam && resourceAccess !== null;
|
|
3120
|
+
if (isSensitive && !hasAuthzCall(arg.body)) {
|
|
3121
|
+
context.report({
|
|
3122
|
+
node: arg,
|
|
3123
|
+
messageId: "missingAuthz",
|
|
3124
|
+
data: { access: resourceAccess }
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3133
|
+
// src/rules/security/require-webhook-signature.ts
|
|
3134
|
+
var import_utils20 = require("@typescript-eslint/utils");
|
|
3135
|
+
var createRule18 = import_utils20.ESLintUtils.RuleCreator(
|
|
3136
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
3137
|
+
);
|
|
3138
|
+
var HTTP_METHODS3 = /* @__PURE__ */ new Set([
|
|
3139
|
+
"get",
|
|
3140
|
+
"post",
|
|
3141
|
+
"put",
|
|
3142
|
+
"patch",
|
|
3143
|
+
"delete",
|
|
3144
|
+
"all"
|
|
3145
|
+
]);
|
|
3146
|
+
var DEFAULT_WEBHOOK_PATTERNS = [
|
|
3147
|
+
/\/webhook/i,
|
|
3148
|
+
/\/webhooks/i,
|
|
3149
|
+
/\/api\/webhook/i,
|
|
3150
|
+
/\/api\/webhooks/i
|
|
3151
|
+
];
|
|
3152
|
+
var VERIFICATION_METHODS = /* @__PURE__ */ new Set([
|
|
3153
|
+
"constructEvent",
|
|
3154
|
+
"constructEventAsync",
|
|
3155
|
+
"verify",
|
|
3156
|
+
"timingSafeEqual",
|
|
3157
|
+
"createSlackEventAdapter"
|
|
3158
|
+
]);
|
|
3159
|
+
var VERIFICATION_CALLERS = /* @__PURE__ */ new Set([
|
|
3160
|
+
"stripe.webhooks.constructEvent",
|
|
3161
|
+
"stripe.webhooks.constructEventAsync",
|
|
3162
|
+
"crypto.timingSafeEqual",
|
|
3163
|
+
"crypto.createHmac"
|
|
3164
|
+
]);
|
|
3165
|
+
var requireWebhookSignature = createRule18({
|
|
3166
|
+
name: "require-webhook-signature",
|
|
3167
|
+
meta: {
|
|
3168
|
+
type: "problem",
|
|
3169
|
+
docs: {
|
|
3170
|
+
description: "Require signature verification in webhook handlers. Detects Stripe, GitHub, Svix, and Slack patterns. Unverified webhooks allow spoofed payloads."
|
|
3171
|
+
},
|
|
3172
|
+
fixable: void 0,
|
|
3173
|
+
schema: [
|
|
3174
|
+
{
|
|
3175
|
+
type: "object",
|
|
3176
|
+
properties: {
|
|
3177
|
+
webhookRoutePatterns: {
|
|
3178
|
+
type: "array",
|
|
3179
|
+
items: { type: "string" },
|
|
3180
|
+
description: "Additional regex patterns for webhook route paths."
|
|
3181
|
+
},
|
|
3182
|
+
verificationFunctions: {
|
|
3183
|
+
type: "array",
|
|
3184
|
+
items: { type: "string" },
|
|
3185
|
+
description: "Additional function names recognized as signature verification."
|
|
3186
|
+
}
|
|
3187
|
+
},
|
|
3188
|
+
additionalProperties: false
|
|
3189
|
+
}
|
|
3190
|
+
],
|
|
3191
|
+
messages: {
|
|
3192
|
+
missingWebhookSig: "Webhook handler `{{method}} {{path}}` has no visible signature verification. Verify the payload signature (e.g., `stripe.webhooks.constructEvent()`, `crypto.timingSafeEqual()`) to prevent spoofing."
|
|
3193
|
+
}
|
|
3194
|
+
},
|
|
3195
|
+
defaultOptions: [{}],
|
|
3196
|
+
create(context, [options]) {
|
|
3197
|
+
const userPatterns = (options.webhookRoutePatterns ?? []).map((p) => {
|
|
3198
|
+
const safe = safeCompileRegex(p);
|
|
3199
|
+
if (!safe) return null;
|
|
3200
|
+
return new RegExp(safe.source, "i");
|
|
3201
|
+
}).filter((r) => r !== null);
|
|
3202
|
+
const allPatterns = [...DEFAULT_WEBHOOK_PATTERNS, ...userPatterns];
|
|
3203
|
+
const userVerifyFns = new Set(options.verificationFunctions ?? []);
|
|
3204
|
+
const allVerifyMethods = /* @__PURE__ */ new Set([...VERIFICATION_METHODS, ...userVerifyFns]);
|
|
3205
|
+
const allVerifyCallers = /* @__PURE__ */ new Set([...VERIFICATION_CALLERS, ...userVerifyFns]);
|
|
3206
|
+
let importMap = null;
|
|
3207
|
+
let hasSvix = false;
|
|
3208
|
+
let hasOctokit = false;
|
|
3209
|
+
function getImports() {
|
|
3210
|
+
if (importMap) return;
|
|
3211
|
+
importMap = buildImportMap(context.sourceCode);
|
|
3212
|
+
hasSvix = hasImport(importMap, "svix");
|
|
3213
|
+
hasOctokit = hasImport(importMap, "@octokit/webhooks");
|
|
3214
|
+
}
|
|
3215
|
+
function isWebhookRoute(path) {
|
|
3216
|
+
return allPatterns.some((p) => p.test(path));
|
|
3217
|
+
}
|
|
3218
|
+
function isWebhookFile() {
|
|
3219
|
+
const fn = context.filename.toLowerCase();
|
|
3220
|
+
if (/[/\\]__tests__[/\\]/.test(fn) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(fn) || /[/\\](tests?|fixtures?|mocks?|__mocks__)[/\\]/.test(fn)) {
|
|
3221
|
+
return false;
|
|
3222
|
+
}
|
|
3223
|
+
return fn.includes("webhook");
|
|
3224
|
+
}
|
|
3225
|
+
function handlerHasVerification(body) {
|
|
3226
|
+
return walkForVerification(body, /* @__PURE__ */ new WeakSet());
|
|
3227
|
+
}
|
|
3228
|
+
function walkForVerification(node, seen) {
|
|
3229
|
+
if (seen.has(node)) return false;
|
|
3230
|
+
seen.add(node);
|
|
3231
|
+
if (node.type === import_utils20.AST_NODE_TYPES.CallExpression) {
|
|
3232
|
+
if (node.callee.type === import_utils20.AST_NODE_TYPES.Identifier && allVerifyMethods.has(node.callee.name)) {
|
|
3233
|
+
return true;
|
|
3234
|
+
}
|
|
3235
|
+
if (node.callee.type === import_utils20.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils20.AST_NODE_TYPES.Identifier) {
|
|
3236
|
+
const method = node.callee.property.name;
|
|
3237
|
+
if (allVerifyMethods.has(method)) {
|
|
3238
|
+
if (method === "verify") {
|
|
3239
|
+
return isLocalFromWebhookLib(node.callee.object);
|
|
3240
|
+
}
|
|
3241
|
+
return true;
|
|
3242
|
+
}
|
|
3243
|
+
const path = buildCallPath(node.callee);
|
|
3244
|
+
if (path && allVerifyCallers.has(path)) return true;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
for (const key of Object.keys(node)) {
|
|
3248
|
+
if (AST_SKIP_KEYS.has(key)) continue;
|
|
3249
|
+
const value = node[key];
|
|
3250
|
+
if (Array.isArray(value)) {
|
|
3251
|
+
for (const child of value) {
|
|
3252
|
+
if (isASTNode(child) && !STOP_DESCENT_NODE_TYPES.has(child.type)) {
|
|
3253
|
+
if (walkForVerification(child, seen)) return true;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
} else if (isASTNode(value) && !STOP_DESCENT_NODE_TYPES.has(value.type)) {
|
|
3257
|
+
if (walkForVerification(value, seen)) return true;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
return false;
|
|
3261
|
+
}
|
|
3262
|
+
function isLocalFromWebhookLib(node) {
|
|
3263
|
+
if (node.type === import_utils20.AST_NODE_TYPES.Identifier && importMap) {
|
|
3264
|
+
if (localComesFrom(importMap, node.name, "svix") || localComesFrom(importMap, node.name, "@octokit/webhooks")) {
|
|
3265
|
+
return true;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
if (node.type === import_utils20.AST_NODE_TYPES.NewExpression && importMap) {
|
|
3269
|
+
if (node.callee.type === import_utils20.AST_NODE_TYPES.Identifier) {
|
|
3270
|
+
if (localComesFrom(importMap, node.callee.name, "svix") || localComesFrom(importMap, node.callee.name, "@octokit/webhooks")) {
|
|
3271
|
+
return true;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
if (!hasSvix && !hasOctokit) return false;
|
|
3276
|
+
if (node.type === import_utils20.AST_NODE_TYPES.Identifier) {
|
|
3277
|
+
return isWebhookBindingName(node.name);
|
|
3278
|
+
}
|
|
3279
|
+
if (node.type === import_utils20.AST_NODE_TYPES.MemberExpression && node.property.type === import_utils20.AST_NODE_TYPES.Identifier) {
|
|
3280
|
+
return isWebhookBindingName(node.property.name);
|
|
3281
|
+
}
|
|
3282
|
+
return false;
|
|
3283
|
+
}
|
|
3284
|
+
function isWebhookBindingName(name) {
|
|
3285
|
+
const lower = name.toLowerCase();
|
|
3286
|
+
return lower === "wh" || lower === "webhook" || lower === "webhooks" || lower === "hook" || lower === "svix" || lower === "octokit" || lower.endsWith("webhook") || lower.endsWith("webhooks");
|
|
3287
|
+
}
|
|
3288
|
+
function buildCallPath(node) {
|
|
3289
|
+
const parts = [];
|
|
3290
|
+
let current = node;
|
|
3291
|
+
while (current.type === import_utils20.AST_NODE_TYPES.MemberExpression && current.property.type === import_utils20.AST_NODE_TYPES.Identifier && !current.computed) {
|
|
3292
|
+
parts.unshift(current.property.name);
|
|
3293
|
+
current = current.object;
|
|
3294
|
+
}
|
|
3295
|
+
if (current.type === import_utils20.AST_NODE_TYPES.Identifier) {
|
|
3296
|
+
parts.unshift(current.name);
|
|
3297
|
+
return parts.join(".");
|
|
3298
|
+
}
|
|
3299
|
+
return null;
|
|
3300
|
+
}
|
|
3301
|
+
function getRoutePathFromChain(start) {
|
|
3302
|
+
let current = start;
|
|
3303
|
+
while (current.type === import_utils20.AST_NODE_TYPES.CallExpression) {
|
|
3304
|
+
if (current.callee.type !== import_utils20.AST_NODE_TYPES.MemberExpression || current.callee.property.type !== import_utils20.AST_NODE_TYPES.Identifier) {
|
|
3305
|
+
return void 0;
|
|
3306
|
+
}
|
|
3307
|
+
const propName = current.callee.property.name;
|
|
3308
|
+
if (propName === "route") {
|
|
3309
|
+
return current.arguments[0] ? getPathString2(current.arguments[0]) : null;
|
|
3310
|
+
}
|
|
3311
|
+
if (!HTTP_METHODS3.has(propName)) return void 0;
|
|
3312
|
+
current = current.callee.object;
|
|
3313
|
+
}
|
|
3314
|
+
return void 0;
|
|
3315
|
+
}
|
|
3316
|
+
return {
|
|
3317
|
+
CallExpression(node) {
|
|
3318
|
+
getImports();
|
|
3319
|
+
if (node.callee.type !== import_utils20.AST_NODE_TYPES.MemberExpression) return;
|
|
3320
|
+
if (node.callee.property.type !== import_utils20.AST_NODE_TYPES.Identifier) return;
|
|
3321
|
+
const methodName = node.callee.property.name;
|
|
3322
|
+
if (!HTTP_METHODS3.has(methodName)) return;
|
|
3323
|
+
const args = node.arguments;
|
|
3324
|
+
let pathStr = null;
|
|
3325
|
+
let firstHandlerArgIndex = 1;
|
|
3326
|
+
const inheritedPath = getRoutePathFromChain(node.callee.object);
|
|
3327
|
+
if (inheritedPath !== void 0) {
|
|
3328
|
+
if (args.length < 1) return;
|
|
3329
|
+
pathStr = inheritedPath;
|
|
3330
|
+
firstHandlerArgIndex = 0;
|
|
3331
|
+
} else {
|
|
3332
|
+
if (args.length < 2) return;
|
|
3333
|
+
pathStr = getPathString2(args[0]);
|
|
3334
|
+
}
|
|
3335
|
+
const routeIsWebhook = pathStr ? isWebhookRoute(pathStr) : isWebhookFile();
|
|
3336
|
+
if (!routeIsWebhook) return;
|
|
3337
|
+
for (let i = args.length - 1; i >= firstHandlerArgIndex; i--) {
|
|
3338
|
+
const arg = args[i];
|
|
3339
|
+
if (arg.type === import_utils20.AST_NODE_TYPES.FunctionExpression || arg.type === import_utils20.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
3340
|
+
if (!handlerHasVerification(arg.body)) {
|
|
3341
|
+
context.report({
|
|
3342
|
+
node,
|
|
3343
|
+
messageId: "missingWebhookSig",
|
|
3344
|
+
data: {
|
|
3345
|
+
method: methodName.toUpperCase(),
|
|
3346
|
+
path: pathStr || "<dynamic>"
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
// src/rules/quality/no-console-in-handler.ts
|
|
3359
|
+
var import_utils21 = require("@typescript-eslint/utils");
|
|
3360
|
+
var createRule19 = import_utils21.ESLintUtils.RuleCreator(
|
|
3361
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
3362
|
+
);
|
|
3363
|
+
var ROUTE_METHODS3 = [
|
|
3364
|
+
"get",
|
|
3365
|
+
"post",
|
|
3366
|
+
"put",
|
|
3367
|
+
"patch",
|
|
3368
|
+
"delete",
|
|
3369
|
+
"options",
|
|
3370
|
+
"all"
|
|
3371
|
+
];
|
|
3372
|
+
function isRouteRegistrationCall2(node) {
|
|
3373
|
+
if (node.callee.type !== import_utils21.AST_NODE_TYPES.MemberExpression || node.callee.property.type !== import_utils21.AST_NODE_TYPES.Identifier) {
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
const methodName = node.callee.property.name;
|
|
3377
|
+
if (!ROUTE_METHODS3.includes(methodName)) {
|
|
3378
|
+
return false;
|
|
3379
|
+
}
|
|
3380
|
+
if (node.arguments.length === 0) {
|
|
3381
|
+
return false;
|
|
3382
|
+
}
|
|
3383
|
+
const firstArg = node.arguments[0];
|
|
3384
|
+
if (firstArg.type === import_utils21.AST_NODE_TYPES.Literal && typeof firstArg.value === "string" && firstArg.value.startsWith("/")) {
|
|
3385
|
+
return true;
|
|
3386
|
+
}
|
|
3387
|
+
if (firstArg.type === import_utils21.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis[0]?.value.raw.startsWith("/")) {
|
|
3388
|
+
return true;
|
|
3389
|
+
}
|
|
3390
|
+
return false;
|
|
3391
|
+
}
|
|
3392
|
+
function traverseForConsoleCalls(node, onConsoleCall) {
|
|
3393
|
+
if (node.type === import_utils21.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils21.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils21.AST_NODE_TYPES.Identifier && node.callee.object.name === "console" && node.callee.property.type === import_utils21.AST_NODE_TYPES.Identifier) {
|
|
3394
|
+
onConsoleCall(node);
|
|
3395
|
+
}
|
|
3396
|
+
if (node.type === import_utils21.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils21.AST_NODE_TYPES.FunctionExpression || node.type === import_utils21.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
const entries = Object.entries(node);
|
|
3400
|
+
for (const [key, value] of entries) {
|
|
3401
|
+
if (key === "parent") {
|
|
3402
|
+
continue;
|
|
3403
|
+
}
|
|
3404
|
+
if (Array.isArray(value)) {
|
|
3405
|
+
for (const child of value) {
|
|
3406
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
3407
|
+
traverseForConsoleCalls(child, onConsoleCall);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
if (value && typeof value === "object" && "type" in value) {
|
|
3413
|
+
traverseForConsoleCalls(value, onConsoleCall);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
var noConsoleInHandler = createRule19({
|
|
3418
|
+
name: "no-console-in-handler",
|
|
3419
|
+
meta: {
|
|
3420
|
+
type: "suggestion",
|
|
3421
|
+
docs: {
|
|
3422
|
+
description: "Disallow console logging inside route handlers. AI tools frequently leave debug logs in request handlers, which can leak operational data and create noisy production logs."
|
|
3423
|
+
},
|
|
3424
|
+
fixable: void 0,
|
|
3425
|
+
hasSuggestions: true,
|
|
3426
|
+
schema: [],
|
|
3427
|
+
messages: {
|
|
3428
|
+
noConsoleInHandler: "Avoid console logging inside route handlers. AI tools frequently leave debug statements in handlers. Use structured application logging instead.",
|
|
3429
|
+
removeConsoleCall: "Remove this console statement from the route handler."
|
|
3430
|
+
}
|
|
3431
|
+
},
|
|
3432
|
+
defaultOptions: [],
|
|
3433
|
+
create(context) {
|
|
3434
|
+
return {
|
|
3435
|
+
CallExpression(node) {
|
|
3436
|
+
if (!isRouteRegistrationCall2(node)) {
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
for (const argument of node.arguments) {
|
|
3440
|
+
if (argument.type !== import_utils21.AST_NODE_TYPES.FunctionExpression && argument.type !== import_utils21.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
traverseForConsoleCalls(argument.body, (callNode) => {
|
|
3444
|
+
const parent = callNode.parent;
|
|
3445
|
+
const canSuggestRemoval = parent?.type === import_utils21.AST_NODE_TYPES.ExpressionStatement;
|
|
3446
|
+
context.report({
|
|
3447
|
+
node: callNode,
|
|
3448
|
+
messageId: "noConsoleInHandler",
|
|
3449
|
+
suggest: canSuggestRemoval ? [
|
|
3450
|
+
{
|
|
3451
|
+
messageId: "removeConsoleCall",
|
|
3452
|
+
fix: (fixer) => fixer.remove(parent)
|
|
3453
|
+
}
|
|
3454
|
+
] : void 0
|
|
3455
|
+
});
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
});
|
|
3462
|
+
|
|
3463
|
+
// src/rules/logic/no-duplicate-logic-block.ts
|
|
3464
|
+
var import_utils22 = require("@typescript-eslint/utils");
|
|
3465
|
+
var createRule20 = import_utils22.ESLintUtils.RuleCreator(
|
|
3466
|
+
(name) => `https://github.com/undercurrentai/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
|
|
3467
|
+
);
|
|
3468
|
+
function normalizeStatementText(source) {
|
|
3469
|
+
return source.replace(/\s+/g, " ").trim();
|
|
3470
|
+
}
|
|
3471
|
+
function isLikelyMeaningfulStatement(node, sourceCodeText) {
|
|
3472
|
+
if (node.type === import_utils22.AST_NODE_TYPES.EmptyStatement || node.type === import_utils22.AST_NODE_TYPES.BreakStatement || node.type === import_utils22.AST_NODE_TYPES.ContinueStatement) {
|
|
3473
|
+
return false;
|
|
3474
|
+
}
|
|
3475
|
+
return sourceCodeText.length >= 30;
|
|
3476
|
+
}
|
|
3477
|
+
var noDuplicateLogicBlock = createRule20({
|
|
3478
|
+
name: "no-duplicate-logic-block",
|
|
3479
|
+
meta: {
|
|
3480
|
+
type: "suggestion",
|
|
3481
|
+
docs: {
|
|
3482
|
+
description: "Disallow consecutive duplicated logic statements. AI tools often copy-paste the same logic block with minimal or no changes, which should usually be extracted or consolidated."
|
|
3483
|
+
},
|
|
3484
|
+
fixable: void 0,
|
|
3485
|
+
schema: [],
|
|
3486
|
+
messages: {
|
|
3487
|
+
duplicateLogic: "Duplicate consecutive logic block detected. AI-generated code often repeats the same block. Consider extracting shared logic into a function or removing duplication."
|
|
3488
|
+
}
|
|
3489
|
+
},
|
|
3490
|
+
defaultOptions: [],
|
|
3491
|
+
create(context) {
|
|
3492
|
+
const checkStatementList = (statements) => {
|
|
3493
|
+
if (statements.length < 2) {
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
for (let i = 0; i < statements.length - 1; i += 1) {
|
|
3497
|
+
const current = statements[i];
|
|
3498
|
+
const next = statements[i + 1];
|
|
3499
|
+
const currentTextRaw = context.sourceCode.getText(current);
|
|
3500
|
+
const nextTextRaw = context.sourceCode.getText(next);
|
|
3501
|
+
if (!isLikelyMeaningfulStatement(current, currentTextRaw)) {
|
|
3502
|
+
continue;
|
|
3503
|
+
}
|
|
3504
|
+
if (!isLikelyMeaningfulStatement(next, nextTextRaw)) {
|
|
3505
|
+
continue;
|
|
3506
|
+
}
|
|
3507
|
+
const currentText = normalizeStatementText(currentTextRaw);
|
|
3508
|
+
const nextText = normalizeStatementText(nextTextRaw);
|
|
3509
|
+
if (currentText === nextText) {
|
|
3510
|
+
context.report({
|
|
3511
|
+
node: next,
|
|
3512
|
+
messageId: "duplicateLogic"
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
return {
|
|
3518
|
+
Program(node) {
|
|
3519
|
+
checkStatementList(node.body);
|
|
3520
|
+
},
|
|
3521
|
+
BlockStatement(node) {
|
|
3522
|
+
checkStatementList(node.body);
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
});
|
|
3527
|
+
|
|
3528
|
+
// src/rules/index.ts
|
|
3529
|
+
var allRules = {
|
|
3530
|
+
"no-empty-catch": noEmptyCatch,
|
|
3531
|
+
"no-broad-exception": noBroadException,
|
|
3532
|
+
"no-catch-log-rethrow": noCatchLogRethrow,
|
|
3533
|
+
"no-catch-without-use": noCatchWithoutUse,
|
|
3534
|
+
"no-async-array-callback": noAsyncArrayCallback,
|
|
3535
|
+
"no-floating-promise": noFloatingPromise,
|
|
3536
|
+
"no-await-in-loop": noAwaitInLoop,
|
|
3537
|
+
"no-async-without-await": noAsyncWithoutAwait,
|
|
3538
|
+
"no-redundant-await": noRedundantAwait,
|
|
3539
|
+
"no-hardcoded-secret": noHardcodedSecret,
|
|
3540
|
+
"no-eval-dynamic": noEvalDynamic,
|
|
3541
|
+
"no-sql-string-concat": noSqlStringConcat,
|
|
3542
|
+
"no-unsafe-deserialize": noUnsafeDeserialize,
|
|
3543
|
+
"require-auth-middleware": requireAuthMiddleware,
|
|
3544
|
+
"require-authz-check": requireAuthzCheck,
|
|
3545
|
+
"require-framework-auth": requireFrameworkAuth,
|
|
3546
|
+
"require-framework-authz": requireFrameworkAuthz,
|
|
3547
|
+
"require-webhook-signature": requireWebhookSignature,
|
|
3548
|
+
"no-console-in-handler": noConsoleInHandler,
|
|
3549
|
+
"no-duplicate-logic-block": noDuplicateLogicBlock
|
|
3550
|
+
};
|
|
3551
|
+
|
|
3552
|
+
// src/configs/recommended.ts
|
|
3553
|
+
var recommended = {
|
|
3554
|
+
plugins: ["ai-guard"],
|
|
3555
|
+
rules: {
|
|
3556
|
+
// Adoption-first default:
|
|
3557
|
+
// Keep only high-confidence, high-impact rules at error so first run is actionable,
|
|
3558
|
+
// not overwhelming. Context-sensitive rules are warn/off by design.
|
|
3559
|
+
//
|
|
3560
|
+
// v2.0: removed 5 rules that overlap with typescript-eslint / ESLint core.
|
|
3561
|
+
// Those rules remain available in the plugin for backwards compatibility but are
|
|
3562
|
+
// not enabled by default. See docs/guides/compat-config.md and
|
|
3563
|
+
// docs/migration/v1-to-v2.md for the upstream replacements.
|
|
3564
|
+
// Critical (error): low-noise, high-impact correctness/security failures.
|
|
3565
|
+
"ai-guard/no-empty-catch": "error",
|
|
3566
|
+
"ai-guard/no-floating-promise": "error",
|
|
3567
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
3568
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
3569
|
+
// Important but noisier/context-dependent (warn): useful guidance without blocking.
|
|
3570
|
+
"ai-guard/require-framework-auth": "warn",
|
|
3571
|
+
"ai-guard/no-sql-string-concat": "warn",
|
|
3572
|
+
// Kept as warn in recommended to reduce false positives in mixed codebases.
|
|
3573
|
+
"ai-guard/no-async-array-callback": "warn",
|
|
3574
|
+
// Optional/contextual (off): available for teams via strict/custom configs.
|
|
3575
|
+
"ai-guard/no-console-in-handler": "off",
|
|
3576
|
+
"ai-guard/no-unsafe-deserialize": "warn",
|
|
3577
|
+
"ai-guard/no-catch-log-rethrow": "off",
|
|
3578
|
+
"ai-guard/require-framework-authz": "warn",
|
|
3579
|
+
"ai-guard/require-webhook-signature": "warn",
|
|
3580
|
+
"ai-guard/no-duplicate-logic-block": "off"
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
var recommended_default = recommended;
|
|
3584
|
+
|
|
3585
|
+
// src/configs/strict.ts
|
|
3586
|
+
var strict = {
|
|
3587
|
+
plugins: ["ai-guard"],
|
|
3588
|
+
rules: {
|
|
3589
|
+
// Strict preset: enforce every active ai-guard rule at error for mature teams
|
|
3590
|
+
// that want maximum coverage and are ready to tune exceptions locally.
|
|
3591
|
+
//
|
|
3592
|
+
// v2.0: 5 deprecated rules removed from strict — users who want them must
|
|
3593
|
+
// opt in explicitly in their own config (they remain available). See
|
|
3594
|
+
// docs/migration/v1-to-v2.md for the upstream replacements.
|
|
3595
|
+
// Error Handling - all at error
|
|
3596
|
+
"ai-guard/no-empty-catch": "error",
|
|
3597
|
+
"ai-guard/no-catch-log-rethrow": "error",
|
|
3598
|
+
// Async - all at error
|
|
3599
|
+
"ai-guard/no-async-array-callback": "error",
|
|
3600
|
+
"ai-guard/no-floating-promise": "error",
|
|
3601
|
+
// Security - all at error
|
|
3602
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
3603
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
3604
|
+
"ai-guard/no-sql-string-concat": "error",
|
|
3605
|
+
"ai-guard/no-unsafe-deserialize": "error",
|
|
3606
|
+
"ai-guard/require-framework-auth": "error",
|
|
3607
|
+
"ai-guard/require-framework-authz": "error",
|
|
3608
|
+
"ai-guard/require-webhook-signature": "error",
|
|
3609
|
+
// Quality - all at error
|
|
3610
|
+
"ai-guard/no-console-in-handler": "error",
|
|
3611
|
+
"ai-guard/no-duplicate-logic-block": "error"
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
var strict_default = strict;
|
|
3615
|
+
|
|
3616
|
+
// src/configs/security.ts
|
|
3617
|
+
var security = {
|
|
3618
|
+
plugins: ["ai-guard"],
|
|
3619
|
+
rules: {
|
|
3620
|
+
// Security-only preset:
|
|
3621
|
+
// Error = direct high-risk patterns. Warn = security-relevant but context-sensitive.
|
|
3622
|
+
"ai-guard/no-hardcoded-secret": "error",
|
|
3623
|
+
"ai-guard/no-eval-dynamic": "error",
|
|
3624
|
+
"ai-guard/no-sql-string-concat": "error",
|
|
3625
|
+
"ai-guard/no-unsafe-deserialize": "warn",
|
|
3626
|
+
"ai-guard/require-framework-auth": "warn",
|
|
3627
|
+
"ai-guard/require-framework-authz": "warn",
|
|
3628
|
+
"ai-guard/require-webhook-signature": "warn"
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3631
|
+
var security_default = security;
|
|
3632
|
+
|
|
3633
|
+
// src/configs/compat.ts
|
|
3634
|
+
var compat = {
|
|
3635
|
+
plugins: ["ai-guard"],
|
|
3636
|
+
rules: {
|
|
3637
|
+
"ai-guard/no-await-in-loop": "off",
|
|
3638
|
+
"ai-guard/no-async-without-await": "off",
|
|
3639
|
+
"ai-guard/no-redundant-await": "off",
|
|
3640
|
+
"ai-guard/no-broad-exception": "off",
|
|
3641
|
+
"ai-guard/no-catch-without-use": "off",
|
|
3642
|
+
"ai-guard/require-auth-middleware": "off",
|
|
3643
|
+
"ai-guard/require-authz-check": "off"
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
var compat_default = compat;
|
|
3647
|
+
|
|
3648
|
+
// src/configs/framework.ts
|
|
3649
|
+
var framework = {
|
|
3650
|
+
plugins: ["ai-guard"],
|
|
3651
|
+
rules: {
|
|
3652
|
+
"ai-guard/require-framework-auth": "error",
|
|
3653
|
+
"ai-guard/require-framework-authz": "warn",
|
|
3654
|
+
"ai-guard/require-webhook-signature": "error"
|
|
3655
|
+
}
|
|
3656
|
+
};
|
|
3657
|
+
var framework_default = framework;
|
|
3658
|
+
|
|
3659
|
+
// src/index.ts
|
|
3660
|
+
var plugin = {
|
|
3661
|
+
meta: {
|
|
3662
|
+
name: "@undercurrentai/eslint-plugin-ai-guard",
|
|
3663
|
+
version: "2.0.0-beta.3"
|
|
3664
|
+
},
|
|
3665
|
+
rules: allRules,
|
|
3666
|
+
configs: {
|
|
3667
|
+
recommended: recommended_default,
|
|
3668
|
+
strict: strict_default,
|
|
3669
|
+
security: security_default,
|
|
3670
|
+
compat: compat_default,
|
|
3671
|
+
framework: framework_default
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
var index_default = plugin;
|
|
3675
|
+
// CJS interop: expose the default export as module.exports directly.
|
|
3676
|
+
// The `typeof module` guard makes this safe to re-run and no-op under
|
|
3677
|
+
// ESM hosts where `module` is not defined.
|
|
3678
|
+
if (typeof module !== "undefined" && module.exports && module.exports.default) { module.exports = module.exports.default; }
|
|
3679
|
+
//# sourceMappingURL=index.js.map
|