eslint-plugin-vercel-ai-security 1.5.2 → 1.5.4

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,116 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noSystemPromptLeak = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.noSystemPromptLeak = (0, eslint_devkit_1.createRule)({
6
- name: 'no-system-prompt-leak',
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/no-system-prompt-leak.md',
11
- description: 'Prevent system prompts from being exposed in API responses or client code',
12
- cwe: 'CWE-200',
13
- cvss: 7.5,
14
- },
15
- messages: {
16
- systemPromptLeak: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.SECURITY,
18
- issueName: 'System Prompt Leakage',
19
- cwe: 'CWE-200',
20
- owasp: 'A01:2021',
21
- cvss: 7.5,
22
- description: 'System prompt "{{variable}}" is exposed in return statement. This reveals AI behavior instructions to users.',
23
- severity: 'HIGH',
24
- compliance: ['SOC2'],
25
- fix: 'Remove system prompt from API response. Only return the AI-generated content.',
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
- systemPromptPatterns: {
34
- type: 'array',
35
- items: { type: 'string' },
36
- description: 'Variable patterns that suggest system prompts',
37
- },
38
- },
39
- additionalProperties: false,
40
- },
41
- ],
42
- },
43
- defaultOptions: [
44
- {
45
- systemPromptPatterns: [
46
- 'systemPrompt', 'system_prompt', 'SYSTEM_PROMPT',
47
- 'systemMessage', 'system_message', 'SYSTEM_MESSAGE',
48
- 'instructions', 'INSTRUCTIONS', 'aiInstructions',
49
- 'agentPrompt', 'basePrompt', 'contextPrompt',
50
- ],
51
- },
52
- ],
53
- create(context) {
54
- const [options = {}] = context.options;
55
- const systemPromptPatterns = options.systemPromptPatterns ?? [
56
- 'systemPrompt', 'system_prompt', 'SYSTEM_PROMPT',
57
- 'systemMessage', 'instructions', 'agentPrompt',
58
- ];
59
- const sourceCode = context.sourceCode;
60
- function isSystemPromptVariable(name) {
61
- const lowerName = name.toLowerCase();
62
- return systemPromptPatterns.some((pattern) => lowerName.includes(pattern.toLowerCase()));
63
- }
64
- function findSystemPromptVar(node) {
65
- if (node.type === 'Identifier' && isSystemPromptVariable(node.name)) {
66
- return node.name;
67
- }
68
- if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
69
- if (isSystemPromptVariable(node.property.name)) {
70
- return sourceCode.getText(node);
71
- }
72
- }
73
- return null;
74
- }
75
- function checkObjectForLeaks(node) {
76
- for (const prop of node.properties) {
77
- if (prop.type !== 'Property')
78
- continue;
79
- const leakedVar = findSystemPromptVar(prop.value);
80
- if (leakedVar) {
81
- context.report({
82
- node: prop,
83
- messageId: 'systemPromptLeak',
84
- data: { variable: leakedVar },
85
- });
86
- }
87
- }
88
- }
89
- return {
90
- ReturnStatement(node) {
91
- if (!node.argument)
92
- return;
93
- if (node.argument.type === 'ObjectExpression') {
94
- checkObjectForLeaks(node.argument);
95
- }
96
- const leakedVar = findSystemPromptVar(node.argument);
97
- if (leakedVar) {
98
- context.report({
99
- node,
100
- messageId: 'systemPromptLeak',
101
- data: { variable: leakedVar },
102
- });
103
- }
104
- },
105
- CallExpression(node) {
106
- const callee = sourceCode.getText(node.callee);
107
- if (!callee.match(/\.(json|send)\s*$/))
108
- return;
109
- const arg = node.arguments[0];
110
- if (arg && arg.type === 'ObjectExpression') {
111
- checkObjectForLeaks(arg);
112
- }
113
- },
114
- };
115
- },
116
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSystemPromptLeak=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noSystemPromptLeak=(0,eslint_devkit_1.createRule)({name:"no-system-prompt-leak",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-system-prompt-leak.md",description:"Prevent system prompts from being exposed in API responses or client code",cwe:"CWE-200",cvss:7.5},messages:{systemPromptLeak:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"System Prompt Leakage",cwe:"CWE-200",owasp:"A01:2021",cvss:7.5,description:'System prompt "{{variable}}" is exposed in return statement. This reveals AI behavior instructions to users.',severity:"HIGH",compliance:["SOC2"],fix:"Remove system prompt from API response. Only return the AI-generated content.",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{systemPromptPatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest system prompts"}},additionalProperties:false}]},defaultOptions:[{systemPromptPatterns:["systemPrompt","system_prompt","SYSTEM_PROMPT","systemMessage","system_message","SYSTEM_MESSAGE","instructions","INSTRUCTIONS","aiInstructions","agentPrompt","basePrompt","contextPrompt"]}],create(context){const[options={}]=context.options;const systemPromptPatterns=options.systemPromptPatterns??["systemPrompt","system_prompt","SYSTEM_PROMPT","systemMessage","instructions","agentPrompt"];const sourceCode=context.sourceCode;function isSystemPromptVariable(name){const lowerName=name.toLowerCase();return systemPromptPatterns.some(pattern=>lowerName.includes(pattern.toLowerCase()))}function findSystemPromptVar(node){if(node.type==="Identifier"&&isSystemPromptVariable(node.name)){return node.name}if(node.type==="MemberExpression"&&node.property.type==="Identifier"){if(isSystemPromptVariable(node.property.name)){return sourceCode.getText(node)}}return null}function checkObjectForLeaks(node){for(const prop of node.properties){if(prop.type!=="Property")continue;const leakedVar=findSystemPromptVar(prop.value);if(leakedVar){context.report({node:prop,messageId:"systemPromptLeak",data:{variable:leakedVar}})}}}return{ReturnStatement(node){if(!node.argument)return;if(node.argument.type==="ObjectExpression"){checkObjectForLeaks(node.argument)}const leakedVar=findSystemPromptVar(node.argument);if(leakedVar){context.report({node,messageId:"systemPromptLeak",data:{variable:leakedVar}})}},CallExpression(node){const callee=sourceCode.getText(node.callee);if(!callee.match(/\.(json|send)\s*$/))return;const arg=node.arguments[0];if(arg&&arg.type==="ObjectExpression"){checkObjectForLeaks(arg)}}}}});
@@ -1,96 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noTrainingDataExposure = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.noTrainingDataExposure = (0, eslint_devkit_1.createRule)({
6
- name: 'no-training-data-exposure',
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/no-training-data-exposure.md',
11
- description: 'Prevent user data from being sent to LLM training endpoints',
12
- cwe: 'CWE-359',
13
- cvss: 7,
14
- },
15
- messages: {
16
- trainingDataExposure: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.SECURITY,
18
- issueName: 'Training Data Exposure',
19
- cwe: 'CWE-359',
20
- owasp: 'A01:2021',
21
- cvss: 7.0,
22
- description: 'User data may be exposed for model training via "{{pattern}}". This can lead to data poisoning and privacy violations.',
23
- severity: 'HIGH',
24
- compliance: ['GDPR', 'SOC2'],
25
- fix: 'Disable training data collection or avoid sending PII to training endpoints',
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
- trainingPatterns: {
34
- type: 'array',
35
- items: { type: 'string' },
36
- description: 'Patterns suggesting training endpoints',
37
- },
38
- },
39
- additionalProperties: false,
40
- },
41
- ],
42
- },
43
- defaultOptions: [
44
- {
45
- trainingPatterns: [
46
- 'train', 'training', 'finetune', 'fine-tune', 'fine_tune',
47
- 'feedback', 'improve', 'learn',
48
- ],
49
- },
50
- ],
51
- create(context) {
52
- const [options = {}] = context.options;
53
- const trainingPatterns = options.trainingPatterns ?? [
54
- 'train', 'training', 'finetune', 'feedback',
55
- ];
56
- function isTrainingRelated(name) {
57
- const lowerName = name.toLowerCase();
58
- return trainingPatterns.some((pattern) => lowerName.includes(pattern.toLowerCase()));
59
- }
60
- return {
61
- Property(node) {
62
- const keyName = node.key.type === 'Identifier'
63
- ? node.key.name
64
- : node.key.type === 'Literal'
65
- ? String(node.key.value)
66
- : null;
67
- if (!keyName)
68
- return;
69
- if (isTrainingRelated(keyName)) {
70
- if (node.value.type === 'Literal' && node.value.value === true) {
71
- context.report({
72
- node,
73
- messageId: 'trainingDataExposure',
74
- data: { pattern: keyName },
75
- });
76
- }
77
- }
78
- },
79
- Literal(node) {
80
- if (typeof node.value !== 'string')
81
- return;
82
- const value = node.value.toLowerCase();
83
- if (value.includes('/train') ||
84
- value.includes('/finetune') ||
85
- value.includes('/fine-tune') ||
86
- value.includes('/feedback')) {
87
- context.report({
88
- node,
89
- messageId: 'trainingDataExposure',
90
- data: { pattern: node.value },
91
- });
92
- }
93
- },
94
- };
95
- },
96
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTrainingDataExposure=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noTrainingDataExposure=(0,eslint_devkit_1.createRule)({name:"no-training-data-exposure",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-training-data-exposure.md",description:"Prevent user data from being sent to LLM training endpoints",cwe:"CWE-359",cvss:7},messages:{trainingDataExposure:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Training Data Exposure",cwe:"CWE-359",owasp:"A01:2021",cvss:7,description:'User data may be exposed for model training via "{{pattern}}". This can lead to data poisoning and privacy violations.',severity:"HIGH",compliance:["GDPR","SOC2"],fix:"Disable training data collection or avoid sending PII to training endpoints",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{trainingPatterns:{type:"array",items:{type:"string"},description:"Patterns suggesting training endpoints"}},additionalProperties:false}]},defaultOptions:[{trainingPatterns:["train","training","finetune","fine-tune","fine_tune","feedback","improve","learn"]}],create(context){const[options={}]=context.options;const trainingPatterns=options.trainingPatterns??["train","training","finetune","feedback"];function isTrainingRelated(name){const lowerName=name.toLowerCase();return trainingPatterns.some(pattern=>lowerName.includes(pattern.toLowerCase()))}return{Property(node){const keyName=node.key.type==="Identifier"?node.key.name:node.key.type==="Literal"?String(node.key.value):null;if(!keyName)return;if(isTrainingRelated(keyName)){if(node.value.type==="Literal"&&node.value.value===true){context.report({node,messageId:"trainingDataExposure",data:{pattern:keyName}})}}},Literal(node){if(typeof node.value!=="string")return;const value=node.value.toLowerCase();if(value.includes("/train")||value.includes("/finetune")||value.includes("/fine-tune")||value.includes("/feedback")){context.report({node,messageId:"trainingDataExposure",data:{pattern:node.value}})}}}}});
@@ -1,193 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noUnsafeOutputHandling = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
6
- name: 'no-unsafe-output-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/no-unsafe-output-handling.md',
11
- description: 'Prevent using AI output directly in dangerous operations (eval, SQL, HTML)',
12
- cwe: 'CWE-94',
13
- cvss: 9.8,
14
- confidence: 'medium',
15
- },
16
- messages: {
17
- unsafeOutputExecution: (0, eslint_devkit_1.formatLLMMessage)({
18
- icon: eslint_devkit_1.MessageIcons.SECURITY,
19
- issueName: 'AI Output Used in Code Execution',
20
- cwe: 'CWE-94',
21
- owasp: 'A03:2021',
22
- cvss: 9.8,
23
- description: 'AI-generated content "{{variable}}" passed to {{function}}. This can lead to Remote Code Execution.',
24
- severity: 'CRITICAL',
25
- compliance: ['SOC2', 'PCI-DSS'],
26
- fix: 'Never execute AI-generated code directly. Use sandboxed execution with validation.',
27
- documentationLink: 'https://owasp.org/www-project-top-10-for-large-language-model-applications/',
28
- }),
29
- unsafeOutputInSQL: (0, eslint_devkit_1.formatLLMMessage)({
30
- icon: eslint_devkit_1.MessageIcons.SECURITY,
31
- issueName: 'AI Output in SQL Query',
32
- cwe: 'CWE-89',
33
- owasp: 'A03:2021',
34
- cvss: 9.0,
35
- description: 'AI-generated content used in SQL query. Use parameterized queries instead.',
36
- severity: 'CRITICAL',
37
- compliance: ['SOC2', 'PCI-DSS'],
38
- fix: 'Use parameterized queries: db.query("SELECT * FROM users WHERE id = ?", [aiOutput])',
39
- documentationLink: 'https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html',
40
- }),
41
- unsafeOutputInHTML: (0, eslint_devkit_1.formatLLMMessage)({
42
- icon: eslint_devkit_1.MessageIcons.SECURITY,
43
- issueName: 'AI Output in innerHTML',
44
- cwe: 'CWE-79',
45
- owasp: 'A03:2021',
46
- cvss: 7.5,
47
- description: 'AI-generated content assigned to innerHTML. This can lead to XSS attacks.',
48
- severity: 'HIGH',
49
- compliance: ['SOC2'],
50
- fix: 'Use textContent or sanitize HTML: element.textContent = aiOutput',
51
- documentationLink: 'https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html',
52
- }),
53
- },
54
- schema: [
55
- {
56
- type: 'object',
57
- properties: {
58
- aiOutputPatterns: {
59
- type: 'array',
60
- items: { type: 'string' },
61
- description: 'Variable patterns that suggest AI output',
62
- },
63
- },
64
- additionalProperties: false,
65
- },
66
- ],
67
- },
68
- defaultOptions: [
69
- {
70
- aiOutputPatterns: [
71
- 'result.text',
72
- 'response.text',
73
- 'completion',
74
- 'generated',
75
- 'aiOutput',
76
- 'aiResponse',
77
- 'llmOutput',
78
- 'llmResponse',
79
- 'modelOutput',
80
- 'textContent',
81
- '.text',
82
- ],
83
- },
84
- ],
85
- create(context) {
86
- const [options = {}] = context.options;
87
- const aiOutputPatterns = options.aiOutputPatterns ?? [
88
- 'result.text', 'response.text', 'completion', 'generated',
89
- 'aiOutput', 'aiResponse', 'llmOutput', '.text',
90
- ];
91
- const sourceCode = context.sourceCode;
92
- const dangerousFunctions = ['eval', 'Function', 'execSync', 'exec', 'spawn', 'execFile'];
93
- const sqlPatterns = ['query', 'execute', 'run', 'raw'];
94
- const aiBoundVariables = new Set();
95
- function isAIBound(node) {
96
- if (node.type !== 'Identifier')
97
- return false;
98
- for (const variable of aiBoundVariables) {
99
- if (variable.references.some((r) => r.identifier === node))
100
- return true;
101
- }
102
- return false;
103
- }
104
- const AI_SDK_CALLS = new Set(['generateText', 'streamText', 'generateObject', 'streamObject']);
105
- function isAISDKCall(node) {
106
- let target = node;
107
- if (target.type === 'AwaitExpression')
108
- target = target.argument;
109
- if (target.type !== 'CallExpression')
110
- return false;
111
- const callee = target.callee;
112
- if (callee.type === 'Identifier' && AI_SDK_CALLS.has(callee.name))
113
- return true;
114
- if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' &&
115
- AI_SDK_CALLS.has(callee.property.name))
116
- return true;
117
- return false;
118
- }
119
- function isLikelyAIOutput(node) {
120
- if (node.type === 'MemberExpression' && isAIBound(node.object)) {
121
- return true;
122
- }
123
- if (isAIBound(node)) {
124
- return true;
125
- }
126
- const text = sourceCode.getText(node);
127
- return aiOutputPatterns.some((pattern) => text.includes(pattern));
128
- }
129
- function containsAIOutput(node) {
130
- if (node.type === 'TemplateLiteral') {
131
- return node.expressions.some(containsAIOutput);
132
- }
133
- if (node.type === 'BinaryExpression') {
134
- return (node.operator === '+' &&
135
- (containsAIOutput(node.left) || containsAIOutput(node.right)));
136
- }
137
- return isLikelyAIOutput(node);
138
- }
139
- return {
140
- VariableDeclarator(node) {
141
- if (!node.init || !isAISDKCall(node.init))
142
- return;
143
- for (const variable of sourceCode.getDeclaredVariables(node)) {
144
- aiBoundVariables.add(variable);
145
- }
146
- },
147
- CallExpression(node) {
148
- const callee = sourceCode.getText(node.callee);
149
- const isDangerous = dangerousFunctions.some(fn => callee.includes(fn));
150
- if (isDangerous) {
151
- for (const arg of node.arguments) {
152
- if (isLikelyAIOutput(arg)) {
153
- context.report({
154
- node: arg,
155
- messageId: 'unsafeOutputExecution',
156
- data: {
157
- variable: sourceCode.getText(arg),
158
- function: callee,
159
- },
160
- });
161
- }
162
- }
163
- }
164
- const isSQLFunction = sqlPatterns.some(fn => callee.includes(fn));
165
- if (isSQLFunction) {
166
- for (const arg of node.arguments) {
167
- if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
168
- if (containsAIOutput(arg)) {
169
- context.report({
170
- node: arg,
171
- messageId: 'unsafeOutputInSQL',
172
- });
173
- }
174
- }
175
- }
176
- }
177
- },
178
- AssignmentExpression(node) {
179
- if (node.left.type === 'MemberExpression') {
180
- const prop = node.left.property;
181
- if (prop.type === 'Identifier' && prop.name === 'innerHTML') {
182
- if (isLikelyAIOutput(node.right)) {
183
- context.report({
184
- node: node.right,
185
- messageId: 'unsafeOutputInHTML',
186
- });
187
- }
188
- }
189
- }
190
- },
191
- };
192
- },
193
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeOutputHandling=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.noUnsafeOutputHandling=(0,eslint_devkit_1.createRule)({name:"no-unsafe-output-handling",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-unsafe-output-handling.md",description:"Prevent using AI output directly in dangerous operations (eval, SQL, HTML)",cwe:"CWE-94",cvss:9.8,confidence:"medium"},messages:{unsafeOutputExecution:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AI Output Used in Code Execution",cwe:"CWE-94",owasp:"A03:2021",cvss:9.8,description:'AI-generated content "{{variable}}" passed to {{function}}. This can lead to Remote Code Execution.',severity:"CRITICAL",compliance:["SOC2","PCI-DSS"],fix:"Never execute AI-generated code directly. Use sandboxed execution with validation.",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"}),unsafeOutputInSQL:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AI Output in SQL Query",cwe:"CWE-89",owasp:"A03:2021",cvss:9,description:"AI-generated content used in SQL query. Use parameterized queries instead.",severity:"CRITICAL",compliance:["SOC2","PCI-DSS"],fix:'Use parameterized queries: db.query("SELECT * FROM users WHERE id = ?", [aiOutput])',documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html"}),unsafeOutputInHTML:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AI Output in innerHTML",cwe:"CWE-79",owasp:"A03:2021",cvss:7.5,description:"AI-generated content assigned to innerHTML. This can lead to XSS attacks.",severity:"HIGH",compliance:["SOC2"],fix:"Use textContent or sanitize HTML: element.textContent = aiOutput",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html"})},schema:[{type:"object",properties:{aiOutputPatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest AI output"}},additionalProperties:false}]},defaultOptions:[{aiOutputPatterns:["result.text","response.text","completion","generated","aiOutput","aiResponse","llmOutput","llmResponse","modelOutput","textContent",".text"]}],create(context){const[options={}]=context.options;const aiOutputPatterns=options.aiOutputPatterns??["result.text","response.text","completion","generated","aiOutput","aiResponse","llmOutput",".text"];const sourceCode=context.sourceCode;const dangerousFunctions=["eval","Function","execSync","exec","spawn","execFile"];const sqlPatterns=["query","execute","run","raw"];const aiBoundVariables=new Set;function isAIBound(node){if(node.type!=="Identifier")return false;for(const variable of aiBoundVariables){if(variable.references.some(r=>r.identifier===node))return true}return false}const AI_SDK_CALLS=new Set(["generateText","streamText","generateObject","streamObject"]);function isAISDKCall(node){let target=node;if(target.type==="AwaitExpression")target=target.argument;if(target.type!=="CallExpression")return false;const callee=target.callee;if(callee.type==="Identifier"&&AI_SDK_CALLS.has(callee.name))return true;if(callee.type==="MemberExpression"&&callee.property.type==="Identifier"&&AI_SDK_CALLS.has(callee.property.name))return true;return false}function isLikelyAIOutput(node){if(node.type==="MemberExpression"&&isAIBound(node.object)){return true}if(isAIBound(node)){return true}const text=sourceCode.getText(node);return aiOutputPatterns.some(pattern=>text.includes(pattern))}function containsAIOutput(node){if(node.type==="TemplateLiteral"){return node.expressions.some(containsAIOutput)}if(node.type==="BinaryExpression"){return node.operator==="+"&&(containsAIOutput(node.left)||containsAIOutput(node.right))}return isLikelyAIOutput(node)}return{VariableDeclarator(node){if(!node.init||!isAISDKCall(node.init))return;for(const variable of sourceCode.getDeclaredVariables(node)){aiBoundVariables.add(variable)}},CallExpression(node){const callee=sourceCode.getText(node.callee);const isDangerous=dangerousFunctions.some(fn=>callee.includes(fn));if(isDangerous){for(const arg of node.arguments){if(isLikelyAIOutput(arg)){context.report({node:arg,messageId:"unsafeOutputExecution",data:{variable:sourceCode.getText(arg),function:callee}})}}}const isSQLFunction=sqlPatterns.some(fn=>callee.includes(fn));if(isSQLFunction){for(const arg of node.arguments){if(arg.type==="TemplateLiteral"||arg.type==="BinaryExpression"){if(containsAIOutput(arg)){context.report({node:arg,messageId:"unsafeOutputInSQL"})}}}}},AssignmentExpression(node){if(node.left.type==="MemberExpression"){const prop=node.left.property;if(prop.type==="Identifier"&&prop.name==="innerHTML"){if(isLikelyAIOutput(node.right)){context.report({node:node.right,messageId:"unsafeOutputInHTML"})}}}}}}});
@@ -1,80 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireAbortSignal = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireAbortSignal = (0, eslint_devkit_1.createRule)({
6
- name: 'require-abort-signal',
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-abort-signal.md',
11
- description: 'Require AbortSignal for streaming AI calls to enable proper cleanup',
12
- cwe: 'CWE-404',
13
- cvss: 4,
14
- },
15
- messages: {
16
- missingAbortSignal: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Missing AbortSignal in Streaming Call',
19
- cwe: 'CWE-404',
20
- owasp: 'A05:2021',
21
- cvss: 4.0,
22
- description: '{{function}} call without abortSignal. Users cannot cancel long-running streams.',
23
- severity: 'LOW',
24
- fix: 'Add abortSignal option: {{function}}({ ..., abortSignal: controller.signal })',
25
- documentationLink: 'https://sdk.vercel.ai/docs/ai-sdk-core/generating-text',
26
- }),
27
- },
28
- schema: [
29
- {
30
- type: 'object',
31
- properties: {
32
- targetFunctions: {
33
- type: 'array',
34
- items: { type: 'string' },
35
- description: 'Functions that should have abort signal',
36
- },
37
- },
38
- additionalProperties: false,
39
- },
40
- ],
41
- },
42
- defaultOptions: [
43
- {
44
- targetFunctions: ['streamText', 'streamObject'],
45
- },
46
- ],
47
- create(context) {
48
- const [options = {}] = context.options;
49
- const targetFunctions = options.targetFunctions ?? ['streamText', 'streamObject'];
50
- const sourceCode = context.sourceCode;
51
- return {
52
- CallExpression(node) {
53
- const callee = sourceCode.getText(node.callee);
54
- const matchedFunction = targetFunctions.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 hasAbortSignal = optionsArg.properties.some(prop => {
61
- if (prop.type !== 'Property')
62
- return false;
63
- const keyName = prop.key.type === 'Identifier'
64
- ? prop.key.name
65
- : prop.key.type === 'Literal'
66
- ? String(prop.key.value)
67
- : null;
68
- return keyName === 'abortSignal' || keyName === 'signal';
69
- });
70
- if (!hasAbortSignal) {
71
- context.report({
72
- node,
73
- messageId: 'missingAbortSignal',
74
- data: { function: matchedFunction },
75
- });
76
- }
77
- },
78
- };
79
- },
80
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAbortSignal=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireAbortSignal=(0,eslint_devkit_1.createRule)({name:"require-abort-signal",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-abort-signal.md",description:"Require AbortSignal for streaming AI calls to enable proper cleanup",cwe:"CWE-404",cvss:4},messages:{missingAbortSignal:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Missing AbortSignal in Streaming Call",cwe:"CWE-404",owasp:"A05:2021",cvss:4,description:"{{function}} call without abortSignal. Users cannot cancel long-running streams.",severity:"LOW",fix:"Add abortSignal option: {{function}}({ ..., abortSignal: controller.signal })",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/generating-text"})},schema:[{type:"object",properties:{targetFunctions:{type:"array",items:{type:"string"},description:"Functions that should have abort signal"}},additionalProperties:false}]},defaultOptions:[{targetFunctions:["streamText","streamObject"]}],create(context){const[options={}]=context.options;const targetFunctions=options.targetFunctions??["streamText","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 hasAbortSignal=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==="abortSignal"||keyName==="signal"});if(!hasAbortSignal){context.report({node,messageId:"missingAbortSignal",data:{function:matchedFunction}})}}}}});
@@ -1,110 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireAuditLogging = void 0;
4
- const eslint_devkit_1 = require("@interlace/eslint-devkit");
5
- exports.requireAuditLogging = (0, eslint_devkit_1.createRule)({
6
- name: 'require-audit-logging',
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-audit-logging.md',
11
- description: 'Suggest audit logging for AI SDK operations',
12
- cwe: 'CWE-778',
13
- cvss: 4,
14
- },
15
- messages: {
16
- missingAuditLogging: (0, eslint_devkit_1.formatLLMMessage)({
17
- icon: eslint_devkit_1.MessageIcons.WARNING,
18
- issueName: 'Missing Audit Logging',
19
- cwe: 'CWE-778',
20
- owasp: 'A09:2021',
21
- cvss: 4.0,
22
- description: '{{function}} call lacks audit logging. AI operations should be logged for security monitoring.',
23
- severity: 'LOW',
24
- compliance: ['SOC2', 'PCI-DSS'],
25
- fix: 'Add logging: logger.info("AI call", { userId }); before the call',
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: 'Allow missing logging 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 loggingPatterns = [
57
- 'log', 'logger', 'console', 'winston', 'pino', 'bunyan',
58
- 'debug', 'info', 'warn', 'error', 'trace',
59
- ];
60
- function isLoggingStatement(node) {
61
- if (node.type !== 'ExpressionStatement')
62
- return false;
63
- if (node.expression.type !== 'CallExpression')
64
- return false;
65
- const callee = sourceCode.getText(node.expression.callee);
66
- return loggingPatterns.some((pattern) => callee.toLowerCase().includes(pattern.toLowerCase()));
67
- }
68
- function hasNearbyLogging(node) {
69
- const parent = node.parent;
70
- if (!parent)
71
- return false;
72
- let statement = node;
73
- while (statement && statement.type !== 'ExpressionStatement' &&
74
- statement.type !== 'VariableDeclaration' &&
75
- statement.type !== 'ReturnStatement') {
76
- statement = statement.parent ?? null;
77
- }
78
- if (!statement)
79
- return false;
80
- const block = statement.parent;
81
- if (!block || (block.type !== 'BlockStatement' && block.type !== 'Program')) {
82
- return false;
83
- }
84
- const statements = block.type === 'BlockStatement' ? block.body : block.body;
85
- const idx = statements.indexOf(statement);
86
- if (idx === -1)
87
- return false;
88
- for (let i = Math.max(0, idx - 3); i < idx; i++) {
89
- if (isLoggingStatement(statements[i]))
90
- return true;
91
- }
92
- return false;
93
- }
94
- return {
95
- CallExpression(node) {
96
- const callee = sourceCode.getText(node.callee);
97
- const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
98
- if (!matchedFunction)
99
- return;
100
- if (!hasNearbyLogging(node)) {
101
- context.report({
102
- node,
103
- messageId: 'missingAuditLogging',
104
- data: { function: matchedFunction },
105
- });
106
- }
107
- },
108
- };
109
- },
110
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAuditLogging=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.requireAuditLogging=(0,eslint_devkit_1.createRule)({name:"require-audit-logging",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-audit-logging.md",description:"Suggest audit logging for AI SDK operations",cwe:"CWE-778",cvss:4},messages:{missingAuditLogging:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Missing Audit Logging",cwe:"CWE-778",owasp:"A09:2021",cvss:4,description:"{{function}} call lacks audit logging. AI operations should be logged for security monitoring.",severity:"LOW",compliance:["SOC2","PCI-DSS"],fix:'Add logging: logger.info("AI call", { userId }); before the call',documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{allowInTests:{type:"boolean",description:"Allow missing logging 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 loggingPatterns=["log","logger","console","winston","pino","bunyan","debug","info","warn","error","trace"];function isLoggingStatement(node){if(node.type!=="ExpressionStatement")return false;if(node.expression.type!=="CallExpression")return false;const callee=sourceCode.getText(node.expression.callee);return loggingPatterns.some(pattern=>callee.toLowerCase().includes(pattern.toLowerCase()))}function hasNearbyLogging(node){const parent=node.parent;if(!parent)return false;let statement=node;while(statement&&statement.type!=="ExpressionStatement"&&statement.type!=="VariableDeclaration"&&statement.type!=="ReturnStatement"){statement=statement.parent??null}if(!statement)return false;const block=statement.parent;if(!block||block.type!=="BlockStatement"&&block.type!=="Program"){return false}const statements=block.type==="BlockStatement"?block.body:block.body;const idx=statements.indexOf(statement);if(idx===-1)return false;for(let i=Math.max(0,idx-3);i<idx;i++){if(isLoggingStatement(statements[i]))return true}return false}return{CallExpression(node){const callee=sourceCode.getText(node.callee);const matchedFunction=aiSDKFunctions.find(fn=>callee.includes(fn));if(!matchedFunction)return;if(!hasNearbyLogging(node)){context.report({node,messageId:"missingAuditLogging",data:{function:matchedFunction}})}}}}});