eslint-plugin-vercel-ai-security 1.4.0 → 1.4.1
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/package.json +5 -20
- package/src/index.d.ts +17 -74
- package/src/index.js +1 -99
- package/src/oxlint.d.ts +17 -20
- package/src/oxlint.js +0 -18
- package/src/rules/no-dynamic-system-prompt/index.js +0 -22
- package/src/rules/no-hardcoded-api-keys/index.js +5 -28
- package/src/rules/no-sensitive-in-prompt/index.js +0 -20
- package/src/rules/no-system-prompt-leak/index.js +0 -25
- package/src/rules/no-training-data-exposure/index.js +0 -16
- package/src/rules/no-unsafe-output-handling/index.js +0 -35
- package/src/rules/require-abort-signal/index.js +0 -14
- package/src/rules/require-audit-logging/index.js +0 -24
- package/src/rules/require-embedding-validation/index.js +0 -23
- package/src/rules/require-error-handling/index.js +0 -19
- package/src/rules/require-max-steps/index.js +0 -18
- package/src/rules/require-max-tokens/index.js +0 -14
- package/src/rules/require-output-filtering/index.js +0 -23
- package/src/rules/require-output-validation/index.js +0 -23
- package/src/rules/require-rag-content-validation/index.js +0 -28
- package/src/rules/require-request-timeout/index.js +0 -16
- package/src/rules/require-tool-confirmation/index.js +1 -25
- package/src/rules/require-tool-schema/index.js +0 -19
- package/src/rules/require-validated-prompt/index.js +0 -33
- package/src/types/index.js +0 -5
- package/src/utils/prompt-props.js +0 -23
- package/AGENTS.md +0 -131
- package/CHANGELOG.md +0 -255
- package/src/rules/no-dynamic-system-prompt/index.d.ts +0 -14
- package/src/rules/no-hardcoded-api-keys/index.d.ts +0 -14
- package/src/rules/no-sensitive-in-prompt/index.d.ts +0 -14
- package/src/rules/no-system-prompt-leak/index.d.ts +0 -14
- package/src/rules/no-training-data-exposure/index.d.ts +0 -14
- package/src/rules/no-unsafe-output-handling/index.d.ts +0 -15
- package/src/rules/require-abort-signal/index.d.ts +0 -14
- package/src/rules/require-audit-logging/index.d.ts +0 -14
- package/src/rules/require-embedding-validation/index.d.ts +0 -16
- package/src/rules/require-error-handling/index.d.ts +0 -14
- package/src/rules/require-max-steps/index.d.ts +0 -14
- package/src/rules/require-max-tokens/index.d.ts +0 -16
- package/src/rules/require-output-filtering/index.d.ts +0 -16
- package/src/rules/require-output-validation/index.d.ts +0 -16
- package/src/rules/require-rag-content-validation/index.d.ts +0 -16
- package/src/rules/require-request-timeout/index.d.ts +0 -14
- package/src/rules/require-tool-confirmation/index.d.ts +0 -14
- package/src/rules/require-tool-schema/index.d.ts +0 -15
- package/src/rules/require-validated-prompt/index.d.ts +0 -19
- 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')
|