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.
@@ -1,119 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireEmbeddingValidation = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireEmbeddingValidation = (0, eslint_devkit_1.createRule)({
6
- name: 'require-embedding-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-embedding-validation.md',
11
- description: 'Require validation of embeddings before storage or similarity search',
12
- cwe: 'CWE-20',
13
- cvss: 5.5,
14
- },
15
- messages: {
16
- unvalidatedEmbedding: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Unvalidated Embedding',
19
- cwe: 'CWE-20',
20
- owasp: 'A03:2021',
21
- cvss: 5.5,
22
- description: 'Embedding from "{{source}}" used without validation. Malicious embeddings can poison vector stores.',
23
- severity: 'MEDIUM',
24
- compliance: ['SOC2'],
25
- fix: 'Validate embeddings before use: const validated = validateEmbedding(embedding)',
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
- embeddingPatterns: {
34
- type: 'array',
35
- items: { type: 'string' },
36
- description: 'Patterns suggesting embedding operations',
37
- },
38
- validatorFunctions: {
39
- type: 'array',
40
- items: { type: 'string' },
41
- description: 'Functions that validate embeddings',
42
- },
43
- },
44
- additionalProperties: false,
45
- },
46
- ],
47
- },
48
- defaultOptions: [
49
- {
50
- embeddingPatterns: [
51
- 'embed', 'embedding', 'embeddings', 'vector', 'encode',
52
- 'createEmbedding', 'getEmbedding', 'generateEmbedding',
53
- ],
54
- validatorFunctions: [
55
- 'validate', 'verify', 'check', 'sanitize', 'normalize',
56
- 'validateEmbedding', 'verifyVector',
57
- ],
58
- },
59
- ],
60
- create(context) {
61
- const [options = {}] = context.options;
62
- const embeddingPatterns = options.embeddingPatterns ?? [
63
- 'embed', 'embedding', 'vector', 'encode',
64
- ];
65
- const validatorFunctions = options.validatorFunctions ?? [
66
- 'validate', 'verify', 'check', 'sanitize',
67
- ];
68
- const sourceCode = context.sourceCode;
69
- const vectorStoreOps = ['upsert', 'insert', 'add', 'store', 'index', 'save'];
70
- function isEmbeddingCall(node) {
71
- const callee = sourceCode.getText(node.callee);
72
- for (const pattern of embeddingPatterns) {
73
- if (callee.toLowerCase().includes(pattern.toLowerCase())) {
74
- return callee;
75
- }
76
- }
77
- return null;
78
- }
79
- function isValidated(node) {
80
- const callee = sourceCode.getText(node.callee);
81
- return validatorFunctions.some((fn) => callee.toLowerCase().includes(fn.toLowerCase()));
82
- }
83
- function isVectorStoreOp(node) {
84
- const callee = sourceCode.getText(node.callee);
85
- return vectorStoreOps.some(op => callee.toLowerCase().includes(op.toLowerCase()));
86
- }
87
- return {
88
- CallExpression(node) {
89
- if (!isVectorStoreOp(node))
90
- return;
91
- for (const arg of node.arguments) {
92
- if (arg.type === 'ObjectExpression') {
93
- for (const prop of arg.properties) {
94
- if (prop.type !== 'Property')
95
- continue;
96
- const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
97
- if (keyName === 'embedding' || keyName === 'vector' || keyName === 'values') {
98
- let valueNode = prop.value;
99
- if (valueNode.type === 'AwaitExpression') {
100
- valueNode = valueNode.argument;
101
- }
102
- if (valueNode.type === 'CallExpression') {
103
- const embeddingSource = isEmbeddingCall(valueNode);
104
- if (embeddingSource && !isValidated(valueNode)) {
105
- context.report({
106
- node: prop.value,
107
- messageId: 'unvalidatedEmbedding',
108
- data: { source: embeddingSource },
109
- });
110
- }
111
- }
112
- }
113
- }
114
- }
115
- }
116
- },
117
- };
118
- },
119
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireEmbeddingValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireEmbeddingValidation=(0,eslint_devkit_1.createRule)({name:"require-embedding-validation",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-embedding-validation.md",description:"Require validation of embeddings before storage or similarity search",cwe:"CWE-20",cvss:5.5},messages:{unvalidatedEmbedding:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unvalidated Embedding",cwe:"CWE-20",owasp:"A03:2021",cvss:5.5,description:'Embedding from "{{source}}" used without validation. Malicious embeddings can poison vector stores.',severity:"MEDIUM",compliance:["SOC2"],fix:"Validate embeddings before use: const validated = validateEmbedding(embedding)",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{embeddingPatterns:{type:"array",items:{type:"string"},description:"Patterns suggesting embedding operations"},validatorFunctions:{type:"array",items:{type:"string"},description:"Functions that validate embeddings"}},additionalProperties:false}]},defaultOptions:[{embeddingPatterns:["embed","embedding","embeddings","vector","encode","createEmbedding","getEmbedding","generateEmbedding"],validatorFunctions:["validate","verify","check","sanitize","normalize","validateEmbedding","verifyVector"]}],create(context){const[options={}]=context.options;const embeddingPatterns=options.embeddingPatterns??["embed","embedding","vector","encode"];const validatorFunctions=options.validatorFunctions??["validate","verify","check","sanitize"];const sourceCode=context.sourceCode;const vectorStoreOps=["upsert","insert","add","store","index","save"];function isEmbeddingCall(node){const callee=sourceCode.getText(node.callee);for(const pattern of embeddingPatterns){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 isVectorStoreOp(node){const callee=sourceCode.getText(node.callee);return vectorStoreOps.some(op=>callee.toLowerCase().includes(op.toLowerCase()))}return{CallExpression(node){if(!isVectorStoreOp(node))return;for(const arg of node.arguments){if(arg.type==="ObjectExpression"){for(const prop of arg.properties){if(prop.type!=="Property")continue;const keyName=prop.key.type==="Identifier"?prop.key.name:null;if(keyName==="embedding"||keyName==="vector"||keyName==="values"){let valueNode=prop.value;if(valueNode.type==="AwaitExpression"){valueNode=valueNode.argument}if(valueNode.type==="CallExpression"){const embeddingSource=isEmbeddingCall(valueNode);if(embeddingSource&&!isValidated(valueNode)){context.report({node:prop.value,messageId:"unvalidatedEmbedding",data:{source:embeddingSource}})}}}}}}}}}});
@@ -1,92 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireErrorHandling = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireErrorHandling = (0, eslint_devkit_1.createRule)({
6
- name: 'require-error-handling',
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-error-handling.md',
11
- description: 'Require error handling for AI SDK calls to prevent cascading failures',
12
- cwe: 'CWE-755',
13
- cvss: 5,
14
- },
15
- messages: {
16
- missingErrorHandling: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Unhandled AI SDK Call',
19
- cwe: 'CWE-755',
20
- owasp: 'A05:2021',
21
- cvss: 5.0,
22
- description: '{{function}} call is not wrapped in try-catch. AI API failures can cause cascading errors.',
23
- severity: 'MEDIUM',
24
- compliance: ['SOC2'],
25
- fix: 'Wrap in try-catch: try { await {{function}}(...) } catch (e) { /* handle error */ }',
26
- documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/error-handling',
27
- }),
28
- },
29
- schema: [
30
- {
31
- type: 'object',
32
- properties: {
33
- allowInTests: {
34
- type: 'boolean',
35
- description: 'Allow unhandled AI calls 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
- function isInsideTryBlock(node) {
57
- let parent = node.parent;
58
- while (parent) {
59
- if (parent.type === 'TryStatement') {
60
- return true;
61
- }
62
- parent = parent.parent;
63
- }
64
- return false;
65
- }
66
- return {
67
- CallExpression(node) {
68
- const callee = sourceCode.getText(node.callee);
69
- const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
70
- if (!matchedFunction)
71
- return;
72
- const parent = node.parent;
73
- if (parent?.type === 'AwaitExpression') {
74
- if (!isInsideTryBlock(parent)) {
75
- context.report({
76
- node,
77
- messageId: 'missingErrorHandling',
78
- data: { function: matchedFunction },
79
- });
80
- }
81
- }
82
- else if (!isInsideTryBlock(node)) {
83
- context.report({
84
- node,
85
- messageId: 'missingErrorHandling',
86
- data: { function: matchedFunction },
87
- });
88
- }
89
- },
90
- };
91
- },
92
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireErrorHandling=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireErrorHandling=(0,eslint_devkit_1.createRule)({name:"require-error-handling",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-error-handling.md",description:"Require error handling for AI SDK calls to prevent cascading failures",cwe:"CWE-755",cvss:5},messages:{missingErrorHandling:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unhandled AI SDK Call",cwe:"CWE-755",owasp:"A05:2021",cvss:5,description:"{{function}} call is not wrapped in try-catch. AI API failures can cause cascading errors.",severity:"MEDIUM",compliance:["SOC2"],fix:"Wrap in try-catch: try { await {{function}}(...) } catch (e) { /* handle error */ }",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/error-handling"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",description:"Allow unhandled AI calls 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"];function isInsideTryBlock(node){let parent=node.parent;while(parent){if(parent.type==="TryStatement"){return true}parent=parent.parent}return false}return{CallExpression(node){const callee=sourceCode.getText(node.callee);const matchedFunction=aiSDKFunctions.find(fn=>callee.includes(fn));if(!matchedFunction)return;const parent=node.parent;if(parent?.type==="AwaitExpression"){if(!isInsideTryBlock(parent)){context.report({node,messageId:"missingErrorHandling",data:{function:matchedFunction}})}}else if(!isInsideTryBlock(node)){context.report({node,messageId:"missingErrorHandling",data:{function:matchedFunction}})}}}}});
@@ -1,88 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireMaxSteps = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireMaxSteps = (0, eslint_devkit_1.createRule)({
6
- name: 'require-max-steps',
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-max-steps.md',
11
- description: 'Require maxSteps limit for multi-step tool calling to prevent infinite loops',
12
- cwe: 'CWE-834',
13
- cvss: 6.5,
14
- },
15
- messages: {
16
- missingMaxSteps: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Missing Step Limit in Multi-Step Tool Calling',
19
- cwe: 'CWE-834',
20
- owasp: 'A05:2021',
21
- cvss: 6.5,
22
- description: '{{function}} with tools is missing a step limit. Without a limit, tool calls can loop indefinitely.',
23
- severity: 'MEDIUM',
24
- compliance: ['SOC2'],
25
- fix: 'Add a step limit: {{function}}({ ..., stopWhen: stepCountIs(5) }) (v5+) or maxSteps: 5 (v4)',
26
- documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls-using-stopwhen',
27
- }),
28
- },
29
- schema: [
30
- {
31
- type: 'object',
32
- properties: {
33
- suggestedMaxSteps: {
34
- type: 'number',
35
- description: 'Default max steps limit to suggest',
36
- default: 5,
37
- },
38
- },
39
- additionalProperties: false,
40
- },
41
- ],
42
- },
43
- defaultOptions: [
44
- {
45
- suggestedMaxSteps: 5,
46
- },
47
- ],
48
- create(context) {
49
- const sourceCode = context.sourceCode;
50
- const functionsWithTools = ['generateText', 'streamText'];
51
- return {
52
- CallExpression(node) {
53
- const callee = sourceCode.getText(node.callee);
54
- const matchedFunction = functionsWithTools.find(fn => callee.includes(fn));
55
- if (!matchedFunction)
56
- return;
57
- const optionsArg = node.arguments[0];
58
- if (!optionsArg || optionsArg.type !== 'ObjectExpression')
59
- return;
60
- const hasTools = optionsArg.properties.some(prop => {
61
- if (prop.type !== 'Property')
62
- return false;
63
- const keyName = prop.key.type === 'Identifier' ? prop.key.name : null;
64
- return keyName === 'tools';
65
- });
66
- if (!hasTools)
67
- return;
68
- const hasMaxSteps = optionsArg.properties.some(prop => {
69
- if (prop.type !== 'Property')
70
- return false;
71
- const keyName = prop.key.type === 'Identifier'
72
- ? prop.key.name
73
- : prop.key.type === 'Literal'
74
- ? String(prop.key.value)
75
- : null;
76
- return keyName === 'maxSteps' || keyName === 'max_steps' || keyName === 'stopWhen';
77
- });
78
- if (!hasMaxSteps) {
79
- context.report({
80
- node,
81
- messageId: 'missingMaxSteps',
82
- data: { function: matchedFunction },
83
- });
84
- }
85
- },
86
- };
87
- },
88
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireMaxSteps=(0,eslint_devkit_1.createRule)({name:"require-max-steps",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-max-steps.md",description:"Require maxSteps limit for multi-step tool calling to prevent infinite loops",cwe:"CWE-834",cvss:6.5},messages:{missingMaxSteps:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Missing Step Limit in Multi-Step Tool Calling",cwe:"CWE-834",owasp:"A05:2021",cvss:6.5,description:"{{function}} with tools is missing a step limit. Without a limit, tool calls can loop indefinitely.",severity:"MEDIUM",compliance:["SOC2"],fix:"Add a step limit: {{function}}({ ..., stopWhen: stepCountIs(5) }) (v5+) or maxSteps: 5 (v4)",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls-using-stopwhen"})},schema:[{type:"object",properties:{suggestedMaxSteps:{type:"number",description:"Default max steps limit to suggest",default:5}},additionalProperties:false}]},defaultOptions:[{suggestedMaxSteps:5}],create(context){const sourceCode=context.sourceCode;const functionsWithTools=["generateText","streamText"];return{CallExpression(node){const callee=sourceCode.getText(node.callee);const matchedFunction=functionsWithTools.find(fn=>callee.includes(fn));if(!matchedFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;const hasTools=optionsArg.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:null;return keyName==="tools"});if(!hasTools)return;const hasMaxSteps=optionsArg.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:prop.key.type==="Literal"?String(prop.key.value):null;return keyName==="maxSteps"||keyName==="max_steps"||keyName==="stopWhen"});if(!hasMaxSteps){context.report({node,messageId:"missingMaxSteps",data:{function:matchedFunction}})}}}}});
@@ -1,94 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireMaxTokens = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireMaxTokens = (0, eslint_devkit_1.createRule)({
6
- name: 'require-max-tokens',
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-max-tokens.md',
11
- description: 'Require maxTokens limit in generateText and streamText calls',
12
- cwe: 'CWE-770',
13
- cvss: 6.5,
14
- },
15
- messages: {
16
- missingMaxTokens: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Missing Token Limit in Vercel AI SDK',
19
- cwe: 'CWE-770',
20
- owasp: 'A05:2021',
21
- cvss: 6.5,
22
- description: '{{function}} call without a token limit can lead to excessive resource consumption',
23
- severity: 'MEDIUM',
24
- compliance: ['SOC2'],
25
- fix: 'Add a token limit: {{function}}({ maxOutputTokens: 4096, ... }) (v5+) or maxTokens (v4)',
26
- documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/generating-text',
27
- }),
28
- },
29
- schema: [
30
- {
31
- type: 'object',
32
- properties: {
33
- suggestedLimit: {
34
- type: 'number',
35
- description: 'Default max tokens limit to suggest',
36
- default: 4096,
37
- },
38
- targetFunctions: {
39
- type: 'array',
40
- items: { type: 'string' },
41
- description: 'Function names that require max tokens',
42
- },
43
- },
44
- additionalProperties: false,
45
- },
46
- ],
47
- },
48
- defaultOptions: [
49
- {
50
- suggestedLimit: 4096,
51
- targetFunctions: ['generateText', 'streamText', 'generateObject', 'streamObject'],
52
- },
53
- ],
54
- create(context) {
55
- const [options = {}] = context.options;
56
- const targetFunctions = options.targetFunctions ?? [
57
- 'generateText', 'streamText', 'generateObject', 'streamObject',
58
- ];
59
- const sourceCode = context.sourceCode;
60
- return {
61
- CallExpression(node) {
62
- const callee = sourceCode.getText(node.callee);
63
- const matchedFunction = targetFunctions.find((fn) => callee.includes(fn));
64
- if (!matchedFunction) {
65
- return;
66
- }
67
- const optionsArg = node.arguments[0];
68
- if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
69
- return;
70
- }
71
- const hasMaxTokens = optionsArg.properties.some(prop => {
72
- if (prop.type !== 'Property')
73
- return false;
74
- const keyName = prop.key.type === 'Identifier'
75
- ? prop.key.name
76
- : prop.key.type === 'Literal'
77
- ? String(prop.key.value)
78
- : null;
79
- return (keyName === 'maxTokens' ||
80
- keyName === 'max_tokens' ||
81
- keyName === 'maxOutputTokens' ||
82
- keyName === 'max_output_tokens');
83
- });
84
- if (!hasMaxTokens) {
85
- context.report({
86
- node,
87
- messageId: 'missingMaxTokens',
88
- data: { function: matchedFunction },
89
- });
90
- }
91
- },
92
- };
93
- },
94
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxTokens=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireMaxTokens=(0,eslint_devkit_1.createRule)({name:"require-max-tokens",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-max-tokens.md",description:"Require maxTokens limit in generateText and streamText calls",cwe:"CWE-770",cvss:6.5},messages:{missingMaxTokens:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Missing Token Limit in Vercel AI SDK",cwe:"CWE-770",owasp:"A05:2021",cvss:6.5,description:"{{function}} call without a token limit can lead to excessive resource consumption",severity:"MEDIUM",compliance:["SOC2"],fix:"Add a token limit: {{function}}({ maxOutputTokens: 4096, ... }) (v5+) or maxTokens (v4)",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/generating-text"})},schema:[{type:"object",properties:{suggestedLimit:{type:"number",description:"Default max tokens limit to suggest",default:4096},targetFunctions:{type:"array",items:{type:"string"},description:"Function names that require max tokens"}},additionalProperties:false}]},defaultOptions:[{suggestedLimit:4096,targetFunctions:["generateText","streamText","generateObject","streamObject"]}],create(context){const[options={}]=context.options;const targetFunctions=options.targetFunctions??["generateText","streamText","generateObject","streamObject"];const sourceCode=context.sourceCode;return{CallExpression(node){const callee=sourceCode.getText(node.callee);const matchedFunction=targetFunctions.find(fn=>callee.includes(fn));if(!matchedFunction){return}const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression"){return}const hasMaxTokens=optionsArg.properties.some(prop=>{if(prop.type!=="Property")return false;const keyName=prop.key.type==="Identifier"?prop.key.name:prop.key.type==="Literal"?String(prop.key.value):null;return keyName==="maxTokens"||keyName==="max_tokens"||keyName==="maxOutputTokens"||keyName==="max_output_tokens"});if(!hasMaxTokens){context.report({node,messageId:"missingMaxTokens",data:{function:matchedFunction}})}}}}});
@@ -1,123 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireOutputFiltering = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireOutputFiltering = (0, eslint_devkit_1.createRule)({
6
- name: 'require-output-filtering',
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-filtering.md',
11
- description: 'Require filtering of sensitive data returned by AI tools',
12
- cwe: 'CWE-200',
13
- cvss: 6.5,
14
- },
15
- messages: {
16
- missingOutputFilter: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Unfiltered Tool Output',
19
- cwe: 'CWE-200',
20
- owasp: 'A01:2021',
21
- cvss: 6.5,
22
- description: 'Tool "{{toolName}}" returns data from "{{source}}" without filtering. This may leak sensitive information.',
23
- severity: 'MEDIUM',
24
- compliance: ['SOC2', 'GDPR'],
25
- fix: 'Filter sensitive fields before returning: return filterSensitive(data)',
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
- dataSourcePatterns: {
34
- type: 'array',
35
- items: { type: 'string' },
36
- description: 'Patterns suggesting data sources',
37
- },
38
- filterFunctions: {
39
- type: 'array',
40
- items: { type: 'string' },
41
- description: 'Functions considered safe filters',
42
- },
43
- },
44
- additionalProperties: false,
45
- },
46
- ],
47
- },
48
- defaultOptions: [
49
- {
50
- dataSourcePatterns: [
51
- 'query', 'find', 'select', 'fetch', 'get', 'read', 'load',
52
- 'database', 'db', 'sql', 'mongo', 'prisma', 'supabase',
53
- ],
54
- filterFunctions: [
55
- 'filter', 'sanitize', 'redact', 'mask', 'clean',
56
- 'filterSensitive', 'removePII', 'scrub',
57
- ],
58
- },
59
- ],
60
- create(context) {
61
- const [options = {}] = context.options;
62
- const dataSourcePatterns = options.dataSourcePatterns ?? [
63
- 'query', 'find', 'select', 'fetch', 'get', 'read',
64
- ];
65
- const filterFunctions = options.filterFunctions ?? [
66
- 'filter', 'sanitize', 'redact', 'mask',
67
- ];
68
- const sourceCode = context.sourceCode;
69
- function isDataSourceCall(node) {
70
- const callee = sourceCode.getText(node.callee);
71
- for (const pattern of dataSourcePatterns) {
72
- if (callee.toLowerCase().includes(pattern.toLowerCase())) {
73
- return callee;
74
- }
75
- }
76
- return null;
77
- }
78
- function isFilteredCall(node) {
79
- const callee = sourceCode.getText(node.callee);
80
- return filterFunctions.some((filter) => callee.toLowerCase().includes(filter.toLowerCase()));
81
- }
82
- function getToolName(node) {
83
- let current = node.parent;
84
- while (current) {
85
- if (current.type === 'Property' && current.parent?.type === 'ObjectExpression') {
86
- const grandparent = current.parent.parent;
87
- if (grandparent?.type === 'Property' &&
88
- grandparent.key.type === 'Identifier' &&
89
- grandparent.key.name === 'tools') {
90
- if (current.key.type === 'Identifier') {
91
- return current.key.name;
92
- }
93
- }
94
- }
95
- current = current.parent;
96
- }
97
- return 'unknown';
98
- }
99
- return {
100
- ArrowFunctionExpression(node) {
101
- const parent = node.parent;
102
- if (parent?.type !== 'Property')
103
- return;
104
- const keyName = parent.key.type === 'Identifier' ? parent.key.name : null;
105
- if (keyName !== 'execute')
106
- return;
107
- if (node.body.type === 'CallExpression') {
108
- const source = isDataSourceCall(node.body);
109
- if (source && !isFilteredCall(node.body)) {
110
- context.report({
111
- node: node.body,
112
- messageId: 'missingOutputFilter',
113
- data: {
114
- toolName: getToolName(node),
115
- source,
116
- },
117
- });
118
- }
119
- }
120
- },
121
- };
122
- },
123
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireOutputFiltering=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireOutputFiltering=(0,eslint_devkit_1.createRule)({name:"require-output-filtering",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-output-filtering.md",description:"Require filtering of sensitive data returned by AI tools",cwe:"CWE-200",cvss:6.5},messages:{missingOutputFilter:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unfiltered Tool Output",cwe:"CWE-200",owasp:"A01:2021",cvss:6.5,description:'Tool "{{toolName}}" returns data from "{{source}}" without filtering. This may leak sensitive information.',severity:"MEDIUM",compliance:["SOC2","GDPR"],fix:"Filter sensitive fields before returning: return filterSensitive(data)",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{dataSourcePatterns:{type:"array",items:{type:"string"},description:"Patterns suggesting data sources"},filterFunctions:{type:"array",items:{type:"string"},description:"Functions considered safe filters"}},additionalProperties:false}]},defaultOptions:[{dataSourcePatterns:["query","find","select","fetch","get","read","load","database","db","sql","mongo","prisma","supabase"],filterFunctions:["filter","sanitize","redact","mask","clean","filterSensitive","removePII","scrub"]}],create(context){const[options={}]=context.options;const dataSourcePatterns=options.dataSourcePatterns??["query","find","select","fetch","get","read"];const filterFunctions=options.filterFunctions??["filter","sanitize","redact","mask"];const sourceCode=context.sourceCode;function isDataSourceCall(node){const callee=sourceCode.getText(node.callee);for(const pattern of dataSourcePatterns){if(callee.toLowerCase().includes(pattern.toLowerCase())){return callee}}return null}function isFilteredCall(node){const callee=sourceCode.getText(node.callee);return filterFunctions.some(filter=>callee.toLowerCase().includes(filter.toLowerCase()))}function getToolName(node){let current=node.parent;while(current){if(current.type==="Property"&&current.parent?.type==="ObjectExpression"){const grandparent=current.parent.parent;if(grandparent?.type==="Property"&&grandparent.key.type==="Identifier"&&grandparent.key.name==="tools"){if(current.key.type==="Identifier"){return current.key.name}}}current=current.parent}return"unknown"}return{ArrowFunctionExpression(node){const parent=node.parent;if(parent?.type!=="Property")return;const keyName=parent.key.type==="Identifier"?parent.key.name:null;if(keyName!=="execute")return;if(node.body.type==="CallExpression"){const source=isDataSourceCall(node.body);if(source&&!isFilteredCall(node.body)){context.report({node:node.body,messageId:"missingOutputFilter",data:{toolName:getToolName(node),source}})}}}}}});