eslint-plugin-vercel-ai-security 1.4.0 → 1.4.2

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.
Files changed (48) hide show
  1. package/package.json +5 -20
  2. package/src/index.d.ts +17 -74
  3. package/src/index.js +1 -99
  4. package/src/oxlint.d.ts +17 -20
  5. package/src/oxlint.js +0 -18
  6. package/src/rules/no-dynamic-system-prompt/index.js +1 -23
  7. package/src/rules/no-hardcoded-api-keys/index.js +5 -28
  8. package/src/rules/no-sensitive-in-prompt/index.js +1 -21
  9. package/src/rules/no-system-prompt-leak/index.js +0 -25
  10. package/src/rules/no-training-data-exposure/index.js +0 -16
  11. package/src/rules/no-unsafe-output-handling/index.js +0 -35
  12. package/src/rules/require-abort-signal/index.js +0 -14
  13. package/src/rules/require-audit-logging/index.js +0 -24
  14. package/src/rules/require-embedding-validation/index.js +0 -23
  15. package/src/rules/require-error-handling/index.js +0 -19
  16. package/src/rules/require-max-steps/index.js +0 -18
  17. package/src/rules/require-max-tokens/index.js +0 -14
  18. package/src/rules/require-output-filtering/index.js +0 -23
  19. package/src/rules/require-output-validation/index.js +0 -23
  20. package/src/rules/require-rag-content-validation/index.js +1 -29
  21. package/src/rules/require-request-timeout/index.js +0 -16
  22. package/src/rules/require-tool-confirmation/index.js +1 -25
  23. package/src/rules/require-tool-schema/index.js +0 -19
  24. package/src/rules/require-validated-prompt/index.js +1 -34
  25. package/src/types/index.js +0 -5
  26. package/src/utils/prompt-props.js +8 -28
  27. package/AGENTS.md +0 -131
  28. package/CHANGELOG.md +0 -255
  29. package/src/rules/no-dynamic-system-prompt/index.d.ts +0 -14
  30. package/src/rules/no-hardcoded-api-keys/index.d.ts +0 -14
  31. package/src/rules/no-sensitive-in-prompt/index.d.ts +0 -14
  32. package/src/rules/no-system-prompt-leak/index.d.ts +0 -14
  33. package/src/rules/no-training-data-exposure/index.d.ts +0 -14
  34. package/src/rules/no-unsafe-output-handling/index.d.ts +0 -15
  35. package/src/rules/require-abort-signal/index.d.ts +0 -14
  36. package/src/rules/require-audit-logging/index.d.ts +0 -14
  37. package/src/rules/require-embedding-validation/index.d.ts +0 -16
  38. package/src/rules/require-error-handling/index.d.ts +0 -14
  39. package/src/rules/require-max-steps/index.d.ts +0 -14
  40. package/src/rules/require-max-tokens/index.d.ts +0 -16
  41. package/src/rules/require-output-filtering/index.d.ts +0 -16
  42. package/src/rules/require-output-validation/index.d.ts +0 -16
  43. package/src/rules/require-rag-content-validation/index.d.ts +0 -16
  44. package/src/rules/require-request-timeout/index.d.ts +0 -14
  45. package/src/rules/require-tool-confirmation/index.d.ts +0 -14
  46. package/src/rules/require-tool-schema/index.d.ts +0 -15
  47. package/src/rules/require-validated-prompt/index.d.ts +0 -19
  48. package/src/utils/prompt-props.d.ts +0 -27
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.noSystemPromptLeak = void 0;
9
- /**
10
- * @fileoverview Prevent system prompt leakage to clients
11
- * @description Detects when system prompts are exposed in API responses
12
- * @see OWASP LLM07: System Prompt Leakage
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.noSystemPromptLeak = (0, eslint_devkit_1.createRule)({
16
6
  name: 'no-system-prompt-leak',
@@ -67,16 +57,10 @@ exports.noSystemPromptLeak = (0, eslint_devkit_1.createRule)({
67
57
  'systemMessage', 'instructions', 'agentPrompt',
68
58
  ];
69
59
  const sourceCode = context.sourceCode;
70
- /**
71
- * Check if identifier matches system prompt pattern
72
- */
73
60
  function isSystemPromptVariable(name) {
74
61
  const lowerName = name.toLowerCase();
75
62
  return systemPromptPatterns.some((pattern) => lowerName.includes(pattern.toLowerCase()));
76
63
  }
77
- /**
78
- * Find system prompt variables in an expression
79
- */
80
64
  function findSystemPromptVar(node) {
81
65
  if (node.type === 'Identifier' && isSystemPromptVariable(node.name)) {
82
66
  return node.name;
@@ -88,14 +72,10 @@ exports.noSystemPromptLeak = (0, eslint_devkit_1.createRule)({
88
72
  }
89
73
  return null;
90
74
  }
91
- /**
92
- * Check object properties for system prompt leaks
93
- */
94
75
  function checkObjectForLeaks(node) {
95
76
  for (const prop of node.properties) {
96
77
  if (prop.type !== 'Property')
97
78
  continue;
98
- // Check if property value contains system prompt
99
79
  const leakedVar = findSystemPromptVar(prop.value);
100
80
  if (leakedVar) {
101
81
  context.report({
@@ -107,15 +87,12 @@ exports.noSystemPromptLeak = (0, eslint_devkit_1.createRule)({
107
87
  }
108
88
  }
109
89
  return {
110
- // Check return statements in functions
111
90
  ReturnStatement(node) {
112
91
  if (!node.argument)
113
92
  return;
114
- // Check if returning an object with system prompt
115
93
  if (node.argument.type === 'ObjectExpression') {
116
94
  checkObjectForLeaks(node.argument);
117
95
  }
118
- // Check if returning system prompt directly
119
96
  const leakedVar = findSystemPromptVar(node.argument);
120
97
  if (leakedVar) {
121
98
  context.report({
@@ -125,10 +102,8 @@ exports.noSystemPromptLeak = (0, eslint_devkit_1.createRule)({
125
102
  });
126
103
  }
127
104
  },
128
- // Check Response.json() calls
129
105
  CallExpression(node) {
130
106
  const callee = sourceCode.getText(node.callee);
131
- // Check for Response.json, res.json, res.send patterns
132
107
  if (!callee.match(/\.(json|send)\s*$/))
133
108
  return;
134
109
  const arg = node.arguments[0];
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.noTrainingDataExposure = void 0;
9
- /**
10
- * @fileoverview Prevent training data exposure to LLM providers
11
- * @description Detects when user data is sent to training endpoints or with training flags
12
- * @see OWASP LLM03: Training Data Poisoning
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.noTrainingDataExposure = (0, eslint_devkit_1.createRule)({
16
6
  name: 'no-training-data-exposure',
@@ -63,15 +53,11 @@ exports.noTrainingDataExposure = (0, eslint_devkit_1.createRule)({
63
53
  const trainingPatterns = options.trainingPatterns ?? [
64
54
  'train', 'training', 'finetune', 'feedback',
65
55
  ];
66
- /**
67
- * Check if identifier suggests training
68
- */
69
56
  function isTrainingRelated(name) {
70
57
  const lowerName = name.toLowerCase();
71
58
  return trainingPatterns.some((pattern) => lowerName.includes(pattern.toLowerCase()));
72
59
  }
73
60
  return {
74
- // Check for training-related properties
75
61
  Property(node) {
76
62
  const keyName = node.key.type === 'Identifier'
77
63
  ? node.key.name
@@ -80,7 +66,6 @@ exports.noTrainingDataExposure = (0, eslint_devkit_1.createRule)({
80
66
  : null;
81
67
  if (!keyName)
82
68
  return;
83
- // Check for training flags like { training: true, allowTraining: true }
84
69
  if (isTrainingRelated(keyName)) {
85
70
  if (node.value.type === 'Literal' && node.value.value === true) {
86
71
  context.report({
@@ -91,7 +76,6 @@ exports.noTrainingDataExposure = (0, eslint_devkit_1.createRule)({
91
76
  }
92
77
  }
93
78
  },
94
- // Check for training endpoint URLs
95
79
  Literal(node) {
96
80
  if (typeof node.value !== 'string')
97
81
  return;
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.noUnsafeOutputHandling = void 0;
9
- /**
10
- * @fileoverview Prevent using AI output directly in dangerous operations
11
- * @description Detects when AI-generated content is used in eval, exec, or SQL
12
- * @see OWASP LLM05: Improper Output Handling
13
- * @see OWASP ASI05: Unexpected Code Execution
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
17
6
  name: 'no-unsafe-output-handling',
@@ -100,21 +89,11 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
100
89
  'aiOutput', 'aiResponse', 'llmOutput', '.text',
101
90
  ];
102
91
  const sourceCode = context.sourceCode;
103
- // Dangerous execution functions
104
92
  const dangerousFunctions = ['eval', 'Function', 'execSync', 'exec', 'spawn', 'execFile'];
105
- // SQL execution patterns
106
93
  const sqlPatterns = ['query', 'execute', 'run', 'raw'];
107
- /**
108
- * Names of variables locally bound to the result of a known AI SDK call.
109
- * Tracks the idiomatic `const { text } = await generateText(...)` and
110
- * `const result = await streamText(...)` patterns. Without this, the
111
- * heuristic pattern match (`result.text`, `aiOutput`, …) missed every
112
- * destructured-`text` case (real FN found by the OWASP-LLM02 corpus).
113
- */
114
94
  const aiBoundNames = new Set();
115
95
  const AI_SDK_CALLS = new Set(['generateText', 'streamText', 'generateObject', 'streamObject']);
116
96
  function isAISDKCall(node) {
117
- // generateText(...) | ai.generateText(...) | sdk.generateText(...)
118
97
  let target = node;
119
98
  if (target.type === 'AwaitExpression')
120
99
  target = target.argument;
@@ -128,27 +107,18 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
128
107
  return true;
129
108
  return false;
130
109
  }
131
- /**
132
- * Check if a node likely contains AI output
133
- */
134
110
  function isLikelyAIOutput(node) {
135
- // Direct member access into a tracked variable: `result.text`, `out.text`
136
111
  if (node.type === 'MemberExpression' && node.object.type === 'Identifier' &&
137
112
  aiBoundNames.has(node.object.name)) {
138
113
  return true;
139
114
  }
140
- // Bare reference to a tracked variable (covers destructured `text` from
141
- // `const { text } = await generateText(...)`).
142
115
  if (node.type === 'Identifier' && aiBoundNames.has(node.name)) {
143
116
  return true;
144
117
  }
145
- // Original heuristic — still useful for `result.text`-shaped source even
146
- // when scope tracking missed the binding.
147
118
  const text = sourceCode.getText(node);
148
119
  return aiOutputPatterns.some((pattern) => text.includes(pattern));
149
120
  }
150
121
  return {
151
- // Track `const r = await generateText(...)` / `const { text } = ...` shapes
152
122
  VariableDeclarator(node) {
153
123
  if (!node.init || !isAISDKCall(node.init))
154
124
  return;
@@ -163,10 +133,8 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
163
133
  }
164
134
  }
165
135
  },
166
- // Check for eval() and similar with AI output
167
136
  CallExpression(node) {
168
137
  const callee = sourceCode.getText(node.callee);
169
- // Check dangerous execution functions
170
138
  const isDangerous = dangerousFunctions.some(fn => callee.includes(fn));
171
139
  if (isDangerous) {
172
140
  for (const arg of node.arguments) {
@@ -182,12 +150,10 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
182
150
  }
183
151
  }
184
152
  }
185
- // Check SQL query functions
186
153
  const isSQLFunction = sqlPatterns.some(fn => callee.includes(fn));
187
154
  if (isSQLFunction) {
188
155
  for (const arg of node.arguments) {
189
156
  if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
190
- // Check if template/concatenation includes AI output
191
157
  if (isLikelyAIOutput(arg)) {
192
158
  context.report({
193
159
  node: arg,
@@ -198,7 +164,6 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
198
164
  }
199
165
  }
200
166
  },
201
- // Check for innerHTML assignment
202
167
  AssignmentExpression(node) {
203
168
  if (node.left.type === 'MemberExpression') {
204
169
  const prop = node.left.property;
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireAbortSignal = void 0;
9
- /**
10
- * @fileoverview Require abort signal for streaming AI calls
11
- * @description Ensures streamText/streamObject have AbortSignal for proper cleanup
12
- * @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text
13
- * @see OWASP LLM10: Unbounded Consumption
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.requireAbortSignal = (0, eslint_devkit_1.createRule)({
17
6
  name: 'require-abort-signal',
@@ -62,15 +51,12 @@ exports.requireAbortSignal = (0, eslint_devkit_1.createRule)({
62
51
  return {
63
52
  CallExpression(node) {
64
53
  const callee = sourceCode.getText(node.callee);
65
- // Check if this is a target streaming function
66
54
  const matchedFunction = targetFunctions.find((fn) => callee.includes(fn));
67
55
  if (!matchedFunction)
68
56
  return;
69
- // Check first argument (options object)
70
57
  const optionsArg = node.arguments[0];
71
58
  if (!optionsArg || optionsArg.type !== 'ObjectExpression')
72
59
  return;
73
- // Check if abortSignal is present
74
60
  const hasAbortSignal = optionsArg.properties.some(prop => {
75
61
  if (prop.type !== 'Property')
76
62
  return false;
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireAuditLogging = void 0;
9
- /**
10
- * @fileoverview Suggest audit logging for AI operations
11
- * @description Detects AI calls without surrounding logging statements
12
- * @see OWASP ASI10: Logging & Monitoring
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
16
6
  name: 'require-audit-logging',
@@ -59,20 +49,14 @@ exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
59
49
  const allowInTests = options.allowInTests ?? true;
60
50
  const sourceCode = context.sourceCode;
61
51
  const filename = context.filename;
62
- // Skip test files if allowed
63
52
  if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
64
53
  return {};
65
54
  }
66
- // Vercel AI SDK functions
67
55
  const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
68
- // Logging patterns
69
56
  const loggingPatterns = [
70
57
  'log', 'logger', 'console', 'winston', 'pino', 'bunyan',
71
58
  'debug', 'info', 'warn', 'error', 'trace',
72
59
  ];
73
- /**
74
- * Check if a statement is a logging call
75
- */
76
60
  function isLoggingStatement(node) {
77
61
  if (node.type !== 'ExpressionStatement')
78
62
  return false;
@@ -81,14 +65,10 @@ exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
81
65
  const callee = sourceCode.getText(node.expression.callee);
82
66
  return loggingPatterns.some((pattern) => callee.toLowerCase().includes(pattern.toLowerCase()));
83
67
  }
84
- /**
85
- * Check if there's logging nearby (within 3 statements)
86
- */
87
68
  function hasNearbyLogging(node) {
88
69
  const parent = node.parent;
89
70
  if (!parent)
90
71
  return false;
91
- // Find the statement containing this call
92
72
  let statement = node;
93
73
  while (statement && statement.type !== 'ExpressionStatement' &&
94
74
  statement.type !== 'VariableDeclaration' &&
@@ -97,7 +77,6 @@ exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
97
77
  }
98
78
  if (!statement)
99
79
  return false;
100
- // Get the block or program containing this statement
101
80
  const block = statement.parent;
102
81
  if (!block || (block.type !== 'BlockStatement' && block.type !== 'Program')) {
103
82
  return false;
@@ -106,7 +85,6 @@ exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
106
85
  const idx = statements.indexOf(statement);
107
86
  if (idx === -1)
108
87
  return false;
109
- // Check 3 statements before
110
88
  for (let i = Math.max(0, idx - 3); i < idx; i++) {
111
89
  if (isLoggingStatement(statements[i]))
112
90
  return true;
@@ -116,11 +94,9 @@ exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
116
94
  return {
117
95
  CallExpression(node) {
118
96
  const callee = sourceCode.getText(node.callee);
119
- // Check if this is an AI SDK function
120
97
  const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
121
98
  if (!matchedFunction)
122
99
  return;
123
- // Check for nearby logging
124
100
  if (!hasNearbyLogging(node)) {
125
101
  context.report({
126
102
  node,
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireEmbeddingValidation = void 0;
9
- /**
10
- * @fileoverview Require validation of embeddings before storage or search
11
- * @description Detects when embeddings are used without validation
12
- * @see OWASP LLM08: Vector & Embedding Weaknesses
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.requireEmbeddingValidation = (0, eslint_devkit_1.createRule)({
16
6
  name: 'require-embedding-validation',
@@ -76,11 +66,7 @@ exports.requireEmbeddingValidation = (0, eslint_devkit_1.createRule)({
76
66
  'validate', 'verify', 'check', 'sanitize',
77
67
  ];
78
68
  const sourceCode = context.sourceCode;
79
- // Vector store operations
80
69
  const vectorStoreOps = ['upsert', 'insert', 'add', 'store', 'index', 'save'];
81
- /**
82
- * Check if expression is an embedding call
83
- */
84
70
  function isEmbeddingCall(node) {
85
71
  const callee = sourceCode.getText(node.callee);
86
72
  for (const pattern of embeddingPatterns) {
@@ -90,26 +76,18 @@ exports.requireEmbeddingValidation = (0, eslint_devkit_1.createRule)({
90
76
  }
91
77
  return null;
92
78
  }
93
- /**
94
- * Check if expression is validated
95
- */
96
79
  function isValidated(node) {
97
80
  const callee = sourceCode.getText(node.callee);
98
81
  return validatorFunctions.some((fn) => callee.toLowerCase().includes(fn.toLowerCase()));
99
82
  }
100
- /**
101
- * Check if call is a vector store operation
102
- */
103
83
  function isVectorStoreOp(node) {
104
84
  const callee = sourceCode.getText(node.callee);
105
85
  return vectorStoreOps.some(op => callee.toLowerCase().includes(op.toLowerCase()));
106
86
  }
107
87
  return {
108
88
  CallExpression(node) {
109
- // Check if this is a vector store operation
110
89
  if (!isVectorStoreOp(node))
111
90
  return;
112
- // Check arguments for unvalidated embeddings
113
91
  for (const arg of node.arguments) {
114
92
  if (arg.type === 'ObjectExpression') {
115
93
  for (const prop of arg.properties) {
@@ -117,7 +95,6 @@ exports.requireEmbeddingValidation = (0, eslint_devkit_1.createRule)({
117
95
  continue;
118
96
  const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
119
97
  if (keyName === 'embedding' || keyName === 'vector' || keyName === 'values') {
120
- // Check if value is an unvalidated embedding call
121
98
  let valueNode = prop.value;
122
99
  if (valueNode.type === 'AwaitExpression') {
123
100
  valueNode = valueNode.argument;
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireErrorHandling = void 0;
9
- /**
10
- * @fileoverview Require error handling for AI SDK calls
11
- * @description Ensures generateText/streamText calls are wrapped in try-catch
12
- * @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text
13
- * @see OWASP ASI08: Cascading Failures
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.requireErrorHandling = (0, eslint_devkit_1.createRule)({
17
6
  name: 'require-error-handling',
@@ -60,16 +49,10 @@ exports.requireErrorHandling = (0, eslint_devkit_1.createRule)({
60
49
  const allowInTests = options.allowInTests ?? true;
61
50
  const sourceCode = context.sourceCode;
62
51
  const filename = context.filename;
63
- // Skip test files if allowed
64
52
  if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
65
53
  return {};
66
54
  }
67
- // Vercel AI SDK functions
68
55
  const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
69
- /**
70
- * Check if node is inside a try block
71
- */
72
- // oxlint-disable-next-line consistent-function-scoping
73
56
  function isInsideTryBlock(node) {
74
57
  let parent = node.parent;
75
58
  while (parent) {
@@ -83,11 +66,9 @@ exports.requireErrorHandling = (0, eslint_devkit_1.createRule)({
83
66
  return {
84
67
  CallExpression(node) {
85
68
  const callee = sourceCode.getText(node.callee);
86
- // Check if this is an AI SDK function
87
69
  const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
88
70
  if (!matchedFunction)
89
71
  return;
90
- // Check if inside try block
91
72
  const parent = node.parent;
92
73
  if (parent?.type === 'AwaitExpression') {
93
74
  if (!isInsideTryBlock(parent)) {
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireMaxSteps = void 0;
9
- /**
10
- * @fileoverview Require step limits for multi-step AI tool calls
11
- * @description Ensures multi-step tool calling has maxSteps to prevent infinite loops
12
- * @see https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling
13
- * @see OWASP LLM10: Unbounded Consumption
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.requireMaxSteps = (0, eslint_devkit_1.createRule)({
17
6
  name: 'require-max-steps',
@@ -58,30 +47,24 @@ exports.requireMaxSteps = (0, eslint_devkit_1.createRule)({
58
47
  ],
59
48
  create(context) {
60
49
  const sourceCode = context.sourceCode;
61
- // Vercel AI SDK functions that support tools
62
50
  const functionsWithTools = ['generateText', 'streamText'];
63
51
  return {
64
52
  CallExpression(node) {
65
53
  const callee = sourceCode.getText(node.callee);
66
- // Check if this is a target AI function
67
54
  const matchedFunction = functionsWithTools.find(fn => callee.includes(fn));
68
55
  if (!matchedFunction)
69
56
  return;
70
- // Check first argument (options object)
71
57
  const optionsArg = node.arguments[0];
72
58
  if (!optionsArg || optionsArg.type !== 'ObjectExpression')
73
59
  return;
74
- // Check if tools property exists
75
60
  const hasTools = optionsArg.properties.some(prop => {
76
61
  if (prop.type !== 'Property')
77
62
  return false;
78
63
  const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
79
64
  return keyName === 'tools';
80
65
  });
81
- // Only check for maxSteps if tools are present
82
66
  if (!hasTools)
83
67
  return;
84
- // Check if a step limit is present: maxSteps (v4) or stopWhen (v5+, e.g. stopWhen: stepCountIs(5))
85
68
  const hasMaxSteps = optionsArg.properties.some(prop => {
86
69
  if (prop.type !== 'Property')
87
70
  return false;
@@ -90,7 +73,6 @@ exports.requireMaxSteps = (0, eslint_devkit_1.createRule)({
90
73
  : prop.key.type === 'Literal'
91
74
  ? String(prop.key.value)
92
75
  : null;
93
- // ponytail: any stopWhen counts — statically proving it bounds steps (arrays, custom conditions) isn't worth it; documented FN
94
76
  return keyName === 'maxSteps' || keyName === 'max_steps' || keyName === 'stopWhen';
95
77
  });
96
78
  if (!hasMaxSteps) {
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireMaxTokens = void 0;
9
- /**
10
- * @fileoverview Require max tokens limit in generateText/streamText calls
11
- * @description Prevents unbounded token consumption in AI requests
12
- * @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text
13
- * @see https://owasp.org/www-project-top-10-for-large-language-model-applications/
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.requireMaxTokens = (0, eslint_devkit_1.createRule)({
17
6
  name: 'require-max-tokens',
@@ -71,17 +60,14 @@ exports.requireMaxTokens = (0, eslint_devkit_1.createRule)({
71
60
  return {
72
61
  CallExpression(node) {
73
62
  const callee = sourceCode.getText(node.callee);
74
- // Check if this is a target AI function
75
63
  const matchedFunction = targetFunctions.find((fn) => callee.includes(fn));
76
64
  if (!matchedFunction) {
77
65
  return;
78
66
  }
79
- // Check first argument (options object)
80
67
  const optionsArg = node.arguments[0];
81
68
  if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
82
69
  return;
83
70
  }
84
- // Check if a token limit is present: maxTokens (v4) or maxOutputTokens (v5+ rename)
85
71
  const hasMaxTokens = optionsArg.properties.some(prop => {
86
72
  if (prop.type !== 'Property')
87
73
  return false;
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireOutputFiltering = void 0;
9
- /**
10
- * @fileoverview Require output filtering for tool data
11
- * @description Detects when tools return raw data without filtering
12
- * @see OWASP ASI04: Data Exfiltration
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.requireOutputFiltering = (0, eslint_devkit_1.createRule)({
16
6
  name: 'require-output-filtering',
@@ -76,9 +66,6 @@ exports.requireOutputFiltering = (0, eslint_devkit_1.createRule)({
76
66
  'filter', 'sanitize', 'redact', 'mask',
77
67
  ];
78
68
  const sourceCode = context.sourceCode;
79
- /**
80
- * Check if expression is a data source call
81
- */
82
69
  function isDataSourceCall(node) {
83
70
  const callee = sourceCode.getText(node.callee);
84
71
  for (const pattern of dataSourcePatterns) {
@@ -88,17 +75,10 @@ exports.requireOutputFiltering = (0, eslint_devkit_1.createRule)({
88
75
  }
89
76
  return null;
90
77
  }
91
- /**
92
- * Check if expression is wrapped in a filter function
93
- */
94
78
  function isFilteredCall(node) {
95
79
  const callee = sourceCode.getText(node.callee);
96
80
  return filterFunctions.some((filter) => callee.toLowerCase().includes(filter.toLowerCase()));
97
81
  }
98
- /**
99
- * Get tool name from context
100
- */
101
- // oxlint-disable-next-line consistent-function-scoping
102
82
  function getToolName(node) {
103
83
  let current = node.parent;
104
84
  while (current) {
@@ -117,16 +97,13 @@ exports.requireOutputFiltering = (0, eslint_devkit_1.createRule)({
117
97
  return 'unknown';
118
98
  }
119
99
  return {
120
- // Check arrow function returns in tool execute
121
100
  ArrowFunctionExpression(node) {
122
- // Only check if this looks like a tool execute function
123
101
  const parent = node.parent;
124
102
  if (parent?.type !== 'Property')
125
103
  return;
126
104
  const keyName = parent.key.type === 'Identifier' ? parent.key.name : null;
127
105
  if (keyName !== 'execute')
128
106
  return;
129
- // Check the body for direct data source returns
130
107
  if (node.body.type === 'CallExpression') {
131
108
  const source = isDataSourceCall(node.body);
132
109
  if (source && !isFilteredCall(node.body)) {
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireOutputValidation = void 0;
9
- /**
10
- * @fileoverview Require validation of AI output before display
11
- * @description Detects when AI output is displayed without validation
12
- * @see OWASP LLM09: Misinformation
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.requireOutputValidation = (0, eslint_devkit_1.createRule)({
16
6
  name: 'require-output-validation',
@@ -73,13 +63,8 @@ exports.requireOutputValidation = (0, eslint_devkit_1.createRule)({
73
63
  'render', 'display', 'show', 'send', 'respond',
74
64
  ];
75
65
  const sourceCode = context.sourceCode;
76
- // AI output property patterns
77
66
  const aiOutputPatterns = ['.text', '.content', '.message', '.response', '.output'];
78
- // Track variables that hold AI results
79
67
  const aiResultVariables = new Set();
80
- /**
81
- * Check if expression accesses AI output
82
- */
83
68
  function isAIOutput(node) {
84
69
  if (node.type === 'MemberExpression') {
85
70
  const text = sourceCode.getText(node);
@@ -91,13 +76,11 @@ exports.requireOutputValidation = (0, eslint_devkit_1.createRule)({
91
76
  return false;
92
77
  }
93
78
  return {
94
- // Track AI result assignments
95
79
  VariableDeclarator(node) {
96
80
  if (node.id.type !== 'Identifier')
97
81
  return;
98
82
  if (!node.init)
99
83
  return;
100
- // Check if init is AI result access
101
84
  if (node.init.type === 'MemberExpression') {
102
85
  const text = sourceCode.getText(node.init);
103
86
  if (aiOutputPatterns.some(pattern => text.includes(pattern))) {
@@ -107,15 +90,10 @@ exports.requireOutputValidation = (0, eslint_devkit_1.createRule)({
107
90
  },
108
91
  CallExpression(node) {
109
92
  const callee = sourceCode.getText(node.callee);
110
- // Check if this is a display operation
111
93
  const isDisplay = displayPatterns.some(pattern => callee.toLowerCase().includes(pattern.toLowerCase()));
112
94
  if (!isDisplay)
113
95
  return;
114
- // Check arguments for unvalidated AI output
115
96
  for (const arg of node.arguments) {
116
- // NOTE: `isAIOutput` only matches MemberExpression/Identifier nodes,
117
- // so a wrapped call like `render(validate(result.text))` never
118
- // reaches here — validated output is inherently not flagged.
119
97
  if (isAIOutput(arg)) {
120
98
  context.report({
121
99
  node: arg,
@@ -123,7 +101,6 @@ exports.requireOutputValidation = (0, eslint_devkit_1.createRule)({
123
101
  data: { method: callee },
124
102
  });
125
103
  }
126
- // Check if argument is an object with AI output
127
104
  if (arg.type === 'ObjectExpression') {
128
105
  for (const prop of arg.properties) {
129
106
  if (prop.type !== 'Property')