eslint-plugin-ai-guard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,898 @@
1
+ // src/rules/error-handling/no-empty-catch.ts
2
+ import { ESLintUtils } from "@typescript-eslint/utils";
3
+ var createRule = ESLintUtils.RuleCreator(
4
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
5
+ );
6
+ var noEmptyCatch = createRule({
7
+ name: "no-empty-catch",
8
+ meta: {
9
+ type: "problem",
10
+ docs: {
11
+ description: "Disallow empty catch blocks. AI tools frequently generate try/catch with empty catch bodies that silently swallow errors, hiding failures in production."
12
+ },
13
+ fixable: void 0,
14
+ schema: [],
15
+ messages: {
16
+ 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."
17
+ }
18
+ },
19
+ defaultOptions: [],
20
+ create(context) {
21
+ return {
22
+ CatchClause(node) {
23
+ if (node.body.body.length === 0) {
24
+ const sourceCode = context.sourceCode;
25
+ const comments = sourceCode.getCommentsInside(node.body);
26
+ if (comments.length > 0) {
27
+ return;
28
+ }
29
+ context.report({
30
+ node,
31
+ messageId: "emptyCatch"
32
+ });
33
+ }
34
+ }
35
+ };
36
+ }
37
+ });
38
+
39
+ // src/rules/error-handling/no-broad-exception.ts
40
+ import { ESLintUtils as ESLintUtils2, AST_NODE_TYPES } from "@typescript-eslint/utils";
41
+ var createRule2 = ESLintUtils2.RuleCreator(
42
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
43
+ );
44
+ var noBroadException = createRule2({
45
+ name: "no-broad-exception",
46
+ meta: {
47
+ type: "suggestion",
48
+ docs: {
49
+ description: "Disallow overly broad catch clause types such as `catch (e: any)` or `catch (e: unknown)` without narrowing. AI tools frequently use `any` type annotations on catch parameters, hiding the actual error type and preventing proper error handling."
50
+ },
51
+ fixable: void 0,
52
+ schema: [],
53
+ messages: {
54
+ broadException: "Catch parameter has an overly broad type annotation `{{type}}`. AI tools frequently generate `catch (e: any)` which hides the real error type. Use a specific error type or narrow with `instanceof` checks inside the catch block."
55
+ }
56
+ },
57
+ defaultOptions: [],
58
+ create(context) {
59
+ return {
60
+ CatchClause(node) {
61
+ const param = node.param;
62
+ if (!param) return;
63
+ if (param.type === AST_NODE_TYPES.Identifier && param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
64
+ const typeAnnotation = param.typeAnnotation.typeAnnotation;
65
+ if (typeAnnotation.type === AST_NODE_TYPES.TSAnyKeyword) {
66
+ context.report({
67
+ node: param,
68
+ messageId: "broadException",
69
+ data: { type: "any" }
70
+ });
71
+ return;
72
+ }
73
+ if (typeAnnotation.type === AST_NODE_TYPES.TSUnknownKeyword) {
74
+ const hasNarrowing = checkForNarrowing(node, param.name);
75
+ if (!hasNarrowing) {
76
+ context.report({
77
+ node: param,
78
+ messageId: "broadException",
79
+ data: { type: "unknown" }
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ };
86
+ }
87
+ });
88
+ function checkForNarrowing(catchClause, paramName) {
89
+ const bodyStatements = catchClause.body.body;
90
+ for (const stmt of bodyStatements) {
91
+ if (containsInstanceofCheck(stmt, paramName)) {
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+ function containsInstanceofCheck(node, paramName) {
98
+ if (!node || typeof node !== "object") return false;
99
+ if (node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.left?.type === AST_NODE_TYPES.Identifier && node.left.name === paramName) {
100
+ return true;
101
+ }
102
+ for (const key of Object.keys(node)) {
103
+ if (key === "parent") continue;
104
+ const child = node[key];
105
+ if (Array.isArray(child)) {
106
+ for (const item of child) {
107
+ if (containsInstanceofCheck(item, paramName)) return true;
108
+ }
109
+ } else if (child && typeof child === "object" && child.type) {
110
+ if (containsInstanceofCheck(child, paramName)) return true;
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+
116
+ // src/rules/async/no-async-array-callback.ts
117
+ import { ESLintUtils as ESLintUtils3, AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
118
+ var createRule3 = ESLintUtils3.RuleCreator(
119
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
120
+ );
121
+ var ARRAY_CALLBACK_METHODS = ["map", "filter", "forEach", "reduce", "flatMap", "find", "findIndex", "some", "every"];
122
+ var PROMISE_COMBINATORS = ["all", "allSettled", "race", "any"];
123
+ function isWrappedInPromiseCombinator(node, context) {
124
+ const ancestors = context.sourceCode.getAncestors(node);
125
+ for (let i = ancestors.length - 1; i >= 0; i--) {
126
+ const ancestor = ancestors[i];
127
+ if (ancestor.type === AST_NODE_TYPES2.CallExpression && ancestor.callee.type === AST_NODE_TYPES2.MemberExpression && ancestor.callee.object.type === AST_NODE_TYPES2.Identifier && ancestor.callee.object.name === "Promise" && ancestor.callee.property.type === AST_NODE_TYPES2.Identifier && PROMISE_COMBINATORS.includes(
128
+ ancestor.callee.property.name
129
+ )) {
130
+ return true;
131
+ }
132
+ if (ancestor.type === AST_NODE_TYPES2.CallExpression) {
133
+ break;
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+ function isAsyncCallback(node) {
139
+ return (node.type === AST_NODE_TYPES2.FunctionExpression || node.type === AST_NODE_TYPES2.ArrowFunctionExpression) && node.async;
140
+ }
141
+ var noAsyncArrayCallback = createRule3({
142
+ name: "no-async-array-callback",
143
+ meta: {
144
+ type: "problem",
145
+ docs: {
146
+ 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."
147
+ },
148
+ fixable: void 0,
149
+ schema: [],
150
+ messages: {
151
+ 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."
152
+ }
153
+ },
154
+ defaultOptions: [],
155
+ create(context) {
156
+ return {
157
+ CallExpression(node) {
158
+ if (node.callee.type !== AST_NODE_TYPES2.MemberExpression || node.callee.property.type !== AST_NODE_TYPES2.Identifier) {
159
+ return;
160
+ }
161
+ const methodName = node.callee.property.name;
162
+ if (!ARRAY_CALLBACK_METHODS.includes(
163
+ methodName
164
+ )) {
165
+ return;
166
+ }
167
+ const callback = node.arguments[0];
168
+ if (!callback) {
169
+ return;
170
+ }
171
+ if (isAsyncCallback(callback)) {
172
+ if (isWrappedInPromiseCombinator(node, context)) {
173
+ return;
174
+ }
175
+ context.report({
176
+ node: callback,
177
+ messageId: "asyncArrayCallback",
178
+ data: {
179
+ method: methodName
180
+ }
181
+ });
182
+ }
183
+ }
184
+ };
185
+ }
186
+ });
187
+
188
+ // src/rules/async/no-floating-promise.ts
189
+ import { ESLintUtils as ESLintUtils4, AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
190
+ var createRule4 = ESLintUtils4.RuleCreator(
191
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
192
+ );
193
+ function looksLikeAsyncCall(node) {
194
+ if (node.callee.type === AST_NODE_TYPES3.Identifier) {
195
+ return hasAsyncishName(node.callee.name);
196
+ }
197
+ if (node.callee.type === AST_NODE_TYPES3.MemberExpression && node.callee.property.type === AST_NODE_TYPES3.Identifier) {
198
+ return hasAsyncishName(node.callee.property.name);
199
+ }
200
+ return false;
201
+ }
202
+ function hasAsyncishName(name) {
203
+ const lower = name.toLowerCase();
204
+ return lower.startsWith("fetch") || lower.startsWith("load") || lower.startsWith("save") || lower.startsWith("delete") || lower.startsWith("create") || lower.startsWith("update") || lower.startsWith("get") || lower.startsWith("post") || lower.startsWith("put") || lower.startsWith("patch") || lower.startsWith("send") || lower.startsWith("upload") || lower.startsWith("download") || lower.startsWith("connect") || lower.startsWith("disconnect") || lower.startsWith("subscribe") || lower.startsWith("publish") || lower.startsWith("request") || lower.startsWith("query") || lower === "axios" || lower === "fetch";
205
+ }
206
+ function isCalleeAsync(node, asyncFunctionNames) {
207
+ if (node.callee.type === AST_NODE_TYPES3.Identifier) {
208
+ if (asyncFunctionNames.has(node.callee.name)) {
209
+ return true;
210
+ }
211
+ }
212
+ return looksLikeAsyncCall(node);
213
+ }
214
+ function isExpressionHandled(node) {
215
+ const expr = node.expression;
216
+ if (expr.type === AST_NODE_TYPES3.CallExpression) {
217
+ if (expr.callee.type === AST_NODE_TYPES3.MemberExpression) {
218
+ const prop = expr.callee.property;
219
+ if (prop.type === AST_NODE_TYPES3.Identifier) {
220
+ if (prop.name === "then" || prop.name === "catch" || prop.name === "finally") {
221
+ return true;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ if (expr.type === AST_NODE_TYPES3.UnaryExpression && expr.operator === "void") {
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ var noFloatingPromise = createRule4({
232
+ name: "no-floating-promise",
233
+ meta: {
234
+ type: "problem",
235
+ docs: {
236
+ 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."
237
+ },
238
+ fixable: void 0,
239
+ schema: [],
240
+ messages: {
241
+ 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."
242
+ }
243
+ },
244
+ defaultOptions: [],
245
+ create(context) {
246
+ const asyncFunctionNames = /* @__PURE__ */ new Set();
247
+ return {
248
+ // Track async function declarations
249
+ FunctionDeclaration(node) {
250
+ if (node.async && node.id) {
251
+ asyncFunctionNames.add(node.id.name);
252
+ }
253
+ },
254
+ // Track async arrow functions assigned to variables
255
+ VariableDeclarator(node) {
256
+ if (node.init && (node.init.type === AST_NODE_TYPES3.ArrowFunctionExpression || node.init.type === AST_NODE_TYPES3.FunctionExpression) && node.init.async && node.id.type === AST_NODE_TYPES3.Identifier) {
257
+ asyncFunctionNames.add(node.id.name);
258
+ }
259
+ },
260
+ // Check ExpressionStatements — standalone call expressions
261
+ ExpressionStatement(node) {
262
+ if (node.expression.type !== AST_NODE_TYPES3.CallExpression) {
263
+ return;
264
+ }
265
+ if (isExpressionHandled(node)) {
266
+ return;
267
+ }
268
+ const callExpr = node.expression;
269
+ if (isCalleeAsync(callExpr, asyncFunctionNames)) {
270
+ context.report({
271
+ node,
272
+ messageId: "floatingPromise"
273
+ });
274
+ }
275
+ }
276
+ };
277
+ }
278
+ });
279
+
280
+ // src/rules/async/no-await-in-loop.ts
281
+ import { ESLintUtils as ESLintUtils5, AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
282
+ var createRule5 = ESLintUtils5.RuleCreator(
283
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
284
+ );
285
+ var LOOP_TYPES = /* @__PURE__ */ new Set([
286
+ AST_NODE_TYPES4.ForStatement,
287
+ AST_NODE_TYPES4.ForInStatement,
288
+ AST_NODE_TYPES4.ForOfStatement,
289
+ AST_NODE_TYPES4.WhileStatement,
290
+ AST_NODE_TYPES4.DoWhileStatement
291
+ ]);
292
+ var noAwaitInLoop = createRule5({
293
+ name: "no-await-in-loop",
294
+ meta: {
295
+ type: "suggestion",
296
+ docs: {
297
+ description: "Disallow `await` inside loops. AI tools frequently generate sequential awaits inside for/while loops where `Promise.all()` would be more efficient and correct. Each iteration waits for the previous one to complete, causing O(n) latency instead of O(1)."
298
+ },
299
+ fixable: void 0,
300
+ schema: [],
301
+ messages: {
302
+ awaitInLoop: "Unexpected `await` inside a {{loopType}}. AI tools frequently generate sequential awaits in loops, causing O(n) latency. Consider collecting promises and using `Promise.all()` for parallel execution."
303
+ }
304
+ },
305
+ defaultOptions: [],
306
+ create(context) {
307
+ const functionBoundary = [];
308
+ function enterFunction(node) {
309
+ functionBoundary.push(node);
310
+ }
311
+ function exitFunction() {
312
+ functionBoundary.pop();
313
+ }
314
+ return {
315
+ FunctionDeclaration: enterFunction,
316
+ FunctionExpression: enterFunction,
317
+ ArrowFunctionExpression: enterFunction,
318
+ "FunctionDeclaration:exit": exitFunction,
319
+ "FunctionExpression:exit": exitFunction,
320
+ "ArrowFunctionExpression:exit": exitFunction,
321
+ AwaitExpression(node) {
322
+ const ancestors = context.sourceCode.getAncestors(node);
323
+ const currentFunction = functionBoundary[functionBoundary.length - 1];
324
+ for (let i = ancestors.length - 1; i >= 0; i--) {
325
+ const ancestor = ancestors[i];
326
+ if (ancestor === currentFunction) {
327
+ break;
328
+ }
329
+ if (LOOP_TYPES.has(ancestor.type)) {
330
+ const loopName = getLoopName(ancestor.type);
331
+ context.report({
332
+ node,
333
+ messageId: "awaitInLoop",
334
+ data: { loopType: loopName }
335
+ });
336
+ return;
337
+ }
338
+ }
339
+ }
340
+ };
341
+ }
342
+ });
343
+ function getLoopName(type) {
344
+ switch (type) {
345
+ case AST_NODE_TYPES4.ForStatement:
346
+ return "for loop";
347
+ case AST_NODE_TYPES4.ForInStatement:
348
+ return "for...in loop";
349
+ case AST_NODE_TYPES4.ForOfStatement:
350
+ return "for...of loop";
351
+ case AST_NODE_TYPES4.WhileStatement:
352
+ return "while loop";
353
+ case AST_NODE_TYPES4.DoWhileStatement:
354
+ return "do...while loop";
355
+ default:
356
+ return "loop";
357
+ }
358
+ }
359
+
360
+ // src/rules/security/no-hardcoded-secret.ts
361
+ import { ESLintUtils as ESLintUtils6, AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
362
+ var createRule6 = ESLintUtils6.RuleCreator(
363
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
364
+ );
365
+ var SECRET_NAME_PATTERN = /(?:secret|password|passwd|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key|client[_-]?secret|jwt[_-]?secret|encryption[_-]?key|signing[_-]?key)/i;
366
+ var FALSE_POSITIVE_VALUES = /* @__PURE__ */ new Set([
367
+ "password",
368
+ "secret",
369
+ "token",
370
+ "key",
371
+ "api_key",
372
+ "apikey",
373
+ "",
374
+ "undefined",
375
+ "null",
376
+ "test",
377
+ "example",
378
+ "placeholder",
379
+ "changeme",
380
+ "your-api-key",
381
+ "your-secret",
382
+ "YOUR_API_KEY",
383
+ "YOUR_SECRET",
384
+ "xxx",
385
+ "TODO"
386
+ ]);
387
+ var noHardcodedSecret = createRule6({
388
+ name: "no-hardcoded-secret",
389
+ meta: {
390
+ type: "problem",
391
+ docs: {
392
+ 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."
393
+ },
394
+ fixable: void 0,
395
+ schema: [],
396
+ messages: {
397
+ 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."
398
+ }
399
+ },
400
+ defaultOptions: [],
401
+ create(context) {
402
+ return {
403
+ VariableDeclarator(node) {
404
+ if (!node.init) return;
405
+ if (!node.id || node.id.type !== AST_NODE_TYPES5.Identifier) return;
406
+ const varName = node.id.name;
407
+ if (!SECRET_NAME_PATTERN.test(varName)) return;
408
+ const value = getStringValue(node.init);
409
+ if (value === null) return;
410
+ if (FALSE_POSITIVE_VALUES.has(value)) return;
411
+ if (value.length < 8) return;
412
+ if (isProcessEnvAccess(node.init)) return;
413
+ context.report({
414
+ node: node.init,
415
+ messageId: "hardcodedSecret",
416
+ data: {
417
+ name: varName,
418
+ envName: toEnvVarName(varName)
419
+ }
420
+ });
421
+ },
422
+ // Also check: obj.secret = 'literal_value'
423
+ AssignmentExpression(node) {
424
+ if (node.left.type !== AST_NODE_TYPES5.MemberExpression) return;
425
+ if (node.left.property.type !== AST_NODE_TYPES5.Identifier) return;
426
+ const propName = node.left.property.name;
427
+ if (!SECRET_NAME_PATTERN.test(propName)) return;
428
+ const value = getStringValue(node.right);
429
+ if (value === null) return;
430
+ if (FALSE_POSITIVE_VALUES.has(value)) return;
431
+ if (value.length < 8) return;
432
+ if (isProcessEnvAccess(node.right)) return;
433
+ context.report({
434
+ node: node.right,
435
+ messageId: "hardcodedSecret",
436
+ data: {
437
+ name: propName,
438
+ envName: toEnvVarName(propName)
439
+ }
440
+ });
441
+ },
442
+ // Check property assignments in object literals: { secret: 'value' }
443
+ Property(node) {
444
+ if (node.key.type !== AST_NODE_TYPES5.Identifier) return;
445
+ const propName = node.key.name;
446
+ if (!SECRET_NAME_PATTERN.test(propName)) return;
447
+ const value = getStringValue(node.value);
448
+ if (value === null) return;
449
+ if (FALSE_POSITIVE_VALUES.has(value)) return;
450
+ if (value.length < 8) return;
451
+ if (isProcessEnvAccess(node.value)) return;
452
+ context.report({
453
+ node: node.value,
454
+ messageId: "hardcodedSecret",
455
+ data: {
456
+ name: propName,
457
+ envName: toEnvVarName(propName)
458
+ }
459
+ });
460
+ }
461
+ };
462
+ }
463
+ });
464
+ function getStringValue(node) {
465
+ if (node.type === AST_NODE_TYPES5.Literal && typeof node.value === "string") {
466
+ return node.value;
467
+ }
468
+ if (node.type === AST_NODE_TYPES5.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1) {
469
+ return node.quasis[0].value.cooked ?? null;
470
+ }
471
+ return null;
472
+ }
473
+ function isProcessEnvAccess(node) {
474
+ return node.type === AST_NODE_TYPES5.MemberExpression && node.object.type === AST_NODE_TYPES5.MemberExpression && node.object.object.type === AST_NODE_TYPES5.Identifier && node.object.object.name === "process" && node.object.property.type === AST_NODE_TYPES5.Identifier && node.object.property.name === "env";
475
+ }
476
+ function toEnvVarName(name) {
477
+ return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]/g, "_").toUpperCase();
478
+ }
479
+
480
+ // src/rules/security/no-eval-dynamic.ts
481
+ import { ESLintUtils as ESLintUtils7, AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
482
+ var createRule7 = ESLintUtils7.RuleCreator(
483
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
484
+ );
485
+ var noEvalDynamic = createRule7({
486
+ name: "no-eval-dynamic",
487
+ meta: {
488
+ type: "problem",
489
+ docs: {
490
+ 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."
491
+ },
492
+ fixable: void 0,
493
+ schema: [],
494
+ messages: {
495
+ 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.",
496
+ 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."
497
+ }
498
+ },
499
+ defaultOptions: [],
500
+ create(context) {
501
+ return {
502
+ CallExpression(node) {
503
+ if (node.callee.type === AST_NODE_TYPES6.Identifier && node.callee.name === "eval") {
504
+ if (node.arguments.length === 0) return;
505
+ const firstArg = node.arguments[0];
506
+ if (isLiteral(firstArg)) return;
507
+ context.report({
508
+ node,
509
+ messageId: "evalDynamic",
510
+ data: { callee: "eval()" }
511
+ });
512
+ return;
513
+ }
514
+ if (node.callee.type === AST_NODE_TYPES6.MemberExpression && node.callee.property.type === AST_NODE_TYPES6.Identifier && node.callee.property.name === "eval" && node.callee.object.type === AST_NODE_TYPES6.Identifier && (node.callee.object.name === "window" || node.callee.object.name === "globalThis")) {
515
+ if (node.arguments.length === 0) return;
516
+ const firstArg = node.arguments[0];
517
+ if (isLiteral(firstArg)) return;
518
+ context.report({
519
+ node,
520
+ messageId: "evalDynamic",
521
+ data: { callee: `${node.callee.object.name}.eval()` }
522
+ });
523
+ }
524
+ },
525
+ NewExpression(node) {
526
+ if (node.callee.type === AST_NODE_TYPES6.Identifier && node.callee.name === "Function") {
527
+ if (node.arguments.length === 0) return;
528
+ const allLiteral = node.arguments.every(isLiteral);
529
+ if (allLiteral) return;
530
+ context.report({
531
+ node,
532
+ messageId: "newFunctionDynamic"
533
+ });
534
+ }
535
+ }
536
+ };
537
+ }
538
+ });
539
+ function isLiteral(node) {
540
+ if (node.type === AST_NODE_TYPES6.Literal) return true;
541
+ if (node.type === AST_NODE_TYPES6.TemplateLiteral && node.expressions.length === 0) {
542
+ return true;
543
+ }
544
+ return false;
545
+ }
546
+
547
+ // src/rules/security/no-sql-string-concat.ts
548
+ import { ESLintUtils as ESLintUtils8, AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
549
+ var createRule8 = ESLintUtils8.RuleCreator(
550
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
551
+ );
552
+ var SQL_KEYWORDS = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|WHERE|FROM|JOIN|INTO|VALUES|SET|TABLE|DATABASE|GRANT|REVOKE|TRUNCATE)\b/i;
553
+ var noSqlStringConcat = createRule8({
554
+ name: "no-sql-string-concat",
555
+ meta: {
556
+ type: "problem",
557
+ docs: {
558
+ 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."
559
+ },
560
+ fixable: void 0,
561
+ schema: [],
562
+ messages: {
563
+ 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.'
564
+ }
565
+ },
566
+ defaultOptions: [],
567
+ create(context) {
568
+ return {
569
+ // Template literals: `SELECT * FROM users WHERE id = ${userId}`
570
+ TemplateLiteral(node) {
571
+ if (node.expressions.length === 0) return;
572
+ const staticText = node.quasis.map((q) => q.value.raw).join("");
573
+ if (SQL_KEYWORDS.test(staticText)) {
574
+ context.report({
575
+ node,
576
+ messageId: "sqlStringConcat"
577
+ });
578
+ }
579
+ },
580
+ // Binary expressions: "SELECT * FROM users WHERE id = " + userId
581
+ BinaryExpression(node) {
582
+ if (node.operator !== "+") return;
583
+ const leftStr = getStringLiteralValue(node.left);
584
+ const rightStr = getStringLiteralValue(node.right);
585
+ if (leftStr && SQL_KEYWORDS.test(leftStr) || rightStr && SQL_KEYWORDS.test(rightStr)) {
586
+ const leftIsDynamic = !isStaticString(node.left);
587
+ const rightIsDynamic = !isStaticString(node.right);
588
+ if (leftIsDynamic || rightIsDynamic) {
589
+ context.report({
590
+ node,
591
+ messageId: "sqlStringConcat"
592
+ });
593
+ }
594
+ }
595
+ }
596
+ };
597
+ }
598
+ });
599
+ function getStringLiteralValue(node) {
600
+ if (node.type === AST_NODE_TYPES7.Literal && typeof node.value === "string") {
601
+ return node.value;
602
+ }
603
+ return null;
604
+ }
605
+ function isStaticString(node) {
606
+ if (node.type === AST_NODE_TYPES7.Literal && typeof node.value === "string") {
607
+ return true;
608
+ }
609
+ if (node.type === AST_NODE_TYPES7.TemplateLiteral && node.expressions.length === 0) {
610
+ return true;
611
+ }
612
+ return false;
613
+ }
614
+
615
+ // src/rules/security/require-auth-middleware.ts
616
+ import { ESLintUtils as ESLintUtils9, AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
617
+ var createRule9 = ESLintUtils9.RuleCreator(
618
+ (name) => `https://github.com/YashJadhav21/eslint-plugin-ai-guard/blob/main/docs/rules/${name}.md`
619
+ );
620
+ var HTTP_METHODS = /* @__PURE__ */ new Set([
621
+ "get",
622
+ "post",
623
+ "put",
624
+ "patch",
625
+ "delete",
626
+ "all",
627
+ "options",
628
+ "head"
629
+ ]);
630
+ var DEFAULT_AUTH_MIDDLEWARE_NAMES = /* @__PURE__ */ new Set([
631
+ "authenticate",
632
+ "requireAuth",
633
+ "isAuthenticated",
634
+ "verifyToken",
635
+ "protect",
636
+ "authorized",
637
+ "authorize",
638
+ "isAdmin",
639
+ "ensureAuthenticated",
640
+ "ensureLoggedIn",
641
+ "auth",
642
+ "authMiddleware",
643
+ "requireLogin",
644
+ "checkAuth",
645
+ "validateToken",
646
+ "passport.authenticate",
647
+ "jwt",
648
+ "requireSession"
649
+ ]);
650
+ var PUBLIC_ROUTE_PATTERNS = [
651
+ /^\/?$/,
652
+ // '/' root
653
+ /^\/health/,
654
+ // health checks
655
+ /^\/ping/,
656
+ // ping
657
+ /^\/status/,
658
+ // status
659
+ /^\/api\/v\d+\/auth/,
660
+ // auth routes
661
+ /^\/auth/,
662
+ // auth routes
663
+ /^\/login/,
664
+ // login
665
+ /^\/register/,
666
+ // register
667
+ /^\/signup/,
668
+ // signup
669
+ /^\/forgot/,
670
+ // forgot password
671
+ /^\/reset/,
672
+ // reset password
673
+ /^\/webhook/,
674
+ // webhooks
675
+ /^\/public/,
676
+ // explicitly public
677
+ /^\/assets/,
678
+ // static assets
679
+ /^\/static/,
680
+ // static files
681
+ /^\/favicon/,
682
+ // favicon
683
+ /^\/robots/,
684
+ // robots.txt
685
+ /^\/sitemap/
686
+ // sitemap
687
+ ];
688
+ var requireAuthMiddleware = createRule9({
689
+ name: "require-auth-middleware",
690
+ meta: {
691
+ type: "suggestion",
692
+ docs: {
693
+ 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."
694
+ },
695
+ fixable: void 0,
696
+ schema: [
697
+ {
698
+ type: "object",
699
+ properties: {
700
+ authMiddlewareNames: {
701
+ type: "array",
702
+ items: { type: "string" },
703
+ description: "Additional custom middleware names to recognize as authentication middleware."
704
+ }
705
+ },
706
+ additionalProperties: false
707
+ }
708
+ ],
709
+ messages: {
710
+ missingAuth: "Route `{{method}} {{path}}` appears to have no authentication middleware. AI tools frequently generate routes without auth checks, creating unprotected endpoints. Add authentication middleware (e.g., `protect`, `authenticate`) to the handler chain."
711
+ }
712
+ },
713
+ defaultOptions: [{}],
714
+ create(context, [options]) {
715
+ const customNames = new Set(options.authMiddlewareNames ?? []);
716
+ const allAuthNames = /* @__PURE__ */ new Set([...DEFAULT_AUTH_MIDDLEWARE_NAMES, ...customNames]);
717
+ let hasRouterUseAuth = false;
718
+ return {
719
+ CallExpression(node) {
720
+ if (isRouterUseAuth(node, allAuthNames)) {
721
+ hasRouterUseAuth = true;
722
+ return;
723
+ }
724
+ if (!isRouteDefinition(node)) return;
725
+ if (hasRouterUseAuth) return;
726
+ const callee = node.callee;
727
+ const method = callee.property.name;
728
+ const args = node.arguments;
729
+ if (args.length < 2) return;
730
+ const pathArg = args[0];
731
+ const pathStr = getPathString(pathArg);
732
+ if (pathStr && isPublicRoute(pathStr)) return;
733
+ const middlewareArgs = args.slice(1, -1);
734
+ if (middlewareArgs.length === 0 && args.length === 2) {
735
+ context.report({
736
+ node,
737
+ messageId: "missingAuth",
738
+ data: {
739
+ method: method.toUpperCase(),
740
+ path: pathStr || "<dynamic>"
741
+ }
742
+ });
743
+ return;
744
+ }
745
+ const hasAuth = middlewareArgs.some((arg) => isAuthMiddleware(arg, allAuthNames));
746
+ if (!hasAuth) {
747
+ context.report({
748
+ node,
749
+ messageId: "missingAuth",
750
+ data: {
751
+ method: method.toUpperCase(),
752
+ path: pathStr || "<dynamic>"
753
+ }
754
+ });
755
+ }
756
+ }
757
+ };
758
+ }
759
+ });
760
+ function isRouteDefinition(node) {
761
+ if (node.callee.type !== AST_NODE_TYPES8.MemberExpression) return false;
762
+ if (node.callee.property.type !== AST_NODE_TYPES8.Identifier) return false;
763
+ const methodName = node.callee.property.name;
764
+ if (!HTTP_METHODS.has(methodName)) return false;
765
+ const obj = node.callee.object;
766
+ if (obj.type === AST_NODE_TYPES8.Identifier) {
767
+ const name = obj.name.toLowerCase();
768
+ return name === "router" || name === "app" || name.includes("router");
769
+ }
770
+ if (obj.type === AST_NODE_TYPES8.CallExpression) return true;
771
+ return false;
772
+ }
773
+ function isRouterUseAuth(node, authNames) {
774
+ if (node.callee.type !== AST_NODE_TYPES8.MemberExpression) return false;
775
+ if (node.callee.property.type !== AST_NODE_TYPES8.Identifier) return false;
776
+ if (node.callee.property.name !== "use") return false;
777
+ return node.arguments.some((arg) => isAuthMiddleware(arg, authNames));
778
+ }
779
+ function isAuthMiddleware(node, authNames) {
780
+ if (node.type === AST_NODE_TYPES8.Identifier) {
781
+ return authNames.has(node.name);
782
+ }
783
+ if (node.type === AST_NODE_TYPES8.CallExpression) {
784
+ if (node.callee.type === AST_NODE_TYPES8.Identifier) {
785
+ return authNames.has(node.callee.name);
786
+ }
787
+ if (node.callee.type === AST_NODE_TYPES8.MemberExpression && node.callee.object.type === AST_NODE_TYPES8.Identifier && node.callee.property.type === AST_NODE_TYPES8.Identifier) {
788
+ const fullName = `${node.callee.object.name}.${node.callee.property.name}`;
789
+ return authNames.has(fullName) || authNames.has(node.callee.property.name);
790
+ }
791
+ }
792
+ return false;
793
+ }
794
+ function getPathString(node) {
795
+ if (node.type === AST_NODE_TYPES8.Literal && typeof node.value === "string") {
796
+ return node.value;
797
+ }
798
+ if (node.type === AST_NODE_TYPES8.TemplateLiteral) {
799
+ if (node.expressions.length === 0 && node.quasis.length === 1) {
800
+ return node.quasis[0].value.cooked ?? null;
801
+ }
802
+ if (node.quasis.length > 0) {
803
+ return node.quasis[0].value.cooked ?? null;
804
+ }
805
+ }
806
+ return null;
807
+ }
808
+ function isPublicRoute(pathStr) {
809
+ return PUBLIC_ROUTE_PATTERNS.some((pattern) => pattern.test(pathStr));
810
+ }
811
+
812
+ // src/rules/index.ts
813
+ var allRules = {
814
+ "no-empty-catch": noEmptyCatch,
815
+ "no-broad-exception": noBroadException,
816
+ "no-async-array-callback": noAsyncArrayCallback,
817
+ "no-floating-promise": noFloatingPromise,
818
+ "no-await-in-loop": noAwaitInLoop,
819
+ "no-hardcoded-secret": noHardcodedSecret,
820
+ "no-eval-dynamic": noEvalDynamic,
821
+ "no-sql-string-concat": noSqlStringConcat,
822
+ "require-auth-middleware": requireAuthMiddleware
823
+ };
824
+
825
+ // src/configs/recommended.ts
826
+ var recommended = {
827
+ plugins: ["ai-guard"],
828
+ rules: {
829
+ // Error Handling
830
+ "ai-guard/no-empty-catch": "error",
831
+ "ai-guard/no-broad-exception": "warn",
832
+ // Async
833
+ "ai-guard/no-async-array-callback": "error",
834
+ "ai-guard/no-floating-promise": "error",
835
+ "ai-guard/no-await-in-loop": "warn",
836
+ // Security
837
+ "ai-guard/no-hardcoded-secret": "error",
838
+ "ai-guard/no-eval-dynamic": "error",
839
+ "ai-guard/no-sql-string-concat": "error",
840
+ "ai-guard/require-auth-middleware": "warn"
841
+ }
842
+ };
843
+ var recommended_default = recommended;
844
+
845
+ // src/configs/strict.ts
846
+ var strict = {
847
+ plugins: ["ai-guard"],
848
+ rules: {
849
+ // Error Handling - all at error
850
+ "ai-guard/no-empty-catch": "error",
851
+ "ai-guard/no-broad-exception": "error",
852
+ // Async - all at error
853
+ "ai-guard/no-async-array-callback": "error",
854
+ "ai-guard/no-floating-promise": "error",
855
+ "ai-guard/no-await-in-loop": "error",
856
+ // Security - all at error
857
+ "ai-guard/no-hardcoded-secret": "error",
858
+ "ai-guard/no-eval-dynamic": "error",
859
+ "ai-guard/no-sql-string-concat": "error",
860
+ "ai-guard/require-auth-middleware": "error"
861
+ }
862
+ };
863
+ var strict_default = strict;
864
+
865
+ // src/configs/security.ts
866
+ var security = {
867
+ plugins: ["ai-guard"],
868
+ rules: {
869
+ "ai-guard/no-hardcoded-secret": "error",
870
+ "ai-guard/no-eval-dynamic": "error",
871
+ "ai-guard/no-sql-string-concat": "error",
872
+ "ai-guard/require-auth-middleware": "error"
873
+ }
874
+ };
875
+ var security_default = security;
876
+
877
+ // src/index.ts
878
+ var plugin = {
879
+ meta: {
880
+ name: "eslint-plugin-ai-guard",
881
+ version: "1.0.0"
882
+ },
883
+ rules: allRules,
884
+ configs: {
885
+ recommended: recommended_default,
886
+ strict: strict_default,
887
+ security: security_default
888
+ }
889
+ };
890
+ var index_default = plugin;
891
+ export {
892
+ index_default as default,
893
+ recommended_default as recommended,
894
+ allRules as rules,
895
+ security_default as security,
896
+ strict_default as strict
897
+ };
898
+ //# sourceMappingURL=index.mjs.map