getdoorman 1.0.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. package/src/worker.js +31 -0
@@ -0,0 +1,1177 @@
1
+ /**
2
+ * AI API Security Rules (SEC-AI-001 through SEC-AI-050)
3
+ *
4
+ * Detects security issues in AI API usage (OpenAI, Anthropic, Google AI,
5
+ * Cohere, Mistral, etc.): leaked keys, prompt injection, PII exposure,
6
+ * missing cost limits, and more.
7
+ */
8
+
9
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
10
+ const PY_EXT = ['.py'];
11
+ const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
12
+ const isPy = (f) => PY_EXT.some(ext => f.endsWith(ext));
13
+
14
+ const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|__snapshots__|node_modules|vendor|dist|build)[/\\]/i;
15
+ const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
16
+
17
+ function scanLines(content, regex, file, rule) {
18
+ const findings = [];
19
+ const lines = content.split('\n');
20
+ for (let i = 0; i < lines.length; i++) {
21
+ const line = lines[i];
22
+ if (COMMENT_LINE.test(line)) continue;
23
+ if (regex.test(line)) {
24
+ findings.push({
25
+ ruleId: rule.id,
26
+ category: rule.category,
27
+ severity: rule.severity,
28
+ title: rule.title,
29
+ description: rule.description,
30
+ confidence: rule.confidence,
31
+ file,
32
+ line: i + 1,
33
+ fix: rule.fix || null,
34
+ });
35
+ }
36
+ }
37
+ return findings;
38
+ }
39
+
40
+ /** Check if file content uses AI APIs */
41
+ function isAiApiFile(content) {
42
+ return /(?:openai|anthropic|@google-ai|@google\/generative|cohere|mistral|replicate|huggingface|together|groq|perplexity|ai\.google|palm|gemini|claude|gpt|chat\.completions|messages\.create)/i.test(content);
43
+ }
44
+
45
+ /** Check if file is likely client/frontend code */
46
+ function isClientCode(filePath) {
47
+ return /(?:client|frontend|public|static|pages|components|src\/app|src\/views|browser|www)[/\\]/i.test(filePath) ||
48
+ /\.(?:jsx|tsx|vue|svelte)$/.test(filePath);
49
+ }
50
+
51
+ const rules = [
52
+ // SEC-AI-001: Hardcoded OpenAI API key
53
+ {
54
+ id: 'SEC-AI-001',
55
+ category: 'security',
56
+ severity: 'critical',
57
+ confidence: 'definite',
58
+ title: 'Hardcoded OpenAI API key',
59
+ description: 'OpenAI API key is hardcoded in source code. This will be exposed in version control and builds.',
60
+ fix: { suggestion: 'Use environment variables (process.env.OPENAI_API_KEY) or a secrets manager instead of hardcoding API keys.' },
61
+ check({ files }) {
62
+ const findings = [];
63
+ const pattern = /(?:OPENAI_API_KEY|openai[._]?api[._]?key|apiKey)\s*[:=]\s*['"]sk-[A-Za-z0-9]{20,}['"]/i;
64
+ for (const [path, content] of files) {
65
+ if (SKIP_PATH.test(path)) continue;
66
+ if (!isJS(path) && !isPy(path)) continue;
67
+ findings.push(...scanLines(content, pattern, path, this));
68
+ }
69
+ return findings;
70
+ },
71
+ },
72
+
73
+ // SEC-AI-002: Hardcoded Anthropic API key
74
+ {
75
+ id: 'SEC-AI-002',
76
+ category: 'security',
77
+ severity: 'critical',
78
+ confidence: 'definite',
79
+ title: 'Hardcoded Anthropic API key',
80
+ description: 'Anthropic API key is hardcoded in source code.',
81
+ fix: { suggestion: 'Use environment variables (process.env.ANTHROPIC_API_KEY) or a secrets manager.' },
82
+ check({ files }) {
83
+ const findings = [];
84
+ const pattern = /(?:ANTHROPIC_API_KEY|anthropic[._]?api[._]?key|apiKey)\s*[:=]\s*['"]sk-ant-[A-Za-z0-9]{20,}['"]/i;
85
+ for (const [path, content] of files) {
86
+ if (SKIP_PATH.test(path)) continue;
87
+ if (!isJS(path) && !isPy(path)) continue;
88
+ findings.push(...scanLines(content, pattern, path, this));
89
+ }
90
+ return findings;
91
+ },
92
+ },
93
+
94
+ // SEC-AI-003: Hardcoded Google AI API key
95
+ {
96
+ id: 'SEC-AI-003',
97
+ category: 'security',
98
+ severity: 'critical',
99
+ confidence: 'definite',
100
+ title: 'Hardcoded Google AI API key',
101
+ description: 'Google AI (Gemini/PaLM) API key is hardcoded in source code.',
102
+ fix: { suggestion: 'Use environment variables or a secrets manager for Google AI API keys.' },
103
+ check({ files }) {
104
+ const findings = [];
105
+ const pattern = /(?:GOOGLE_AI_KEY|GOOGLE_API_KEY|GEMINI_API_KEY|google[._]?api[._]?key)\s*[:=]\s*['"]AIza[A-Za-z0-9_-]{30,}['"]/i;
106
+ for (const [path, content] of files) {
107
+ if (SKIP_PATH.test(path)) continue;
108
+ if (!isJS(path) && !isPy(path)) continue;
109
+ findings.push(...scanLines(content, pattern, path, this));
110
+ }
111
+ return findings;
112
+ },
113
+ },
114
+
115
+ // SEC-AI-004: Hardcoded Cohere/Mistral/other AI API key
116
+ {
117
+ id: 'SEC-AI-004',
118
+ category: 'security',
119
+ severity: 'critical',
120
+ confidence: 'likely',
121
+ title: 'Hardcoded AI provider API key',
122
+ description: 'AI provider API key (Cohere, Mistral, Replicate, HuggingFace, etc.) is hardcoded in source code.',
123
+ fix: { suggestion: 'Use environment variables or a secrets manager for all AI provider API keys.' },
124
+ check({ files }) {
125
+ const findings = [];
126
+ const pattern = /(?:COHERE_API_KEY|MISTRAL_API_KEY|REPLICATE_API_TOKEN|HF_TOKEN|HUGGINGFACE|TOGETHER_API_KEY|GROQ_API_KEY|PERPLEXITY_API_KEY)\s*[:=]\s*['"][A-Za-z0-9_-]{10,}['"]/i;
127
+ for (const [path, content] of files) {
128
+ if (SKIP_PATH.test(path)) continue;
129
+ if (!isJS(path) && !isPy(path)) continue;
130
+ findings.push(...scanLines(content, pattern, path, this));
131
+ }
132
+ return findings;
133
+ },
134
+ },
135
+
136
+ // SEC-AI-005: API key in frontend/client code
137
+ {
138
+ id: 'SEC-AI-005',
139
+ category: 'security',
140
+ severity: 'critical',
141
+ confidence: 'likely',
142
+ title: 'AI API key exposed in client-side code',
143
+ description: 'AI API key is used in frontend/client code, exposing it to end users via browser DevTools or source maps.',
144
+ fix: { suggestion: 'Move AI API calls to a backend/server-side proxy. Never expose AI API keys in client-side bundles.' },
145
+ check({ files }) {
146
+ const findings = [];
147
+ const apiKeyUsage = /(?:openai|anthropic|google.*generative|cohere|mistral).*(?:apiKey|api_key)\s*[:=]/i;
148
+ for (const [path, content] of files) {
149
+ if (SKIP_PATH.test(path)) continue;
150
+ if (!isJS(path)) continue;
151
+ if (!isClientCode(path)) continue;
152
+ findings.push(...scanLines(content, apiKeyUsage, path, this));
153
+ }
154
+ return findings;
155
+ },
156
+ },
157
+
158
+ // SEC-AI-006: Missing cost limits/budgets
159
+ {
160
+ id: 'SEC-AI-006',
161
+ category: 'security',
162
+ severity: 'high',
163
+ confidence: 'likely',
164
+ title: 'AI API usage without cost limits',
165
+ description: 'AI API calls are made without cost tracking or spending limits, risking unexpected charges.',
166
+ fix: { suggestion: 'Implement cost tracking, set max_tokens limits, and use API provider spending caps.' },
167
+ check({ files }) {
168
+ const findings = [];
169
+ const aiCall = /(?:chat\.completions\.create|messages\.create|generate_content|completions\.create|\.generate\()/i;
170
+ const hasCostLimit = /(?:max_tokens|maxTokens|budget|cost|spending|limit|usage\.total_tokens|token_count|price)/i;
171
+ for (const [path, content] of files) {
172
+ if (SKIP_PATH.test(path)) continue;
173
+ if (!isJS(path) && !isPy(path)) continue;
174
+ if (!isAiApiFile(content)) continue;
175
+ const lines = content.split('\n');
176
+ for (let i = 0; i < lines.length; i++) {
177
+ if (COMMENT_LINE.test(lines[i])) continue;
178
+ if (aiCall.test(lines[i])) {
179
+ const block = lines.slice(Math.max(0, i - 5), Math.min(i + 10, lines.length)).join('\n');
180
+ if (!hasCostLimit.test(block)) {
181
+ findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
182
+ }
183
+ }
184
+ }
185
+ }
186
+ return findings;
187
+ },
188
+ },
189
+
190
+ // SEC-AI-007: Prompt injection vulnerability
191
+ {
192
+ id: 'SEC-AI-007',
193
+ category: 'security',
194
+ severity: 'critical',
195
+ confidence: 'likely',
196
+ title: 'Prompt injection vulnerability',
197
+ description: 'User input is directly concatenated or interpolated into AI prompts without sanitization, enabling prompt injection.',
198
+ fix: { suggestion: 'Separate system prompts from user input. Use structured message arrays with distinct roles. Sanitize user input.' },
199
+ check({ files }) {
200
+ const findings = [];
201
+ const promptInjection = /(?:prompt|message|content)\s*[:=]\s*(?:`[^`]*\$\{.*(?:req\.|user|input|query|body|params)|['"][^'"]*['"]\s*\+\s*(?:req\.|user|input|query|body|params))/i;
202
+ for (const [path, content] of files) {
203
+ if (SKIP_PATH.test(path)) continue;
204
+ if (!isJS(path) && !isPy(path)) continue;
205
+ if (!isAiApiFile(content)) continue;
206
+ findings.push(...scanLines(content, promptInjection, path, this));
207
+ }
208
+ return findings;
209
+ },
210
+ },
211
+
212
+ // SEC-AI-008: User input in system prompt
213
+ {
214
+ id: 'SEC-AI-008',
215
+ category: 'security',
216
+ severity: 'critical',
217
+ confidence: 'likely',
218
+ title: 'User input injected into system prompt',
219
+ description: 'User-controlled data is included in the system prompt, allowing privilege escalation via prompt injection.',
220
+ fix: { suggestion: 'Keep system prompts static. Pass user data only in user messages, never in system messages.' },
221
+ check({ files }) {
222
+ const findings = [];
223
+ const systemPromptInjection = /role\s*:\s*['"]system['"].*(?:content\s*:\s*(?:`[^`]*\$\{|.*\+\s*(?:req|user|input|query|body|params)))/i;
224
+ const systemFstring = /role\s*[:=]\s*['"]system['"].*content\s*[:=]\s*f['"]/;
225
+ for (const [path, content] of files) {
226
+ if (SKIP_PATH.test(path)) continue;
227
+ if (!isJS(path) && !isPy(path)) continue;
228
+ if (!isAiApiFile(content)) continue;
229
+ findings.push(...scanLines(content, systemPromptInjection, path, this));
230
+ if (isPy(path)) {
231
+ findings.push(...scanLines(content, systemFstring, path, this));
232
+ }
233
+ }
234
+ return findings;
235
+ },
236
+ },
237
+
238
+ // SEC-AI-009: PII sent to AI API without redaction
239
+ {
240
+ id: 'SEC-AI-009',
241
+ category: 'security',
242
+ severity: 'high',
243
+ confidence: 'likely',
244
+ title: 'PII potentially sent to AI API without redaction',
245
+ description: 'User personal data (email, phone, SSN, name) is passed to AI API calls without PII redaction.',
246
+ fix: { suggestion: 'Implement PII detection and redaction before sending data to AI APIs. Use libraries like presidio or scrubadub.' },
247
+ check({ files }) {
248
+ const findings = [];
249
+ const piiInPrompt = /(?:content|prompt|message|text)\s*[:=].*(?:\.email|\.phone|\.ssn|\.address|\.name|\.dob|\.dateOfBirth|socialSecurity|creditCard)/i;
250
+ for (const [path, content] of files) {
251
+ if (SKIP_PATH.test(path)) continue;
252
+ if (!isJS(path) && !isPy(path)) continue;
253
+ if (!isAiApiFile(content)) continue;
254
+ findings.push(...scanLines(content, piiInPrompt, path, this));
255
+ }
256
+ return findings;
257
+ },
258
+ },
259
+
260
+ // SEC-AI-010: Missing rate limiting on AI API calls
261
+ {
262
+ id: 'SEC-AI-010',
263
+ category: 'security',
264
+ severity: 'high',
265
+ confidence: 'likely',
266
+ title: 'Missing rate limiting on AI API calls',
267
+ description: 'AI API calls are not rate-limited, enabling abuse that could exhaust API quotas and budgets.',
268
+ fix: { suggestion: 'Add rate limiting per user/session for AI API endpoints. Use token bucket or sliding window algorithms.' },
269
+ check({ files }) {
270
+ const findings = [];
271
+ const aiEndpoint = /(?:app\.|router\.|express)(?:\.post|\.get|\.all)\s*\(.*(?:ai|chat|completion|generate|prompt)/i;
272
+ const hasRateLimit = /(?:rateLimit|rate_limit|throttle|limiter|rateLimiter|bottleneck)/i;
273
+ for (const [path, content] of files) {
274
+ if (SKIP_PATH.test(path)) continue;
275
+ if (!isJS(path) && !isPy(path)) continue;
276
+ if (!isAiApiFile(content)) continue;
277
+ if (aiEndpoint.test(content) && !hasRateLimit.test(content)) {
278
+ findings.push(...scanLines(content, aiEndpoint, path, this));
279
+ }
280
+ }
281
+ return findings;
282
+ },
283
+ },
284
+
285
+ // SEC-AI-011: Missing error handling for AI API failures
286
+ {
287
+ id: 'SEC-AI-011',
288
+ category: 'security',
289
+ severity: 'medium',
290
+ confidence: 'likely',
291
+ title: 'Missing error handling for AI API calls',
292
+ description: 'AI API call lacks try-catch or error handling, which may expose API errors or crash the application.',
293
+ fix: { suggestion: 'Wrap AI API calls in try-catch. Handle rate limit errors (429), auth errors (401), and server errors (500) gracefully.' },
294
+ check({ files }) {
295
+ const findings = [];
296
+ const aiCall = /(?:chat\.completions\.create|messages\.create|generate_content|completions\.create)/;
297
+ const hasErrorHandling = /(?:try\s*\{|\.catch\s*\(|catch\s*\(|except\s|on_error|onerror)/i;
298
+ for (const [path, content] of files) {
299
+ if (SKIP_PATH.test(path)) continue;
300
+ if (!isJS(path) && !isPy(path)) continue;
301
+ if (!isAiApiFile(content)) continue;
302
+ const lines = content.split('\n');
303
+ for (let i = 0; i < lines.length; i++) {
304
+ if (COMMENT_LINE.test(lines[i])) continue;
305
+ if (aiCall.test(lines[i])) {
306
+ const block = lines.slice(Math.max(0, i - 10), Math.min(i + 10, lines.length)).join('\n');
307
+ if (!hasErrorHandling.test(block)) {
308
+ findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
309
+ }
310
+ }
311
+ }
312
+ }
313
+ return findings;
314
+ },
315
+ },
316
+
317
+ // SEC-AI-012: Insecure API key storage in .env committed to git
318
+ {
319
+ id: 'SEC-AI-012',
320
+ category: 'security',
321
+ severity: 'critical',
322
+ confidence: 'definite',
323
+ title: 'AI API key in committed .env file',
324
+ description: 'AI API key found in a .env file that appears to be committed to version control.',
325
+ fix: { suggestion: 'Add .env to .gitignore. Rotate any exposed API keys immediately. Use .env.example with placeholder values.' },
326
+ check({ files }) {
327
+ const findings = [];
328
+ const envKeyPattern = /(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_AI_KEY|GEMINI_API_KEY|COHERE_API_KEY|MISTRAL_API_KEY|REPLICATE_API_TOKEN|HF_TOKEN|TOGETHER_API_KEY|GROQ_API_KEY)\s*=\s*\S{10,}/;
329
+ for (const [path, content] of files) {
330
+ if (SKIP_PATH.test(path)) continue;
331
+ if (!path.endsWith('.env') && !path.match(/\.env\.\w+$/)) continue;
332
+ findings.push(...scanLines(content, envKeyPattern, path, this));
333
+ }
334
+ return findings;
335
+ },
336
+ },
337
+
338
+ // SEC-AI-013: Missing content filtering on AI responses
339
+ {
340
+ id: 'SEC-AI-013',
341
+ category: 'security',
342
+ severity: 'medium',
343
+ confidence: 'likely',
344
+ title: 'AI response used without content filtering',
345
+ description: 'AI API response is used directly without content safety filtering, potentially passing harmful content to users.',
346
+ fix: { suggestion: 'Add content safety checks on AI responses before displaying to users. Use moderation APIs or content filters.' },
347
+ check({ files }) {
348
+ const findings = [];
349
+ const directResponse = /(?:response|completion|result)\.(?:choices|content|text|message|data)\s*[\[.]/;
350
+ const hasFilter = /(?:moderat|filter|safe|content.*check|toxic|harmful|block|censor|sanitize)/i;
351
+ for (const [path, content] of files) {
352
+ if (SKIP_PATH.test(path)) continue;
353
+ if (!isJS(path) && !isPy(path)) continue;
354
+ if (!isAiApiFile(content)) continue;
355
+ if (directResponse.test(content) && !hasFilter.test(content)) {
356
+ findings.push(...scanLines(content, directResponse, path, this));
357
+ }
358
+ }
359
+ return findings;
360
+ },
361
+ },
362
+
363
+ // SEC-AI-014: Streaming responses without validation
364
+ {
365
+ id: 'SEC-AI-014',
366
+ category: 'security',
367
+ severity: 'medium',
368
+ confidence: 'likely',
369
+ title: 'AI streaming response without validation',
370
+ description: 'AI API streaming response chunks are forwarded to users without validation or content checking.',
371
+ fix: { suggestion: 'Validate each streaming chunk before forwarding. Buffer and check content safety periodically.' },
372
+ check({ files }) {
373
+ const findings = [];
374
+ const streamUsage = /(?:stream\s*:\s*true|\.stream\(|for\s+await.*(?:chunk|delta|event)|onmessage|on\s*\(\s*['"]data)/i;
375
+ const hasStreamValidation = /(?:validate|filter|check|sanitize|moderat).*(?:chunk|delta|stream)/i;
376
+ for (const [path, content] of files) {
377
+ if (SKIP_PATH.test(path)) continue;
378
+ if (!isJS(path) && !isPy(path)) continue;
379
+ if (!isAiApiFile(content)) continue;
380
+ if (streamUsage.test(content) && !hasStreamValidation.test(content)) {
381
+ findings.push(...scanLines(content, streamUsage, path, this));
382
+ }
383
+ }
384
+ return findings;
385
+ },
386
+ },
387
+
388
+ // SEC-AI-015: Missing model version pinning
389
+ {
390
+ id: 'SEC-AI-015',
391
+ category: 'security',
392
+ severity: 'medium',
393
+ confidence: 'likely',
394
+ title: 'AI model not version-pinned',
395
+ description: 'AI API call uses a non-versioned model name (e.g., "gpt-4" instead of "gpt-4-0613"), risking unexpected behavior changes.',
396
+ fix: { suggestion: 'Pin AI model versions explicitly (e.g., "gpt-4-0613", "claude-3-opus-20240229") for reproducible behavior.' },
397
+ check({ files }) {
398
+ const findings = [];
399
+ const unpinnedModel = /model\s*[:=]\s*['"](?:gpt-4|gpt-3\.5-turbo|claude-3-opus|claude-3-sonnet|claude-3-haiku|gemini-pro|gemini-ultra)['"](?!\s*[-_]?\d)/;
400
+ for (const [path, content] of files) {
401
+ if (SKIP_PATH.test(path)) continue;
402
+ if (!isJS(path) && !isPy(path)) continue;
403
+ if (!isAiApiFile(content)) continue;
404
+ findings.push(...scanLines(content, unpinnedModel, path, this));
405
+ }
406
+ return findings;
407
+ },
408
+ },
409
+
410
+ // SEC-AI-016: Token limit misconfiguration
411
+ {
412
+ id: 'SEC-AI-016',
413
+ category: 'security',
414
+ severity: 'medium',
415
+ confidence: 'likely',
416
+ title: 'AI API token limit misconfiguration',
417
+ description: 'AI API call sets excessively high max_tokens without apparent need, increasing cost and latency.',
418
+ fix: { suggestion: 'Set max_tokens to the minimum needed for your use case. Monitor token usage patterns.' },
419
+ check({ files }) {
420
+ const findings = [];
421
+ const highTokens = /max_tokens\s*[:=]\s*(?:100000|128000|200000|1000000|\d{6,})/;
422
+ for (const [path, content] of files) {
423
+ if (SKIP_PATH.test(path)) continue;
424
+ if (!isJS(path) && !isPy(path)) continue;
425
+ if (!isAiApiFile(content)) continue;
426
+ findings.push(...scanLines(content, highTokens, path, this));
427
+ }
428
+ return findings;
429
+ },
430
+ },
431
+
432
+ // SEC-AI-017: Missing response validation
433
+ {
434
+ id: 'SEC-AI-017',
435
+ category: 'security',
436
+ severity: 'medium',
437
+ confidence: 'likely',
438
+ title: 'AI API response used without validation',
439
+ description: 'AI API response is used in application logic without validating its structure or content.',
440
+ fix: { suggestion: 'Validate AI response structure and content before using it. Check for expected fields and types.' },
441
+ check({ files }) {
442
+ const findings = [];
443
+ const responseAccess = /(?:response|completion|result)\s*\.(?:choices\[0\]|content\[0\]|text|data)(?!\s*[?])/;
444
+ const hasValidation = /(?:if\s*\(.*response|validate|check|assert|schema|typeof.*response|response\s*\?\.|optional.*chain)/i;
445
+ for (const [path, content] of files) {
446
+ if (SKIP_PATH.test(path)) continue;
447
+ if (!isJS(path) && !isPy(path)) continue;
448
+ if (!isAiApiFile(content)) continue;
449
+ const lines = content.split('\n');
450
+ for (let i = 0; i < lines.length; i++) {
451
+ if (COMMENT_LINE.test(lines[i])) continue;
452
+ if (responseAccess.test(lines[i])) {
453
+ const block = lines.slice(Math.max(0, i - 5), Math.min(i + 5, lines.length)).join('\n');
454
+ if (!hasValidation.test(block)) {
455
+ findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
456
+ }
457
+ }
458
+ }
459
+ }
460
+ return findings;
461
+ },
462
+ },
463
+
464
+ // SEC-AI-018: Unsafe function/tool calling
465
+ {
466
+ id: 'SEC-AI-018',
467
+ category: 'security',
468
+ severity: 'critical',
469
+ confidence: 'likely',
470
+ title: 'Unsafe AI function/tool calling execution',
471
+ description: 'AI function call results are executed without validation, allowing the AI to trigger arbitrary functions.',
472
+ fix: { suggestion: 'Validate function names against an allowlist before execution. Validate all arguments with schemas.' },
473
+ check({ files }) {
474
+ const findings = [];
475
+ const unsafeFnCall = /(?:eval\s*\(.*function_call|eval\s*\(.*tool_call|\[.*function_call.*name\]|functions\[.*name\])/i;
476
+ const dynamicExec = /(?:function_call|tool_calls?).*(?:eval|exec|Function\(|require\(|import\()/i;
477
+ for (const [path, content] of files) {
478
+ if (SKIP_PATH.test(path)) continue;
479
+ if (!isJS(path) && !isPy(path)) continue;
480
+ if (!isAiApiFile(content)) continue;
481
+ findings.push(...scanLines(content, unsafeFnCall, path, this));
482
+ findings.push(...scanLines(content, dynamicExec, path, this));
483
+ }
484
+ return findings;
485
+ },
486
+ },
487
+
488
+ // SEC-AI-019: Missing user consent for AI processing
489
+ {
490
+ id: 'SEC-AI-019',
491
+ category: 'security',
492
+ severity: 'medium',
493
+ confidence: 'suggestion',
494
+ title: 'AI processing without user consent verification',
495
+ description: 'User data is sent to AI APIs without checking consent flags, potentially violating privacy regulations.',
496
+ fix: { suggestion: 'Check user consent preferences before sending their data to AI providers. Support opt-out mechanisms.' },
497
+ check({ files }) {
498
+ const findings = [];
499
+ const aiCallWithUserData = /(?:chat\.completions\.create|messages\.create|generate_content).*(?:user|userData|userMessage|userInput)/i;
500
+ const hasConsent = /(?:consent|gdpr|privacy|opt.?in|opt.?out|permission|agreement|tos|terms)/i;
501
+ for (const [path, content] of files) {
502
+ if (SKIP_PATH.test(path)) continue;
503
+ if (!isJS(path) && !isPy(path)) continue;
504
+ if (!isAiApiFile(content)) continue;
505
+ if (aiCallWithUserData.test(content) && !hasConsent.test(content)) {
506
+ findings.push(...scanLines(content, aiCallWithUserData, path, this));
507
+ }
508
+ }
509
+ return findings;
510
+ },
511
+ },
512
+
513
+ // SEC-AI-020: Debug/verbose mode in production
514
+ {
515
+ id: 'SEC-AI-020',
516
+ category: 'security',
517
+ severity: 'medium',
518
+ confidence: 'likely',
519
+ title: 'AI API client in debug/verbose mode',
520
+ description: 'AI API client has debug or verbose logging enabled, potentially logging prompts, responses, and API keys.',
521
+ fix: { suggestion: 'Disable debug/verbose mode in production. Use environment-based configuration for logging levels.' },
522
+ check({ files }) {
523
+ const findings = [];
524
+ const debugMode = /(?:debug\s*[:=]\s*true|verbose\s*[:=]\s*true|logLevel\s*[:=]\s*['"]debug|OPENAI_LOG\s*=\s*['"]debug)/i;
525
+ for (const [path, content] of files) {
526
+ if (SKIP_PATH.test(path)) continue;
527
+ if (!isJS(path) && !isPy(path)) continue;
528
+ if (!isAiApiFile(content)) continue;
529
+ findings.push(...scanLines(content, debugMode, path, this));
530
+ }
531
+ return findings;
532
+ },
533
+ },
534
+
535
+ // SEC-AI-021: Missing retry logic with backoff
536
+ {
537
+ id: 'SEC-AI-021',
538
+ category: 'security',
539
+ severity: 'low',
540
+ confidence: 'likely',
541
+ title: 'AI API calls without retry logic',
542
+ description: 'AI API calls lack retry logic with exponential backoff, causing failures on transient errors.',
543
+ fix: { suggestion: 'Implement retry with exponential backoff for AI API calls. Handle 429 (rate limit) and 5xx errors with retries.' },
544
+ check({ files }) {
545
+ const findings = [];
546
+ const aiCall = /(?:chat\.completions\.create|messages\.create|generate_content|completions\.create)/;
547
+ const hasRetry = /(?:retry|retries|backoff|exponential|maxRetries|max_retries|attempt|p-retry|axios-retry)/i;
548
+ for (const [path, content] of files) {
549
+ if (SKIP_PATH.test(path)) continue;
550
+ if (!isJS(path) && !isPy(path)) continue;
551
+ if (!isAiApiFile(content)) continue;
552
+ if (aiCall.test(content) && !hasRetry.test(content)) {
553
+ findings.push(...scanLines(content, aiCall, path, this));
554
+ }
555
+ }
556
+ return findings;
557
+ },
558
+ },
559
+
560
+ // SEC-AI-022: Missing audit logging of AI interactions
561
+ {
562
+ id: 'SEC-AI-022',
563
+ category: 'security',
564
+ severity: 'medium',
565
+ confidence: 'likely',
566
+ title: 'AI API interactions not audit-logged',
567
+ description: 'AI API calls are not logged for audit purposes, making it impossible to review AI usage or investigate incidents.',
568
+ fix: { suggestion: 'Log AI API calls with timestamps, user IDs, model used, token counts, and response metadata (not full prompts with PII).' },
569
+ check({ files }) {
570
+ const findings = [];
571
+ const aiCall = /(?:chat\.completions\.create|messages\.create|generate_content|completions\.create)/;
572
+ const hasAudit = /(?:audit|log.*ai|log.*completion|log.*prompt|track.*usage|analytics|telemetry)/i;
573
+ for (const [path, content] of files) {
574
+ if (SKIP_PATH.test(path)) continue;
575
+ if (!isJS(path) && !isPy(path)) continue;
576
+ if (!isAiApiFile(content)) continue;
577
+ if (aiCall.test(content) && !hasAudit.test(content)) {
578
+ findings.push(...scanLines(content, aiCall, path, this));
579
+ }
580
+ }
581
+ return findings;
582
+ },
583
+ },
584
+
585
+ // SEC-AI-023: Sensitive data in system prompts
586
+ {
587
+ id: 'SEC-AI-023',
588
+ category: 'security',
589
+ severity: 'high',
590
+ confidence: 'likely',
591
+ title: 'Sensitive data in AI system prompt',
592
+ description: 'System prompt contains sensitive information (credentials, internal URLs, database names) that could be extracted via prompt injection.',
593
+ fix: { suggestion: 'Remove sensitive data from system prompts. Use server-side lookups instead of embedding secrets in prompts.' },
594
+ check({ files }) {
595
+ const findings = [];
596
+ const sensitiveInPrompt = /(?:system|role.*system).*(?:password|secret|api[_-]?key|database.*url|mongodb|postgres|mysql|internal\.\w+\.com|10\.\d+\.\d+|192\.168\.|172\.(?:1[6-9]|2\d|3[01]))/i;
597
+ for (const [path, content] of files) {
598
+ if (SKIP_PATH.test(path)) continue;
599
+ if (!isJS(path) && !isPy(path)) continue;
600
+ if (!isAiApiFile(content)) continue;
601
+ findings.push(...scanLines(content, sensitiveInPrompt, path, this));
602
+ }
603
+ return findings;
604
+ },
605
+ },
606
+
607
+ // SEC-AI-024: Missing input sanitization before AI calls
608
+ {
609
+ id: 'SEC-AI-024',
610
+ category: 'security',
611
+ severity: 'high',
612
+ confidence: 'likely',
613
+ title: 'User input sent to AI API without sanitization',
614
+ description: 'User input is passed directly to AI API calls without sanitization or length limits.',
615
+ fix: { suggestion: 'Sanitize and truncate user input before including in AI API calls. Remove control characters and limit length.' },
616
+ check({ files }) {
617
+ const findings = [];
618
+ const directUserInput = /(?:content|prompt|message)\s*[:=]\s*(?:req\.body|req\.query|request\.body|request\.json|params\[|args\[)/i;
619
+ for (const [path, content] of files) {
620
+ if (SKIP_PATH.test(path)) continue;
621
+ if (!isJS(path) && !isPy(path)) continue;
622
+ if (!isAiApiFile(content)) continue;
623
+ findings.push(...scanLines(content, directUserInput, path, this));
624
+ }
625
+ return findings;
626
+ },
627
+ },
628
+
629
+ // SEC-AI-025: AI response rendered as HTML without escaping
630
+ {
631
+ id: 'SEC-AI-025',
632
+ category: 'security',
633
+ severity: 'high',
634
+ confidence: 'likely',
635
+ title: 'AI response rendered as HTML without escaping',
636
+ description: 'AI API response is rendered as HTML (innerHTML, dangerouslySetInnerHTML) without escaping, enabling XSS.',
637
+ fix: { suggestion: 'Escape AI responses before rendering as HTML, or use textContent instead of innerHTML.' },
638
+ check({ files }) {
639
+ const findings = [];
640
+ const unsafeRender = /(?:innerHTML|dangerouslySetInnerHTML|v-html)\s*=\s*.*(?:response|completion|result|aiOutput|generated)/i;
641
+ for (const [path, content] of files) {
642
+ if (SKIP_PATH.test(path)) continue;
643
+ if (!isJS(path)) continue;
644
+ findings.push(...scanLines(content, unsafeRender, path, this));
645
+ }
646
+ return findings;
647
+ },
648
+ },
649
+
650
+ // SEC-AI-026: API key in URL query parameter
651
+ {
652
+ id: 'SEC-AI-026',
653
+ category: 'security',
654
+ severity: 'critical',
655
+ confidence: 'definite',
656
+ title: 'AI API key passed in URL query parameter',
657
+ description: 'AI API key is passed as a URL query parameter, which gets logged in server logs, browser history, and proxies.',
658
+ fix: { suggestion: 'Pass API keys in the Authorization header, not as URL parameters.' },
659
+ check({ files }) {
660
+ const findings = [];
661
+ const keyInUrl = /(?:api_key|apiKey|key)=(?:sk-|sk-ant-|AIza|hf_)\w+/;
662
+ for (const [path, content] of files) {
663
+ if (SKIP_PATH.test(path)) continue;
664
+ if (!isJS(path) && !isPy(path)) continue;
665
+ findings.push(...scanLines(content, keyInUrl, path, this));
666
+ }
667
+ return findings;
668
+ },
669
+ },
670
+
671
+ // SEC-AI-027: Logging full prompts with user data
672
+ {
673
+ id: 'SEC-AI-027',
674
+ category: 'security',
675
+ severity: 'high',
676
+ confidence: 'likely',
677
+ title: 'AI prompts with user data logged to console/file',
678
+ description: 'Full AI prompts containing user data are logged, potentially exposing PII in log storage.',
679
+ fix: { suggestion: 'Log metadata (model, tokens, latency) not full prompts. If logging prompts for debugging, redact PII first.' },
680
+ check({ files }) {
681
+ const findings = [];
682
+ const logPrompt = /(?:console\.log|logger\.\w+|log\.(?:info|debug))\s*\(.*(?:prompt|messages|system.*content|user.*content)/i;
683
+ for (const [path, content] of files) {
684
+ if (SKIP_PATH.test(path)) continue;
685
+ if (!isJS(path) && !isPy(path)) continue;
686
+ if (!isAiApiFile(content)) continue;
687
+ findings.push(...scanLines(content, logPrompt, path, this));
688
+ }
689
+ return findings;
690
+ },
691
+ },
692
+
693
+ // SEC-AI-028: Storing AI responses with user data permanently
694
+ {
695
+ id: 'SEC-AI-028',
696
+ category: 'security',
697
+ severity: 'medium',
698
+ confidence: 'suggestion',
699
+ title: 'AI conversations stored without retention policy',
700
+ description: 'AI chat/completion history is stored indefinitely without a data retention or deletion policy.',
701
+ fix: { suggestion: 'Implement data retention policies for stored AI conversations. Add TTL/expiration and user deletion capabilities.' },
702
+ check({ files }) {
703
+ const findings = [];
704
+ const storeConversation = /(?:save|store|insert|create|write).*(?:conversation|chat.*history|messages|completion.*log)/i;
705
+ const hasRetention = /(?:ttl|expir|retention|delete.*after|cleanup|purge|rotate)/i;
706
+ for (const [path, content] of files) {
707
+ if (SKIP_PATH.test(path)) continue;
708
+ if (!isJS(path) && !isPy(path)) continue;
709
+ if (!isAiApiFile(content)) continue;
710
+ if (storeConversation.test(content) && !hasRetention.test(content)) {
711
+ findings.push(...scanLines(content, storeConversation, path, this));
712
+ }
713
+ }
714
+ return findings;
715
+ },
716
+ },
717
+
718
+ // SEC-AI-029: Temperature set to maximum
719
+ {
720
+ id: 'SEC-AI-029',
721
+ category: 'security',
722
+ severity: 'low',
723
+ confidence: 'likely',
724
+ title: 'AI temperature set to maximum',
725
+ description: 'AI API temperature set to 2.0 (maximum), producing highly random outputs that may be unreliable or unsafe.',
726
+ fix: { suggestion: 'Use lower temperature values (0.0-1.0) for most use cases. Reserve high temperatures for creative tasks only.' },
727
+ check({ files }) {
728
+ const findings = [];
729
+ const maxTemp = /temperature\s*[:=]\s*2(?:\.0)?(?:\s*[,}\n])/;
730
+ for (const [path, content] of files) {
731
+ if (SKIP_PATH.test(path)) continue;
732
+ if (!isJS(path) && !isPy(path)) continue;
733
+ if (!isAiApiFile(content)) continue;
734
+ findings.push(...scanLines(content, maxTemp, path, this));
735
+ }
736
+ return findings;
737
+ },
738
+ },
739
+
740
+ // SEC-AI-030: Using deprecated AI models
741
+ {
742
+ id: 'SEC-AI-030',
743
+ category: 'security',
744
+ severity: 'medium',
745
+ confidence: 'definite',
746
+ title: 'Using deprecated AI model',
747
+ description: 'Code references deprecated AI models that may be removed or lack security updates.',
748
+ fix: { suggestion: 'Upgrade to the latest supported model versions. Check provider documentation for deprecation schedules.' },
749
+ check({ files }) {
750
+ const findings = [];
751
+ const deprecatedModels = /model\s*[:=]\s*['"](?:text-davinci-003|text-davinci-002|code-davinci-002|code-cushman-001|text-ada-001|text-babbage-001|text-curie-001|davinci|curie|babbage|ada|gpt-3\.5-turbo-0301|gpt-4-0314)['"]/;
752
+ for (const [path, content] of files) {
753
+ if (SKIP_PATH.test(path)) continue;
754
+ if (!isJS(path) && !isPy(path)) continue;
755
+ findings.push(...scanLines(content, deprecatedModels, path, this));
756
+ }
757
+ return findings;
758
+ },
759
+ },
760
+
761
+ // SEC-AI-031: AI output used in code execution
762
+ {
763
+ id: 'SEC-AI-031',
764
+ category: 'security',
765
+ severity: 'critical',
766
+ confidence: 'likely',
767
+ title: 'AI output used in code execution',
768
+ description: 'AI API response is passed to eval(), exec(), or code execution functions, enabling arbitrary code execution.',
769
+ fix: { suggestion: 'Never execute AI-generated code directly. Use sandboxed environments (VM2, Docker) if code execution is needed.' },
770
+ check({ files }) {
771
+ const findings = [];
772
+ const aiToExec = /(?:eval|exec|execSync|Function|vm\.run)\s*\(.*(?:response|completion|result|generated|aiOutput|output)/i;
773
+ for (const [path, content] of files) {
774
+ if (SKIP_PATH.test(path)) continue;
775
+ if (!isJS(path) && !isPy(path)) continue;
776
+ findings.push(...scanLines(content, aiToExec, path, this));
777
+ }
778
+ return findings;
779
+ },
780
+ },
781
+
782
+ // SEC-AI-032: AI output used in SQL queries
783
+ {
784
+ id: 'SEC-AI-032',
785
+ category: 'security',
786
+ severity: 'critical',
787
+ confidence: 'likely',
788
+ title: 'AI output used in SQL query construction',
789
+ description: 'AI API response is interpolated into SQL queries, enabling SQL injection via AI-generated content.',
790
+ fix: { suggestion: 'Never interpolate AI output into SQL. Use parameterized queries even for AI-generated query parts.' },
791
+ check({ files }) {
792
+ const findings = [];
793
+ const aiToSql = /(?:query|execute|sql)\s*\(.*(?:response|completion|result|generated|aiOutput)/i;
794
+ for (const [path, content] of files) {
795
+ if (SKIP_PATH.test(path)) continue;
796
+ if (!isJS(path) && !isPy(path)) continue;
797
+ if (!isAiApiFile(content)) continue;
798
+ findings.push(...scanLines(content, aiToSql, path, this));
799
+ }
800
+ return findings;
801
+ },
802
+ },
803
+
804
+ // SEC-AI-033: Embedding API key in mobile app
805
+ {
806
+ id: 'SEC-AI-033',
807
+ category: 'security',
808
+ severity: 'critical',
809
+ confidence: 'likely',
810
+ title: 'AI API key embedded in mobile application',
811
+ description: 'AI API key found in mobile app code (React Native, Flutter, Swift, Kotlin) where it can be extracted.',
812
+ fix: { suggestion: 'Route all AI API calls through your backend server. Never embed API keys in mobile apps.' },
813
+ check({ files }) {
814
+ const findings = [];
815
+ const mobileApiKey = /(?:apiKey|api_key|OPENAI_API_KEY|ANTHROPIC_API_KEY)\s*[:=]\s*['"][A-Za-z0-9_-]{20,}['"]/i;
816
+ for (const [path, content] of files) {
817
+ if (SKIP_PATH.test(path)) continue;
818
+ if (!/(?:\.dart|\.swift|\.kt|\.java)$/.test(path) && !/(?:react-native|expo|capacitor|ionic)/i.test(content)) continue;
819
+ findings.push(...scanLines(content, mobileApiKey, path, this));
820
+ }
821
+ return findings;
822
+ },
823
+ },
824
+
825
+ // SEC-AI-034: Missing input length limits for AI prompts
826
+ {
827
+ id: 'SEC-AI-034',
828
+ category: 'security',
829
+ severity: 'medium',
830
+ confidence: 'likely',
831
+ title: 'No input length limit for AI API prompts',
832
+ description: 'User input sent to AI API is not length-limited, enabling cost attacks via extremely long prompts.',
833
+ fix: { suggestion: 'Truncate or reject user input exceeding a reasonable length before sending to AI APIs.' },
834
+ check({ files }) {
835
+ const findings = [];
836
+ const userToAI = /(?:content|prompt|message)\s*[:=]\s*(?:req\.body|userInput|input|query|message)/i;
837
+ const hasLengthCheck = /(?:\.length|\.trim\(\)|maxLength|max_length|truncat|slice\(|substring\(|limit)/i;
838
+ for (const [path, content] of files) {
839
+ if (SKIP_PATH.test(path)) continue;
840
+ if (!isJS(path) && !isPy(path)) continue;
841
+ if (!isAiApiFile(content)) continue;
842
+ const lines = content.split('\n');
843
+ for (let i = 0; i < lines.length; i++) {
844
+ if (COMMENT_LINE.test(lines[i])) continue;
845
+ if (userToAI.test(lines[i])) {
846
+ const block = lines.slice(Math.max(0, i - 10), Math.min(i + 5, lines.length)).join('\n');
847
+ if (!hasLengthCheck.test(block)) {
848
+ findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
849
+ }
850
+ }
851
+ }
852
+ }
853
+ return findings;
854
+ },
855
+ },
856
+
857
+ // SEC-AI-035: Caching AI responses with sensitive data
858
+ {
859
+ id: 'SEC-AI-035',
860
+ category: 'security',
861
+ severity: 'medium',
862
+ confidence: 'likely',
863
+ title: 'AI responses cached without considering sensitivity',
864
+ description: 'AI API responses are cached without checking if they contain sensitive or user-specific data.',
865
+ fix: { suggestion: 'Do not cache AI responses that contain user-specific or sensitive data. Use cache keys that include user context.' },
866
+ check({ files }) {
867
+ const findings = [];
868
+ const cacheAI = /(?:cache|redis|memcache|localStorage|sessionStorage).*(?:set|put|store|save)\s*\(.*(?:response|completion|result|aiOutput)/i;
869
+ for (const [path, content] of files) {
870
+ if (SKIP_PATH.test(path)) continue;
871
+ if (!isJS(path) && !isPy(path)) continue;
872
+ if (!isAiApiFile(content)) continue;
873
+ findings.push(...scanLines(content, cacheAI, path, this));
874
+ }
875
+ return findings;
876
+ },
877
+ },
878
+
879
+ // SEC-AI-036: Multiple AI providers without consistent security
880
+ {
881
+ id: 'SEC-AI-036',
882
+ category: 'security',
883
+ severity: 'medium',
884
+ confidence: 'suggestion',
885
+ title: 'Multiple AI providers without unified security controls',
886
+ description: 'Code uses multiple AI providers without a unified abstraction layer for consistent security controls.',
887
+ fix: { suggestion: 'Create a unified AI client wrapper that enforces consistent security controls (auth, logging, validation) across all providers.' },
888
+ check({ files }) {
889
+ const findings = [];
890
+ const providers = new Set();
891
+ for (const [path, content] of files) {
892
+ if (SKIP_PATH.test(path)) continue;
893
+ if (!isJS(path) && !isPy(path)) continue;
894
+ if (/(?:openai|OpenAI)\s*\(/.test(content)) providers.add('openai');
895
+ if (/(?:anthropic|Anthropic)\s*\(/.test(content)) providers.add('anthropic');
896
+ if (/(?:GoogleGenerativeAI|google.*generative)\s*\(/.test(content)) providers.add('google');
897
+ if (/(?:cohere|Cohere)\s*\(/.test(content)) providers.add('cohere');
898
+ if (/(?:mistral|Mistral)\s*\(/.test(content)) providers.add('mistral');
899
+ }
900
+ if (providers.size >= 2) {
901
+ for (const [path, content] of files) {
902
+ if (SKIP_PATH.test(path)) continue;
903
+ if (!isJS(path) && !isPy(path)) continue;
904
+ const initPattern = /(?:new\s+(?:OpenAI|Anthropic|GoogleGenerativeAI|Cohere|Mistral))\s*\(/;
905
+ if (initPattern.test(content)) {
906
+ findings.push(...scanLines(content, initPattern, path, this));
907
+ }
908
+ }
909
+ }
910
+ return findings;
911
+ },
912
+ },
913
+
914
+ // SEC-AI-037: Embedding model output in templates without escaping
915
+ {
916
+ id: 'SEC-AI-037',
917
+ category: 'security',
918
+ severity: 'high',
919
+ confidence: 'likely',
920
+ title: 'AI output embedded in template without escaping',
921
+ description: 'AI-generated content is injected into templates (EJS, Handlebars, Jinja) without escaping, enabling XSS or injection.',
922
+ fix: { suggestion: 'Always escape AI output when embedding in templates. Use auto-escaping template engines.' },
923
+ check({ files }) {
924
+ const findings = [];
925
+ const unescapedTemplate = /(?:<%[-=]\s*|{{{|<%!\s*|{%\s*raw).*(?:response|completion|aiOutput|generated)/i;
926
+ for (const [path, content] of files) {
927
+ if (SKIP_PATH.test(path)) continue;
928
+ findings.push(...scanLines(content, unescapedTemplate, path, this));
929
+ }
930
+ return findings;
931
+ },
932
+ },
933
+
934
+ // SEC-AI-038: AI API called in a loop without limits
935
+ {
936
+ id: 'SEC-AI-038',
937
+ category: 'security',
938
+ severity: 'high',
939
+ confidence: 'likely',
940
+ title: 'AI API called in unbounded loop',
941
+ description: 'AI API is called inside a loop or recursive function without iteration limits, risking runaway costs.',
942
+ fix: { suggestion: 'Add maximum iteration limits when calling AI APIs in loops. Implement circuit breakers for repeated calls.' },
943
+ check({ files }) {
944
+ const findings = [];
945
+ const aiInLoop = /(?:while\s*\(true|for\s*\(;\s*;|while\s*\(1\)|\.forEach|\.map)\s*(?:\{|=>).*(?:chat\.completions|messages\.create|generate_content)/i;
946
+ for (const [path, content] of files) {
947
+ if (SKIP_PATH.test(path)) continue;
948
+ if (!isJS(path) && !isPy(path)) continue;
949
+ if (!isAiApiFile(content)) continue;
950
+ findings.push(...scanLines(content, aiInLoop, path, this));
951
+ }
952
+ return findings;
953
+ },
954
+ },
955
+
956
+ // SEC-AI-039: Hardcoded AI API base URL
957
+ {
958
+ id: 'SEC-AI-039',
959
+ category: 'security',
960
+ severity: 'medium',
961
+ confidence: 'likely',
962
+ title: 'Hardcoded AI API base URL',
963
+ description: 'AI API base URL is hardcoded, preventing easy rotation if the endpoint is compromised or needs to change.',
964
+ fix: { suggestion: 'Use environment variables for AI API base URLs to enable easy rotation and proxy configuration.' },
965
+ check({ files }) {
966
+ const findings = [];
967
+ const hardcodedUrl = /(?:baseURL|base_url|apiBase|api_base)\s*[:=]\s*['"]https?:\/\/api\.(?:openai|anthropic|cohere|mistral)\.com/i;
968
+ for (const [path, content] of files) {
969
+ if (SKIP_PATH.test(path)) continue;
970
+ if (!isJS(path) && !isPy(path)) continue;
971
+ findings.push(...scanLines(content, hardcodedUrl, path, this));
972
+ }
973
+ return findings;
974
+ },
975
+ },
976
+
977
+ // SEC-AI-040: AI API key shared across environments
978
+ {
979
+ id: 'SEC-AI-040',
980
+ category: 'security',
981
+ severity: 'high',
982
+ confidence: 'likely',
983
+ title: 'Same AI API key used across environments',
984
+ description: 'Same API key is used in development, staging, and production environments, increasing blast radius if compromised.',
985
+ fix: { suggestion: 'Use separate API keys per environment. Implement key rotation policies.' },
986
+ check({ files }) {
987
+ const findings = [];
988
+ const sameKeyAcrossEnv = /(?:OPENAI_API_KEY|ANTHROPIC_API_KEY)\s*[:=]\s*(?:process\.env\.(?!NODE_ENV)\w+\s*\|\|\s*['"](?:sk-|sk-ant-))/i;
989
+ for (const [path, content] of files) {
990
+ if (SKIP_PATH.test(path)) continue;
991
+ if (!isJS(path) && !isPy(path)) continue;
992
+ findings.push(...scanLines(content, sameKeyAcrossEnv, path, this));
993
+ }
994
+ return findings;
995
+ },
996
+ },
997
+ // SEC-AI-041: AI response used in eval
998
+ {
999
+ id: 'SEC-AI-041', category: 'security', severity: 'high', confidence: 'likely',
1000
+ title: 'AI response used in eval',
1001
+ description: 'AI model response is passed to eval(), allowing arbitrary code execution from AI output.',
1002
+ fix: { suggestion: 'Never eval() AI responses. Parse structured output with JSON.parse() and validate.' },
1003
+ check({ files }) {
1004
+ const findings = [];
1005
+ for (const [path, content] of files) {
1006
+ if (SKIP_PATH.test(path)) continue;
1007
+ if (!isJS(path)) continue;
1008
+ if (!isAiApiFile(content)) continue;
1009
+ findings.push(...scanLines(content, /eval\(\s*(?:response|completion|result)\./, path, this));
1010
+ }
1011
+ return findings;
1012
+ },
1013
+ },
1014
+ // SEC-AI-042: AI response in innerHTML
1015
+ {
1016
+ id: 'SEC-AI-042', category: 'security', severity: 'high', confidence: 'likely',
1017
+ title: 'AI response inserted into DOM unsanitized',
1018
+ description: 'AI model output is assigned to innerHTML without sanitization, enabling XSS.',
1019
+ fix: { suggestion: 'Sanitize AI output with DOMPurify before inserting into DOM. Use textContent for plain text.' },
1020
+ check({ files }) {
1021
+ const findings = [];
1022
+ for (const [path, content] of files) {
1023
+ if (SKIP_PATH.test(path)) continue;
1024
+ if (!isJS(path)) continue;
1025
+ findings.push(...scanLines(content, /innerHTML\s*=\s*(?:response|completion|result)\./, path, this));
1026
+ }
1027
+ return findings;
1028
+ },
1029
+ },
1030
+ // SEC-AI-043: AI response in SQL
1031
+ {
1032
+ id: 'SEC-AI-043', category: 'security', severity: 'medium', confidence: 'suggestion',
1033
+ title: 'AI response used in SQL query',
1034
+ description: 'AI model output is interpolated into SQL queries, risking SQL injection from AI hallucinations.',
1035
+ fix: { suggestion: 'Use parameterized queries. Never interpolate AI output directly into SQL strings.' },
1036
+ check({ files }) {
1037
+ const findings = [];
1038
+ for (const [path, content] of files) {
1039
+ if (SKIP_PATH.test(path)) continue;
1040
+ if (!isJS(path) && !isPy(path)) continue;
1041
+ findings.push(...scanLines(content, /(?:query|execute)\(\s*`[^`]*\$\{(?:response|completion|result)\./, path, this));
1042
+ }
1043
+ return findings;
1044
+ },
1045
+ },
1046
+ // SEC-AI-044: Missing content filter
1047
+ {
1048
+ id: 'SEC-AI-044', category: 'security', severity: 'medium', confidence: 'suggestion',
1049
+ title: 'Missing AI output content filter',
1050
+ description: 'AI response content accessed directly without filtering for harmful or inappropriate content.',
1051
+ fix: { suggestion: 'Add content filtering/moderation before displaying AI output to users.' },
1052
+ check({ files }) {
1053
+ const findings = [];
1054
+ const pattern = /(?:choices|completions)\[0\]\.(?:message|text)/;
1055
+ const hasFilter = /(?:moderate|filter|safety|content_filter|moderation)/i;
1056
+ for (const [path, content] of files) {
1057
+ if (SKIP_PATH.test(path)) continue;
1058
+ if (!isJS(path) && !isPy(path)) continue;
1059
+ if (!isAiApiFile(content)) continue;
1060
+ if (pattern.test(content) && !hasFilter.test(content)) {
1061
+ findings.push(...scanLines(content, pattern, path, this));
1062
+ }
1063
+ }
1064
+ return findings;
1065
+ },
1066
+ },
1067
+ // SEC-AI-045: Streaming without timeout
1068
+ {
1069
+ id: 'SEC-AI-045', category: 'security', severity: 'medium', confidence: 'likely',
1070
+ title: 'AI streaming response without timeout',
1071
+ description: 'AI streaming enabled without timeout, risking resource exhaustion from long-running streams.',
1072
+ fix: { suggestion: 'Set a timeout for streaming AI responses. Use AbortController or socket timeout.' },
1073
+ check({ files }) {
1074
+ const findings = [];
1075
+ for (const [path, content] of files) {
1076
+ if (SKIP_PATH.test(path)) continue;
1077
+ if (!isJS(path) && !isPy(path)) continue;
1078
+ if (!isAiApiFile(content)) continue;
1079
+ findings.push(...scanLines(content, /stream:\s*true/, path, this));
1080
+ }
1081
+ return findings;
1082
+ },
1083
+ },
1084
+ // SEC-AI-046: Hardcoded endpoint
1085
+ {
1086
+ id: 'SEC-AI-046', category: 'security', severity: 'high', confidence: 'likely',
1087
+ title: 'Hardcoded AI model endpoint',
1088
+ description: 'AI API base URL is hardcoded instead of using environment variables.',
1089
+ fix: { suggestion: 'Use process.env for API endpoints. Hardcoded URLs leak infrastructure details.' },
1090
+ check({ files }) {
1091
+ const findings = [];
1092
+ for (const [path, content] of files) {
1093
+ if (SKIP_PATH.test(path)) continue;
1094
+ if (!isJS(path) && !isPy(path)) continue;
1095
+ if (!isAiApiFile(content)) continue;
1096
+ findings.push(...scanLines(content, /(?:baseURL|endpoint|api_base)\s*[:=]\s*['"]https?:\/\//, path, this));
1097
+ }
1098
+ return findings;
1099
+ },
1100
+ },
1101
+ // SEC-AI-047: Function calling without validation
1102
+ {
1103
+ id: 'SEC-AI-047', category: 'security', severity: 'medium', confidence: 'suggestion',
1104
+ title: 'AI function calling without argument validation',
1105
+ description: 'AI function call arguments are used without validation before execution.',
1106
+ fix: { suggestion: 'Validate AI function call arguments against a schema before executing functions.' },
1107
+ check({ files }) {
1108
+ const findings = [];
1109
+ const fnCallPattern = /function_call.*(?:name|arguments)/;
1110
+ const hasValidation = /(?:validate|schema|zod|joi|yup|ajv)/i;
1111
+ for (const [path, content] of files) {
1112
+ if (SKIP_PATH.test(path)) continue;
1113
+ if (!isJS(path) && !isPy(path)) continue;
1114
+ if (!isAiApiFile(content)) continue;
1115
+ if (fnCallPattern.test(content) && !hasValidation.test(content)) {
1116
+ findings.push(...scanLines(content, fnCallPattern, path, this));
1117
+ }
1118
+ }
1119
+ return findings;
1120
+ },
1121
+ },
1122
+ // SEC-AI-048: Tool use without permission
1123
+ {
1124
+ id: 'SEC-AI-048', category: 'security', severity: 'low', confidence: 'suggestion',
1125
+ title: 'AI tool use without permission check',
1126
+ description: 'AI tools are configured without verifying user permissions for each tool.',
1127
+ fix: { suggestion: 'Check user permissions before allowing AI tool execution. Use allowlists per user role.' },
1128
+ check({ files }) {
1129
+ const findings = [];
1130
+ const toolPattern = /tools?\s*:\s*\[/;
1131
+ const hasPermCheck = /(?:permission|authorize|allowed|canUse|checkAccess)/i;
1132
+ for (const [path, content] of files) {
1133
+ if (SKIP_PATH.test(path)) continue;
1134
+ if (!isJS(path) && !isPy(path)) continue;
1135
+ if (!isAiApiFile(content)) continue;
1136
+ if (toolPattern.test(content) && !hasPermCheck.test(content)) {
1137
+ findings.push(...scanLines(content, toolPattern, path, this));
1138
+ }
1139
+ }
1140
+ return findings;
1141
+ },
1142
+ },
1143
+ // SEC-AI-049: Embedding without encryption
1144
+ {
1145
+ id: 'SEC-AI-049', category: 'security', severity: 'medium', confidence: 'likely',
1146
+ title: 'AI embedding stored without encryption',
1147
+ description: 'Vector embeddings are stored without encryption, potentially leaking semantic content.',
1148
+ fix: { suggestion: 'Encrypt embeddings at rest. Embeddings can be reversed to approximate original content.' },
1149
+ check({ files }) {
1150
+ const findings = [];
1151
+ for (const [path, content] of files) {
1152
+ if (SKIP_PATH.test(path)) continue;
1153
+ if (!isJS(path) && !isPy(path)) continue;
1154
+ if (!isAiApiFile(content)) continue;
1155
+ findings.push(...scanLines(content, /embedding.*(?:insert|save|store|push)/, path, this));
1156
+ }
1157
+ return findings;
1158
+ },
1159
+ },
1160
+ // SEC-AI-050: Vendor lock-in
1161
+ {
1162
+ id: 'SEC-AI-050', category: 'security', severity: 'low', confidence: 'suggestion',
1163
+ title: 'AI vendor lock-in without abstraction layer',
1164
+ description: 'Direct AI vendor SDK calls without abstraction make it hard to switch providers or add fallbacks.',
1165
+ fix: { suggestion: 'Create an abstraction layer for AI calls. This enables provider switching, fallbacks, and centralized security controls.' },
1166
+ check({ files }) {
1167
+ const findings = [];
1168
+ for (const [path, content] of files) {
1169
+ if (SKIP_PATH.test(path)) continue;
1170
+ if (!isJS(path) && !isPy(path)) continue;
1171
+ findings.push(...scanLines(content, /(?:openai|anthropic|cohere)\.(?:chat|complete|generate)/, path, this));
1172
+ }
1173
+ return findings;
1174
+ },
1175
+ },
1176
+ ];
1177
+ export default rules;