eslint-plugin-vercel-ai-security 2.2.0 β†’ 2.4.0

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/README.md CHANGED
@@ -48,10 +48,34 @@ That trade costs recall, and we measure it:
48
48
 
49
49
  - To check out the [guide](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security), visit [eslint.interlace.tools](https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security). πŸ“š
50
50
 
51
+ <!-- AUTO-GENERATED:INSTALL:START - Do not edit manually -->
52
+
53
+ **oxlint / Vite+** β€” the same rules under the same `vercel-ai-security/<rule>` IDs, without ESLint:
54
+
55
+ ```bash
56
+ npm install oxlint-plugin-vercel-ai-security --save-dev
57
+ ```
58
+
59
+ ```jsonc
60
+ // .oxlintrc.json
61
+ { "jsPlugins": ["oxlint-plugin-vercel-ai-security"] }
62
+ ```
63
+
64
+ ```ts
65
+ // vite.config.ts
66
+ import { defineConfig } from 'vite-plus';
67
+
68
+ export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-vercel-ai-security'] } });
69
+ ```
70
+
71
+ **ESLint:**
72
+
51
73
  ```bash
52
74
  npm install eslint-plugin-vercel-ai-security --save-dev
53
75
  ```
54
76
 
77
+ <!-- AUTO-GENERATED:INSTALL:END -->
78
+
55
79
  ## βš™οΈ Configuration Presets
56
80
  | Preset | Description |
57
81
  | :------------ | :--------------------------------------- |
@@ -149,7 +173,7 @@ See the [ESLint Version Support Policy](../../docs/ESLINT_VERSION_SUPPORT.md) fo
149
173
  | [require-audit-logging](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-audit-logging?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-778 | | | This rule identifies AI SDK calls that aren't preceded by logging statements | 🟒 | | | | | |
150
174
  | [require-embedding-validation](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-embedding-validation?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-20 | | | This rule identifies code patterns where embeddings are stored in vector databases without validation. | 🟒 | | | | | |
151
175
  | [require-error-handling](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-error-handling?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-755 | | | This rule identifies AI SDK calls that aren't wrapped in try-catch blocks | 🟒 | | | | | |
152
- | [require-max-steps](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-max-steps?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-834 | | | This rule identifies tool-calling AI SDK calls whose step limit does not bound the loop: a hasToolCall-only stop… | 🟒 | | | | | |
176
+ | [require-max-steps](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-max-steps?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-834 | | | This rule identifies tool-calling AI SDK calls whose step limit does not bound the loop: a hasToolCall-only… | 🟒 | | | | | |
153
177
  | [require-max-tokens](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-max-tokens?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-770 | | | This rule identifies AI SDK calls that don't specify a token limit (maxTokens or maxOutputTokens) | 🟒 | | | | | |
154
178
  | [require-output-filtering](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-output-filtering?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-200 | | | This rule identifies tool execute functions that return raw data from data sources (databases, APIs, file s… | 🟒 | | | | | |
155
179
  | [require-output-validation](https://eslint.interlace.tools/docs/security/plugin-vercel-ai-security/rules/require-output-validation?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-vercel-ai-security) | CWE-707 | | | This rule identifies code patterns where AI-generated output is displayed to users without validation or fa… | 🟒 | | | | | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-vercel-ai-security",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "ESLint plugin for Vercel AI SDK security β€” detects prompt injection, system-prompt leaks, hardcoded API keys, and unvalidated model output in generateText and streamText.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -78,12 +78,15 @@
78
78
  "ai": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
+ "eslint": {
82
+ "optional": true
83
+ },
81
84
  "ai": {
82
85
  "optional": true
83
86
  }
84
87
  },
85
88
  "dependencies": {
86
- "@interlace/eslint-devkit": "^1.19.0"
89
+ "@interlace/eslint-devkit": "^1.21.0"
87
90
  },
88
91
  "funding": {
89
92
  "type": "github",
package/src/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=exports.requireAuditLogging=exports.requireErrorHandling=exports.requireRagContentValidation=exports.requireOutputFiltering=exports.noHardcodedApiKeys=exports.requireToolSchema=exports.noDynamicSystemPrompt=exports.requireAbortSignal=exports.requireMaxSteps=exports.requireMaxTokens=exports.requireOutputValidation=exports.requireEmbeddingValidation=exports.noSystemPromptLeak=exports.requireToolConfirmation=exports.noUnsafeOutputHandling=exports.requireRequestTimeout=exports.noTrainingDataExposure=exports.noSensitiveInPrompt=exports.requireValidatedPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");var require_validated_prompt_2=require("./rules/require-validated-prompt");Object.defineProperty(exports,"requireValidatedPrompt",{enumerable:true,get:function(){return require_validated_prompt_2.requireValidatedPrompt}});var no_sensitive_in_prompt_2=require("./rules/no-sensitive-in-prompt");Object.defineProperty(exports,"noSensitiveInPrompt",{enumerable:true,get:function(){return no_sensitive_in_prompt_2.noSensitiveInPrompt}});var no_training_data_exposure_2=require("./rules/no-training-data-exposure");Object.defineProperty(exports,"noTrainingDataExposure",{enumerable:true,get:function(){return no_training_data_exposure_2.noTrainingDataExposure}});var require_request_timeout_2=require("./rules/require-request-timeout");Object.defineProperty(exports,"requireRequestTimeout",{enumerable:true,get:function(){return require_request_timeout_2.requireRequestTimeout}});var no_unsafe_output_handling_2=require("./rules/no-unsafe-output-handling");Object.defineProperty(exports,"noUnsafeOutputHandling",{enumerable:true,get:function(){return no_unsafe_output_handling_2.noUnsafeOutputHandling}});var require_tool_confirmation_2=require("./rules/require-tool-confirmation");Object.defineProperty(exports,"requireToolConfirmation",{enumerable:true,get:function(){return require_tool_confirmation_2.requireToolConfirmation}});var no_system_prompt_leak_2=require("./rules/no-system-prompt-leak");Object.defineProperty(exports,"noSystemPromptLeak",{enumerable:true,get:function(){return no_system_prompt_leak_2.noSystemPromptLeak}});var require_embedding_validation_2=require("./rules/require-embedding-validation");Object.defineProperty(exports,"requireEmbeddingValidation",{enumerable:true,get:function(){return require_embedding_validation_2.requireEmbeddingValidation}});var require_output_validation_2=require("./rules/require-output-validation");Object.defineProperty(exports,"requireOutputValidation",{enumerable:true,get:function(){return require_output_validation_2.requireOutputValidation}});var require_max_tokens_2=require("./rules/require-max-tokens");Object.defineProperty(exports,"requireMaxTokens",{enumerable:true,get:function(){return require_max_tokens_2.requireMaxTokens}});var require_max_steps_2=require("./rules/require-max-steps");Object.defineProperty(exports,"requireMaxSteps",{enumerable:true,get:function(){return require_max_steps_2.requireMaxSteps}});var require_abort_signal_2=require("./rules/require-abort-signal");Object.defineProperty(exports,"requireAbortSignal",{enumerable:true,get:function(){return require_abort_signal_2.requireAbortSignal}});var no_dynamic_system_prompt_2=require("./rules/no-dynamic-system-prompt");Object.defineProperty(exports,"noDynamicSystemPrompt",{enumerable:true,get:function(){return no_dynamic_system_prompt_2.noDynamicSystemPrompt}});var require_tool_schema_2=require("./rules/require-tool-schema");Object.defineProperty(exports,"requireToolSchema",{enumerable:true,get:function(){return require_tool_schema_2.requireToolSchema}});var no_hardcoded_api_keys_2=require("./rules/no-hardcoded-api-keys");Object.defineProperty(exports,"noHardcodedApiKeys",{enumerable:true,get:function(){return no_hardcoded_api_keys_2.noHardcodedApiKeys}});var require_output_filtering_2=require("./rules/require-output-filtering");Object.defineProperty(exports,"requireOutputFiltering",{enumerable:true,get:function(){return require_output_filtering_2.requireOutputFiltering}});var require_rag_content_validation_2=require("./rules/require-rag-content-validation");Object.defineProperty(exports,"requireRagContentValidation",{enumerable:true,get:function(){return require_rag_content_validation_2.requireRagContentValidation}});var require_error_handling_2=require("./rules/require-error-handling");Object.defineProperty(exports,"requireErrorHandling",{enumerable:true,get:function(){return require_error_handling_2.requireErrorHandling}});var require_audit_logging_2=require("./rules/require-audit-logging");Object.defineProperty(exports,"requireAuditLogging",{enumerable:true,get:function(){return require_audit_logging_2.requireAuditLogging}});exports.rules={get"require-validated-prompt"(){return require("./rules/require-validated-prompt").requireValidatedPrompt},get"no-sensitive-in-prompt"(){return require("./rules/no-sensitive-in-prompt").noSensitiveInPrompt},get"no-training-data-exposure"(){return require("./rules/no-training-data-exposure").noTrainingDataExposure},get"require-request-timeout"(){return require("./rules/require-request-timeout").requireRequestTimeout},get"no-unsafe-output-handling"(){return require("./rules/no-unsafe-output-handling").noUnsafeOutputHandling},get"require-tool-confirmation"(){return require("./rules/require-tool-confirmation").requireToolConfirmation},get"no-system-prompt-leak"(){return require("./rules/no-system-prompt-leak").noSystemPromptLeak},get"require-embedding-validation"(){return require("./rules/require-embedding-validation").requireEmbeddingValidation},get"require-output-validation"(){return require("./rules/require-output-validation").requireOutputValidation},get"require-max-tokens"(){return require("./rules/require-max-tokens").requireMaxTokens},get"require-max-steps"(){return require("./rules/require-max-steps").requireMaxSteps},get"require-abort-signal"(){return require("./rules/require-abort-signal").requireAbortSignal},get"no-dynamic-system-prompt"(){return require("./rules/no-dynamic-system-prompt").noDynamicSystemPrompt},get"require-tool-schema"(){return require("./rules/require-tool-schema").requireToolSchema},get"no-hardcoded-api-keys"(){return require("./rules/no-hardcoded-api-keys").noHardcodedApiKeys},get"require-output-filtering"(){return require("./rules/require-output-filtering").requireOutputFiltering},get"require-rag-content-validation"(){return require("./rules/require-rag-content-validation").requireRagContentValidation},get"require-error-handling"(){return require("./rules/require-error-handling").requireErrorHandling},get"require-audit-logging"(){return require("./rules/require-audit-logging").requireAuditLogging}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-vercel-ai-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-vercel-ai-security",version:"2.2.0"},rules:exports.rules};const minimalConfig={plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/require-validated-prompt":"error","vercel-ai-security/no-hardcoded-api-keys":"error"}};const recommendedConfig={plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/require-validated-prompt":"error","vercel-ai-security/no-hardcoded-api-keys":"error","vercel-ai-security/no-unsafe-output-handling":"error","vercel-ai-security/no-sensitive-in-prompt":"error","vercel-ai-security/no-system-prompt-leak":"error","vercel-ai-security/no-dynamic-system-prompt":"error","vercel-ai-security/require-tool-confirmation":"error","vercel-ai-security/require-tool-schema":"error","vercel-ai-security/require-max-tokens":"warn","vercel-ai-security/require-rag-content-validation":"warn","vercel-ai-security/require-max-steps":"off","vercel-ai-security/require-abort-signal":"off","vercel-ai-security/require-request-timeout":"off","vercel-ai-security/no-training-data-exposure":"off","vercel-ai-security/require-output-filtering":"off","vercel-ai-security/require-error-handling":"off","vercel-ai-security/require-audit-logging":"off","vercel-ai-security/require-embedding-validation":"off","vercel-ai-security/require-output-validation":"off"}};const strictConfig={plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/require-validated-prompt":"error","vercel-ai-security/no-hardcoded-api-keys":"error","vercel-ai-security/no-unsafe-output-handling":"error","vercel-ai-security/no-sensitive-in-prompt":"error","vercel-ai-security/no-system-prompt-leak":"error","vercel-ai-security/no-dynamic-system-prompt":"error","vercel-ai-security/require-tool-confirmation":"error","vercel-ai-security/require-tool-schema":"error","vercel-ai-security/require-max-tokens":"error","vercel-ai-security/require-max-steps":"error","vercel-ai-security/require-output-filtering":"error","vercel-ai-security/require-rag-content-validation":"error","vercel-ai-security/no-training-data-exposure":"error","vercel-ai-security/require-request-timeout":"error","vercel-ai-security/require-embedding-validation":"error","vercel-ai-security/require-output-validation":"error","vercel-ai-security/require-error-handling":"error","vercel-ai-security/require-abort-signal":"error","vercel-ai-security/require-audit-logging":"warn"}};exports.configs={flagship:{plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/no-unsafe-output-handling":"error"}},minimal:minimalConfig,recommended:recommendedConfig,strict:strictConfig};exports.default=exports.plugin;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.configs=exports.plugin=exports.rules=exports.requireAuditLogging=exports.requireErrorHandling=exports.requireRagContentValidation=exports.requireOutputFiltering=exports.noHardcodedApiKeys=exports.requireToolSchema=exports.noDynamicSystemPrompt=exports.requireAbortSignal=exports.requireMaxSteps=exports.requireMaxTokens=exports.requireOutputValidation=exports.requireEmbeddingValidation=exports.noSystemPromptLeak=exports.requireToolConfirmation=exports.noUnsafeOutputHandling=exports.requireRequestTimeout=exports.noTrainingDataExposure=exports.noSensitiveInPrompt=exports.requireValidatedPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");var require_validated_prompt_2=require("./rules/require-validated-prompt");Object.defineProperty(exports,"requireValidatedPrompt",{enumerable:true,get:function(){return require_validated_prompt_2.requireValidatedPrompt}});var no_sensitive_in_prompt_2=require("./rules/no-sensitive-in-prompt");Object.defineProperty(exports,"noSensitiveInPrompt",{enumerable:true,get:function(){return no_sensitive_in_prompt_2.noSensitiveInPrompt}});var no_training_data_exposure_2=require("./rules/no-training-data-exposure");Object.defineProperty(exports,"noTrainingDataExposure",{enumerable:true,get:function(){return no_training_data_exposure_2.noTrainingDataExposure}});var require_request_timeout_2=require("./rules/require-request-timeout");Object.defineProperty(exports,"requireRequestTimeout",{enumerable:true,get:function(){return require_request_timeout_2.requireRequestTimeout}});var no_unsafe_output_handling_2=require("./rules/no-unsafe-output-handling");Object.defineProperty(exports,"noUnsafeOutputHandling",{enumerable:true,get:function(){return no_unsafe_output_handling_2.noUnsafeOutputHandling}});var require_tool_confirmation_2=require("./rules/require-tool-confirmation");Object.defineProperty(exports,"requireToolConfirmation",{enumerable:true,get:function(){return require_tool_confirmation_2.requireToolConfirmation}});var no_system_prompt_leak_2=require("./rules/no-system-prompt-leak");Object.defineProperty(exports,"noSystemPromptLeak",{enumerable:true,get:function(){return no_system_prompt_leak_2.noSystemPromptLeak}});var require_embedding_validation_2=require("./rules/require-embedding-validation");Object.defineProperty(exports,"requireEmbeddingValidation",{enumerable:true,get:function(){return require_embedding_validation_2.requireEmbeddingValidation}});var require_output_validation_2=require("./rules/require-output-validation");Object.defineProperty(exports,"requireOutputValidation",{enumerable:true,get:function(){return require_output_validation_2.requireOutputValidation}});var require_max_tokens_2=require("./rules/require-max-tokens");Object.defineProperty(exports,"requireMaxTokens",{enumerable:true,get:function(){return require_max_tokens_2.requireMaxTokens}});var require_max_steps_2=require("./rules/require-max-steps");Object.defineProperty(exports,"requireMaxSteps",{enumerable:true,get:function(){return require_max_steps_2.requireMaxSteps}});var require_abort_signal_2=require("./rules/require-abort-signal");Object.defineProperty(exports,"requireAbortSignal",{enumerable:true,get:function(){return require_abort_signal_2.requireAbortSignal}});var no_dynamic_system_prompt_2=require("./rules/no-dynamic-system-prompt");Object.defineProperty(exports,"noDynamicSystemPrompt",{enumerable:true,get:function(){return no_dynamic_system_prompt_2.noDynamicSystemPrompt}});var require_tool_schema_2=require("./rules/require-tool-schema");Object.defineProperty(exports,"requireToolSchema",{enumerable:true,get:function(){return require_tool_schema_2.requireToolSchema}});var no_hardcoded_api_keys_2=require("./rules/no-hardcoded-api-keys");Object.defineProperty(exports,"noHardcodedApiKeys",{enumerable:true,get:function(){return no_hardcoded_api_keys_2.noHardcodedApiKeys}});var require_output_filtering_2=require("./rules/require-output-filtering");Object.defineProperty(exports,"requireOutputFiltering",{enumerable:true,get:function(){return require_output_filtering_2.requireOutputFiltering}});var require_rag_content_validation_2=require("./rules/require-rag-content-validation");Object.defineProperty(exports,"requireRagContentValidation",{enumerable:true,get:function(){return require_rag_content_validation_2.requireRagContentValidation}});var require_error_handling_2=require("./rules/require-error-handling");Object.defineProperty(exports,"requireErrorHandling",{enumerable:true,get:function(){return require_error_handling_2.requireErrorHandling}});var require_audit_logging_2=require("./rules/require-audit-logging");Object.defineProperty(exports,"requireAuditLogging",{enumerable:true,get:function(){return require_audit_logging_2.requireAuditLogging}});exports.rules={get"require-validated-prompt"(){return require("./rules/require-validated-prompt").requireValidatedPrompt},get"no-sensitive-in-prompt"(){return require("./rules/no-sensitive-in-prompt").noSensitiveInPrompt},get"no-training-data-exposure"(){return require("./rules/no-training-data-exposure").noTrainingDataExposure},get"require-request-timeout"(){return require("./rules/require-request-timeout").requireRequestTimeout},get"no-unsafe-output-handling"(){return require("./rules/no-unsafe-output-handling").noUnsafeOutputHandling},get"require-tool-confirmation"(){return require("./rules/require-tool-confirmation").requireToolConfirmation},get"no-system-prompt-leak"(){return require("./rules/no-system-prompt-leak").noSystemPromptLeak},get"require-embedding-validation"(){return require("./rules/require-embedding-validation").requireEmbeddingValidation},get"require-output-validation"(){return require("./rules/require-output-validation").requireOutputValidation},get"require-max-tokens"(){return require("./rules/require-max-tokens").requireMaxTokens},get"require-max-steps"(){return require("./rules/require-max-steps").requireMaxSteps},get"require-abort-signal"(){return require("./rules/require-abort-signal").requireAbortSignal},get"no-dynamic-system-prompt"(){return require("./rules/no-dynamic-system-prompt").noDynamicSystemPrompt},get"require-tool-schema"(){return require("./rules/require-tool-schema").requireToolSchema},get"no-hardcoded-api-keys"(){return require("./rules/no-hardcoded-api-keys").noHardcodedApiKeys},get"require-output-filtering"(){return require("./rules/require-output-filtering").requireOutputFiltering},get"require-rag-content-validation"(){return require("./rules/require-rag-content-validation").requireRagContentValidation},get"require-error-handling"(){return require("./rules/require-error-handling").requireErrorHandling},get"require-audit-logging"(){return require("./rules/require-audit-logging").requireAuditLogging}};(0,eslint_devkit_1.withCanonicalDocsUrls)("plugin-vercel-ai-security",exports.rules);exports.plugin={meta:{name:"eslint-plugin-vercel-ai-security",version:"2.4.0"},rules:exports.rules};const minimalConfig={plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/require-validated-prompt":"error","vercel-ai-security/no-hardcoded-api-keys":"error"}};const recommendedConfig={plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/require-validated-prompt":"error","vercel-ai-security/no-hardcoded-api-keys":"error","vercel-ai-security/no-unsafe-output-handling":"error","vercel-ai-security/no-sensitive-in-prompt":"error","vercel-ai-security/no-system-prompt-leak":"error","vercel-ai-security/no-dynamic-system-prompt":"error","vercel-ai-security/require-tool-confirmation":"error","vercel-ai-security/require-tool-schema":"error","vercel-ai-security/require-max-tokens":"warn","vercel-ai-security/require-rag-content-validation":"warn","vercel-ai-security/require-max-steps":"off","vercel-ai-security/require-abort-signal":"off","vercel-ai-security/require-request-timeout":"off","vercel-ai-security/no-training-data-exposure":"off","vercel-ai-security/require-output-filtering":"off","vercel-ai-security/require-error-handling":"off","vercel-ai-security/require-audit-logging":"off","vercel-ai-security/require-embedding-validation":"off","vercel-ai-security/require-output-validation":"off"}};const strictConfig={plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/require-validated-prompt":"error","vercel-ai-security/no-hardcoded-api-keys":"error","vercel-ai-security/no-unsafe-output-handling":"error","vercel-ai-security/no-sensitive-in-prompt":"error","vercel-ai-security/no-system-prompt-leak":"error","vercel-ai-security/no-dynamic-system-prompt":"error","vercel-ai-security/require-tool-confirmation":"error","vercel-ai-security/require-tool-schema":"error","vercel-ai-security/require-max-tokens":"error","vercel-ai-security/require-max-steps":"error","vercel-ai-security/require-output-filtering":"error","vercel-ai-security/require-rag-content-validation":"error","vercel-ai-security/no-training-data-exposure":"error","vercel-ai-security/require-request-timeout":"error","vercel-ai-security/require-embedding-validation":"error","vercel-ai-security/require-output-validation":"error","vercel-ai-security/require-error-handling":"error","vercel-ai-security/require-abort-signal":"error","vercel-ai-security/require-audit-logging":"warn"}};exports.configs={flagship:{plugins:{"vercel-ai-security":exports.plugin},rules:{"vercel-ai-security/no-unsafe-output-handling":"error"}},minimal:minimalConfig,recommended:recommendedConfig,strict:strictConfig};exports.default=exports.plugin;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicSystemPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.noDynamicSystemPrompt=(0,eslint_devkit_1.createRule)({name:"no-dynamic-system-prompt",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-dynamic-system-prompt.md",description:"Prevent dynamic content in system prompts to avoid agent confusion attacks",cwe:"CWE-74",cvss:8},messages:{dynamicSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Dynamic System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8,description:"System prompt contains dynamic content. This can lead to agent confusion attacks.",severity:"HIGH",compliance:["SOC2"],fix:"Use a static system prompt defined as a constant. Avoid template literals or concatenation.",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"}),userControlledSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User-Controlled System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8,description:"System prompt is read straight from the request body. The caller can replace the agent's instructions.",severity:"HIGH",compliance:["SOC2"],fix:"Keep the system prompt server-side; let the request select from a fixed set of prompts by id",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{allowStaticTemplates:{type:"boolean",description:"Allow template literals without expressions"}},additionalProperties:false}]},defaultOptions:[{allowStaticTemplates:true}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{allowStaticTemplates}=options;const sourceCode=context.sourceCode;function isDynamicContent(node){const scope=sourceCode.getScope(node);if(node.type==="TemplateLiteral"){if(node.expressions.length===0)return!allowStaticTemplates;return!(0,sdk_1.isStaticText)(node,scope)}if(node.type==="BinaryExpression"&&node.operator==="+"){return!(0,sdk_1.isStaticText)(node,scope)}if(node.type==="CallExpression")return!(0,sdk_1.isDateValue)(node);return node.type==="AwaitExpression"}return{CallExpression(node){if(!(0,sdk_1.sdkCallName)(node))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(!(0,prompt_props_1.isSystemPromptProp)(keyName))continue;if(isDynamicContent(prop.value)){context.report({node:prop.value,messageId:"dynamicSystemPrompt"})}else if((0,sdk_1.isRequestDerived)(prop.value,sourceCode.getScope(prop.value))){context.report({node:prop.value,messageId:"userControlledSystemPrompt"})}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noDynamicSystemPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.noDynamicSystemPrompt=(0,eslint_devkit_1.createRule)({name:"no-dynamic-system-prompt",meta:{interop:{runtimes:["eslint","oxlint"]},type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-dynamic-system-prompt.md",description:"Prevent dynamic content in system prompts to avoid agent confusion attacks",cwe:"CWE-74",cvss:8},messages:{dynamicSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Dynamic System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8,description:"System prompt contains dynamic content. This can lead to agent confusion attacks.",severity:"HIGH",compliance:["SOC2"],fix:"Use a static system prompt defined as a constant. Avoid template literals or concatenation.",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"}),userControlledSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"User-Controlled System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8,description:"System prompt is read straight from the request body. The caller can replace the agent's instructions.",severity:"HIGH",compliance:["SOC2"],fix:"Keep the system prompt server-side; let the request select from a fixed set of prompts by id",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{allowStaticTemplates:{type:"boolean",description:"Allow template literals without expressions"}},additionalProperties:false}]},defaultOptions:[{allowStaticTemplates:true}],createOnce(context,currentOptions){let allowStaticTemplates;let sourceCode;function isDynamicContent(node){if(node.type==="TemplateLiteral"){if(node.expressions.length===0)return!allowStaticTemplates;return!(0,sdk_1.isStaticText)(node,sourceCode)}if(node.type==="BinaryExpression"&&node.operator==="+"){return!(0,sdk_1.isStaticText)(node,sourceCode)}if(node.type==="CallExpression")return!(0,sdk_1.isStaticText)(node,sourceCode);return node.type==="AwaitExpression"}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({allowStaticTemplates}=options);sourceCode=context.sourceCode;return true},CallExpression(node){if(!(0,sdk_1.sdkCallName)(node))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(!(0,prompt_props_1.isSystemPromptProp)(keyName))continue;if(isDynamicContent(prop.value)){context.report({node:prop.value,messageId:"dynamicSystemPrompt"})}else if((0,sdk_1.isRequestDerived)(prop.value,sourceCode)){context.report({node:prop.value,messageId:"userControlledSystemPrompt"})}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noHardcodedApiKeys=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");exports.noHardcodedApiKeys=(0,eslint_devkit_1.createRule)({name:"no-hardcoded-api-keys",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-hardcoded-api-keys.md",description:"Prevent hardcoded API keys in AI SDK model configuration",cwe:"CWE-798",cvss:8.5},messages:{hardcodedApiKey:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Hardcoded API Key Detected",cwe:"CWE-798",owasp:"A02:2021",cvss:8.5,description:'Hardcoded API key "{{key}}" detected. Use environment variables instead.',severity:"CRITICAL",compliance:["SOC2","PCI-DSS","GDPR"],fix:"Use environment variable: apiKey: process.env.OPENAI_API_KEY",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/settings"})},schema:[{type:"object",properties:{apiKeyPatterns:{type:"array",items:{type:"string"},description:"Property names that contain API keys"}},additionalProperties:false}]},defaultOptions:[{apiKeyPatterns:["apiKey","api_key","token","secret","credentials","authorization"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{apiKeyPatterns}=options;function holderName(node){const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property&&parent.value===node){return(0,eslint_devkit_1.objectKeyName)(parent)}if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return parent.id.name}return null}function check(node){const raw=(0,eslint_devkit_1.staticString)(node);if(raw===null)return;const value=raw.replace(/^Bearer\s+/i,"");const holder=holderName(node);const isKey=PROVIDER_KEY_SHAPES.some(shape=>shape.test(value))||holder!==null&&apiKeyPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(holder,pattern))&&isGenericSecret(value);if(!isKey)return;context.report({node,messageId:"hardcodedApiKey",data:{key:value.substring(0,10)+"..."}})}return{Literal:check,TemplateLiteral:check}}});const PROVIDER_KEY_SHAPES=[/^sk-(?:proj-|ant-|svcacct-|admin-)?(?=[\w-]*\d)[\w-]{20,}$/,/^AIza[\w-]{35}$/,/^(?:gsk|hf|r8)_(?=\w*\d)\w{20,}$/,/^(?:xai|pplx)-(?=\w*\d)\w{20,}$/,/^AKIA[0-9A-Z]{16}$/];function isGenericSecret(value){return value.length>20&&!value.startsWith("$")&&!/\s/.test(value)&&!value.includes("://")&&!(value.includes("_")&&/^[A-Z][A-Z0-9_]*$/.test(value))&&!/^[\w.-]+(?:\/[\w.-]+){2,}$/.test(value)}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noHardcodedApiKeys=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");exports.noHardcodedApiKeys=(0,eslint_devkit_1.createRule)({name:"no-hardcoded-api-keys",meta:{interop:{runtimes:["eslint","oxlint"]},type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-hardcoded-api-keys.md",description:"Prevent hardcoded API keys in AI SDK model configuration",cwe:"CWE-798",cvss:8.5},messages:{hardcodedApiKey:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Hardcoded API Key Detected",cwe:"CWE-798",owasp:"A02:2021",cvss:8.5,description:'Hardcoded API key "{{key}}" detected. Use environment variables instead.',severity:"CRITICAL",compliance:["SOC2","PCI-DSS","GDPR"],fix:"Use environment variable: apiKey: process.env.OPENAI_API_KEY",documentationLink:"https://sdk.vercel.ai/docs/ai-sdk-core/settings"})},schema:[{type:"object",properties:{apiKeyPatterns:{type:"array",items:{type:"string"},description:"Property names that contain API keys"}},additionalProperties:false}]},defaultOptions:[{apiKeyPatterns:["apiKey","api_key","token","secret","credentials","authorization"]}],createOnce(context,currentOptions){let apiKeyPatterns;function holderName(node){const parent=node.parent;if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property&&parent.value===node){return(0,eslint_devkit_1.objectKeyName)(parent)}if(parent?.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){return parent.id.name}return null}function check(node){const raw=(0,eslint_devkit_1.staticString)(node);if(raw===null)return;const value=raw.replace(/^Bearer\s+/i,"");const holder=holderName(node);const isKey=PROVIDER_KEY_SHAPES.some(shape=>shape.test(value))||holder!==null&&apiKeyPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(holder,pattern))&&isGenericSecret(value);if(!isKey)return;context.report({node,messageId:"hardcodedApiKey",data:{key:value.substring(0,10)+"..."}})}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({apiKeyPatterns}=options);return true},Literal:check,TemplateLiteral:check}}});const PROVIDER_KEY_SHAPES=[/^sk-(?:proj-|ant-|svcacct-|admin-)?(?=[\w-]*\d)[\w-]{20,}$/,/^AIza[\w-]{35}$/,/^(?:gsk|hf|r8)_(?=\w*\d)\w{20,}$/,/^(?:xai|pplx)-(?=\w*\d)\w{20,}$/,/^AKIA[0-9A-Z]{16}$/];function isGenericSecret(value){return value.length>20&&!value.startsWith("$")&&!/\s/.test(value)&&!value.includes("://")&&!(value.includes("_")&&/^[A-Z][A-Z0-9_]*$/.test(value))&&!/^[\w.-]+(?:\/[\w.-]+){2,}$/.test(value)}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSensitiveInPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.noSensitiveInPrompt=(0,eslint_devkit_1.createRule)({name:"no-sensitive-in-prompt",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-sensitive-in-prompt.md",description:"Prevent sensitive data (secrets, credentials, PII) from being passed to LLM prompts",cwe:"CWE-200",cvss:8},messages:{sensitiveInPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Sensitive Data in LLM Prompt",cwe:"CWE-200",owasp:"A01:2021",cvss:8,description:'Sensitive data "{{variable}}" detected in prompt. Secrets and PII should never be sent to LLMs.',severity:"CRITICAL",compliance:["SOC2","GDPR","HIPAA","PCI-DSS"],fix:"Remove sensitive data from prompt or redact before sending: prompt: redact(sensitiveData)",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{sensitivePatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest sensitive data"}},additionalProperties:false}]},defaultOptions:[{sensitivePatterns:["password","secret","apiKey","api_key","token","credential","ssn","socialSecurity","creditCard","cardNumber","cvv","privateKey","private_key","accessToken","access_token","refreshToken","refresh_token","authToken","bearer","connectionString","dbPassword","dbUser"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{sensitivePatterns}=options;const sourceCode=context.sourceCode;function isSensitiveIdentifier(name){return sensitivePatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(name,pattern))}function findSensitiveData(node){if(node===null)return null;switch(node.type){case"Identifier":return isSensitiveIdentifier(node.name)?node.name:null;case"MemberExpression":{const name=(0,eslint_devkit_1.propertyName)(node);return name!==null&&isSensitiveIdentifier(name)?sourceCode.getText(node):null}case"TemplateLiteral":return firstSensitive(node.expressions);case"BinaryExpression":return findSensitiveData(node.left)??findSensitiveData(node.right);case"ArrayExpression":return firstSensitive(node.elements);case"SpreadElement":return findSensitiveData(node.argument);case"ObjectExpression":return firstSensitive(node.properties.map(prop=>prop.type==="Property"?prop.value:prop));default:return null}}function firstSensitive(nodes){for(const node of nodes){const found=findSensitiveData(node);if(found)return found}return null}const propsToCheck=new Set(["prompt","messages",...prompt_props_1.SYSTEM_PROMPT_PROPS]);return{CallExpression(node){if(!(0,sdk_1.sdkCallName)(node))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||!propsToCheck.has(keyName))continue;const sensitiveVar=findSensitiveData(prop.value);if(sensitiveVar){context.report({node:prop.value,messageId:"sensitiveInPrompt",data:{variable:sensitiveVar}})}}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSensitiveInPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");const flow_1=require("../../utils/flow");const records_1=require("../../utils/records");const type_info_1=require("../../utils/type-info");exports.noSensitiveInPrompt=(0,eslint_devkit_1.createRule)({name:"no-sensitive-in-prompt",meta:{interop:{runtimes:["eslint","oxlint"]},type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/no-sensitive-in-prompt.md",description:"Prevent sensitive data (secrets, credentials, PII) from being passed to LLM prompts",cwe:"CWE-200",cvss:8},messages:{sensitiveInPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Sensitive Data in LLM Prompt",cwe:"CWE-200",owasp:"A01:2021",cvss:8,description:'Sensitive data "{{variable}}" detected in prompt. Secrets and PII should never be sent to LLMs.',severity:"CRITICAL",compliance:["SOC2","GDPR","HIPAA","PCI-DSS"],fix:"Remove sensitive data from prompt or redact before sending: prompt: redact(sensitiveData)",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{sensitivePatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest sensitive data"}},additionalProperties:false}]},defaultOptions:[{sensitivePatterns:["password","secret","apiKey","api_key","token","credential","ssn","socialSecurity","creditCard","cardNumber","cvv","privateKey","private_key","accessToken","access_token","refreshToken","refresh_token","authToken","bearer","connectionString","dbPassword","dbUser"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{sensitivePatterns}=options;const sourceCode=context.sourceCode;function isSensitiveIdentifier(name){return sensitivePatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(name,pattern))}function findSensitiveData(node){if(node===null)return null;switch(node.type){case"Identifier":return isSensitiveIdentifier(node.name)?node.name:null;case"MemberExpression":{const name=(0,eslint_devkit_1.propertyName)(node);return name!==null&&isSensitiveIdentifier(name)?sourceCode.getText(node):null}case"TemplateLiteral":for(const expr of node.expressions){const found=findSensitiveData(expr)??wholeRecord(expr);if(found)return found}return null;case"CallExpression":{const path=(0,eslint_devkit_1.memberPath)(node.callee)?.join(".");const[first]=node.arguments;if(path!=="JSON.stringify"&&path!=="String"||first===void 0)return null;return findSensitiveData(first)??wholeRecord(first)}case"BinaryExpression":return findSensitiveData(node.left)??findSensitiveData(node.right);case"ArrayExpression":return firstSensitive(node.elements);case"SpreadElement":return findSensitiveData(node.argument);case"ObjectExpression":return firstSensitive(node.properties.map(prop=>prop.type==="Property"?prop.value:prop));default:return null}}function wholeRecord(node){const label=sourceCode.getText(node);const names=(0,type_info_1.typePropertyNames)(node,sourceCode);if(names!==null)return names.some(isSensitiveIdentifier)?label:null;if(node.type==="MemberExpression")return null;return(0,flow_1.derivesFrom)(node,recordFlow)?label:null}const recordFlow={sourceCode,isSource:candidate=>candidate.type==="ObjectExpression"&&candidate.properties.some(prop=>prop.type==="Property"&&isSensitiveIdentifier((0,eslint_devkit_1.objectKeyName)(prop)??""))||(0,records_1.isDatabaseRowRead)(candidate,sourceCode)};function firstSensitive(nodes){for(const node of nodes){const found=findSensitiveData(node);if(found)return found}return null}const propsToCheck=new Set(["prompt","messages",...prompt_props_1.SYSTEM_PROMPT_PROPS]);return{CallExpression(node){if(!(0,sdk_1.sdkCallName)(node))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||!propsToCheck.has(keyName))continue;const sensitiveVar=findSensitiveData(prop.value);if(sensitiveVar){context.report({node:prop.value,messageId:"sensitiveInPrompt",data:{variable:sensitiveVar}})}}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSystemPromptLeak=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const prompt_props_1=require("../../utils/prompt-props");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{systemPromptPatterns}=options;const sourceCode=context.sourceCode;function isSystemPromptVariable(name){return systemPromptPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(name,pattern))}function findSystemPromptVar(node){if(node.type==="Identifier"&&isSystemPromptVariable(node.name)){return node.name}if(node.type==="MemberExpression"){const name=(0,eslint_devkit_1.propertyName)(node);if(name!==null&&isSystemPromptVariable(name))return sourceCode.getText(node)}return null}function checkObjectForLeaks(node,inResponse){for(const prop of node.properties){if(prop.type!=="Property")continue;const key=(0,eslint_devkit_1.objectKeyName)(prop);const leaked=findSystemPromptVar(prop.value)??(inResponse&&key!==null&&((0,prompt_props_1.isSystemPromptProp)(key)||isSystemPromptVariable(key))?key:null);if(leaked){context.report({node:prop,messageId:"systemPromptLeak",data:{variable:leaked}})}else if(inResponse&&prop.value.type==="ObjectExpression"){checkObjectForLeaks(prop.value,true)}}}return{ReturnStatement(node){if(!node.argument)return;if(node.argument.type==="ObjectExpression"){if((0,sdk_1.optionValue)(node.argument,"model"))return;checkObjectForLeaks(node.argument,false)}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,true)}},NewExpression(node){if(node.callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||node.callee.name!=="Response")return;const body=node.arguments[0];if(body?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return;if((0,eslint_devkit_1.memberPath)(body.callee)?.join(".")!=="JSON.stringify")return;const payload=body.arguments[0];if(payload?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){checkObjectForLeaks(payload,true)}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noSystemPromptLeak=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const prompt_props_1=require("../../utils/prompt-props");const sdk_1=require("../../utils/sdk");exports.noSystemPromptLeak=(0,eslint_devkit_1.createRule)({name:"no-system-prompt-leak",meta:{interop:{runtimes:["eslint","oxlint"]},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"]}],createOnce(context,currentOptions){let systemPromptPatterns;let sourceCode;function isSystemPromptVariable(name){return systemPromptPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(name,pattern))}function findSystemPromptVar(node){if(node.type==="Identifier"&&isSystemPromptVariable(node.name)){return node.name}if(node.type==="MemberExpression"){const name=(0,eslint_devkit_1.propertyName)(node);if(name!==null&&isSystemPromptVariable(name))return sourceCode.getText(node)}return null}function checkObjectForLeaks(node,inResponse){for(const prop of node.properties){if(prop.type!=="Property")continue;const key=(0,eslint_devkit_1.objectKeyName)(prop);const leaked=findSystemPromptVar(prop.value)??(inResponse&&key!==null&&((0,prompt_props_1.isSystemPromptProp)(key)||isSystemPromptVariable(key))?key:null);if(leaked){context.report({node:prop,messageId:"systemPromptLeak",data:{variable:leaked}})}else if(inResponse&&prop.value.type==="ObjectExpression"){checkObjectForLeaks(prop.value,true)}}}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({systemPromptPatterns}=options);sourceCode=context.sourceCode;return true},ReturnStatement(node){if(!node.argument)return;if(node.argument.type==="ObjectExpression"){if((0,sdk_1.optionValue)(node.argument,"model"))return;checkObjectForLeaks(node.argument,false)}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,true)}},NewExpression(node){if(node.callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier||node.callee.name!=="Response")return;const body=node.arguments[0];if(body?.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return;if((0,eslint_devkit_1.memberPath)(body.callee)?.join(".")!=="JSON.stringify")return;const payload=body.arguments[0];if(payload?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){checkObjectForLeaks(payload,true)}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTrainingDataExposure=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");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"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{trainingPatterns}=options;return{Property(node){const keyName=(0,eslint_devkit_1.objectKeyName)(node);if(keyName===null)return;if(node.value.type!=="Literal"||node.value.value!==true)return;if(!trainingPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(keyName,pattern)))return;context.report({node,messageId:"trainingDataExposure",data:{pattern:keyName}})},Literal(node){if(typeof node.value!=="string")return;const path=node.value.toLowerCase().split(/[?#]/)[0];if(!path.split("/").slice(1).some(segment=>TRAINING_SEGMENTS.has(segment)))return;context.report({node,messageId:"trainingDataExposure",data:{pattern:node.value}})}}}});const TRAINING_SEGMENTS=new Set(["train","training","finetune","finetunes","fine-tune","fine-tunes","fine_tune","fine_tunes","fine-tuning","fine_tuning"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noTrainingDataExposure=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");exports.noTrainingDataExposure=(0,eslint_devkit_1.createRule)({name:"no-training-data-exposure",meta:{interop:{runtimes:["eslint","oxlint"]},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"]}],createOnce(context,currentOptions){let trainingPatterns;return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({trainingPatterns}=options);return true},Property(node){const keyName=(0,eslint_devkit_1.objectKeyName)(node);if(keyName===null)return;if(node.value.type!=="Literal"||node.value.value!==true)return;if(!trainingPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(keyName,pattern)))return;context.report({node,messageId:"trainingDataExposure",data:{pattern:keyName}})},Literal(node){if(typeof node.value!=="string")return;const path=node.value.toLowerCase().split(/[?#]/)[0];if(!path.split("/").slice(1).some(segment=>TRAINING_SEGMENTS.has(segment)))return;context.report({node,messageId:"trainingDataExposure",data:{pattern:node.value}})}}}});const TRAINING_SEGMENTS=new Set(["train","training","finetune","finetunes","fine-tune","fine-tunes","fine_tune","fine_tunes","fine-tuning","fine_tuning"]);
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeOutputHandling=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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"}),unsafeOutputInRequest:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AI-Chosen Request URL",cwe:"CWE-918",owasp:"A10:2021",cvss:8.6,description:"Model-controlled value chooses the URL passed to fetch(). This can lead to Server-Side Request Forgery.",severity:"HIGH",compliance:["SOC2"],fix:"Fix the host and pass model input only as a path or query value, or check the URL against an allow-list",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_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",".text"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{aiOutputPatterns}=options;const sourceCode=context.sourceCode;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"&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),AI_SDK_CALLS))return true;return false}function isBoundOutput(node){if(isAIBound(node))return true;if(node.type==="MemberExpression")return isBoundOutput(node.object);return node.type==="CallExpression"&&node.callee.type==="MemberExpression"&&isBoundOutput(node.callee.object)}function isLikelyAIOutput(node){if(isBoundOutput(node))return true;if(node.type!=="Identifier"&&node.type!=="MemberExpression")return false;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)}function isGlobal(node,name){return node.type==="Identifier"&&node.name===name&&!(0,sdk_1.lookupVariable)(name,sourceCode.getScope(node))?.defs.length}function isCodeExecutionCallee(callee){if(isGlobal(callee,"eval")||isGlobal(callee,"Function"))return true;const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,sourceCode.getScope(callee));if(binding){const fn=binding.path[binding.path.length-1];return binding.module==="child_process"&&CHILD_PROCESS_SINKS.has(fn)||binding.module==="vm"&&VM_SINKS.has(fn)}return callee.type==="Identifier"&&CHILD_PROCESS_SINKS.has(callee.name)&&!(0,sdk_1.lookupVariable)(callee.name,sourceCode.getScope(callee))?.defs.length}function isToolExecute(node){if((0,eslint_devkit_1.objectKeyName)(node)!=="execute")return false;if(node.value.type!=="ArrowFunctionExpression"&&node.value.type!=="FunctionExpression"){return false}const definition=node.parent;const owner=definition.parent;if(owner.type==="CallExpression"&&TOOL_FACTORIES.has((0,sdk_1.calleeName)(owner.callee))){return true}return definition.properties.some(prop=>prop.type==="Property"&&SCHEMA_KEYS.has((0,eslint_devkit_1.objectKeyName)(prop)))}function choosesRequestTarget(node){if(node.type==="TemplateLiteral"){return node.quasis[0].value.raw===""&&isBoundOutput(node.expressions[0])}return isBoundOutput(node)}function reportExecution(node,callee,args){for(const arg of args){if(containsAIOutput(arg)){context.report({node:arg,messageId:"unsafeOutputExecution",data:{variable:sourceCode.getText(arg),function:sourceCode.getText(callee)}})}}}return{VariableDeclarator(node){if(!node.init)return;const init=(0,sdk_1.unwrap)(node.init);const bound=isAISDKCall(node.init)||init.type==="MemberExpression"&&(isBoundOutput(init.object)||isAISDKCall(init.object));if(!bound)return;for(const variable of sourceCode.getDeclaredVariables(node)){aiBoundVariables.add(variable)}},Property(node){if(!isToolExecute(node))return;for(const variable of sourceCode.getDeclaredVariables(node.value)){aiBoundVariables.add(variable)}},NewExpression(node){const isFunction=isGlobal(node.callee,"Function");const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,sourceCode.getScope(node));if(isFunction||binding?.module==="vm"&&binding.path[0]==="Script"){reportExecution(node,node.callee,node.arguments)}},CallExpression(node){if(isCodeExecutionCallee(node.callee)){reportExecution(node,node.callee,node.arguments);return}const name=(0,sdk_1.calleeName)(node.callee);if(isGlobal(node.callee,"fetch")){const[target]=node.arguments;if(target&&choosesRequestTarget(target)){context.report({node:target,messageId:"unsafeOutputInRequest"})}return}if(!SQL_SINKS.has(name))return;const[query]=node.arguments;if(query&&containsAIOutput(query)){context.report({node:query,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"})}}}},JSXAttribute(node){if(node.name.name!=="dangerouslySetInnerHTML")return;const container=node.value;if(container?.type!==eslint_devkit_1.AST_NODE_TYPES.JSXExpressionContainer||container.expression.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return}for(const prop of container.expression.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&(0,eslint_devkit_1.objectKeyName)(prop)==="__html"&&isLikelyAIOutput(prop.value)){context.report({node:prop.value,messageId:"unsafeOutputInHTML"})}}}}}});const CHILD_PROCESS_SINKS=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork"]);const VM_SINKS=new Set(["runInNewContext","runInThisContext","runInContext","compileFunction"]);const SQL_SINKS=new Set(["query","execute","raw","run","unsafe","$queryRawUnsafe","$executeRawUnsafe"]);const TOOL_FACTORIES=new Set(["tool","dynamicTool"]);const SCHEMA_KEYS=new Set(["inputSchema","parameters"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.noUnsafeOutputHandling=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");const flow_1=require("../../utils/flow");exports.noUnsafeOutputHandling=(0,eslint_devkit_1.createRule)({name:"no-unsafe-output-handling",meta:{interop:{runtimes:["eslint"]},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"}),unsafeOutputInRequest:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"AI-Chosen Request URL",cwe:"CWE-918",owasp:"A10:2021",cvss:8.6,description:"Model-controlled value chooses the URL passed to fetch(). This can lead to Server-Side Request Forgery.",severity:"HIGH",compliance:["SOC2"],fix:"Fix the host and pass model input only as a path or query value, or check the URL against an allow-list",documentationLink:"https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_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",".text"]}],createOnce(context,currentOptions){let aiOutputPatterns;let sourceCode;let AI_SDK_CALLS;let outputFlow;function isAISDKCall(call){const callee=call.callee;if(callee.type==="Identifier")return AI_SDK_CALLS.has(callee.name);return callee.type==="MemberExpression"&&(0,eslint_devkit_1.namesOneOf)((0,eslint_devkit_1.propertyName)(callee),AI_SDK_CALLS)}function isBoundOutput(node){return(0,flow_1.derivesFrom)(node,outputFlow)}function isLikelyAIOutput(node){if(isBoundOutput(node))return true;if(node.type!=="Identifier"&&node.type!=="MemberExpression")return false;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)}function isGlobal(node,name){return node.type==="Identifier"&&node.name===name&&!(0,sdk_1.lookupVariable)(name,sourceCode.getScope(node))?.defs.length}function isCodeExecutionCallee(callee){if(isGlobal(callee,"eval")||isGlobal(callee,"Function"))return true;const binding=(0,eslint_devkit_1.resolveModuleBinding)(callee,sourceCode.getScope(callee));if(binding){const fn=binding.path[binding.path.length-1];return binding.module==="child_process"&&CHILD_PROCESS_SINKS.has(fn)||binding.module==="vm"&&VM_SINKS.has(fn)}return callee.type==="Identifier"&&CHILD_PROCESS_SINKS.has(callee.name)&&!(0,sdk_1.lookupVariable)(callee.name,sourceCode.getScope(callee))?.defs.length}function isToolExecute(node){if((0,eslint_devkit_1.objectKeyName)(node)!=="execute")return false;const definition=node.parent;const owner=definition.parent;if(owner.type==="CallExpression"&&TOOL_FACTORIES.has((0,sdk_1.calleeName)(owner.callee))){return true}return definition.properties.some(prop=>prop.type==="Property"&&SCHEMA_KEYS.has((0,eslint_devkit_1.objectKeyName)(prop)))}function choosesRequestTarget(node){if(node.type==="TemplateLiteral"){return node.quasis[0].value.raw===""&&isBoundOutput(node.expressions[0])}return isBoundOutput(node)}function reportExecution(node,callee,args){for(const arg of args){if(containsAIOutput(arg)){context.report({node:arg,messageId:"unsafeOutputExecution",data:{variable:sourceCode.getText(arg),function:sourceCode.getText(callee)}})}}}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({aiOutputPatterns}=options);sourceCode=context.sourceCode;AI_SDK_CALLS=new Set(["generateText","streamText","generateObject","streamObject"]);outputFlow={sourceCode,isSource:node=>node.type==="CallExpression"&&isAISDKCall(node)||(0,sdk_1.isSdkHookCall)(node,sourceCode),isSourceParameter:(variable,fn)=>fn.parent.type==="Property"&&isToolExecute(fn.parent)||(0,sdk_1.isSdkTypedParameter)(variable,sourceCode)};return true},NewExpression(node){const isFunction=isGlobal(node.callee,"Function");const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,sourceCode.getScope(node));if(isFunction||binding?.module==="vm"&&binding.path[0]==="Script"){reportExecution(node,node.callee,node.arguments)}},CallExpression(node){if(isCodeExecutionCallee(node.callee)){reportExecution(node,node.callee,node.arguments);return}const name=(0,sdk_1.calleeName)(node.callee);if(isGlobal(node.callee,"fetch")){const[target]=node.arguments;if(target&&choosesRequestTarget(target)){context.report({node:target,messageId:"unsafeOutputInRequest"})}return}if(!SQL_SINKS.has(name))return;const[query]=node.arguments;if(query&&containsAIOutput(query)){context.report({node:query,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"})}}}},JSXAttribute(node){if(node.name.name!=="dangerouslySetInnerHTML")return;const container=node.value;if(container?.type!==eslint_devkit_1.AST_NODE_TYPES.JSXExpressionContainer||container.expression.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return}for(const prop of container.expression.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&(0,eslint_devkit_1.objectKeyName)(prop)==="__html"&&isLikelyAIOutput(prop.value)){context.report({node:prop.value,messageId:"unsafeOutputInHTML"})}}}}}});const CHILD_PROCESS_SINKS=new Set(["exec","execSync","execFile","execFileSync","spawn","spawnSync","fork"]);const VM_SINKS=new Set(["runInNewContext","runInThisContext","runInContext","compileFunction"]);const SQL_SINKS=new Set(["query","execute","raw","run","unsafe","$queryRawUnsafe","$executeRawUnsafe"]);const TOOL_FACTORIES=new Set(["tool","dynamicTool"]);const SCHEMA_KEYS=new Set(["inputSchema","parameters"]);
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAbortSignal=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{targetFunctions}=options;return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node,targetFunctions);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;if(!(0,sdk_1.declaresOption)(optionsArg,["abortSignal","signal"])){context.report({node,messageId:"missingAbortSignal",data:{function:matchedFunction}})}}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAbortSignal=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireAbortSignal=(0,eslint_devkit_1.createRule)({name:"require-abort-signal",meta:{interop:{runtimes:["eslint","oxlint"]},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"]}],createOnce(context,currentOptions){let targetFunctions;return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({targetFunctions}=options);return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node,targetFunctions);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;if(!(0,sdk_1.declaresOption)(optionsArg,["abortSignal","signal"])){context.report({node,messageId:"missingAbortSignal",data:{function:matchedFunction}})}}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAuditLogging=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{allowInTests}=options;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return{}}function isLoggingStatement(node){if(node.type!=="ExpressionStatement")return false;if(node.expression.type!=="CallExpression")return false;const path=(0,eslint_devkit_1.memberPath)(node.expression.callee);if(path===null)return false;return LOGGER_ROOTS.has(path[0].toLowerCase())||path.length>1&&LOG_LEVELS.has(path[path.length-1])}function hasTelemetry(node){const optionsArg=node.arguments[0];if(optionsArg?.type!=="ObjectExpression")return false;return["experimental_telemetry","telemetry"].some(key=>{const value=(0,sdk_1.optionValue)(optionsArg,key);return value?.type==="ObjectExpression"&&(0,sdk_1.optionValue)(value,"isEnabled")?.type==="Literal"&&(0,sdk_1.optionValue)(value,"isEnabled").value===true})}function hasNearbyLogging(node){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.body;const idx=statements.indexOf(statement);for(let i=Math.max(0,idx-3);i<idx;i++){if(isLoggingStatement(statements[i]))return true}return false}return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)return;if(!hasTelemetry(node)&&!hasNearbyLogging(node)){context.report({node,messageId:"missingAuditLogging",data:{function:matchedFunction}})}}}}});const LOGGER_ROOTS=new Set(["log","logger","console","debug","winston","pino","bunyan","audit"]);const LOG_LEVELS=new Set(["log","info","warn","error","debug","trace","fatal","audit"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireAuditLogging=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireAuditLogging=(0,eslint_devkit_1.createRule)({name:"require-audit-logging",meta:{interop:{runtimes:["eslint","oxlint"]},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}],createOnce(context,currentOptions){function isLoggingStatement(node){if(node.type!=="ExpressionStatement")return false;if(node.expression.type!=="CallExpression")return false;const path=(0,eslint_devkit_1.memberPath)(node.expression.callee);if(path===null)return false;return LOGGER_ROOTS.has(path[0].toLowerCase())||path.length>1&&LOG_LEVELS.has(path[path.length-1])}function hasTelemetry(node){const optionsArg=node.arguments[0];if(optionsArg?.type!=="ObjectExpression")return false;return["experimental_telemetry","telemetry"].some(key=>{const value=(0,sdk_1.optionValue)(optionsArg,key);return value?.type==="ObjectExpression"&&(0,sdk_1.optionValue)(value,"isEnabled")?.type==="Literal"&&(0,sdk_1.optionValue)(value,"isEnabled").value===true})}function hasNearbyLogging(node){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.body;const idx=statements.indexOf(statement);for(let i=Math.max(0,idx-3);i<idx;i++){if(isLoggingStatement(statements[i]))return true}return false}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;const{allowInTests}=options;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return false}return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)return;if(!hasTelemetry(node)&&!hasNearbyLogging(node)){context.report({node,messageId:"missingAuditLogging",data:{function:matchedFunction}})}}}}});const LOGGER_ROOTS=new Set(["log","logger","console","debug","winston","pino","bunyan","audit"]);const LOG_LEVELS=new Set(["log","info","warn","error","debug","trace","fatal","audit"]);
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireEmbeddingValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{embeddingPatterns,validatorFunctions}=options;const sourceCode=context.sourceCode;const vectorStoreOps=["upsert","insert","add","store","index","save"];function isEmbeddingCall(node){const chain=(0,sdk_1.calleeChain)(node.callee);return embeddingPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(chain,pattern))?sourceCode.getText(node.callee):null}function isValidated(node){const chain=(0,sdk_1.calleeChain)(node.callee);return validatorFunctions.some(fn=>(0,eslint_devkit_1.nameHasWord)(chain,fn))}function isVectorStoreOp(node){const chain=(0,sdk_1.calleeChain)(node.callee);return vectorStoreOps.some(op=>(0,eslint_devkit_1.nameHasWord)(chain,op))}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=(0,eslint_devkit_1.objectKeyName)(prop);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
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireEmbeddingValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireEmbeddingValidation=(0,eslint_devkit_1.createRule)({name:"require-embedding-validation",meta:{interop:{runtimes:["eslint","oxlint"]},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"]}],createOnce(context,currentOptions){let embeddingPatterns;let validatorFunctions;let sourceCode;let vectorStoreOps;function isEmbeddingCall(node){const chain=(0,sdk_1.calleeChain)(node.callee);return embeddingPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(chain,pattern))?sourceCode.getText(node.callee):null}function isValidated(node){const chain=(0,sdk_1.calleeChain)(node.callee);return validatorFunctions.some(fn=>(0,eslint_devkit_1.nameHasWord)(chain,fn))}function isVectorStoreOp(node){const chain=(0,sdk_1.calleeChain)(node.callee);return vectorStoreOps.some(op=>(0,eslint_devkit_1.nameHasWord)(chain,op))}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({embeddingPatterns,validatorFunctions}=options);sourceCode=context.sourceCode;vectorStoreOps=["upsert","insert","add","store","index","save"];return true},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=(0,eslint_devkit_1.objectKeyName)(prop);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 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireErrorHandling=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{allowInTests}=options;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return{}}function isInsideTryBlock(node){let parent=node.parent;while(parent){if(parent.type==="TryStatement"){return true}parent=parent.parent}return false}function hasStreamErrorHandler(node,fn){const optionsArg=node.arguments[0];return STREAMING.has(fn)&&optionsArg?.type==="ObjectExpression"&&(0,sdk_1.optionValue)(optionsArg,"onError")!==void 0}return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)return;if(hasStreamErrorHandler(node,matchedFunction))return;if(!isInsideTryBlock(node)){context.report({node,messageId:"missingErrorHandling",data:{function:matchedFunction}})}}}}});const STREAMING=new Set(["streamText","streamObject"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireErrorHandling=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireErrorHandling=(0,eslint_devkit_1.createRule)({name:"require-error-handling",meta:{interop:{runtimes:["eslint","oxlint"]},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}],createOnce(context,currentOptions){function isInsideTryBlock(node){let parent=node.parent;while(parent){if(parent.type==="TryStatement"){return true}parent=parent.parent}return false}function hasStreamErrorHandler(node,fn){const optionsArg=node.arguments[0];return STREAMING.has(fn)&&optionsArg?.type==="ObjectExpression"&&(0,sdk_1.optionValue)(optionsArg,"onError")!==void 0}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;const{allowInTests}=options;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return false}return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)return;if(hasStreamErrorHandler(node,matchedFunction))return;if(!isInsideTryBlock(node)){context.report({node,messageId:"missingErrorHandling",data:{function:matchedFunction}})}}}}});const STREAMING=new Set(["streamText","streamObject"]);
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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:"Disallow unbounded multi-step tool loops (hasToolCall-only stop conditions, request-controlled step limits)",cwe:"CWE-834",cvss:6.5},messages:{unboundedSteps:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unbounded Multi-Step Tool Loop",cwe:"CWE-834",owasp:"A05:2021",cvss:6.5,description:"{{function}} with tools sets a step limit that does not bound the loop ({{reason}}).",severity:"MEDIUM",compliance:["SOC2"],fix:"Bound the loop with a server-side constant: stopWhen: [hasToolCall(...), stepCountIs(20)] (v5+) or maxSteps: 20 (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){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const sourceCode=context.sourceCode;function unboundedReason(key,value){const scope=sourceCode.getScope(value);if((0,sdk_1.isRequestDerived)(value,scope))return`${key} comes from the request`;if(value.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&value.name==="Infinity"){return`${key} is Infinity`}if(key!=="stopWhen")return null;const conditions=value.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression?value.elements:[value];const named=conditions.map(c=>c?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression?(0,sdk_1.calleeName)(c.callee):null);if(named.every(name=>name==="hasToolCall")){return"hasToolCall only stops if the model calls that tool"}for(const condition of conditions){if(condition?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&STEP_COUNTERS.has((0,sdk_1.calleeName)(condition.callee))&&condition.arguments.some(arg=>(0,sdk_1.isRequestDerived)(arg,scope))){return"the step count comes from the request"}}return null}return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node,FUNCTIONS_WITH_TOOLS);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(optionsArg?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;if(!(0,sdk_1.optionValue)(optionsArg,"tools"))return;for(const key of STEP_KEYS){const value=(0,sdk_1.optionValue)(optionsArg,key);const reason=value&&unboundedReason(key,value);if(reason){context.report({node:value,messageId:"unboundedSteps",data:{function:matchedFunction,reason}})}}}}}});const FUNCTIONS_WITH_TOOLS=["generateText","streamText"];const STEP_KEYS=["maxSteps","max_steps","stopWhen"];const STEP_COUNTERS=new Set(["stepCountIs","isStepCount"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxSteps=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireMaxSteps=(0,eslint_devkit_1.createRule)({name:"require-max-steps",meta:{interop:{runtimes:["eslint","oxlint"]},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:"Disallow unbounded multi-step tool loops (hasToolCall-only stop conditions, request-controlled step limits)",cwe:"CWE-834",cvss:6.5},messages:{unboundedSteps:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unbounded Multi-Step Tool Loop",cwe:"CWE-834",owasp:"A05:2021",cvss:6.5,description:"{{function}} with tools sets a step limit that does not bound the loop ({{reason}}).",severity:"MEDIUM",compliance:["SOC2"],fix:"Bound the loop with a server-side constant: stopWhen: [hasToolCall(...), stepCountIs(20)] (v5+) or maxSteps: 20 (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}],createOnce(context){let sourceCode;function unboundedReason(key,value){if((0,sdk_1.isRequestDerived)(value,sourceCode))return`${key} comes from the request`;if(value.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&value.name==="Infinity"){return`${key} is Infinity`}if(key!=="stopWhen")return null;const conditions=value.type===eslint_devkit_1.AST_NODE_TYPES.ArrayExpression?value.elements:[value];const named=conditions.map(c=>c?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression?(0,sdk_1.calleeName)(c.callee):null);if(named.every(name=>name==="hasToolCall")){return"hasToolCall only stops if the model calls that tool"}for(const condition of conditions){if(condition?.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&STEP_COUNTERS.has((0,sdk_1.calleeName)(condition.callee))&&condition.arguments.some(arg=>(0,sdk_1.isRequestDerived)(arg,sourceCode))){return"the step count comes from the request"}}return null}return{before(){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;sourceCode=context.sourceCode;return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node,FUNCTIONS_WITH_TOOLS);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(optionsArg?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;if(!(0,sdk_1.optionValue)(optionsArg,"tools"))return;for(const key of STEP_KEYS){const value=(0,sdk_1.optionValue)(optionsArg,key);const reason=value&&unboundedReason(key,value);if(reason){context.report({node:value,messageId:"unboundedSteps",data:{function:matchedFunction,reason}})}}}}}});const FUNCTIONS_WITH_TOOLS=["generateText","streamText"];const STEP_KEYS=["maxSteps","max_steps","stopWhen"];const STEP_COUNTERS=new Set(["stepCountIs","isStepCount"]);
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxTokens=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{targetFunctions}=options;return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node,targetFunctions);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;if(!(0,sdk_1.declaresOption)(optionsArg,TOKEN_LIMIT_KEYS)){context.report({node,messageId:"missingMaxTokens",data:{function:matchedFunction}})}}}}});const TOKEN_LIMIT_KEYS=["maxTokens","max_tokens","maxOutputTokens","max_output_tokens"];
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireMaxTokens=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireMaxTokens=(0,eslint_devkit_1.createRule)({name:"require-max-tokens",meta:{interop:{runtimes:["eslint","oxlint"]},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"]}],createOnce(context,currentOptions){let targetFunctions;return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({targetFunctions}=options);return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node,targetFunctions);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(!optionsArg||optionsArg.type!=="ObjectExpression")return;if(!(0,sdk_1.declaresOption)(optionsArg,TOKEN_LIMIT_KEYS)){context.report({node,messageId:"missingMaxTokens",data:{function:matchedFunction}})}}}}});const TOKEN_LIMIT_KEYS=["maxTokens","max_tokens","maxOutputTokens","max_output_tokens"];
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireOutputFiltering=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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","aggregate","database","db","sql","mongo","prisma","supabase"],filterFunctions:["filter","sanitize","redact","mask","clean","filterSensitive","removePII","scrub"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{dataSourcePatterns,filterFunctions}=options;const sourceCode=context.sourceCode;function isDataSourceCall(node){const target=(0,sdk_1.unwrap)(node);if(target.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return null;if(target.callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;const words=(0,sdk_1.calleeChain)(target.callee);if(!dataSourcePatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(words,pattern)))return null;if(filterFunctions.some(filter=>(0,eslint_devkit_1.nameHasWord)(words,filter)))return null;return sourceCode.getText(target.callee)}function boundDataSource(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return isDataSourceCall(node);const def=(0,sdk_1.lookupVariable)(node.name,sourceCode.getScope(node))?.defs[0];return def?.type==="Variable"&&def.node.init!==null?isDataSourceCall(def.node.init):null}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"}function isExecute(fn){const parent=fn.parent;return parent?.type==="Property"&&(0,eslint_devkit_1.objectKeyName)(parent)==="execute"}function report(node,fn,source){context.report({node,messageId:"missingOutputFilter",data:{toolName:getToolName(fn),source}})}return{ArrowFunctionExpression(node){if(!isExecute(node)||node.body.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return;const source=isDataSourceCall(node.body);if(source)report(node.body,node,source)},ReturnStatement(node){if(!node.argument)return;let fn=node.parent;while(fn&&fn.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&fn.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionExpression&&fn.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration){fn=fn.parent}if(!fn||!isExecute(fn))return;const source=boundDataSource((0,sdk_1.unwrap)(node.argument));if(source)report(node.argument,fn,source)}}}});
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireOutputFiltering=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireOutputFiltering=(0,eslint_devkit_1.createRule)({name:"require-output-filtering",meta:{interop:{runtimes:["eslint","oxlint"]},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","aggregate","database","db","sql","mongo","prisma","supabase"],filterFunctions:["filter","sanitize","redact","mask","clean","filterSensitive","removePII","scrub"]}],createOnce(context,currentOptions){let dataSourcePatterns;let filterFunctions;let sourceCode;function isDataSourceCall(node){const target=(0,sdk_1.unwrap)(node);if(target.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return null;if(target.callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return null;const words=(0,sdk_1.calleeChain)(target.callee);if(!dataSourcePatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(words,pattern)))return null;if(filterFunctions.some(filter=>(0,eslint_devkit_1.nameHasWord)(words,filter)))return null;return sourceCode.getText(target.callee)}function boundDataSource(node){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return isDataSourceCall(node);const def=(0,sdk_1.lookupVariable)(node.name,sourceCode.getScope(node))?.defs[0];return def?.type==="Variable"&&def.node.init!==null?isDataSourceCall(def.node.init):null}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"}function isExecute(fn){const parent=fn.parent;return parent?.type==="Property"&&(0,eslint_devkit_1.objectKeyName)(parent)==="execute"}function report(node,fn,source){context.report({node,messageId:"missingOutputFilter",data:{toolName:getToolName(fn),source}})}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({dataSourcePatterns,filterFunctions}=options);sourceCode=context.sourceCode;return true},ArrowFunctionExpression(node){if(!isExecute(node)||node.body.type===eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return;const source=isDataSourceCall(node.body);if(source)report(node.body,node,source)},ReturnStatement(node){if(!node.argument)return;let fn=node.parent;while(fn&&fn.type!==eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression&&fn.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionExpression&&fn.type!==eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration){fn=fn.parent}if(!fn||!isExecute(fn))return;const source=boundDataSource((0,sdk_1.unwrap)(node.argument));if(source)report(node.argument,fn,source)}}}});
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireOutputValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{displayPatterns}=options;const aiResultVariables=new Set;function isOutputProperty(node){return node.type==="MemberExpression"&&AI_OUTPUT_PROPERTIES.has((0,eslint_devkit_1.propertyName)(node))}function isAIOutput(node){return isOutputProperty(node)||node.type==="Identifier"&&aiResultVariables.has(node.name)}return{VariableDeclarator(node){if(node.id.type!=="Identifier")return;if(node.init&&isOutputProperty(node.init)){aiResultVariables.add(node.id.name)}},CallExpression(node){const chain=(0,sdk_1.calleeChain)(node.callee);if(!displayPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(chain,pattern)))return;for(const arg of node.arguments){if(isAIOutput(arg)){context.report({node:arg,messageId:"unvalidatedOutput",data:{method:calleeText(node)}})}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:calleeText(node)}})}}}}}};function calleeText(node){return context.sourceCode.getText(node.callee)}}});const AI_OUTPUT_PROPERTIES=new Set(["text","content","output"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireOutputValidation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireOutputValidation=(0,eslint_devkit_1.createRule)({name:"require-output-validation",meta:{interop:{runtimes:["eslint","oxlint"]},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"]}],createOnce(context,currentOptions){let displayPatterns;let aiResultVariables;function calleeText(node){return context.sourceCode.getText(node.callee)}function isOutputProperty(node){return node.type==="MemberExpression"&&AI_OUTPUT_PROPERTIES.has((0,eslint_devkit_1.propertyName)(node))}function isAIOutput(node){return isOutputProperty(node)||node.type==="Identifier"&&aiResultVariables.has(node.name)}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({displayPatterns}=options);aiResultVariables=new Set;return true},VariableDeclarator(node){if(node.id.type!=="Identifier")return;if(node.init&&isOutputProperty(node.init)){aiResultVariables.add(node.id.name)}},CallExpression(node){const chain=(0,sdk_1.calleeChain)(node.callee);if(!displayPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(chain,pattern)))return;for(const arg of node.arguments){if(isAIOutput(arg)){context.report({node:arg,messageId:"unvalidatedOutput",data:{method:calleeText(node)}})}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:calleeText(node)}})}}}}}}}});const AI_OUTPUT_PROPERTIES=new Set(["text","content","output"]);
@@ -1 +1 @@
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");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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"],validatorFunctions:["validate","sanitize","filter","clean","verify","validateRag","sanitizeContent","filterDocs"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{ragPatterns,validatorFunctions}=options;const sourceCode=context.sourceCode;const ragVariables=new Set;function isRagCall(node){if(node.type!=="CallExpression")return null;const chain=(0,sdk_1.calleeChain)(node.callee);return ragPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(chain,pattern))?sourceCode.getText(node.callee):null}function isValidated(node){const chain=(0,sdk_1.calleeChain)(node.callee);return validatorFunctions.some(fn=>(0,eslint_devkit_1.nameHasWord)(chain,fn))}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){if(!(0,sdk_1.sdkCallName)(node))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
+ "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");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");const flow_1=require("../../utils/flow");exports.requireRagContentValidation=(0,eslint_devkit_1.createRule)({name:"require-rag-content-validation",meta:{interop:{runtimes:["eslint","oxlint"]},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"],validatorFunctions:["validate","sanitize","filter","clean","verify","validateRag","sanitizeContent","filterDocs"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{ragPatterns,validatorFunctions}=options;const sourceCode=context.sourceCode;function isRagCall(node){if(node.type!=="CallExpression"||(0,flow_1.isDerivationCall)(node))return false;const chain=(0,sdk_1.calleeChain)(node.callee);return ragPatterns.some(pattern=>(0,eslint_devkit_1.nameHasWord)(chain,pattern))&&!isValidated(node)}function isValidated(node){const chain=(0,sdk_1.calleeChain)(node.callee);return validatorFunctions.some(fn=>(0,eslint_devkit_1.nameHasWord)(chain,fn))}const ragFlow={sourceCode,isSource:isRagCall,isBarrier:node=>node.type==="CallExpression"&&!(0,flow_1.isDerivationCall)(node)&&isValidated(node)};function containsRagContent(node){const parts=node.type==="TemplateLiteral"?node.expressions:[node];const found=parts.find(part=>(0,flow_1.derivesFrom)(part,ragFlow));return found?sourceCode.getText(found):null}return{CallExpression(node){if(!(0,sdk_1.sdkCallName)(node))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 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireRequestTimeout=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{allowInTests}=options;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return{}}return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(optionsArg&&optionsArg.type!=="ObjectExpression")return;if(!optionsArg||!(0,sdk_1.declaresOption)(optionsArg,TIMEOUT_KEYS)){context.report({node,messageId:"missingTimeout",data:{function:matchedFunction}})}}}}});const TIMEOUT_KEYS=["timeout","abortSignal","signal","timeoutMs","requestTimeout"];
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireRequestTimeout=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireRequestTimeout=(0,eslint_devkit_1.createRule)({name:"require-request-timeout",meta:{interop:{runtimes:["eslint","oxlint"]},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}],createOnce(context,currentOptions){return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;const{allowInTests}=options;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return false}return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)return;const optionsArg=node.arguments[0];if(optionsArg&&optionsArg.type!=="ObjectExpression")return;if(!optionsArg||!(0,sdk_1.declaresOption)(optionsArg,TIMEOUT_KEYS)){context.report({node,messageId:"missingTimeout",data:{function:matchedFunction}})}}}}});const TIMEOUT_KEYS=["timeout","abortSignal","signal","timeoutMs","requestTimeout"];
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireToolConfirmation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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 needsApproval: true to the tool (or toolApproval on the call), or omit execute so the client confirms",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","exec","eval","spawn","shell","update","modify","alter","insert","write"]}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{destructivePatterns}=options;function toolDefinition(value){if(value.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return value;if(value.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&TOOL_FACTORIES.has((0,sdk_1.calleeName)(value.callee))&&value.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return value.arguments[0]}return null}function isConfirmed(def){let hasExecute=false;for(const prop of def.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return true;const key=(0,eslint_devkit_1.objectKeyName)(prop);if(CONFIRMATION_PROPS.has(key))return true;if(key==="execute")hasExecute=true}return!hasExecute}return{Property(node){const toolName=(0,eslint_devkit_1.objectKeyName)(node);if(toolName===null)return;const def=toolDefinition(node.value);if(!def)return;const operation=destructivePatterns.find(pattern=>(0,eslint_devkit_1.nameHasWord)(toolName,pattern));if(!operation)return;const tools=node.parent;if(tools?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;const toolsProp=tools.parent;if(toolsProp?.type!==eslint_devkit_1.AST_NODE_TYPES.Property||(0,eslint_devkit_1.objectKeyName)(toolsProp)!=="tools")return;if((0,sdk_1.optionValue)(toolsProp.parent,"toolApproval"))return;if(isConfirmed(def))return;context.report({node,messageId:"missingConfirmation",data:{toolName,operation}})}}}});const TOOL_FACTORIES=new Set(["tool","dynamicTool"]);const CONFIRMATION_PROPS=new Set(["needsApproval","requiresConfirmation","requireConfirmation","confirmation","requiresApproval","requireApproval","approval"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireToolConfirmation=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireToolConfirmation=(0,eslint_devkit_1.createRule)({name:"require-tool-confirmation",meta:{interop:{runtimes:["eslint","oxlint"]},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 needsApproval: true to the tool (or toolApproval on the call), or omit execute so the client confirms",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","exec","eval","spawn","shell","update","modify","alter","insert","write"]}],createOnce(context,currentOptions){let destructivePatterns;function toolDefinition(value){if(value.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return value;if(value.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&TOOL_FACTORIES.has((0,sdk_1.calleeName)(value.callee))&&value.arguments[0]?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression){return value.arguments[0]}return null}function isConfirmed(def){let hasExecute=false;for(const prop of def.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement)return true;const key=(0,eslint_devkit_1.objectKeyName)(prop);if(CONFIRMATION_PROPS.has(key))return true;if(key==="execute")hasExecute=true}return!hasExecute}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({destructivePatterns}=options);return true},Property(node){const toolName=(0,eslint_devkit_1.objectKeyName)(node);if(toolName===null)return;const def=toolDefinition(node.value);if(!def)return;const operation=destructivePatterns.find(pattern=>(0,eslint_devkit_1.nameHasWord)(toolName,pattern));if(!operation)return;const tools=node.parent;if(tools?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;const toolsProp=tools.parent;if(toolsProp?.type!==eslint_devkit_1.AST_NODE_TYPES.Property||(0,eslint_devkit_1.objectKeyName)(toolsProp)!=="tools")return;if((0,sdk_1.optionValue)(toolsProp.parent,"toolApproval"))return;if(isConfirmed(def))return;context.report({node,messageId:"missingConfirmation",data:{toolName,operation}})}}}});const TOOL_FACTORIES=new Set(["tool","dynamicTool"]);const CONFIRMATION_PROPS=new Set(["needsApproval","requiresConfirmation","requireConfirmation","confirmation","requiresApproval","requireApproval","approval"]);
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireToolSchema=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");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,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{allowInTests}=options;const sourceCode=context.sourceCode;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return{}}function toolKey(node){const parent=node.parent;return parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property?(0,eslint_devkit_1.objectKeyName)(parent):null}return{CallExpression(node){const callee=sourceCode.getText(node.callee);const isToolHelper=callee==="tool"||callee.endsWith(".tool");if(isToolHelper){const toolArg=node.arguments[0];if(toolArg?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression&&!(0,sdk_1.declaresOption)(toolArg,SCHEMA_KEYS)){context.report({node:toolArg,messageId:"missingInputSchema",data:{toolName:toolKey(node)??"unnamed tool"}})}return}if(!(0,sdk_1.sdkCallName)(node))return;const optionsArg=node.arguments[0];if(optionsArg?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;const tools=(0,sdk_1.optionValue)(optionsArg,"tools");if(tools?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;for(const toolDef of tools.properties){if(toolDef.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;if(toolDef.value.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)continue;if(!(0,sdk_1.declaresOption)(toolDef.value,SCHEMA_KEYS)){context.report({node:toolDef.value,messageId:"missingInputSchema",data:{toolName:(0,eslint_devkit_1.objectKeyName)(toolDef)??"unknown"}})}}}}}});const SCHEMA_KEYS=["inputSchema","parameters"];
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireToolSchema=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireToolSchema=(0,eslint_devkit_1.createRule)({name:"require-tool-schema",meta:{interop:{runtimes:["eslint","oxlint"]},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}],createOnce(context,currentOptions){let sourceCode;function toolKey(node){const parent=node.parent;return parent?.type===eslint_devkit_1.AST_NODE_TYPES.Property?(0,eslint_devkit_1.objectKeyName)(parent):null}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;const{allowInTests}=options;sourceCode=context.sourceCode;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return false}return true},CallExpression(node){const callee=sourceCode.getText(node.callee);const isToolHelper=callee==="tool"||callee.endsWith(".tool");if(isToolHelper){const toolArg=node.arguments[0];if(toolArg?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression&&!(0,sdk_1.declaresOption)(toolArg,SCHEMA_KEYS)){context.report({node:toolArg,messageId:"missingInputSchema",data:{toolName:toolKey(node)??"unnamed tool"}})}return}if(!(0,sdk_1.sdkCallName)(node))return;const optionsArg=node.arguments[0];if(optionsArg?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;const tools=(0,sdk_1.optionValue)(optionsArg,"tools");if(tools?.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)return;for(const toolDef of tools.properties){if(toolDef.type!==eslint_devkit_1.AST_NODE_TYPES.Property)continue;if(toolDef.value.type!==eslint_devkit_1.AST_NODE_TYPES.ObjectExpression)continue;if(!(0,sdk_1.declaresOption)(toolDef.value,SCHEMA_KEYS)){context.report({node:toolDef.value,messageId:"missingInputSchema",data:{toolName:(0,eslint_devkit_1.objectKeyName)(toolDef)??"unknown"}})}}}}}});const SCHEMA_KEYS=["inputSchema","parameters"];
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireValidatedPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireValidatedPrompt=(0,eslint_devkit_1.createRule)({name:"require-validated-prompt",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-validated-prompt.md",description:"Require validated/sanitized prompts in generateText and streamText calls",cwe:"CWE-74",cvss:9},messages:{unsafePrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Prompt in Vercel AI SDK",cwe:"CWE-74",owasp:"A03:2021",cvss:9,description:'User input "{{input}}" passed directly to {{function}} prompt without validation',severity:"CRITICAL",compliance:["SOC2","GDPR"],fix:"Validate input before use: generateText({ prompt: validateInput(userInput) })",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"}),unsafeSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8.5,description:"Dynamic value in system prompt can lead to prompt injection",severity:"HIGH",fix:"Use static system prompts or validate dynamic content",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{validatorFunctions:{type:"array",items:{type:"string"},description:"Function names considered as input validators"},userInputPatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest user input (regex)"},allowInTests:{type:"boolean",description:"Allow unsafe patterns in test files"}},additionalProperties:false}]},defaultOptions:[{validatorFunctions:["validateInput","sanitizeInput","validatePrompt","sanitizePrompt","escapeInput","cleanInput"],userInputPatterns:["userInput","userPrompt","userMessage","userQuery","userContent","input","query","message","req.body","request.body"],allowInTests:false}],create(context,[options]){if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return{};const{validatorFunctions,userInputPatterns,allowInTests}=options;const sourceCode=context.sourceCode;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return{}}const pathPatterns=userInputPatterns.filter(p=>p.includes(".")).map(p=>p.split("."));const namePatterns=userInputPatterns.filter(p=>!p.includes("."));function isValidatedCall(node){const target=(0,sdk_1.unwrap)(node);if(target.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const name=(0,sdk_1.calleeName)(target.callee);return validatorFunctions.includes(name)||SCHEMA_PARSERS.has(name)}function isValidatedBinding(node){const def=(0,sdk_1.lookupVariable)(node.name,sourceCode.getScope(node))?.defs[0];return def?.type==="Variable"&&def.node.init!==null&&isValidatedCall(def.node.init)}function nameLooksLikeInput(name){return namePatterns.some(pattern=>(0,sdk_1.nameEndsWithWords)(name,pattern))}function pathLooksLikeInput(node){const path=(0,eslint_devkit_1.memberPath)(node);return path!==null&&pathPatterns.some(pattern=>pattern.every((part,i)=>path[i]===part))}function findUserInput(node){if(node.type==="TemplateLiteral"){for(const expr of node.expressions){const found=findUserInput(expr);if(found)return found}return null}if(node.type==="BinaryExpression"){return findUserInput(node.left)??findUserInput(node.right)}if(node.type==="Identifier"){if(isValidatedBinding(node))return null;const unsafe=(0,sdk_1.isRequestDerived)(node,sourceCode.getScope(node))||nameLooksLikeInput(node.name);return unsafe?node.name:null}if(node.type==="MemberExpression"){const unsafe=(0,sdk_1.isRequestDerived)(node,sourceCode.getScope(node))||pathLooksLikeInput(node)||nameLooksLikeInput((0,eslint_devkit_1.propertyName)(node)??"");return unsafe?sourceCode.getText(node):null}return null}return{CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)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);const isPrompt=keyName==="prompt";if(!isPrompt&&!(0,prompt_props_1.isSystemPromptProp)(keyName))continue;const input=findUserInput(prop.value);if(!input)continue;context.report(isPrompt?{node:prop.value,messageId:"unsafePrompt",data:{input,function:matchedFunction}}:{node:prop.value,messageId:"unsafeSystemPrompt"})}}}}});const SCHEMA_PARSERS=new Set(["parse","safeParse","parseAsync","safeParseAsync","validateSync"]);
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireValidatedPrompt=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const prompt_props_1=require("../../utils/prompt-props");const vercel_ai_evidence_1=require("../../utils/vercel-ai-evidence");const sdk_1=require("../../utils/sdk");exports.requireValidatedPrompt=(0,eslint_devkit_1.createRule)({name:"require-validated-prompt",meta:{interop:{runtimes:["eslint","oxlint"]},type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-vercel-ai-security/docs/rules/require-validated-prompt.md",description:"Require validated/sanitized prompts in generateText and streamText calls",cwe:"CWE-74",cvss:9},messages:{unsafePrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe Prompt in Vercel AI SDK",cwe:"CWE-74",owasp:"A03:2021",cvss:9,description:'User input "{{input}}" passed directly to {{function}} prompt without validation',severity:"CRITICAL",compliance:["SOC2","GDPR"],fix:"Validate input before use: generateText({ prompt: validateInput(userInput) })",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"}),unsafeSystemPrompt:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.SECURITY,issueName:"Unsafe System Prompt",cwe:"CWE-74",owasp:"A03:2021",cvss:8.5,description:"Dynamic value in system prompt can lead to prompt injection",severity:"HIGH",fix:"Use static system prompts or validate dynamic content",documentationLink:"https://owasp.org/www-project-top-10-for-large-language-model-applications/"})},schema:[{type:"object",properties:{validatorFunctions:{type:"array",items:{type:"string"},description:"Function names considered as input validators"},userInputPatterns:{type:"array",items:{type:"string"},description:"Variable patterns that suggest user input (regex)"},allowInTests:{type:"boolean",description:"Allow unsafe patterns in test files"}},additionalProperties:false}]},defaultOptions:[{validatorFunctions:["validateInput","sanitizeInput","validatePrompt","sanitizePrompt","escapeInput","cleanInput"],userInputPatterns:["userInput","userPrompt","userMessage","userQuery","userContent","input","query","message","req.body","request.body"],allowInTests:false}],createOnce(context,currentOptions){let validatorFunctions;let userInputPatterns;let allowInTests;let sourceCode;let pathPatterns;let namePatterns;function isValidatedCall(node){const target=(0,sdk_1.unwrap)(node);if(target.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const name=(0,sdk_1.calleeName)(target.callee);return validatorFunctions.includes(name)||SCHEMA_PARSERS.has(name)}function isValidatedBinding(node){const def=(0,sdk_1.lookupVariable)(node.name,sourceCode.getScope(node))?.defs[0];return def?.type==="Variable"&&def.node.init!==null&&isValidatedCall(def.node.init)}function nameLooksLikeInput(name){return namePatterns.some(pattern=>(0,sdk_1.nameEndsWithWords)(name,pattern))}function pathLooksLikeInput(node){const path=(0,eslint_devkit_1.memberPath)(node);return path!==null&&pathPatterns.some(pattern=>pattern.every((part,i)=>path[i]===part))}function findUserInput(node){if(node.type==="TemplateLiteral"){for(const expr of node.expressions){const found=findUserInput(expr);if(found)return found}return null}if(node.type==="BinaryExpression"){return findUserInput(node.left)??findUserInput(node.right)}if(node.type==="Identifier"){if(isValidatedBinding(node))return null;const unsafe=(0,sdk_1.isRequestDerived)(node,sourceCode)||nameLooksLikeInput(node.name);return unsafe?node.name:null}if(node.type==="MemberExpression"){const unsafe=(0,sdk_1.isRequestDerived)(node,sourceCode)||pathLooksLikeInput(node)||nameLooksLikeInput((0,eslint_devkit_1.propertyName)(node)??"");return unsafe?sourceCode.getText(node):null}return null}return{before(){const[options]=currentOptions();if(!(0,vercel_ai_evidence_1.fileUsesVercelAi)(context.sourceCode.ast))return false;({validatorFunctions,userInputPatterns,allowInTests}=options);sourceCode=context.sourceCode;if(allowInTests&&(0,eslint_devkit_1.isTestFilePath)(context.filename)){return false}pathPatterns=userInputPatterns.filter(p=>p.includes(".")).map(p=>p.split("."));namePatterns=userInputPatterns.filter(p=>!p.includes("."));return true},CallExpression(node){const matchedFunction=(0,sdk_1.sdkCallName)(node);if(!matchedFunction)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);const isPrompt=keyName==="prompt";if(!isPrompt&&!(0,prompt_props_1.isSystemPromptProp)(keyName))continue;const input=findUserInput(prop.value);if(!input)continue;context.report(isPrompt?{node:prop.value,messageId:"unsafePrompt",data:{input,function:matchedFunction}}:{node:prop.value,messageId:"unsafeSystemPrompt"})}}}}});const SCHEMA_PARSERS=new Set(["parse","safeParse","parseAsync","safeParseAsync","validateSync"]);
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.lookupVariable=lookupVariable;exports.isDerivationCall=isDerivationCall;exports.sameFileFunction=sameFileFunction;exports.returnsOf=returnsOf;exports.derivesFrom=derivesFrom;const eslint_devkit_1=require("@interlace/eslint-devkit");const MAX_DEPTH=16;const MAX_STEPS=4e3;const DERIVATIONS=new Set(["map","flatMap","filter","slice","flat","join","concat","reduce","find","findLast","at","sort","toSorted","reverse","toString","trim","trimStart","trimEnd","toLowerCase","toUpperCase","replace","replaceAll","split","substring","padStart","padEnd","match","normalize"]);const ELEMENT_CALLBACKS=new Set(["map","flatMap","filter","forEach","find","findLast","some","every","reduce"]);const WRAPPERS=new Set(["JSON.stringify","String","Number","parseInt","parseFloat","Array.from","Object.values","Object.entries"]);function lookupVariable(name,scope){for(let current=scope;current;current=current.upper){const variable=current.set.get(name);if(variable)return variable}return void 0}function isFunction(node){return node?.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration||node?.type===eslint_devkit_1.AST_NODE_TYPES.FunctionExpression||node?.type===eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression}function isDerivationCall(node){return node.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&node.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&DERIVATIONS.has((0,eslint_devkit_1.propertyName)(node.callee))}function sameFileFunction(callee,sourceCode){if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return null;const def=lookupVariable(callee.name,sourceCode.getScope(callee))?.defs[0];if(def?.type==="FunctionName")return def.node;if(def?.type==="Variable"&&isFunction(def.node.init))return def.node.init;return null}function returnsOf(fn,sourceCode){if(fn.body.type!==eslint_devkit_1.AST_NODE_TYPES.BlockStatement)return[fn.body];const found=[];const visit=node=>{if(isFunction(node))return;if(node.type===eslint_devkit_1.AST_NODE_TYPES.ReturnStatement&&node.argument)found.push(node.argument);for(const key of sourceCode.visitorKeys[node.type]){const child=node[key];for(const item of Array.isArray(child)?child:[child]){if(item&&typeof item.type==="string")visit(item)}}};for(const statement of fn.body.body)visit(statement);return found}function contains(outer,inner){return outer.range[0]<=inner.range[0]&&inner.range[1]<=outer.range[1]}function functionVariable(fn,sourceCode){const parent=fn.parent;const id=fn.type===eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration?fn.id:parent.type===eslint_devkit_1.AST_NODE_TYPES.VariableDeclarator&&parent.id.type===eslint_devkit_1.AST_NODE_TYPES.Identifier?parent.id:null;return id?lookupVariable(id.name,sourceCode.getScope(fn)):void 0}const EMPTY_ENV=new Map;function derivesFrom(node,flow){const{sourceCode}=flow;let steps=0;const onPath=new Set;function walk(current,depth,env){if(depth>MAX_DEPTH||++steps>MAX_STEPS)return false;if(flow.isSource(current))return true;if(flow.isBarrier?.(current))return false;const next=child=>child!==null&&walk(child,depth+1,env);switch(current.type){case eslint_devkit_1.AST_NODE_TYPES.AwaitExpression:case eslint_devkit_1.AST_NODE_TYPES.SpreadElement:return next(current.argument);case eslint_devkit_1.AST_NODE_TYPES.TSAsExpression:case eslint_devkit_1.AST_NODE_TYPES.TSNonNullExpression:case eslint_devkit_1.AST_NODE_TYPES.TSSatisfiesExpression:case eslint_devkit_1.AST_NODE_TYPES.ChainExpression:return next(current.expression);case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return current.expressions.some(next);case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:case eslint_devkit_1.AST_NODE_TYPES.LogicalExpression:return next(current.left)||next(current.right);case eslint_devkit_1.AST_NODE_TYPES.ConditionalExpression:return next(current.consequent)||next(current.alternate);case eslint_devkit_1.AST_NODE_TYPES.MemberExpression:return next(current.object);case eslint_devkit_1.AST_NODE_TYPES.ArrayExpression:return current.elements.some(next);case eslint_devkit_1.AST_NODE_TYPES.ObjectExpression:return current.properties.some(prop=>next(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property?prop.value:prop));case eslint_devkit_1.AST_NODE_TYPES.CallExpression:return fromCall(current,depth,env);case eslint_devkit_1.AST_NODE_TYPES.Identifier:return fromIdentifier(current,depth,env);default:return false}}function fromCall(call,depth,env){const callee=call.callee;if(isDerivationCall(call)){return walk(callee.object,depth+1,env)}const path=(0,eslint_devkit_1.memberPath)(callee);if(path!==null&&WRAPPERS.has(path.join("."))){const[first]=call.arguments;return first!==void 0&&walk(first,depth+1,env)}const fn=sameFileFunction(callee,sourceCode);if(!fn)return false;const inner=new Map;for(const variable of sourceCode.getDeclaredVariables(fn)){const index=fn.params.findIndex(param=>contains(param,variable.defs[0].name));const arg=call.arguments[index];if(arg)inner.set(variable,{node:arg,env})}return returnsOf(fn,sourceCode).some(expr=>walk(expr,depth+1,inner))}function fromIdentifier(id,depth,env){const variable=lookupVariable(id.name,sourceCode.getScope(id));if(!variable)return false;const bound=env.get(variable);if(bound)return walk(bound.node,depth+1,bound.env);if(onPath.has(variable))return false;onPath.add(variable);let found=variable.references.some(ref=>ref.writeExpr!=null&&walk(ref.writeExpr,depth+1,EMPTY_ENV));const def=variable.defs[0];if(!found&&def?.type==="Parameter"){found=fromParameter(variable,def.node,depth)}onPath.delete(variable);return found}function fromParameter(variable,fn,depth){if(flow.isSourceParameter?.(variable,fn))return true;const name=variable.defs[0].name;const index=fn.params.findIndex(param2=>contains(param2,name));const param=fn.params[index];const key=param.type===eslint_devkit_1.AST_NODE_TYPES.ObjectPattern&&name.parent.type===eslint_devkit_1.AST_NODE_TYPES.Property?(0,eslint_devkit_1.objectKeyName)(name.parent):null;const owner=fn.parent;if(owner.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&owner.callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&ELEMENT_CALLBACKS.has((0,eslint_devkit_1.propertyName)(owner.callee))){return walk(owner.callee.object,depth+1,EMPTY_ENV)}const fnVariable=functionVariable(fn,sourceCode);return(fnVariable?.references??[]).some(ref=>{const site=ref.identifier.parent;if(site.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression&&site.callee===ref.identifier){const arg=site.arguments[index];return arg!==void 0&&walk(arg,depth+1,EMPTY_ENV)}if(site.type===eslint_devkit_1.AST_NODE_TYPES.JSXOpeningElement&&index===0){return site.attributes.some(attr=>{if(attr.type===eslint_devkit_1.AST_NODE_TYPES.JSXSpreadAttribute){return walk(attr.argument,depth+1,EMPTY_ENV)}return(key===null||attr.name.name===key)&&attr.value?.type===eslint_devkit_1.AST_NODE_TYPES.JSXExpressionContainer&&attr.value.expression.type!==eslint_devkit_1.AST_NODE_TYPES.JSXEmptyExpression&&walk(attr.value.expression,depth+1,EMPTY_ENV)})}return false})}return walk(node,0,EMPTY_ENV)}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.isDatabaseRowRead=isDatabaseRowRead;const eslint_devkit_1=require("@interlace/eslint-devkit");const sdk_1=require("./sdk");const flow_1=require("./flow");const DATABASE_MODULES=new Set(["@prisma/client","pg","postgres","mysql2","mysql2/promise","knex","kysely","@vercel/postgres","better-sqlite3"]);function isDatabaseModule(module){return DATABASE_MODULES.has(module)||module.startsWith("drizzle-orm")}function isDatabaseHandle(root,sourceCode){if(root.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;const scope=sourceCode.getScope(root);const imported=(0,eslint_devkit_1.resolveModuleBinding)(root,scope);if(imported)return isDatabaseModule(imported.module);const def=(0,flow_1.lookupVariable)(root.name,scope)?.defs[0];if(def?.type!=="Variable"||def.node.init===null)return false;let init=def.node.init;if(init.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression)init=init.argument;if(init.type!==eslint_devkit_1.AST_NODE_TYPES.NewExpression&&init.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression){return false}const factory=(0,eslint_devkit_1.resolveModuleBinding)(init.callee,sourceCode.getScope(init));return factory!==void 0&&isDatabaseModule(factory.module)}function isProjection(call){const[first]=call.arguments;if(first?.type===eslint_devkit_1.AST_NODE_TYPES.ObjectExpression&&((0,sdk_1.optionValue)(first,"select")!==void 0||(0,sdk_1.optionValue)(first,"omit")!==void 0)){return true}if((0,sdk_1.calleeName)(call.callee)==="select"&&first!==void 0){return call.arguments.some(arg=>(0,eslint_devkit_1.staticString)(arg)!=="*")}const sql=first===void 0?null:(0,eslint_devkit_1.staticString)(first);return sql!==null&&/^\s*select\b/i.test(sql)&&!/^\s*select\s+\*/i.test(sql)}function isDatabaseRowRead(node,sourceCode){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const calls=[];let current=node;for(;;){if(current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){calls.push(current);current=current.callee}else if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){current=current.object}else{break}}return isDatabaseHandle(current,sourceCode)&&!calls.some(isProjection)}
package/src/utils/sdk.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AI_SDK_CALLS=void 0;exports.calleeName=calleeName;exports.sdkCallName=sdkCallName;exports.declaresOption=declaresOption;exports.optionValue=optionValue;exports.unwrap=unwrap;exports.lookupVariable=lookupVariable;exports.isRequestDerived=isRequestDerived;exports.isStaticText=isStaticText;exports.isDateValue=isDateValue;exports.nameEndsWithWords=nameEndsWithWords;exports.calleeChain=calleeChain;const eslint_devkit_1=require("@interlace/eslint-devkit");exports.AI_SDK_CALLS=["generateText","streamText","generateObject","streamObject"];function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return(0,eslint_devkit_1.propertyName)(callee);return null}function sdkCallName(node,names=exports.AI_SDK_CALLS){const name=calleeName(node.callee);return name!==null&&names.includes(name)?name:null}function declaresOption(options,keys){return options.properties.some(prop=>prop.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement?true:keys.includes((0,eslint_devkit_1.objectKeyName)(prop)))}function optionValue(options,key){for(const prop of options.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&(0,eslint_devkit_1.objectKeyName)(prop)===key){return prop.value}}return void 0}function unwrap(node){let current=node;while(current.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.TSAsExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.TSNonNullExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.TSSatisfiesExpression){current=current.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression?current.argument:current.expression}return current}function lookupVariable(name,scope){for(let current=scope;current;current=current.upper){const variable=current.set.get(name);if(variable)return variable}return void 0}function isParameter(node,scope){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return lookupVariable(node.name,scope)?.defs[0]?.type==="Parameter"}function isRequestBody(node,scope){const target=unwrap(node);if(target.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=target.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;const method=(0,eslint_devkit_1.propertyName)(callee);if((method==="get"||method==="getAll")&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(callee.object)==="searchParams"){return true}return["json","formData","text"].includes(method)&&isParameter(callee.object,scope)}return target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(target)==="body"&&isParameter(target.object,scope)}function isRequestDerived(node,scope){const target=unwrap(node);if(target.type===eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral){return target.expressions.some(expr=>isRequestDerived(expr,scope))}if(target.type===eslint_devkit_1.AST_NODE_TYPES.BinaryExpression||target.type===eslint_devkit_1.AST_NODE_TYPES.LogicalExpression){return isRequestDerived(target.left,scope)||isRequestDerived(target.right,scope)}if(target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){return isRequestBody(target,scope)||isRequestDerived(target.object,scope)}if(target.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return isRequestBody(target,scope);const def=lookupVariable(target.name,scope)?.defs[0];if(def?.type!=="Variable"||def.node.init===null)return false;const init=unwrap(def.node.init);return init.type===eslint_devkit_1.AST_NODE_TYPES.LogicalExpression?isRequestBody(init.left,scope):isRequestBody(init,scope)}function isDateValue(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression){return node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="Date"}if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression||node.callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression){return false}const receiver=node.callee.object;return receiver.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&receiver.name==="Date"||isDateValue(receiver)}function isStaticText(node,scope,resolving=new Set){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Literal:return true;case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.every(expr=>isStaticText(expr,scope,resolving));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return node.operator==="+"&&isStaticText(node.left,scope,resolving)&&isStaticText(node.right,scope,resolving);case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const variable=lookupVariable(node.name,scope);const def=variable?.defs[0];if(def?.type!=="Variable"||def.parent.kind!=="const"||def.node.init===null||resolving.has(variable)){return false}resolving.add(variable);const result=isStaticText(def.node.init,scope,resolving);resolving.delete(variable);return result}default:return isDateValue(node)}}function nameEndsWithWords(name,term){const words=(0,eslint_devkit_1.identifierWords)(name);const tail=(0,eslint_devkit_1.identifierWords)(term);const offset=words.length-tail.length;return offset>=0&&tail.every((word,i)=>words[offset+i]===word)}function calleeChain(callee){const parts=[];let current=unwrap(callee);while(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){parts.unshift((0,eslint_devkit_1.propertyName)(current)??"");current=unwrap(current.object)}else{current=unwrap(current.callee)}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)parts.unshift(current.name);return parts.join(".")}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.lookupVariable=exports.AI_SDK_CALLS=void 0;exports.calleeName=calleeName;exports.sdkCallName=sdkCallName;exports.declaresOption=declaresOption;exports.optionValue=optionValue;exports.unwrap=unwrap;exports.isRequestDerived=isRequestDerived;exports.isStaticText=isStaticText;exports.isDateValue=isDateValue;exports.nameEndsWithWords=nameEndsWithWords;exports.calleeChain=calleeChain;exports.isSdkHookCall=isSdkHookCall;exports.isSdkTypedParameter=isSdkTypedParameter;const eslint_devkit_1=require("@interlace/eslint-devkit");const flow_1=require("./flow");exports.AI_SDK_CALLS=["generateText","streamText","generateObject","streamObject"];function calleeName(callee){if(callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)return callee.name;if(callee.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return(0,eslint_devkit_1.propertyName)(callee);return null}function sdkCallName(node,names=exports.AI_SDK_CALLS){const name=calleeName(node.callee);return name!==null&&names.includes(name)?name:null}function declaresOption(options,keys){return options.properties.some(prop=>prop.type===eslint_devkit_1.AST_NODE_TYPES.SpreadElement?true:keys.includes((0,eslint_devkit_1.objectKeyName)(prop)))}function optionValue(options,key){for(const prop of options.properties){if(prop.type===eslint_devkit_1.AST_NODE_TYPES.Property&&(0,eslint_devkit_1.objectKeyName)(prop)===key){return prop.value}}return void 0}function unwrap(node){let current=node;while(current.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.TSAsExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.TSNonNullExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.TSSatisfiesExpression){current=current.type===eslint_devkit_1.AST_NODE_TYPES.AwaitExpression?current.argument:current.expression}return current}var flow_2=require("./flow");Object.defineProperty(exports,"lookupVariable",{enumerable:true,get:function(){return flow_2.lookupVariable}});function isParameter(node,scope){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.Identifier)return false;return(0,flow_1.lookupVariable)(node.name,scope)?.defs[0]?.type==="Parameter"}function isRequestBody(node,scope){const target=unwrap(node);if(target.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){const callee=target.callee;if(callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression)return false;const method=(0,eslint_devkit_1.propertyName)(callee);if((method==="get"||method==="getAll")&&callee.object.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(callee.object)==="searchParams"){return true}return["json","formData","text"].includes(method)&&isParameter(callee.object,scope)}return target.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression&&(0,eslint_devkit_1.propertyName)(target)==="body"&&isParameter(target.object,scope)}function isRequestDerived(node,sourceCode){return(0,flow_1.derivesFrom)(node,{sourceCode,isSource:candidate=>isRequestBody(candidate,sourceCode.getScope(candidate))})}function isDateValue(node){if(node.type===eslint_devkit_1.AST_NODE_TYPES.NewExpression){return node.callee.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&node.callee.name==="Date"}if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression||node.callee.type!==eslint_devkit_1.AST_NODE_TYPES.MemberExpression){return false}const receiver=node.callee.object;return receiver.type===eslint_devkit_1.AST_NODE_TYPES.Identifier&&receiver.name==="Date"||isDateValue(receiver)}function isStaticText(node,sourceCode,resolving=new Set){switch(node.type){case eslint_devkit_1.AST_NODE_TYPES.Literal:return true;case eslint_devkit_1.AST_NODE_TYPES.TemplateLiteral:return node.expressions.every(expr=>isStaticText(expr,sourceCode,resolving));case eslint_devkit_1.AST_NODE_TYPES.BinaryExpression:return node.operator==="+"&&isStaticText(node.left,sourceCode,resolving)&&isStaticText(node.right,sourceCode,resolving);case eslint_devkit_1.AST_NODE_TYPES.Identifier:{const variable=(0,flow_1.lookupVariable)(node.name,sourceCode.getScope(node));const def=variable?.defs[0];if(def?.type!=="Variable"||def.parent.kind!=="const"||def.node.init===null||resolving.has(variable)){return false}resolving.add(variable);const result=isStaticText(def.node.init,sourceCode,resolving);resolving.delete(variable);return result}case eslint_devkit_1.AST_NODE_TYPES.CallExpression:{const fn=(0,flow_1.sameFileFunction)(node.callee,sourceCode);if(!fn||resolving.has(fn))return isDateValue(node);resolving.add(fn);const returns=(0,flow_1.returnsOf)(fn,sourceCode);const result=returns.length>0&&returns.every(expr=>isStaticText(expr,sourceCode,resolving));resolving.delete(fn);return result}default:return isDateValue(node)}}function nameEndsWithWords(name,term){const words=(0,eslint_devkit_1.identifierWords)(name);const tail=(0,eslint_devkit_1.identifierWords)(term);const offset=words.length-tail.length;return offset>=0&&tail.every((word,i)=>words[offset+i]===word)}function calleeChain(callee){const parts=[];let current=unwrap(callee);while(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression||current.type===eslint_devkit_1.AST_NODE_TYPES.CallExpression){if(current.type===eslint_devkit_1.AST_NODE_TYPES.MemberExpression){parts.unshift((0,eslint_devkit_1.propertyName)(current)??"");current=unwrap(current.object)}else{current=unwrap(current.callee)}}if(current.type===eslint_devkit_1.AST_NODE_TYPES.Identifier)parts.unshift(current.name);return parts.join(".")}function isSdkModule(module){return module==="ai"||module.startsWith("ai/")||module.startsWith("@ai-sdk/")}const SDK_UI_HOOKS=new Set(["useChat","useCompletion","useObject","useAssistant"]);function isSdkHookCall(node,sourceCode){if(node.type!==eslint_devkit_1.AST_NODE_TYPES.CallExpression)return false;const binding=(0,eslint_devkit_1.resolveModuleBinding)(node.callee,sourceCode.getScope(node));return binding!==void 0&&isSdkModule(binding.module)&&SDK_UI_HOOKS.has(binding.path[binding.path.length-1])}function referencesSdkType(node,sourceCode,seen=new Set){if(seen.has(node))return false;seen.add(node);if(node.type===eslint_devkit_1.AST_NODE_TYPES.TSTypeReference){let name=node.typeName;while(name.type===eslint_devkit_1.AST_NODE_TYPES.TSQualifiedName)name=name.left;const id=name;const binding=(0,eslint_devkit_1.resolveModuleBinding)(id,sourceCode.getScope(node));if(binding&&isSdkModule(binding.module))return true;const alias=typeDeclaration(id,sourceCode);if(alias&&referencesSdkType(alias,sourceCode,seen))return true}for(const key of sourceCode.visitorKeys[node.type]){const child=node[key];for(const item of Array.isArray(child)?child:[child]){if(item&&typeof item.type==="string"&&referencesSdkType(item,sourceCode,seen)){return true}}}return false}function typeDeclaration(id,sourceCode){const def=(0,flow_1.lookupVariable)(id.name,sourceCode.getScope(id))?.defs[0];if(def?.node.type===eslint_devkit_1.AST_NODE_TYPES.TSTypeAliasDeclaration)return def.node.typeAnnotation;if(def?.node.type===eslint_devkit_1.AST_NODE_TYPES.TSInterfaceDeclaration)return def.node.body;return void 0}function memberAnnotation(type,key,sourceCode){let body=type;if(body?.type===eslint_devkit_1.AST_NODE_TYPES.TSTypeReference&&body.typeName.type===eslint_devkit_1.AST_NODE_TYPES.Identifier){body=typeDeclaration(body.typeName,sourceCode)}const members=body?.type===eslint_devkit_1.AST_NODE_TYPES.TSTypeLiteral?body.members:body?.type===eslint_devkit_1.AST_NODE_TYPES.TSInterfaceBody?body.body:[];const member=members.find(m=>m.type===eslint_devkit_1.AST_NODE_TYPES.TSPropertySignature&&(0,eslint_devkit_1.objectKeyName)(m)===key);return member?.typeAnnotation}function isSdkTypedParameter(variable,sourceCode){const name=variable.defs[0].name;let annotation=name.typeAnnotation;const holder=name.parent;if(!annotation&&holder.type===eslint_devkit_1.AST_NODE_TYPES.Property&&holder.parent.type===eslint_devkit_1.AST_NODE_TYPES.ObjectPattern){annotation=memberAnnotation(holder.parent.typeAnnotation?.typeAnnotation,(0,eslint_devkit_1.objectKeyName)(holder),sourceCode)}return annotation!==void 0&&referencesSdkType(annotation,sourceCode)}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.typePropertyNames=typePropertyNames;const ANY_OR_UNKNOWN=1|2;function typePropertyNames(node,sourceCode){const services=sourceCode.parserServices;if(!services?.program)return null;const type=services.program.getTypeChecker().getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));if((type.flags&ANY_OR_UNKNOWN)!==0)return null;return type.getProperties().map(property=>property.getName())}