eslint-plugin-vercel-ai-security 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.js +1 -145
- package/src/oxlint.js +1 -3
- package/src/rules/no-dynamic-system-prompt/index.js +1 -96
- package/src/rules/no-hardcoded-api-keys/index.js +1 -122
- package/src/rules/no-sensitive-in-prompt/index.js +1 -121
- package/src/rules/no-system-prompt-leak/index.js +1 -116
- package/src/rules/no-training-data-exposure/index.js +1 -96
- package/src/rules/no-unsafe-output-handling/index.js +1 -193
- package/src/rules/require-abort-signal/index.js +1 -80
- package/src/rules/require-audit-logging/index.js +1 -110
- package/src/rules/require-embedding-validation/index.js +1 -119
- package/src/rules/require-error-handling/index.js +1 -92
- package/src/rules/require-max-steps/index.js +1 -88
- package/src/rules/require-max-tokens/index.js +1 -94
- package/src/rules/require-output-filtering/index.js +1 -123
- package/src/rules/require-output-validation/index.js +1 -121
- package/src/rules/require-rag-content-validation/index.js +1 -150
- package/src/rules/require-request-timeout/index.js +1 -88
- package/src/rules/require-tool-confirmation/index.js +1 -118
- package/src/rules/require-tool-schema/index.js +1 -132
- package/src/rules/require-validated-prompt/index.js +1 -194
- package/src/types/index.js +1 -2
- package/src/utils/prompt-props.js +1 -20
|
@@ -1,121 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireOutputValidation = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
exports.requireOutputValidation = (0, eslint_devkit_1.createRule)({
|
|
6
|
-
name: 'require-output-validation',
|
|
7
|
-
meta: {
|
|
8
|
-
type: 'suggestion',
|
|
9
|
-
docs: {
|
|
10
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-output-validation.md',
|
|
11
|
-
description: 'Require validation of AI output before displaying to users',
|
|
12
|
-
cwe: 'CWE-707',
|
|
13
|
-
cvss: 5,
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
unvalidatedOutput: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
18
|
-
issueName: 'Unvalidated AI Output',
|
|
19
|
-
cwe: 'CWE-707',
|
|
20
|
-
owasp: 'A03:2021',
|
|
21
|
-
cvss: 5.0,
|
|
22
|
-
description: 'AI output displayed via "{{method}}" without validation. This can propagate misinformation.',
|
|
23
|
-
severity: 'MEDIUM',
|
|
24
|
-
compliance: ['SOC2'],
|
|
25
|
-
fix: 'Validate AI output before display: display(validateOutput(result.text))',
|
|
26
|
-
documentationLink: 'https://owasp.org/www-project-top-10-for-large-language-model-applications/',
|
|
27
|
-
}),
|
|
28
|
-
},
|
|
29
|
-
schema: [
|
|
30
|
-
{
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: {
|
|
33
|
-
displayPatterns: {
|
|
34
|
-
type: 'array',
|
|
35
|
-
items: { type: 'string' },
|
|
36
|
-
description: 'Patterns suggesting display operations',
|
|
37
|
-
},
|
|
38
|
-
validatorFunctions: {
|
|
39
|
-
type: 'array',
|
|
40
|
-
items: { type: 'string' },
|
|
41
|
-
description: 'Functions that validate output',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
additionalProperties: false,
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
defaultOptions: [
|
|
49
|
-
{
|
|
50
|
-
displayPatterns: [
|
|
51
|
-
'render', 'display', 'show', 'print', 'send', 'respond',
|
|
52
|
-
'setContent', 'setText', 'setMessage', 'Response.json',
|
|
53
|
-
],
|
|
54
|
-
validatorFunctions: [
|
|
55
|
-
'validate', 'verify', 'check', 'sanitize', 'filter',
|
|
56
|
-
'validateOutput', 'factCheck', 'verifyFacts',
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
create(context) {
|
|
61
|
-
const [options = {}] = context.options;
|
|
62
|
-
const displayPatterns = options.displayPatterns ?? [
|
|
63
|
-
'render', 'display', 'show', 'send', 'respond',
|
|
64
|
-
];
|
|
65
|
-
const sourceCode = context.sourceCode;
|
|
66
|
-
const aiOutputPatterns = ['.text', '.content', '.message', '.response', '.output'];
|
|
67
|
-
const aiResultVariables = new Set();
|
|
68
|
-
function isAIOutput(node) {
|
|
69
|
-
if (node.type === 'MemberExpression') {
|
|
70
|
-
const text = sourceCode.getText(node);
|
|
71
|
-
return aiOutputPatterns.some(pattern => text.includes(pattern));
|
|
72
|
-
}
|
|
73
|
-
if (node.type === 'Identifier' && aiResultVariables.has(node.name)) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
VariableDeclarator(node) {
|
|
80
|
-
if (node.id.type !== 'Identifier')
|
|
81
|
-
return;
|
|
82
|
-
if (!node.init)
|
|
83
|
-
return;
|
|
84
|
-
if (node.init.type === 'MemberExpression') {
|
|
85
|
-
const text = sourceCode.getText(node.init);
|
|
86
|
-
if (aiOutputPatterns.some(pattern => text.includes(pattern))) {
|
|
87
|
-
aiResultVariables.add(node.id.name);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
CallExpression(node) {
|
|
92
|
-
const callee = sourceCode.getText(node.callee);
|
|
93
|
-
const isDisplay = displayPatterns.some(pattern => callee.toLowerCase().includes(pattern.toLowerCase()));
|
|
94
|
-
if (!isDisplay)
|
|
95
|
-
return;
|
|
96
|
-
for (const arg of node.arguments) {
|
|
97
|
-
if (isAIOutput(arg)) {
|
|
98
|
-
context.report({
|
|
99
|
-
node: arg,
|
|
100
|
-
messageId: 'unvalidatedOutput',
|
|
101
|
-
data: { method: callee },
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (arg.type === 'ObjectExpression') {
|
|
105
|
-
for (const prop of arg.properties) {
|
|
106
|
-
if (prop.type !== 'Property')
|
|
107
|
-
continue;
|
|
108
|
-
if (isAIOutput(prop.value)) {
|
|
109
|
-
context.report({
|
|
110
|
-
node: prop.value,
|
|
111
|
-
messageId: 'unvalidatedOutput',
|
|
112
|
-
data: { method: callee },
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
},
|
|
121
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireOutputValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireOutputValidation=(0,eslint_devkit_1.createRule)({name:"require-output-validation",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-output-validation.md",description:"Require validation of AI output before displaying to users",cwe:"CWE-707",cvss:5},messages:{unvalidatedOutput:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unvalidated AI Output",cwe:"CWE-707",owasp:"A03:2021",cvss:5,description:'AI output displayed via "{{method}}" without validation. This can propagate misinformation.',severity:"MEDIUM",compliance:["SOC2"],fix:"Validate AI output before display: display(validateOutput(result.text))",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{displayPatterns:{type:"array",items:{type:"string"},description:"Patterns suggesting display operations"},validatorFunctions:{type:"array",items:{type:"string"},description:"Functions that validate output"}},additionalProperties:false}]},defaultOptions:[{displayPatterns:["render","display","show","print","send","respond","setContent","setText","setMessage","Response.json"],validatorFunctions:["validate","verify","check","sanitize","filter","validateOutput","factCheck","verifyFacts"]}],create(context){const[options={}]=context.options;const displayPatterns=options.displayPatterns??["render","display","show","send","respond"];const sourceCode=context.sourceCode;const aiOutputPatterns=[".text",".content",".message",".response",".output"];const aiResultVariables=new Set;function isAIOutput(node){if(node.type==="MemberExpression"){const text=sourceCode.getText(node);return aiOutputPatterns.some(pattern=>text.includes(pattern))}if(node.type==="Identifier"&&aiResultVariables.has(node.name)){return true}return false}return{VariableDeclarator(node){if(node.id.type!=="Identifier")return;if(!node.init)return;if(node.init.type==="MemberExpression"){const text=sourceCode.getText(node.init);if(aiOutputPatterns.some(pattern=>text.includes(pattern))){aiResultVariables.add(node.id.name)}}},CallExpression(node){const callee=sourceCode.getText(node.callee);const isDisplay=displayPatterns.some(pattern=>callee.toLowerCase().includes(pattern.toLowerCase()));if(!isDisplay)return;for(const arg of node.arguments){if(isAIOutput(arg)){context.report({node:arg,messageId:"unvalidatedOutput",data:{method:callee}})}if(arg.type==="ObjectExpression"){for(const prop of arg.properties){if(prop.type!=="Property")continue;if(isAIOutput(prop.value)){context.report({node:prop.value,messageId:"unvalidatedOutput",data:{method:callee}})}}}}}}}});
|
|
@@ -1,150 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireRagContentValidation = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const prompt_props_1 = require("../../utils/prompt-props");
|
|
6
|
-
exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
|
|
7
|
-
name: 'require-rag-content-validation',
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'suggestion',
|
|
10
|
-
docs: {
|
|
11
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-rag-content-validation.md',
|
|
12
|
-
description: 'Require validation of RAG content before including in AI prompts',
|
|
13
|
-
cwe: 'CWE-74',
|
|
14
|
-
cvss: 6,
|
|
15
|
-
},
|
|
16
|
-
messages: {
|
|
17
|
-
unsanitizedRagContent: (0, eslint_devkit_1.formatLLMMessage)({
|
|
18
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
19
|
-
issueName: 'Unsanitized RAG Content',
|
|
20
|
-
cwe: 'CWE-74',
|
|
21
|
-
owasp: 'A03:2021',
|
|
22
|
-
cvss: 6.0,
|
|
23
|
-
description: 'RAG content from "{{source}}" used directly in prompt without validation. Poisoned documents can inject malicious instructions.',
|
|
24
|
-
severity: 'MEDIUM',
|
|
25
|
-
compliance: ['SOC2'],
|
|
26
|
-
fix: 'Validate RAG content: prompt: buildPrompt(validateRagContent(docs))',
|
|
27
|
-
documentationLink: 'https://owasp.org/www-project-top-10-for-large-language-model-applications/',
|
|
28
|
-
}),
|
|
29
|
-
},
|
|
30
|
-
schema: [
|
|
31
|
-
{
|
|
32
|
-
type: 'object',
|
|
33
|
-
properties: {
|
|
34
|
-
ragPatterns: {
|
|
35
|
-
type: 'array',
|
|
36
|
-
items: { type: 'string' },
|
|
37
|
-
description: 'Patterns suggesting RAG operations',
|
|
38
|
-
},
|
|
39
|
-
validatorFunctions: {
|
|
40
|
-
type: 'array',
|
|
41
|
-
items: { type: 'string' },
|
|
42
|
-
description: 'Functions that validate RAG content',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
additionalProperties: false,
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
defaultOptions: [
|
|
50
|
-
{
|
|
51
|
-
ragPatterns: [
|
|
52
|
-
'search', 'retrieve', 'query', 'vectorStore', 'embeddings',
|
|
53
|
-
'similaritySearch', 'findSimilar', 'getDocuments', 'fetchDocs',
|
|
54
|
-
'documents', 'chunks', 'passages', 'context',
|
|
55
|
-
],
|
|
56
|
-
validatorFunctions: [
|
|
57
|
-
'validate', 'sanitize', 'filter', 'clean', 'verify',
|
|
58
|
-
'validateRag', 'sanitizeContent', 'filterDocs',
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
create(context) {
|
|
63
|
-
const [options = {}] = context.options;
|
|
64
|
-
const ragPatterns = options.ragPatterns ?? [
|
|
65
|
-
'search', 'retrieve', 'query', 'vectorStore', 'documents',
|
|
66
|
-
];
|
|
67
|
-
const validatorFunctions = options.validatorFunctions ?? [
|
|
68
|
-
'validate', 'sanitize', 'filter', 'clean',
|
|
69
|
-
];
|
|
70
|
-
const sourceCode = context.sourceCode;
|
|
71
|
-
const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
|
|
72
|
-
const ragVariables = new Set();
|
|
73
|
-
function isRagCall(node) {
|
|
74
|
-
if (node.type !== 'CallExpression')
|
|
75
|
-
return null;
|
|
76
|
-
const callee = sourceCode.getText(node.callee);
|
|
77
|
-
for (const pattern of ragPatterns) {
|
|
78
|
-
if (callee.toLowerCase().includes(pattern.toLowerCase())) {
|
|
79
|
-
return callee;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
function isValidated(node) {
|
|
85
|
-
const callee = sourceCode.getText(node.callee);
|
|
86
|
-
return validatorFunctions.some((fn) => callee.toLowerCase().includes(fn.toLowerCase()));
|
|
87
|
-
}
|
|
88
|
-
function containsRagContent(node) {
|
|
89
|
-
if (node.type === 'Identifier' && ragVariables.has(node.name)) {
|
|
90
|
-
return node.name;
|
|
91
|
-
}
|
|
92
|
-
if (node.type === 'TemplateLiteral') {
|
|
93
|
-
for (const expr of node.expressions) {
|
|
94
|
-
const rag = containsRagContent(expr);
|
|
95
|
-
if (rag)
|
|
96
|
-
return rag;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (node.type === 'AwaitExpression') {
|
|
100
|
-
return containsRagContent(node.argument);
|
|
101
|
-
}
|
|
102
|
-
if (node.type === 'CallExpression') {
|
|
103
|
-
const ragSource = isRagCall(node);
|
|
104
|
-
if (ragSource && !isValidated(node)) {
|
|
105
|
-
return ragSource;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
VariableDeclarator(node) {
|
|
112
|
-
if (node.id.type !== 'Identifier')
|
|
113
|
-
return;
|
|
114
|
-
if (!node.init)
|
|
115
|
-
return;
|
|
116
|
-
let initNode = node.init;
|
|
117
|
-
if (initNode.type === 'AwaitExpression') {
|
|
118
|
-
initNode = initNode.argument;
|
|
119
|
-
}
|
|
120
|
-
if (isRagCall(initNode)) {
|
|
121
|
-
ragVariables.add(node.id.name);
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
CallExpression(node) {
|
|
125
|
-
const callee = sourceCode.getText(node.callee);
|
|
126
|
-
const isAIFunction = aiSDKFunctions.some(fn => callee.includes(fn));
|
|
127
|
-
if (!isAIFunction)
|
|
128
|
-
return;
|
|
129
|
-
const optionsArg = node.arguments[0];
|
|
130
|
-
if (!optionsArg || optionsArg.type !== 'ObjectExpression')
|
|
131
|
-
return;
|
|
132
|
-
for (const prop of optionsArg.properties) {
|
|
133
|
-
if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
|
|
134
|
-
continue;
|
|
135
|
-
const keyName = (0, prompt_props_1.getStaticPropName)(prop);
|
|
136
|
-
if (keyName !== 'prompt' && !(0, prompt_props_1.isSystemPromptProp)(keyName))
|
|
137
|
-
continue;
|
|
138
|
-
const ragSource = containsRagContent(prop.value);
|
|
139
|
-
if (ragSource) {
|
|
140
|
-
context.report({
|
|
141
|
-
node: prop.value,
|
|
142
|
-
messageId: 'unsanitizedRagContent',
|
|
143
|
-
data: { source: ragSource },
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
},
|
|
150
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireRagContentValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");exports.requireRagContentValidation=(0,eslint_devkit_1.createRule)({name:"require-rag-content-validation",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-rag-content-validation.md",description:"Require validation of RAG content before including in AI prompts",cwe:"CWE-74",cvss:6},messages:{unsanitizedRagContent:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unsanitized RAG Content",cwe:"CWE-74",owasp:"A03:2021",cvss:6,description:'RAG content from "{{source}}" used directly in prompt without validation. Poisoned documents can inject malicious instructions.',severity:"MEDIUM",compliance:["SOC2"],fix:"Validate RAG content: prompt: buildPrompt(validateRagContent(docs))",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{ragPatterns:{type:"array",items:{type:"string"},description:"Patterns suggesting RAG operations"},validatorFunctions:{type:"array",items:{type:"string"},description:"Functions that validate RAG content"}},additionalProperties:false}]},defaultOptions:[{ragPatterns:["search","retrieve","query","vectorStore","embeddings","similaritySearch","findSimilar","getDocuments","fetchDocs","documents","chunks","passages","context"],validatorFunctions:["validate","sanitize","filter","clean","verify","validateRag","sanitizeContent","filterDocs"]}],create(context){const[options={}]=context.options;const ragPatterns=options.ragPatterns??["search","retrieve","query","vectorStore","documents"];const validatorFunctions=options.validatorFunctions??["validate","sanitize","filter","clean"];const sourceCode=context.sourceCode;const aiSDKFunctions=["generateText","streamText","generateObject","streamObject"];const ragVariables=new Set;function isRagCall(node){if(node.type!=="CallExpression")return null;const callee=sourceCode.getText(node.callee);for(const pattern of ragPatterns){if(callee.toLowerCase().includes(pattern.toLowerCase())){return callee}}return null}function isValidated(node){const callee=sourceCode.getText(node.callee);return validatorFunctions.some(fn=>callee.toLowerCase().includes(fn.toLowerCase()))}function containsRagContent(node){if(node.type==="Identifier"&&ragVariables.has(node.name)){return node.name}if(node.type==="TemplateLiteral"){for(const expr of node.expressions){const rag=containsRagContent(expr);if(rag)return rag}}if(node.type==="AwaitExpression"){return containsRagContent(node.argument)}if(node.type==="CallExpression"){const ragSource=isRagCall(node);if(ragSource&&!isValidated(node)){return ragSource}}return null}return{VariableDeclarator(node){if(node.id.type!=="Identifier")return;if(!node.init)return;let initNode=node.init;if(initNode.type==="AwaitExpression"){initNode=initNode.argument}if(isRagCall(initNode)){ragVariables.add(node.id.name)}},CallExpression(node){const callee=sourceCode.getText(node.callee);const isAIFunction=aiSDKFunctions.some(fn=>callee.includes(fn));if(!isAIFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;for(const prop of optionsArg.properties){if(prop.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;const keyName=(0,prompt_props_1.getStaticPropName)(prop);if(keyName!=="prompt"&&!(0,prompt_props_1.isSystemPromptProp)(keyName))continue;const ragSource=containsRagContent(prop.value);if(ragSource){context.report({node:prop.value,messageId:"unsanitizedRagContent",data:{source:ragSource}})}}}}}});
|
|
@@ -1,88 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireRequestTimeout = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
|
|
6
|
-
name: 'require-request-timeout',
|
|
7
|
-
meta: {
|
|
8
|
-
type: 'suggestion',
|
|
9
|
-
docs: {
|
|
10
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-request-timeout.md',
|
|
11
|
-
description: 'Require timeout configuration for AI SDK calls to prevent DoS',
|
|
12
|
-
cwe: 'CWE-400',
|
|
13
|
-
cvss: 5,
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
missingTimeout: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
18
|
-
issueName: 'Missing Request Timeout',
|
|
19
|
-
cwe: 'CWE-400',
|
|
20
|
-
owasp: 'A05:2021',
|
|
21
|
-
cvss: 5.0,
|
|
22
|
-
description: '{{function}} call lacks timeout configuration. This can lead to denial of service.',
|
|
23
|
-
severity: 'MEDIUM',
|
|
24
|
-
compliance: ['SOC2'],
|
|
25
|
-
fix: 'Add timeout configuration or use AbortController with setTimeout',
|
|
26
|
-
documentationLink: 'https://owasp.org/www-project-top-10-for-large-language-model-applications/',
|
|
27
|
-
}),
|
|
28
|
-
},
|
|
29
|
-
schema: [
|
|
30
|
-
{
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: {
|
|
33
|
-
allowInTests: {
|
|
34
|
-
type: 'boolean',
|
|
35
|
-
description: 'Skip in test files',
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
additionalProperties: false,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
defaultOptions: [
|
|
43
|
-
{
|
|
44
|
-
allowInTests: true,
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
create(context) {
|
|
48
|
-
const [options = {}] = context.options;
|
|
49
|
-
const allowInTests = options.allowInTests ?? true;
|
|
50
|
-
const sourceCode = context.sourceCode;
|
|
51
|
-
const filename = context.filename;
|
|
52
|
-
if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
53
|
-
return {};
|
|
54
|
-
}
|
|
55
|
-
const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
|
|
56
|
-
const timeoutProperties = new Set(['timeout', 'abortSignal', 'signal', 'timeoutMs', 'requestTimeout']);
|
|
57
|
-
return {
|
|
58
|
-
CallExpression(node) {
|
|
59
|
-
const callee = sourceCode.getText(node.callee);
|
|
60
|
-
const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
|
|
61
|
-
if (!matchedFunction)
|
|
62
|
-
return;
|
|
63
|
-
const optionsArg = node.arguments[0];
|
|
64
|
-
if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
|
|
65
|
-
context.report({
|
|
66
|
-
node,
|
|
67
|
-
messageId: 'missingTimeout',
|
|
68
|
-
data: { function: matchedFunction },
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const hasTimeout = optionsArg.properties.some(prop => {
|
|
73
|
-
if (prop.type !== 'Property')
|
|
74
|
-
return false;
|
|
75
|
-
const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
|
|
76
|
-
return keyName && timeoutProperties.has(keyName);
|
|
77
|
-
});
|
|
78
|
-
if (!hasTimeout) {
|
|
79
|
-
context.report({
|
|
80
|
-
node,
|
|
81
|
-
messageId: 'missingTimeout',
|
|
82
|
-
data: { function: matchedFunction },
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireRequestTimeout=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireRequestTimeout=(0,eslint_devkit_1.createRule)({name:"require-request-timeout",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-request-timeout.md",description:"Require timeout configuration for AI SDK calls to prevent DoS",cwe:"CWE-400",cvss:5},messages:{missingTimeout:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Missing Request Timeout",cwe:"CWE-400",owasp:"A05:2021",cvss:5,description:"{{function}} call lacks timeout configuration. This can lead to denial of service.",severity:"MEDIUM",compliance:["SOC2"],fix:"Add timeout configuration or use AbortController with setTimeout",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",description:"Skip in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:true}],create(context){const[options={}]=context.options;const allowInTests=options.allowInTests??true;const sourceCode=context.sourceCode;const filename=context.filename;if(allowInTests&&/\.(test|spec)\.[jt]sx?$/.test(filename)){return{}}const aiSDKFunctions=["generateText","streamText","generateObject","streamObject"];const timeoutProperties=new Set(["timeout","abortSignal","signal","timeoutMs","requestTimeout"]);return{CallExpression(node){const callee=sourceCode.getText(node.callee);const matchedFunction=aiSDKFunctions.find(fn=>callee.includes(fn));if(!matchedFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression"){context.report({node,messageId:"missingTimeout",data:{function:matchedFunction}});return}const hasTimeout=optionsArg.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:null;return keyName&&timeoutProperties.has(keyName)});if(!hasTimeout){context.report({node,messageId:"missingTimeout",data:{function:matchedFunction}})}}}}});
|
|
@@ -1,118 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireToolConfirmation = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
|
|
6
|
-
name: 'require-tool-confirmation',
|
|
7
|
-
meta: {
|
|
8
|
-
type: 'suggestion',
|
|
9
|
-
docs: {
|
|
10
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-tool-confirmation.md',
|
|
11
|
-
description: 'Require human confirmation for destructive tool operations (delete, transfer, execute)',
|
|
12
|
-
cwe: 'CWE-862',
|
|
13
|
-
cvss: 7,
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
missingConfirmation: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
18
|
-
issueName: 'Destructive Tool Without Confirmation',
|
|
19
|
-
cwe: 'CWE-862',
|
|
20
|
-
owasp: 'A01:2021',
|
|
21
|
-
cvss: 7.0,
|
|
22
|
-
description: 'Tool "{{toolName}}" performs destructive operation "{{operation}}" without requiring confirmation.',
|
|
23
|
-
severity: 'HIGH',
|
|
24
|
-
compliance: ['SOC2'],
|
|
25
|
-
fix: 'Add requiresConfirmation: true or implement confirmation logic in the tool',
|
|
26
|
-
documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling',
|
|
27
|
-
}),
|
|
28
|
-
},
|
|
29
|
-
schema: [
|
|
30
|
-
{
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: {
|
|
33
|
-
destructivePatterns: {
|
|
34
|
-
type: 'array',
|
|
35
|
-
items: { type: 'string' },
|
|
36
|
-
description: 'Patterns that suggest destructive operations',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
additionalProperties: false,
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
defaultOptions: [
|
|
44
|
-
{
|
|
45
|
-
destructivePatterns: [
|
|
46
|
-
'delete', 'remove', 'drop', 'truncate', 'destroy',
|
|
47
|
-
'transfer', 'send', 'pay', 'withdraw', 'purchase',
|
|
48
|
-
'execute', 'run', 'eval', 'exec', 'spawn',
|
|
49
|
-
'update', 'modify', 'change', 'alter',
|
|
50
|
-
'create', 'insert', 'post', 'write',
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
create(context) {
|
|
55
|
-
const [options = {}] = context.options;
|
|
56
|
-
const destructivePatterns = options.destructivePatterns ?? [
|
|
57
|
-
'delete', 'remove', 'transfer', 'execute', 'update', 'create',
|
|
58
|
-
];
|
|
59
|
-
function isDestructiveTool(name) {
|
|
60
|
-
const lowerName = name.toLowerCase();
|
|
61
|
-
for (const pattern of destructivePatterns) {
|
|
62
|
-
if (lowerName.includes(pattern.toLowerCase())) {
|
|
63
|
-
return pattern;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
function hasConfirmationFlag(toolDef) {
|
|
69
|
-
const confirmationProps = new Set([
|
|
70
|
-
'requiresConfirmation', 'requireConfirmation', 'confirmation',
|
|
71
|
-
'requiresApproval', 'requireApproval', 'approval',
|
|
72
|
-
'dangerouslyAllowBrowser',
|
|
73
|
-
]);
|
|
74
|
-
return toolDef.properties.some(prop => {
|
|
75
|
-
if (prop.type !== 'Property')
|
|
76
|
-
return false;
|
|
77
|
-
const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
|
|
78
|
-
return keyName && confirmationProps.has(keyName);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
Property(node) {
|
|
83
|
-
if (node.key.type !== 'Identifier' && node.key.type !== 'Literal')
|
|
84
|
-
return;
|
|
85
|
-
const toolName = node.key.type === 'Identifier'
|
|
86
|
-
? node.key.name
|
|
87
|
-
: String(node.key.value);
|
|
88
|
-
if (node.value.type !== 'ObjectExpression' && node.value.type !== 'CallExpression') {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const destructiveOp = isDestructiveTool(toolName);
|
|
92
|
-
if (!destructiveOp)
|
|
93
|
-
return;
|
|
94
|
-
const parent = node.parent;
|
|
95
|
-
if (parent?.type !== 'ObjectExpression')
|
|
96
|
-
return;
|
|
97
|
-
const grandparent = parent.parent;
|
|
98
|
-
if (grandparent?.type !== 'Property')
|
|
99
|
-
return;
|
|
100
|
-
const toolsKey = grandparent.key;
|
|
101
|
-
if (toolsKey.type !== 'Identifier' || toolsKey.name !== 'tools')
|
|
102
|
-
return;
|
|
103
|
-
if (node.value.type === 'ObjectExpression') {
|
|
104
|
-
if (!hasConfirmationFlag(node.value)) {
|
|
105
|
-
context.report({
|
|
106
|
-
node,
|
|
107
|
-
messageId: 'missingConfirmation',
|
|
108
|
-
data: {
|
|
109
|
-
toolName,
|
|
110
|
-
operation: destructiveOp,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireToolConfirmation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireToolConfirmation=(0,eslint_devkit_1.createRule)({name:"require-tool-confirmation",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-tool-confirmation.md",description:"Require human confirmation for destructive tool operations (delete, transfer, execute)",cwe:"CWE-862",cvss:7},messages:{missingConfirmation:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Destructive Tool Without Confirmation",cwe:"CWE-862",owasp:"A01:2021",cvss:7,description:'Tool "{{toolName}}" performs destructive operation "{{operation}}" without requiring confirmation.',severity:"HIGH",compliance:["SOC2"],fix:"Add requiresConfirmation: true or implement confirmation logic in the tool",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling"})},schema:[{type:"object",properties:{destructivePatterns:{type:"array",items:{type:"string"},description:"Patterns that suggest destructive operations"}},additionalProperties:false}]},defaultOptions:[{destructivePatterns:["delete","remove","drop","truncate","destroy","transfer","send","pay","withdraw","purchase","execute","run","eval","exec","spawn","update","modify","change","alter","create","insert","post","write"]}],create(context){const[options={}]=context.options;const destructivePatterns=options.destructivePatterns??["delete","remove","transfer","execute","update","create"];function isDestructiveTool(name){const lowerName=name.toLowerCase();for(const pattern of destructivePatterns){if(lowerName.includes(pattern.toLowerCase())){return pattern}}return null}function hasConfirmationFlag(toolDef){const confirmationProps=new Set(["requiresConfirmation","requireConfirmation","confirmation","requiresApproval","requireApproval","approval","dangerouslyAllowBrowser"]);return toolDef.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:null;return keyName&&confirmationProps.has(keyName)})}return{Property(node){if(node.key.type!=="Identifier"&&node.key.type!=="Literal")return;const toolName=node.key.type==="Identifier"?node.key.name:String(node.key.value);if(node.value.type!=="ObjectExpression"&&node.value.type!=="CallExpression"){return}const destructiveOp=isDestructiveTool(toolName);if(!destructiveOp)return;const parent=node.parent;if(parent?.type!=="ObjectExpression")return;const grandparent=parent.parent;if(grandparent?.type!=="Property")return;const toolsKey=grandparent.key;if(toolsKey.type!=="Identifier"||toolsKey.name!=="tools")return;if(node.value.type==="ObjectExpression"){if(!hasConfirmationFlag(node.value)){context.report({node,messageId:"missingConfirmation",data:{toolName,operation:destructiveOp}})}}}}}});
|
|
@@ -1,132 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireToolSchema = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
|
|
6
|
-
name: 'require-tool-schema',
|
|
7
|
-
meta: {
|
|
8
|
-
type: 'problem',
|
|
9
|
-
docs: {
|
|
10
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-tool-schema.md',
|
|
11
|
-
description: 'Require inputSchema (Zod schema) for all AI SDK tools',
|
|
12
|
-
cwe: 'CWE-20',
|
|
13
|
-
cvss: 7.5,
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
missingInputSchema: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
18
|
-
issueName: 'Tool Missing Input Schema',
|
|
19
|
-
cwe: 'CWE-20',
|
|
20
|
-
owasp: 'A03:2021',
|
|
21
|
-
cvss: 7.5,
|
|
22
|
-
description: 'Tool "{{toolName}}" is missing inputSchema. Unvalidated tool parameters can lead to injection attacks.',
|
|
23
|
-
severity: 'HIGH',
|
|
24
|
-
compliance: ['SOC2'],
|
|
25
|
-
fix: 'Add inputSchema using Zod: tool({ inputSchema: z.object({ ... }), execute: ... })',
|
|
26
|
-
documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling',
|
|
27
|
-
}),
|
|
28
|
-
emptyToolsObject: (0, eslint_devkit_1.formatLLMMessage)({
|
|
29
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
30
|
-
issueName: 'Empty Tools Object',
|
|
31
|
-
cwe: 'CWE-20',
|
|
32
|
-
owasp: 'A03:2021',
|
|
33
|
-
description: 'Tools object is empty or has no tool definitions',
|
|
34
|
-
severity: 'LOW',
|
|
35
|
-
fix: 'Define tools with proper schemas or remove tools property',
|
|
36
|
-
documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling',
|
|
37
|
-
}),
|
|
38
|
-
},
|
|
39
|
-
schema: [
|
|
40
|
-
{
|
|
41
|
-
type: 'object',
|
|
42
|
-
properties: {
|
|
43
|
-
allowInTests: {
|
|
44
|
-
type: 'boolean',
|
|
45
|
-
description: 'Allow tools without inputSchema in test files',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
additionalProperties: false,
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
defaultOptions: [
|
|
53
|
-
{
|
|
54
|
-
allowInTests: false,
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
create(context) {
|
|
58
|
-
const [options = {}] = context.options;
|
|
59
|
-
const allowInTests = options.allowInTests ?? false;
|
|
60
|
-
const sourceCode = context.sourceCode;
|
|
61
|
-
const filename = context.filename;
|
|
62
|
-
if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
|
|
63
|
-
return {};
|
|
64
|
-
}
|
|
65
|
-
const functionsWithTools = ['generateText', 'streamText', 'generateObject', 'streamObject'];
|
|
66
|
-
return {
|
|
67
|
-
CallExpression(node) {
|
|
68
|
-
const callee = sourceCode.getText(node.callee);
|
|
69
|
-
const isAIFunction = functionsWithTools.some(fn => callee.includes(fn));
|
|
70
|
-
const isToolHelper = callee === 'tool' || callee.endsWith('.tool');
|
|
71
|
-
if (isToolHelper) {
|
|
72
|
-
const toolArg = node.arguments[0];
|
|
73
|
-
if (toolArg && toolArg.type === 'ObjectExpression') {
|
|
74
|
-
const hasInputSchema = toolArg.properties.some(prop => {
|
|
75
|
-
if (prop.type !== 'Property')
|
|
76
|
-
return false;
|
|
77
|
-
const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
|
|
78
|
-
return keyName === 'inputSchema' || keyName === 'parameters';
|
|
79
|
-
});
|
|
80
|
-
if (!hasInputSchema) {
|
|
81
|
-
context.report({
|
|
82
|
-
node: toolArg,
|
|
83
|
-
messageId: 'missingInputSchema',
|
|
84
|
-
data: { toolName: 'unnamed tool' },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (!isAIFunction)
|
|
91
|
-
return;
|
|
92
|
-
const optionsArg = node.arguments[0];
|
|
93
|
-
if (!optionsArg || optionsArg.type !== 'ObjectExpression')
|
|
94
|
-
return;
|
|
95
|
-
const toolsProp = optionsArg.properties.find(prop => {
|
|
96
|
-
if (prop.type !== 'Property')
|
|
97
|
-
return false;
|
|
98
|
-
const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
|
|
99
|
-
return keyName === 'tools';
|
|
100
|
-
});
|
|
101
|
-
if (!toolsProp)
|
|
102
|
-
return;
|
|
103
|
-
if (toolsProp.value.type === 'ObjectExpression') {
|
|
104
|
-
for (const toolDef of toolsProp.value.properties) {
|
|
105
|
-
if (toolDef.type !== 'Property')
|
|
106
|
-
continue;
|
|
107
|
-
const toolName = toolDef.key.type === 'Identifier'
|
|
108
|
-
? toolDef.key.name
|
|
109
|
-
: toolDef.key.type === 'Literal'
|
|
110
|
-
? String(toolDef.key.value)
|
|
111
|
-
: 'unknown';
|
|
112
|
-
if (toolDef.value.type === 'ObjectExpression') {
|
|
113
|
-
const hasInputSchema = toolDef.value.properties.some(prop => {
|
|
114
|
-
if (prop.type !== 'Property')
|
|
115
|
-
return false;
|
|
116
|
-
const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
|
|
117
|
-
return keyName === 'inputSchema' || keyName === 'parameters';
|
|
118
|
-
});
|
|
119
|
-
if (!hasInputSchema) {
|
|
120
|
-
context.report({
|
|
121
|
-
node: toolDef.value,
|
|
122
|
-
messageId: 'missingInputSchema',
|
|
123
|
-
data: { toolName },
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
},
|
|
132
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireToolSchema=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireToolSchema=(0,eslint_devkit_1.createRule)({name:"require-tool-schema",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-tool-schema.md",description:"Require inputSchema (Zod schema) for all AI SDK tools",cwe:"CWE-20",cvss:7.5},messages:{missingInputSchema:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Tool Missing Input Schema",cwe:"CWE-20",owasp:"A03:2021",cvss:7.5,description:'Tool "{{toolName}}" is missing inputSchema. Unvalidated tool parameters can lead to injection attacks.',severity:"HIGH",compliance:["SOC2"],fix:"Add inputSchema using Zod: tool({ inputSchema: z.object({ ... }), execute: ... })",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling"}),emptyToolsObject:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Empty Tools Object",cwe:"CWE-20",owasp:"A03:2021",description:"Tools object is empty or has no tool definitions",severity:"LOW",fix:"Define tools with proper schemas or remove tools property",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",description:"Allow tools without inputSchema in test files"}},additionalProperties:false}]},defaultOptions:[{allowInTests:false}],create(context){const[options={}]=context.options;const allowInTests=options.allowInTests??false;const sourceCode=context.sourceCode;const filename=context.filename;if(allowInTests&&/\.(test|spec)\.[jt]sx?$/.test(filename)){return{}}const functionsWithTools=["generateText","streamText","generateObject","streamObject"];return{CallExpression(node){const callee=sourceCode.getText(node.callee);const isAIFunction=functionsWithTools.some(fn=>callee.includes(fn));const isToolHelper=callee==="tool"||callee.endsWith(".tool");if(isToolHelper){const toolArg=node.arguments[0];if(toolArg&&toolArg.type==="ObjectExpression"){const hasInputSchema=toolArg.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:null;return keyName==="inputSchema"||keyName==="parameters"});if(!hasInputSchema){context.report({node:toolArg,messageId:"missingInputSchema",data:{toolName:"unnamed tool"}})}}return}if(!isAIFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;const toolsProp=optionsArg.properties.find(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:null;return keyName==="tools"});if(!toolsProp)return;if(toolsProp.value.type==="ObjectExpression"){for(const toolDef of toolsProp.value.properties){if(toolDef.type!=="Property")continue;const toolName=toolDef.key.type==="Identifier"?toolDef.key.name:toolDef.key.type==="Literal"?String(toolDef.key.value):"unknown";if(toolDef.value.type==="ObjectExpression"){const hasInputSchema=toolDef.value.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:null;return keyName==="inputSchema"||keyName==="parameters"});if(!hasInputSchema){context.report({node:toolDef.value,messageId:"missingInputSchema",data:{toolName}})}}}}}}}});
|