@quantracode/vibecheck 0.0.1 → 0.0.2

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 (208) hide show
  1. package/README.md +6 -6
  2. package/dist/index.d.ts +0 -2
  3. package/dist/index.js +7902 -8
  4. package/package.json +13 -7
  5. package/dist/__tests__/cli.test.d.ts +0 -2
  6. package/dist/__tests__/cli.test.d.ts.map +0 -1
  7. package/dist/__tests__/cli.test.js +0 -243
  8. package/dist/__tests__/fixtures/safe-app/app/api/users/route.js +0 -36
  9. package/dist/__tests__/fixtures/vulnerable-app/app/api/users/route.js +0 -28
  10. package/dist/__tests__/fixtures/vulnerable-app/lib/config.d.ts +0 -4
  11. package/dist/__tests__/fixtures/vulnerable-app/lib/config.d.ts.map +0 -1
  12. package/dist/__tests__/fixtures/vulnerable-app/lib/config.js +0 -6
  13. package/dist/__tests__/scanners/env-config.test.d.ts +0 -2
  14. package/dist/__tests__/scanners/env-config.test.d.ts.map +0 -1
  15. package/dist/__tests__/scanners/env-config.test.js +0 -142
  16. package/dist/__tests__/scanners/nextjs-middleware.test.d.ts +0 -2
  17. package/dist/__tests__/scanners/nextjs-middleware.test.d.ts.map +0 -1
  18. package/dist/__tests__/scanners/nextjs-middleware.test.js +0 -193
  19. package/dist/__tests__/scanners/scanner-packs.test.d.ts +0 -2
  20. package/dist/__tests__/scanners/scanner-packs.test.d.ts.map +0 -1
  21. package/dist/__tests__/scanners/scanner-packs.test.js +0 -126
  22. package/dist/__tests__/scanners/unused-security-imports.test.d.ts +0 -2
  23. package/dist/__tests__/scanners/unused-security-imports.test.d.ts.map +0 -1
  24. package/dist/__tests__/scanners/unused-security-imports.test.js +0 -145
  25. package/dist/commands/demo-artifact.d.ts +0 -7
  26. package/dist/commands/demo-artifact.d.ts.map +0 -1
  27. package/dist/commands/demo-artifact.js +0 -322
  28. package/dist/commands/evaluate.d.ts +0 -30
  29. package/dist/commands/evaluate.d.ts.map +0 -1
  30. package/dist/commands/evaluate.js +0 -258
  31. package/dist/commands/explain.d.ts +0 -12
  32. package/dist/commands/explain.d.ts.map +0 -1
  33. package/dist/commands/explain.js +0 -214
  34. package/dist/commands/index.d.ts +0 -7
  35. package/dist/commands/index.d.ts.map +0 -1
  36. package/dist/commands/index.js +0 -6
  37. package/dist/commands/intent.d.ts +0 -21
  38. package/dist/commands/intent.d.ts.map +0 -1
  39. package/dist/commands/intent.js +0 -192
  40. package/dist/commands/scan.d.ts +0 -44
  41. package/dist/commands/scan.d.ts.map +0 -1
  42. package/dist/commands/scan.js +0 -497
  43. package/dist/commands/waivers.d.ts +0 -30
  44. package/dist/commands/waivers.d.ts.map +0 -1
  45. package/dist/commands/waivers.js +0 -249
  46. package/dist/index.d.ts.map +0 -1
  47. package/dist/phase3/index.d.ts +0 -11
  48. package/dist/phase3/index.d.ts.map +0 -1
  49. package/dist/phase3/index.js +0 -12
  50. package/dist/phase3/intent-miner.d.ts +0 -32
  51. package/dist/phase3/intent-miner.d.ts.map +0 -1
  52. package/dist/phase3/intent-miner.js +0 -323
  53. package/dist/phase3/proof-trace-builder.d.ts +0 -42
  54. package/dist/phase3/proof-trace-builder.d.ts.map +0 -1
  55. package/dist/phase3/proof-trace-builder.js +0 -441
  56. package/dist/phase3/scanners/auth-by-ui-server-gap.d.ts +0 -15
  57. package/dist/phase3/scanners/auth-by-ui-server-gap.d.ts.map +0 -1
  58. package/dist/phase3/scanners/auth-by-ui-server-gap.js +0 -237
  59. package/dist/phase3/scanners/comment-claim-unproven.d.ts +0 -14
  60. package/dist/phase3/scanners/comment-claim-unproven.d.ts.map +0 -1
  61. package/dist/phase3/scanners/comment-claim-unproven.js +0 -161
  62. package/dist/phase3/scanners/index.d.ts +0 -31
  63. package/dist/phase3/scanners/index.d.ts.map +0 -1
  64. package/dist/phase3/scanners/index.js +0 -40
  65. package/dist/phase3/scanners/middleware-assumed-not-matching.d.ts +0 -14
  66. package/dist/phase3/scanners/middleware-assumed-not-matching.d.ts.map +0 -1
  67. package/dist/phase3/scanners/middleware-assumed-not-matching.js +0 -172
  68. package/dist/phase3/scanners/validation-claimed-missing.d.ts +0 -15
  69. package/dist/phase3/scanners/validation-claimed-missing.d.ts.map +0 -1
  70. package/dist/phase3/scanners/validation-claimed-missing.js +0 -204
  71. package/dist/scanners/abuse/compute-abuse.d.ts +0 -20
  72. package/dist/scanners/abuse/compute-abuse.d.ts.map +0 -1
  73. package/dist/scanners/abuse/compute-abuse.js +0 -509
  74. package/dist/scanners/abuse/index.d.ts +0 -12
  75. package/dist/scanners/abuse/index.d.ts.map +0 -1
  76. package/dist/scanners/abuse/index.js +0 -15
  77. package/dist/scanners/auth/index.d.ts +0 -5
  78. package/dist/scanners/auth/index.d.ts.map +0 -1
  79. package/dist/scanners/auth/index.js +0 -10
  80. package/dist/scanners/auth/middleware-gap.d.ts +0 -22
  81. package/dist/scanners/auth/middleware-gap.d.ts.map +0 -1
  82. package/dist/scanners/auth/middleware-gap.js +0 -203
  83. package/dist/scanners/auth/unprotected-api-route.d.ts +0 -12
  84. package/dist/scanners/auth/unprotected-api-route.d.ts.map +0 -1
  85. package/dist/scanners/auth/unprotected-api-route.js +0 -126
  86. package/dist/scanners/config/index.d.ts +0 -5
  87. package/dist/scanners/config/index.d.ts.map +0 -1
  88. package/dist/scanners/config/index.js +0 -10
  89. package/dist/scanners/config/insecure-defaults.d.ts +0 -12
  90. package/dist/scanners/config/insecure-defaults.d.ts.map +0 -1
  91. package/dist/scanners/config/insecure-defaults.js +0 -77
  92. package/dist/scanners/config/undocumented-env.d.ts +0 -24
  93. package/dist/scanners/config/undocumented-env.d.ts.map +0 -1
  94. package/dist/scanners/config/undocumented-env.js +0 -159
  95. package/dist/scanners/crypto/index.d.ts +0 -6
  96. package/dist/scanners/crypto/index.d.ts.map +0 -1
  97. package/dist/scanners/crypto/index.js +0 -11
  98. package/dist/scanners/crypto/jwt-decode-unverified.d.ts +0 -14
  99. package/dist/scanners/crypto/jwt-decode-unverified.d.ts.map +0 -1
  100. package/dist/scanners/crypto/jwt-decode-unverified.js +0 -87
  101. package/dist/scanners/crypto/math-random-tokens.d.ts +0 -13
  102. package/dist/scanners/crypto/math-random-tokens.d.ts.map +0 -1
  103. package/dist/scanners/crypto/math-random-tokens.js +0 -80
  104. package/dist/scanners/crypto/weak-hashing.d.ts +0 -11
  105. package/dist/scanners/crypto/weak-hashing.d.ts.map +0 -1
  106. package/dist/scanners/crypto/weak-hashing.js +0 -95
  107. package/dist/scanners/env-config.d.ts +0 -24
  108. package/dist/scanners/env-config.d.ts.map +0 -1
  109. package/dist/scanners/env-config.js +0 -164
  110. package/dist/scanners/hallucinations/index.d.ts +0 -4
  111. package/dist/scanners/hallucinations/index.d.ts.map +0 -1
  112. package/dist/scanners/hallucinations/index.js +0 -8
  113. package/dist/scanners/hallucinations/unused-security-imports.d.ts +0 -36
  114. package/dist/scanners/hallucinations/unused-security-imports.d.ts.map +0 -1
  115. package/dist/scanners/hallucinations/unused-security-imports.js +0 -309
  116. package/dist/scanners/helpers/ast-helpers.d.ts +0 -6
  117. package/dist/scanners/helpers/ast-helpers.d.ts.map +0 -1
  118. package/dist/scanners/helpers/ast-helpers.js +0 -945
  119. package/dist/scanners/helpers/context-builder.d.ts +0 -17
  120. package/dist/scanners/helpers/context-builder.d.ts.map +0 -1
  121. package/dist/scanners/helpers/context-builder.js +0 -148
  122. package/dist/scanners/helpers/index.d.ts +0 -3
  123. package/dist/scanners/helpers/index.d.ts.map +0 -1
  124. package/dist/scanners/helpers/index.js +0 -2
  125. package/dist/scanners/index.d.ts +0 -30
  126. package/dist/scanners/index.d.ts.map +0 -1
  127. package/dist/scanners/index.js +0 -102
  128. package/dist/scanners/middleware/index.d.ts +0 -4
  129. package/dist/scanners/middleware/index.d.ts.map +0 -1
  130. package/dist/scanners/middleware/index.js +0 -7
  131. package/dist/scanners/middleware/missing-rate-limit.d.ts +0 -13
  132. package/dist/scanners/middleware/missing-rate-limit.d.ts.map +0 -1
  133. package/dist/scanners/middleware/missing-rate-limit.js +0 -140
  134. package/dist/scanners/network/cors-misconfiguration.d.ts +0 -14
  135. package/dist/scanners/network/cors-misconfiguration.d.ts.map +0 -1
  136. package/dist/scanners/network/cors-misconfiguration.js +0 -89
  137. package/dist/scanners/network/index.d.ts +0 -7
  138. package/dist/scanners/network/index.d.ts.map +0 -1
  139. package/dist/scanners/network/index.js +0 -18
  140. package/dist/scanners/network/missing-timeout.d.ts +0 -15
  141. package/dist/scanners/network/missing-timeout.d.ts.map +0 -1
  142. package/dist/scanners/network/missing-timeout.js +0 -93
  143. package/dist/scanners/network/open-redirect.d.ts +0 -15
  144. package/dist/scanners/network/open-redirect.d.ts.map +0 -1
  145. package/dist/scanners/network/open-redirect.js +0 -88
  146. package/dist/scanners/network/ssrf-prone-fetch.d.ts +0 -12
  147. package/dist/scanners/network/ssrf-prone-fetch.d.ts.map +0 -1
  148. package/dist/scanners/network/ssrf-prone-fetch.js +0 -90
  149. package/dist/scanners/nextjs-middleware.d.ts +0 -26
  150. package/dist/scanners/nextjs-middleware.d.ts.map +0 -1
  151. package/dist/scanners/nextjs-middleware.js +0 -246
  152. package/dist/scanners/privacy/debug-flags.d.ts +0 -13
  153. package/dist/scanners/privacy/debug-flags.d.ts.map +0 -1
  154. package/dist/scanners/privacy/debug-flags.js +0 -124
  155. package/dist/scanners/privacy/index.d.ts +0 -6
  156. package/dist/scanners/privacy/index.d.ts.map +0 -1
  157. package/dist/scanners/privacy/index.js +0 -11
  158. package/dist/scanners/privacy/over-broad-response.d.ts +0 -15
  159. package/dist/scanners/privacy/over-broad-response.d.ts.map +0 -1
  160. package/dist/scanners/privacy/over-broad-response.js +0 -109
  161. package/dist/scanners/privacy/sensitive-logging.d.ts +0 -11
  162. package/dist/scanners/privacy/sensitive-logging.d.ts.map +0 -1
  163. package/dist/scanners/privacy/sensitive-logging.js +0 -78
  164. package/dist/scanners/types.d.ts +0 -456
  165. package/dist/scanners/types.d.ts.map +0 -1
  166. package/dist/scanners/types.js +0 -16
  167. package/dist/scanners/unused-security-imports.d.ts +0 -34
  168. package/dist/scanners/unused-security-imports.d.ts.map +0 -1
  169. package/dist/scanners/unused-security-imports.js +0 -206
  170. package/dist/scanners/uploads/index.d.ts +0 -5
  171. package/dist/scanners/uploads/index.d.ts.map +0 -1
  172. package/dist/scanners/uploads/index.js +0 -9
  173. package/dist/scanners/uploads/missing-constraints.d.ts +0 -15
  174. package/dist/scanners/uploads/missing-constraints.d.ts.map +0 -1
  175. package/dist/scanners/uploads/missing-constraints.js +0 -109
  176. package/dist/scanners/uploads/public-path.d.ts +0 -11
  177. package/dist/scanners/uploads/public-path.d.ts.map +0 -1
  178. package/dist/scanners/uploads/public-path.js +0 -87
  179. package/dist/scanners/validation/client-side-only.d.ts +0 -14
  180. package/dist/scanners/validation/client-side-only.d.ts.map +0 -1
  181. package/dist/scanners/validation/client-side-only.js +0 -140
  182. package/dist/scanners/validation/ignored-validation.d.ts +0 -12
  183. package/dist/scanners/validation/ignored-validation.d.ts.map +0 -1
  184. package/dist/scanners/validation/ignored-validation.js +0 -119
  185. package/dist/scanners/validation/index.d.ts +0 -5
  186. package/dist/scanners/validation/index.d.ts.map +0 -1
  187. package/dist/scanners/validation/index.js +0 -9
  188. package/dist/utils/exclude-patterns.d.ts +0 -35
  189. package/dist/utils/exclude-patterns.d.ts.map +0 -1
  190. package/dist/utils/exclude-patterns.js +0 -78
  191. package/dist/utils/file-utils.d.ts +0 -37
  192. package/dist/utils/file-utils.d.ts.map +0 -1
  193. package/dist/utils/file-utils.js +0 -77
  194. package/dist/utils/fingerprint.d.ts +0 -25
  195. package/dist/utils/fingerprint.d.ts.map +0 -1
  196. package/dist/utils/fingerprint.js +0 -28
  197. package/dist/utils/git-info.d.ts +0 -14
  198. package/dist/utils/git-info.d.ts.map +0 -1
  199. package/dist/utils/git-info.js +0 -55
  200. package/dist/utils/index.d.ts +0 -4
  201. package/dist/utils/index.d.ts.map +0 -1
  202. package/dist/utils/index.js +0 -3
  203. package/dist/utils/progress.d.ts +0 -42
  204. package/dist/utils/progress.d.ts.map +0 -1
  205. package/dist/utils/progress.js +0 -165
  206. package/dist/utils/sarif-formatter.d.ts +0 -92
  207. package/dist/utils/sarif-formatter.d.ts.map +0 -1
  208. package/dist/utils/sarif-formatter.js +0 -172
@@ -1,945 +0,0 @@
1
- import { Project, Node, SyntaxKind } from "ts-morph";
2
- const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
3
- /**
4
- * Auth check function/method names to look for
5
- */
6
- const AUTH_CHECK_PATTERNS = [
7
- "getServerSession",
8
- "getSession",
9
- "auth",
10
- "requireAuth",
11
- "withAuth",
12
- "verifyJwt",
13
- "verifyToken",
14
- "authenticate",
15
- "isAuthenticated",
16
- "checkAuth",
17
- "validateSession",
18
- "getToken",
19
- "verifyAuth",
20
- ];
21
- /**
22
- * Prisma operations that are dangerous without auth
23
- */
24
- const PRISMA_WRITE_OPS = [
25
- "create",
26
- "createMany",
27
- "update",
28
- "updateMany",
29
- "upsert",
30
- "delete",
31
- "deleteMany",
32
- ];
33
- const CRITICAL_OPS = ["delete", "deleteMany"];
34
- /**
35
- * Sensitive variable name patterns
36
- */
37
- const SENSITIVE_VAR_PATTERNS = /^(password|token|auth|authorization|cookie|session|secret|apikey|api_key|private|credential|bearer)/i;
38
- /**
39
- * Critical env var patterns for insecure defaults
40
- */
41
- const CRITICAL_ENV_PATTERNS = /JWT|SESSION|NEXTAUTH|SECRET|AUTH|TOKEN|KEY|PASSWORD/i;
42
- /**
43
- * Create AST helpers with a ts-morph project
44
- */
45
- export function createAstHelpers(repoRoot, totalFiles, onFileProgress) {
46
- const project = new Project({
47
- skipAddingFilesFromTsConfig: true,
48
- compilerOptions: {
49
- allowJs: true,
50
- checkJs: false,
51
- noEmit: true,
52
- skipLibCheck: true,
53
- },
54
- });
55
- const fileCache = new Map();
56
- let filesParsed = 0;
57
- function parseFile(filePath) {
58
- if (fileCache.has(filePath)) {
59
- return fileCache.get(filePath);
60
- }
61
- try {
62
- const sourceFile = project.addSourceFileAtPath(filePath);
63
- fileCache.set(filePath, sourceFile);
64
- filesParsed++;
65
- // Report progress if callback provided
66
- if (onFileProgress && totalFiles) {
67
- onFileProgress(filePath, filesParsed, totalFiles);
68
- }
69
- return sourceFile;
70
- }
71
- catch {
72
- filesParsed++;
73
- if (onFileProgress && totalFiles) {
74
- onFileProgress(filePath, filesParsed, totalFiles);
75
- }
76
- return null;
77
- }
78
- }
79
- function findRouteHandlers(sourceFile) {
80
- const handlers = [];
81
- // Find exported function declarations
82
- for (const func of sourceFile.getFunctions()) {
83
- if (!func.isExported())
84
- continue;
85
- const name = func.getName();
86
- if (!name)
87
- continue;
88
- const method = HTTP_METHODS.find((m) => m === name);
89
- if (method) {
90
- handlers.push({
91
- method,
92
- functionNode: func,
93
- exportName: name,
94
- startLine: func.getStartLineNumber(),
95
- endLine: func.getEndLineNumber(),
96
- });
97
- }
98
- }
99
- // Find exported variable declarations with arrow functions
100
- for (const varStatement of sourceFile.getVariableStatements()) {
101
- if (!varStatement.isExported())
102
- continue;
103
- for (const decl of varStatement.getDeclarations()) {
104
- const name = decl.getName();
105
- const method = HTTP_METHODS.find((m) => m === name);
106
- if (!method)
107
- continue;
108
- const initializer = decl.getInitializer();
109
- if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) {
110
- handlers.push({
111
- method,
112
- functionNode: initializer,
113
- exportName: name,
114
- startLine: decl.getStartLineNumber(),
115
- endLine: decl.getEndLineNumber(),
116
- });
117
- }
118
- }
119
- }
120
- return handlers;
121
- }
122
- function containsAuthCheck(node) {
123
- const text = node.getText();
124
- // Quick regex check first for performance
125
- const hasAuthPattern = AUTH_CHECK_PATTERNS.some((pattern) => text.includes(pattern));
126
- if (!hasAuthPattern) {
127
- // Also check for header/cookie checks followed by early returns
128
- const hasHeaderCheck = /request\.headers|req\.headers|headers\.get/i.test(text);
129
- const hasUnauthorizedReturn = /401|403|unauthorized|unauthenticated/i.test(text);
130
- return hasHeaderCheck && hasUnauthorizedReturn;
131
- }
132
- // Verify with AST
133
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
134
- for (const call of calls) {
135
- const callText = call.getExpression().getText();
136
- // Check for auth function calls
137
- if (AUTH_CHECK_PATTERNS.some((pattern) => callText.includes(pattern))) {
138
- return true;
139
- }
140
- // Check for await auth() pattern
141
- if (callText === "auth" || callText.endsWith(".auth")) {
142
- return true;
143
- }
144
- }
145
- return false;
146
- }
147
- function findDbSinks(node) {
148
- const sinks = [];
149
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
150
- for (const call of calls) {
151
- const callText = call.getExpression().getText();
152
- // Prisma patterns: prisma.user.create, db.user.delete, etc.
153
- const prismaMatch = callText.match(/(?:prisma|db)\.(\w+)\.(create|createMany|update|updateMany|upsert|delete|deleteMany)/);
154
- if (prismaMatch) {
155
- const operation = prismaMatch[2];
156
- sinks.push({
157
- kind: "prisma",
158
- operation,
159
- node: call,
160
- line: call.getStartLineNumber(),
161
- snippet: call.getText().slice(0, 100),
162
- isCritical: CRITICAL_OPS.includes(operation),
163
- });
164
- continue;
165
- }
166
- // SQL patterns: db.query, sql`...`, knex.insert/update/del
167
- if (/db\.query|\.execute|sql`/.test(callText)) {
168
- sinks.push({
169
- kind: "sql",
170
- operation: "query",
171
- node: call,
172
- line: call.getStartLineNumber(),
173
- snippet: call.getText().slice(0, 100),
174
- isCritical: /delete|drop|truncate/i.test(call.getText()),
175
- });
176
- continue;
177
- }
178
- // Knex patterns
179
- const knexMatch = callText.match(/\.(insert|update|del|delete)\s*\(/);
180
- if (knexMatch) {
181
- const operation = knexMatch[1];
182
- sinks.push({
183
- kind: "knex",
184
- operation,
185
- node: call,
186
- line: call.getStartLineNumber(),
187
- snippet: call.getText().slice(0, 100),
188
- isCritical: operation === "del" || operation === "delete",
189
- });
190
- continue;
191
- }
192
- // Export patterns: exportToCsv, generateExport, etc.
193
- if (/export/i.test(callText) && /csv|data|report|file/i.test(call.getText())) {
194
- sinks.push({
195
- kind: "export",
196
- operation: "export",
197
- node: call,
198
- line: call.getStartLineNumber(),
199
- snippet: call.getText().slice(0, 100),
200
- isCritical: true,
201
- });
202
- }
203
- }
204
- return sinks;
205
- }
206
- function findValidationUsage(node) {
207
- const usages = [];
208
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
209
- const text = node.getText();
210
- // Track if raw body is used after validation
211
- const hasRawBodyAccess = /req\.body|request\.body|await\s+\w+\.json\(\)/i.test(text);
212
- for (const call of calls) {
213
- const callText = call.getExpression().getText();
214
- // Zod: schema.parse(), schema.safeParse()
215
- if (/\.parse\s*\(|\.safeParse\s*\(/.test(callText)) {
216
- const parent = call.getParent();
217
- const isAssigned = Node.isVariableDeclaration(parent) ||
218
- Node.isPropertyAssignment(parent) ||
219
- (Node.isBinaryExpression(parent) && parent.getOperatorToken().getText() === "=");
220
- // Check if result is used later (simplified check)
221
- let resultUsed = false;
222
- if (isAssigned && Node.isVariableDeclaration(parent)) {
223
- const varName = parent.getName();
224
- const afterCall = text.slice(text.indexOf(call.getText()) + call.getText().length);
225
- resultUsed = new RegExp(`\\b${varName}\\b`).test(afterCall);
226
- }
227
- usages.push({
228
- library: "zod",
229
- method: callText.includes("safeParse") ? "safeParse" : "parse",
230
- resultAssigned: isAssigned,
231
- resultUsed,
232
- rawBodyUsedAfter: hasRawBodyAccess && !resultUsed,
233
- node: call,
234
- line: call.getStartLineNumber(),
235
- });
236
- continue;
237
- }
238
- // Yup: schema.validate()
239
- if (/\.validate\s*\(/.test(callText) && /yup|schema/i.test(text)) {
240
- const parent = call.getParent();
241
- const isAssigned = Node.isVariableDeclaration(parent) ||
242
- (Node.isAwaitExpression(parent) && Node.isVariableDeclaration(parent.getParent()));
243
- usages.push({
244
- library: "yup",
245
- method: "validate",
246
- resultAssigned: isAssigned,
247
- resultUsed: isAssigned, // Simplified
248
- rawBodyUsedAfter: hasRawBodyAccess && !isAssigned,
249
- node: call,
250
- line: call.getStartLineNumber(),
251
- });
252
- continue;
253
- }
254
- // Joi: joi.validate() or schema.validate()
255
- if (/joi\.validate|\.validate\s*\(/.test(callText) && /joi/i.test(text)) {
256
- const parent = call.getParent();
257
- const isAssigned = Node.isVariableDeclaration(parent);
258
- usages.push({
259
- library: "joi",
260
- method: "validate",
261
- resultAssigned: isAssigned,
262
- resultUsed: isAssigned,
263
- rawBodyUsedAfter: hasRawBodyAccess && !isAssigned,
264
- node: call,
265
- line: call.getStartLineNumber(),
266
- });
267
- }
268
- }
269
- return usages;
270
- }
271
- function findSensitiveLogCalls(node) {
272
- const calls = [];
273
- const callExprs = node.getDescendantsOfKind(SyntaxKind.CallExpression);
274
- for (const call of callExprs) {
275
- const callText = call.getExpression().getText();
276
- // Match console.log/info/error/warn or logger.log/info/error/warn
277
- if (!/^(console|logger)\.(log|info|error|warn|debug)$/.test(callText)) {
278
- continue;
279
- }
280
- const args = call.getArguments();
281
- const sensitiveVars = [];
282
- let hasHighSeverity = false;
283
- for (const arg of args) {
284
- const argText = arg.getText();
285
- // Check for direct sensitive variable references
286
- const identifiers = arg.getDescendantsOfKind(SyntaxKind.Identifier);
287
- for (const id of identifiers) {
288
- const name = id.getText();
289
- if (SENSITIVE_VAR_PATTERNS.test(name)) {
290
- sensitiveVars.push(name);
291
- if (/password|secret|credential|bearer|authorization/i.test(name)) {
292
- hasHighSeverity = true;
293
- }
294
- }
295
- }
296
- // Check for object properties with sensitive names
297
- if (/password|token|secret|authorization|apikey|cookie|session/i.test(argText)) {
298
- if (!sensitiveVars.some((v) => argText.includes(v))) {
299
- const match = argText.match(/(password|token|secret|authorization|apikey|api_key|cookie|session)/i);
300
- if (match) {
301
- sensitiveVars.push(match[1]);
302
- if (/password|secret|authorization/i.test(match[1])) {
303
- hasHighSeverity = true;
304
- }
305
- }
306
- }
307
- }
308
- }
309
- if (sensitiveVars.length > 0) {
310
- calls.push({
311
- logMethod: callText,
312
- sensitiveVars,
313
- severity: hasHighSeverity ? "high" : "medium",
314
- node: call,
315
- line: call.getStartLineNumber(),
316
- snippet: call.getText().slice(0, 150),
317
- });
318
- }
319
- }
320
- return calls;
321
- }
322
- function findInsecureDefaults(sourceFile) {
323
- const defaults = [];
324
- // Find binary expressions with || or ??
325
- const binaries = sourceFile.getDescendantsOfKind(SyntaxKind.BinaryExpression);
326
- for (const binary of binaries) {
327
- const op = binary.getOperatorToken().getText();
328
- if (op !== "||" && op !== "??")
329
- continue;
330
- const left = binary.getLeft().getText();
331
- const right = binary.getRight();
332
- // Check for process.env.VAR pattern on left
333
- const envMatch = left.match(/process\.env\.([A-Z_][A-Z0-9_]*)/);
334
- if (!envMatch)
335
- continue;
336
- const envVar = envMatch[1];
337
- // Check if right side is a string literal (hardcoded fallback)
338
- if (Node.isStringLiteral(right)) {
339
- const fallbackValue = right.getLiteralValue();
340
- // Skip empty strings or obvious placeholders
341
- if (!fallbackValue || fallbackValue === "" || fallbackValue === "undefined")
342
- continue;
343
- // Check if this is a critical secret
344
- const isCritical = CRITICAL_ENV_PATTERNS.test(envVar);
345
- defaults.push({
346
- envVar,
347
- fallbackValue,
348
- isCritical,
349
- node: binary,
350
- line: binary.getStartLineNumber(),
351
- snippet: binary.getText().slice(0, 100),
352
- });
353
- }
354
- }
355
- // Also check variable declarations with hardcoded secrets
356
- const varDecls = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
357
- for (const decl of varDecls) {
358
- const name = decl.getName();
359
- if (!CRITICAL_ENV_PATTERNS.test(name))
360
- continue;
361
- const init = decl.getInitializer();
362
- if (!init || !Node.isStringLiteral(init))
363
- continue;
364
- const value = init.getLiteralValue();
365
- if (!value || value.length < 8)
366
- continue; // Skip short/empty values
367
- // Skip if it's reading from env
368
- if (value.includes("process.env"))
369
- continue;
370
- defaults.push({
371
- envVar: name,
372
- fallbackValue: value,
373
- isCritical: true,
374
- node: decl,
375
- line: decl.getStartLineNumber(),
376
- snippet: decl.getText().slice(0, 100),
377
- });
378
- }
379
- return defaults;
380
- }
381
- function findSsrfProneFetch(node) {
382
- const fetches = [];
383
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
384
- for (const call of calls) {
385
- const callText = call.getExpression().getText();
386
- // Match fetch, axios.get/post/etc
387
- if (!/^(fetch|axios\.get|axios\.post|axios\.put|axios\.delete|axios)$/.test(callText)) {
388
- continue;
389
- }
390
- const args = call.getArguments();
391
- if (args.length === 0)
392
- continue;
393
- const firstArg = args[0].getText();
394
- // Check for user-controlled URL patterns
395
- // body.url, query.url, params.url, data.url, req.body.url, request.query.url
396
- const ssrfPattern = /(?:body|query|params|data|req\.body|req\.query|request\.body|request\.query)\s*\.?\s*(?:\[\s*['"]?)?(url|uri|link|endpoint|target|destination)(?:['"]?\s*\])?/i;
397
- const match = firstArg.match(ssrfPattern);
398
- if (match) {
399
- fetches.push({
400
- fetchMethod: callText,
401
- userInputSource: firstArg,
402
- node: call,
403
- line: call.getStartLineNumber(),
404
- snippet: call.getText().slice(0, 150),
405
- });
406
- }
407
- }
408
- return fetches;
409
- }
410
- function getNodeText(node) {
411
- return node.getText();
412
- }
413
- function getNodeLine(node) {
414
- return node.getStartLineNumber();
415
- }
416
- // Phase 2 helpers
417
- /**
418
- * User-controlled redirect parameter names
419
- */
420
- const REDIRECT_PARAM_NAMES = ["next", "redirect", "returnTo", "url", "returnUrl", "callback", "callbackUrl", "goto", "destination"];
421
- /**
422
- * Find redirect calls with potential user-controlled input
423
- */
424
- function findRedirectCalls(node) {
425
- const redirects = [];
426
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
427
- const text = node.getText();
428
- // Track variables that hold user-controlled values
429
- const userControlledVars = new Set();
430
- // Find user-controlled sources
431
- for (const paramName of REDIRECT_PARAM_NAMES) {
432
- // searchParams.get("next"), req.nextUrl.searchParams.get("next")
433
- const searchParamMatch = new RegExp(`searchParams\\.get\\s*\\(\\s*['"]${paramName}['"]\\s*\\)`, "i");
434
- if (searchParamMatch.test(text)) {
435
- // Find the variable it's assigned to
436
- const assignMatch = text.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=.*searchParams\\.get\\s*\\(\\s*['"]${paramName}['"]\\s*\\)`, "i"));
437
- if (assignMatch) {
438
- userControlledVars.add(assignMatch[1]);
439
- }
440
- }
441
- // body.next, body.redirect, etc.
442
- const bodyMatch = new RegExp(`(?:body|data)\\s*\\.\\s*${paramName}\\b`, "i");
443
- if (bodyMatch.test(text)) {
444
- const assignMatch = text.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=.*(?:body|data)\\.${paramName}\\b`, "i"));
445
- if (assignMatch) {
446
- userControlledVars.add(assignMatch[1]);
447
- }
448
- userControlledVars.add(paramName);
449
- }
450
- }
451
- for (const call of calls) {
452
- const callText = call.getExpression().getText();
453
- // Match redirect calls: NextResponse.redirect, res.redirect, redirect
454
- if (!/^(NextResponse\.redirect|res\.redirect|redirect)$/.test(callText)) {
455
- continue;
456
- }
457
- const args = call.getArguments();
458
- if (args.length === 0)
459
- continue;
460
- const targetArg = args[0].getText();
461
- // Check if the target uses user-controlled input
462
- let isUserControlled = false;
463
- let userControlledSource;
464
- // Direct searchParams.get usage
465
- for (const paramName of REDIRECT_PARAM_NAMES) {
466
- if (new RegExp(`searchParams\\.get\\s*\\(\\s*['"]${paramName}['"]\\s*\\)`, "i").test(targetArg)) {
467
- isUserControlled = true;
468
- userControlledSource = `searchParams.get('${paramName}')`;
469
- break;
470
- }
471
- // Direct body property usage
472
- if (new RegExp(`(?:body|data)\\.${paramName}\\b`, "i").test(targetArg)) {
473
- isUserControlled = true;
474
- userControlledSource = `body.${paramName}`;
475
- break;
476
- }
477
- }
478
- // Check if uses a tracked user-controlled variable
479
- if (!isUserControlled) {
480
- for (const varName of userControlledVars) {
481
- if (new RegExp(`\\b${varName}\\b`).test(targetArg)) {
482
- isUserControlled = true;
483
- userControlledSource = varName;
484
- break;
485
- }
486
- }
487
- }
488
- if (isUserControlled) {
489
- redirects.push({
490
- method: callText,
491
- targetExpression: targetArg,
492
- isUserControlled: true,
493
- userControlledSource,
494
- node: call,
495
- line: call.getStartLineNumber(),
496
- snippet: call.getText().slice(0, 150),
497
- });
498
- }
499
- }
500
- return redirects;
501
- }
502
- /**
503
- * Find CORS configuration that may be insecure
504
- */
505
- function findCorsConfig(sourceFile) {
506
- const configs = [];
507
- const text = sourceFile.getText();
508
- // Check for cors({ origin: "*", credentials: true }) pattern
509
- const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
510
- for (const call of calls) {
511
- const callText = call.getExpression().getText();
512
- // Match cors() or cors middleware
513
- if (callText !== "cors")
514
- continue;
515
- const args = call.getArguments();
516
- if (args.length === 0)
517
- continue;
518
- const configArg = args[0];
519
- if (!Node.isObjectLiteralExpression(configArg))
520
- continue;
521
- const configText = configArg.getText();
522
- const hasWildcardOrigin = /origin\s*:\s*['"]?\*['"]?/.test(configText) ||
523
- /origin\s*:\s*true/.test(configText);
524
- const hasCredentials = /credentials\s*:\s*true/.test(configText);
525
- if (hasWildcardOrigin || hasCredentials) {
526
- configs.push({
527
- hasWildcardOrigin,
528
- hasCredentials,
529
- originValue: hasWildcardOrigin ? "*" : undefined,
530
- credentialsValue: hasCredentials ? "true" : undefined,
531
- node: call,
532
- line: call.getStartLineNumber(),
533
- snippet: call.getText().slice(0, 150),
534
- });
535
- }
536
- }
537
- // Check for res.setHeader patterns
538
- const headerPattern = /setHeader\s*\(\s*['"]Access-Control-Allow-Origin['"]\s*,\s*['"]?\*['"]?\s*\)/i;
539
- const credentialsPattern = /setHeader\s*\(\s*['"]Access-Control-Allow-Credentials['"]\s*,\s*['"]?true['"]?\s*\)/i;
540
- if (headerPattern.test(text) && credentialsPattern.test(text)) {
541
- // Find the actual calls
542
- for (const call of calls) {
543
- const callText = call.getText();
544
- if (headerPattern.test(callText)) {
545
- configs.push({
546
- hasWildcardOrigin: true,
547
- hasCredentials: credentialsPattern.test(text),
548
- originValue: "*",
549
- credentialsValue: "true",
550
- node: call,
551
- line: call.getStartLineNumber(),
552
- snippet: callText.slice(0, 150),
553
- });
554
- }
555
- }
556
- }
557
- return configs;
558
- }
559
- /**
560
- * Find outbound HTTP calls that may lack timeout
561
- */
562
- function findOutboundCalls(node) {
563
- const outboundCalls = [];
564
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
565
- const text = node.getText();
566
- // Check for AbortController usage which indicates timeout handling
567
- const hasAbortController = /AbortController|signal\s*:/.test(text);
568
- for (const call of calls) {
569
- const callText = call.getExpression().getText();
570
- // Match fetch or axios calls
571
- if (!/^(fetch|axios\.get|axios\.post|axios\.put|axios\.delete|axios\.request|axios)$/.test(callText)) {
572
- continue;
573
- }
574
- const args = call.getArguments();
575
- if (args.length === 0)
576
- continue;
577
- const firstArg = args[0].getText();
578
- const fullCallText = call.getText();
579
- // Check if URL is external (starts with http and not localhost)
580
- const urlMatch = firstArg.match(/['"`](https?:\/\/[^'"`]+)['"`]/);
581
- const isExternalUrl = urlMatch
582
- ? !/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(urlMatch[1])
583
- : false;
584
- // Check for timeout in options
585
- const hasTimeout = /timeout\s*:/.test(fullCallText) || hasAbortController;
586
- // Only flag external URLs without timeout
587
- if (isExternalUrl && !hasTimeout) {
588
- outboundCalls.push({
589
- method: callText,
590
- urlExpression: firstArg,
591
- hasTimeout: false,
592
- isExternalUrl: true,
593
- node: call,
594
- line: call.getStartLineNumber(),
595
- snippet: fullCallText.slice(0, 150),
596
- });
597
- }
598
- }
599
- return outboundCalls;
600
- }
601
- /**
602
- * Sensitive model names that warrant extra caution
603
- */
604
- const SENSITIVE_MODEL_NAMES = /^(user|account|customer|member|profile|employee|admin|session|token)$/i;
605
- /**
606
- * Find Prisma queries that may return too much data
607
- */
608
- function findPrismaQueries(node) {
609
- const queries = [];
610
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
611
- const text = node.getText();
612
- for (const call of calls) {
613
- const callText = call.getExpression().getText();
614
- // Match prisma.user.findMany, db.account.findUnique, etc.
615
- const prismaMatch = callText.match(/(?:prisma|db)\.(\w+)\.(findMany|findFirst|findUnique|findFirstOrThrow|findUniqueOrThrow)/);
616
- if (!prismaMatch)
617
- continue;
618
- const modelName = prismaMatch[1];
619
- const operation = prismaMatch[2];
620
- // Check if model name is sensitive
621
- if (!SENSITIVE_MODEL_NAMES.test(modelName))
622
- continue;
623
- const args = call.getArguments();
624
- const fullCallText = call.getText();
625
- // Check for select or include
626
- const hasSelect = /select\s*:/.test(fullCallText);
627
- const hasInclude = /include\s*:/.test(fullCallText);
628
- // Check if result is directly returned (very simplified check)
629
- // Look for: return prisma... or Response.json(prisma...) patterns
630
- const isDirectlyReturned = /return\s+(?:await\s+)?(?:Response\.json\s*\()?\s*(?:await\s+)?(?:prisma|db)\./.test(text) ||
631
- /Response\.json\s*\(\s*(?:await\s+)?(?:prisma|db)\./.test(text);
632
- // Only flag if no select restriction and might be directly returned
633
- if (!hasSelect) {
634
- queries.push({
635
- model: modelName,
636
- operation,
637
- hasSelect,
638
- hasInclude,
639
- isDirectlyReturned,
640
- node: call,
641
- line: call.getStartLineNumber(),
642
- snippet: fullCallText.slice(0, 150),
643
- });
644
- }
645
- }
646
- return queries;
647
- }
648
- /**
649
- * Sensitive token/key variable patterns
650
- */
651
- const SENSITIVE_TOKEN_PATTERNS = /token|secret|key|session|reset|code|nonce|password|apikey|api_key/i;
652
- /**
653
- * Find Math.random usage in sensitive contexts
654
- */
655
- function findMathRandomUsage(sourceFile) {
656
- const usages = [];
657
- const text = sourceFile.getText();
658
- // Quick check if Math.random exists
659
- if (!text.includes("Math.random"))
660
- return usages;
661
- // Find variable declarations that use Math.random
662
- const varDecls = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
663
- for (const decl of varDecls) {
664
- const name = decl.getName();
665
- const init = decl.getInitializer();
666
- if (!init)
667
- continue;
668
- const initText = init.getText();
669
- if (!initText.includes("Math.random"))
670
- continue;
671
- const isSensitiveContext = SENSITIVE_TOKEN_PATTERNS.test(name);
672
- if (isSensitiveContext) {
673
- usages.push({
674
- variableName: name,
675
- isSensitiveContext: true,
676
- node: decl,
677
- line: decl.getStartLineNumber(),
678
- snippet: decl.getText().slice(0, 150),
679
- });
680
- }
681
- }
682
- // Also check function calls where Math.random result flows to sensitive assignments
683
- const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
684
- for (const call of calls) {
685
- const callText = call.getText();
686
- if (!callText.includes("Math.random"))
687
- continue;
688
- // Check parent context
689
- const parent = call.getParent();
690
- if (Node.isVariableDeclaration(parent)) {
691
- const varName = parent.getName();
692
- if (SENSITIVE_TOKEN_PATTERNS.test(varName)) {
693
- // Already handled above
694
- continue;
695
- }
696
- }
697
- // Check for function names that suggest token generation
698
- const funcParent = call.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration);
699
- if (funcParent) {
700
- const funcName = funcParent.getName() ?? "";
701
- if (SENSITIVE_TOKEN_PATTERNS.test(funcName)) {
702
- usages.push({
703
- variableName: funcName,
704
- isSensitiveContext: true,
705
- node: call,
706
- line: call.getStartLineNumber(),
707
- snippet: callText.slice(0, 150),
708
- });
709
- }
710
- }
711
- }
712
- return usages;
713
- }
714
- /**
715
- * Find JWT decode calls without corresponding verify
716
- */
717
- function findJwtDecodeWithoutVerify(sourceFile) {
718
- const decodes = [];
719
- const text = sourceFile.getText();
720
- // Check if file uses jsonwebtoken
721
- if (!/jwt\.decode|jsonwebtoken/.test(text))
722
- return decodes;
723
- const hasVerify = /jwt\.verify|\.verify\s*\(/.test(text);
724
- const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
725
- for (const call of calls) {
726
- const callText = call.getExpression().getText();
727
- if (callText === "jwt.decode" || callText.endsWith(".decode")) {
728
- decodes.push({
729
- hasVerifyInFile: hasVerify,
730
- node: call,
731
- line: call.getStartLineNumber(),
732
- snippet: call.getText().slice(0, 150),
733
- });
734
- }
735
- }
736
- return decodes;
737
- }
738
- /**
739
- * Find weak hash algorithm usage
740
- */
741
- function findWeakHashUsage(sourceFile) {
742
- const usages = [];
743
- const text = sourceFile.getText();
744
- // Check for createHash with weak algorithms
745
- const weakAlgos = ["md5", "sha1"];
746
- const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
747
- for (const call of calls) {
748
- const callText = call.getExpression().getText();
749
- if (!/createHash/.test(callText))
750
- continue;
751
- const args = call.getArguments();
752
- if (args.length === 0)
753
- continue;
754
- const algoArg = args[0].getText().toLowerCase();
755
- for (const weakAlgo of weakAlgos) {
756
- if (algoArg.includes(weakAlgo)) {
757
- // Check if in password context
758
- const contextText = call.getParent()?.getText() ?? "";
759
- const fullContext = text.slice(Math.max(0, text.indexOf(call.getText()) - 200), text.indexOf(call.getText()) + 200);
760
- const isPasswordContext = /password|passwd|pwd|credential/i.test(fullContext);
761
- usages.push({
762
- algorithm: weakAlgo,
763
- isPasswordContext,
764
- node: call,
765
- line: call.getStartLineNumber(),
766
- snippet: call.getText().slice(0, 150),
767
- });
768
- }
769
- }
770
- }
771
- // Check for bcrypt with low salt rounds
772
- const bcryptPattern = /bcrypt\.hash\s*\([^,]+,\s*(\d+)/;
773
- const bcryptMatch = text.match(bcryptPattern);
774
- if (bcryptMatch && parseInt(bcryptMatch[1], 10) < 10) {
775
- // Find the actual call node
776
- for (const call of calls) {
777
- if (/bcrypt\.hash/.test(call.getExpression().getText())) {
778
- usages.push({
779
- algorithm: `bcrypt (saltRounds=${bcryptMatch[1]})`,
780
- isPasswordContext: true,
781
- node: call,
782
- line: call.getStartLineNumber(),
783
- snippet: call.getText().slice(0, 150),
784
- });
785
- }
786
- }
787
- }
788
- return usages;
789
- }
790
- /**
791
- * Find file upload handlers without proper validation
792
- */
793
- function findFileUploadHandlers(node) {
794
- const handlers = [];
795
- const text = node.getText();
796
- const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
797
- // Check for formData().get() patterns (Next.js)
798
- const hasFormData = /formData\s*\(\s*\)|\.formData\s*\(\s*\)/.test(text);
799
- const hasFileGet = /\.get\s*\(\s*['"]file['"]\s*\)|\.getAll\s*\(\s*['"]files?['"]\s*\)/.test(text);
800
- if (hasFormData && hasFileGet) {
801
- const hasSizeCheck = /\.size\s*[<>]|size\s*[<>]|maxSize|MAX_SIZE/.test(text);
802
- const hasTypeCheck = /\.type\s*===|type\s*===|contentType|mime|accept/.test(text);
803
- if (!hasSizeCheck || !hasTypeCheck) {
804
- // Find the formData call
805
- for (const call of calls) {
806
- if (/formData/.test(call.getText())) {
807
- handlers.push({
808
- uploadMethod: "formData",
809
- hasSizeCheck,
810
- hasTypeCheck,
811
- hasLimits: hasSizeCheck && hasTypeCheck,
812
- node: call,
813
- line: call.getStartLineNumber(),
814
- snippet: call.getText().slice(0, 150),
815
- });
816
- break;
817
- }
818
- }
819
- }
820
- }
821
- // Check for multer without limits
822
- if (/multer/.test(text)) {
823
- for (const call of calls) {
824
- const callText = call.getExpression().getText();
825
- if (callText !== "multer")
826
- continue;
827
- const configText = call.getText();
828
- const hasLimits = /limits\s*:/.test(configText);
829
- const hasFileFilter = /fileFilter\s*:/.test(configText);
830
- if (!hasLimits && !hasFileFilter) {
831
- handlers.push({
832
- uploadMethod: "multer",
833
- hasSizeCheck: hasLimits,
834
- hasTypeCheck: hasFileFilter,
835
- hasLimits,
836
- node: call,
837
- line: call.getStartLineNumber(),
838
- snippet: configText.slice(0, 150),
839
- });
840
- }
841
- }
842
- }
843
- return handlers;
844
- }
845
- /**
846
- * Find file writes to public directories
847
- */
848
- function findPublicFileWrites(sourceFile) {
849
- const writes = [];
850
- const text = sourceFile.getText();
851
- const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
852
- const publicDirPatterns = /['"`](public|static|app\/public|uploads)['"`]|['"`]\.\/public|['"`]\/public/i;
853
- for (const call of calls) {
854
- const callText = call.getExpression().getText();
855
- // Check for fs.writeFile, writeFileSync, etc.
856
- if (!/writeFile|writeFileSync|createWriteStream|copyFile|rename/.test(callText)) {
857
- continue;
858
- }
859
- const args = call.getArguments();
860
- if (args.length === 0)
861
- continue;
862
- const pathArg = args[0].getText();
863
- // Check if writing to public directory
864
- if (publicDirPatterns.test(pathArg)) {
865
- // Check if using user-supplied filename
866
- const usesUserFilename = /filename|originalname|name\s*\+|name\s*\$|\$\{.*name/i.test(pathArg);
867
- writes.push({
868
- writePath: pathArg,
869
- isPublicDir: true,
870
- usesUserFilename,
871
- node: call,
872
- line: call.getStartLineNumber(),
873
- snippet: call.getText().slice(0, 150),
874
- });
875
- }
876
- }
877
- // Also check path.join patterns
878
- for (const call of calls) {
879
- const callText = call.getExpression().getText();
880
- if (!/path\.join|path\.resolve/.test(callText))
881
- continue;
882
- const fullCallText = call.getText();
883
- if (publicDirPatterns.test(fullCallText)) {
884
- const usesUserFilename = /filename|originalname|name\s*\+|name\s*\$|\$\{.*name/i.test(fullCallText);
885
- writes.push({
886
- writePath: fullCallText,
887
- isPublicDir: true,
888
- usesUserFilename,
889
- node: call,
890
- line: call.getStartLineNumber(),
891
- snippet: fullCallText.slice(0, 150),
892
- });
893
- }
894
- }
895
- return writes;
896
- }
897
- /**
898
- * Check if file contains rate limiting signals
899
- */
900
- function hasRateLimitSignals(sourceFile) {
901
- const text = sourceFile.getText();
902
- const patterns = [
903
- "rateLimit",
904
- "rateLimiter",
905
- "limiter",
906
- "@upstash/ratelimit",
907
- "express-rate-limit",
908
- "next-rate-limit",
909
- "slowDown",
910
- "throttle",
911
- ];
912
- return patterns.some((p) => text.includes(p));
913
- }
914
- /**
915
- * Check if file contains validation schema usage
916
- */
917
- function hasValidationSchemas(sourceFile) {
918
- const text = sourceFile.getText();
919
- return /\bz\.|zod|yup|joi|\.parse\s*\(|\.validate\s*\(|\.safeParse\s*\(/.test(text);
920
- }
921
- return {
922
- parseFile,
923
- findRouteHandlers,
924
- containsAuthCheck,
925
- findDbSinks,
926
- findValidationUsage,
927
- findSensitiveLogCalls,
928
- findInsecureDefaults,
929
- findSsrfProneFetch,
930
- getNodeText,
931
- getNodeLine,
932
- // Phase 2
933
- findRedirectCalls,
934
- findCorsConfig,
935
- findOutboundCalls,
936
- findPrismaQueries,
937
- findMathRandomUsage,
938
- findJwtDecodeWithoutVerify,
939
- findWeakHashUsage,
940
- findFileUploadHandlers,
941
- findPublicFileWrites,
942
- hasRateLimitSignals,
943
- hasValidationSchemas,
944
- };
945
- }