@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.
- package/README.md +6 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +7902 -8
- package/package.json +13 -7
- package/dist/__tests__/cli.test.d.ts +0 -2
- package/dist/__tests__/cli.test.d.ts.map +0 -1
- package/dist/__tests__/cli.test.js +0 -243
- package/dist/__tests__/fixtures/safe-app/app/api/users/route.js +0 -36
- package/dist/__tests__/fixtures/vulnerable-app/app/api/users/route.js +0 -28
- package/dist/__tests__/fixtures/vulnerable-app/lib/config.d.ts +0 -4
- package/dist/__tests__/fixtures/vulnerable-app/lib/config.d.ts.map +0 -1
- package/dist/__tests__/fixtures/vulnerable-app/lib/config.js +0 -6
- package/dist/__tests__/scanners/env-config.test.d.ts +0 -2
- package/dist/__tests__/scanners/env-config.test.d.ts.map +0 -1
- package/dist/__tests__/scanners/env-config.test.js +0 -142
- package/dist/__tests__/scanners/nextjs-middleware.test.d.ts +0 -2
- package/dist/__tests__/scanners/nextjs-middleware.test.d.ts.map +0 -1
- package/dist/__tests__/scanners/nextjs-middleware.test.js +0 -193
- package/dist/__tests__/scanners/scanner-packs.test.d.ts +0 -2
- package/dist/__tests__/scanners/scanner-packs.test.d.ts.map +0 -1
- package/dist/__tests__/scanners/scanner-packs.test.js +0 -126
- package/dist/__tests__/scanners/unused-security-imports.test.d.ts +0 -2
- package/dist/__tests__/scanners/unused-security-imports.test.d.ts.map +0 -1
- package/dist/__tests__/scanners/unused-security-imports.test.js +0 -145
- package/dist/commands/demo-artifact.d.ts +0 -7
- package/dist/commands/demo-artifact.d.ts.map +0 -1
- package/dist/commands/demo-artifact.js +0 -322
- package/dist/commands/evaluate.d.ts +0 -30
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/evaluate.js +0 -258
- package/dist/commands/explain.d.ts +0 -12
- package/dist/commands/explain.d.ts.map +0 -1
- package/dist/commands/explain.js +0 -214
- package/dist/commands/index.d.ts +0 -7
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js +0 -6
- package/dist/commands/intent.d.ts +0 -21
- package/dist/commands/intent.d.ts.map +0 -1
- package/dist/commands/intent.js +0 -192
- package/dist/commands/scan.d.ts +0 -44
- package/dist/commands/scan.d.ts.map +0 -1
- package/dist/commands/scan.js +0 -497
- package/dist/commands/waivers.d.ts +0 -30
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/waivers.js +0 -249
- package/dist/index.d.ts.map +0 -1
- package/dist/phase3/index.d.ts +0 -11
- package/dist/phase3/index.d.ts.map +0 -1
- package/dist/phase3/index.js +0 -12
- package/dist/phase3/intent-miner.d.ts +0 -32
- package/dist/phase3/intent-miner.d.ts.map +0 -1
- package/dist/phase3/intent-miner.js +0 -323
- package/dist/phase3/proof-trace-builder.d.ts +0 -42
- package/dist/phase3/proof-trace-builder.d.ts.map +0 -1
- package/dist/phase3/proof-trace-builder.js +0 -441
- package/dist/phase3/scanners/auth-by-ui-server-gap.d.ts +0 -15
- package/dist/phase3/scanners/auth-by-ui-server-gap.d.ts.map +0 -1
- package/dist/phase3/scanners/auth-by-ui-server-gap.js +0 -237
- package/dist/phase3/scanners/comment-claim-unproven.d.ts +0 -14
- package/dist/phase3/scanners/comment-claim-unproven.d.ts.map +0 -1
- package/dist/phase3/scanners/comment-claim-unproven.js +0 -161
- package/dist/phase3/scanners/index.d.ts +0 -31
- package/dist/phase3/scanners/index.d.ts.map +0 -1
- package/dist/phase3/scanners/index.js +0 -40
- package/dist/phase3/scanners/middleware-assumed-not-matching.d.ts +0 -14
- package/dist/phase3/scanners/middleware-assumed-not-matching.d.ts.map +0 -1
- package/dist/phase3/scanners/middleware-assumed-not-matching.js +0 -172
- package/dist/phase3/scanners/validation-claimed-missing.d.ts +0 -15
- package/dist/phase3/scanners/validation-claimed-missing.d.ts.map +0 -1
- package/dist/phase3/scanners/validation-claimed-missing.js +0 -204
- package/dist/scanners/abuse/compute-abuse.d.ts +0 -20
- package/dist/scanners/abuse/compute-abuse.d.ts.map +0 -1
- package/dist/scanners/abuse/compute-abuse.js +0 -509
- package/dist/scanners/abuse/index.d.ts +0 -12
- package/dist/scanners/abuse/index.d.ts.map +0 -1
- package/dist/scanners/abuse/index.js +0 -15
- package/dist/scanners/auth/index.d.ts +0 -5
- package/dist/scanners/auth/index.d.ts.map +0 -1
- package/dist/scanners/auth/index.js +0 -10
- package/dist/scanners/auth/middleware-gap.d.ts +0 -22
- package/dist/scanners/auth/middleware-gap.d.ts.map +0 -1
- package/dist/scanners/auth/middleware-gap.js +0 -203
- package/dist/scanners/auth/unprotected-api-route.d.ts +0 -12
- package/dist/scanners/auth/unprotected-api-route.d.ts.map +0 -1
- package/dist/scanners/auth/unprotected-api-route.js +0 -126
- package/dist/scanners/config/index.d.ts +0 -5
- package/dist/scanners/config/index.d.ts.map +0 -1
- package/dist/scanners/config/index.js +0 -10
- package/dist/scanners/config/insecure-defaults.d.ts +0 -12
- package/dist/scanners/config/insecure-defaults.d.ts.map +0 -1
- package/dist/scanners/config/insecure-defaults.js +0 -77
- package/dist/scanners/config/undocumented-env.d.ts +0 -24
- package/dist/scanners/config/undocumented-env.d.ts.map +0 -1
- package/dist/scanners/config/undocumented-env.js +0 -159
- package/dist/scanners/crypto/index.d.ts +0 -6
- package/dist/scanners/crypto/index.d.ts.map +0 -1
- package/dist/scanners/crypto/index.js +0 -11
- package/dist/scanners/crypto/jwt-decode-unverified.d.ts +0 -14
- package/dist/scanners/crypto/jwt-decode-unverified.d.ts.map +0 -1
- package/dist/scanners/crypto/jwt-decode-unverified.js +0 -87
- package/dist/scanners/crypto/math-random-tokens.d.ts +0 -13
- package/dist/scanners/crypto/math-random-tokens.d.ts.map +0 -1
- package/dist/scanners/crypto/math-random-tokens.js +0 -80
- package/dist/scanners/crypto/weak-hashing.d.ts +0 -11
- package/dist/scanners/crypto/weak-hashing.d.ts.map +0 -1
- package/dist/scanners/crypto/weak-hashing.js +0 -95
- package/dist/scanners/env-config.d.ts +0 -24
- package/dist/scanners/env-config.d.ts.map +0 -1
- package/dist/scanners/env-config.js +0 -164
- package/dist/scanners/hallucinations/index.d.ts +0 -4
- package/dist/scanners/hallucinations/index.d.ts.map +0 -1
- package/dist/scanners/hallucinations/index.js +0 -8
- package/dist/scanners/hallucinations/unused-security-imports.d.ts +0 -36
- package/dist/scanners/hallucinations/unused-security-imports.d.ts.map +0 -1
- package/dist/scanners/hallucinations/unused-security-imports.js +0 -309
- package/dist/scanners/helpers/ast-helpers.d.ts +0 -6
- package/dist/scanners/helpers/ast-helpers.d.ts.map +0 -1
- package/dist/scanners/helpers/ast-helpers.js +0 -945
- package/dist/scanners/helpers/context-builder.d.ts +0 -17
- package/dist/scanners/helpers/context-builder.d.ts.map +0 -1
- package/dist/scanners/helpers/context-builder.js +0 -148
- package/dist/scanners/helpers/index.d.ts +0 -3
- package/dist/scanners/helpers/index.d.ts.map +0 -1
- package/dist/scanners/helpers/index.js +0 -2
- package/dist/scanners/index.d.ts +0 -30
- package/dist/scanners/index.d.ts.map +0 -1
- package/dist/scanners/index.js +0 -102
- package/dist/scanners/middleware/index.d.ts +0 -4
- package/dist/scanners/middleware/index.d.ts.map +0 -1
- package/dist/scanners/middleware/index.js +0 -7
- package/dist/scanners/middleware/missing-rate-limit.d.ts +0 -13
- package/dist/scanners/middleware/missing-rate-limit.d.ts.map +0 -1
- package/dist/scanners/middleware/missing-rate-limit.js +0 -140
- package/dist/scanners/network/cors-misconfiguration.d.ts +0 -14
- package/dist/scanners/network/cors-misconfiguration.d.ts.map +0 -1
- package/dist/scanners/network/cors-misconfiguration.js +0 -89
- package/dist/scanners/network/index.d.ts +0 -7
- package/dist/scanners/network/index.d.ts.map +0 -1
- package/dist/scanners/network/index.js +0 -18
- package/dist/scanners/network/missing-timeout.d.ts +0 -15
- package/dist/scanners/network/missing-timeout.d.ts.map +0 -1
- package/dist/scanners/network/missing-timeout.js +0 -93
- package/dist/scanners/network/open-redirect.d.ts +0 -15
- package/dist/scanners/network/open-redirect.d.ts.map +0 -1
- package/dist/scanners/network/open-redirect.js +0 -88
- package/dist/scanners/network/ssrf-prone-fetch.d.ts +0 -12
- package/dist/scanners/network/ssrf-prone-fetch.d.ts.map +0 -1
- package/dist/scanners/network/ssrf-prone-fetch.js +0 -90
- package/dist/scanners/nextjs-middleware.d.ts +0 -26
- package/dist/scanners/nextjs-middleware.d.ts.map +0 -1
- package/dist/scanners/nextjs-middleware.js +0 -246
- package/dist/scanners/privacy/debug-flags.d.ts +0 -13
- package/dist/scanners/privacy/debug-flags.d.ts.map +0 -1
- package/dist/scanners/privacy/debug-flags.js +0 -124
- package/dist/scanners/privacy/index.d.ts +0 -6
- package/dist/scanners/privacy/index.d.ts.map +0 -1
- package/dist/scanners/privacy/index.js +0 -11
- package/dist/scanners/privacy/over-broad-response.d.ts +0 -15
- package/dist/scanners/privacy/over-broad-response.d.ts.map +0 -1
- package/dist/scanners/privacy/over-broad-response.js +0 -109
- package/dist/scanners/privacy/sensitive-logging.d.ts +0 -11
- package/dist/scanners/privacy/sensitive-logging.d.ts.map +0 -1
- package/dist/scanners/privacy/sensitive-logging.js +0 -78
- package/dist/scanners/types.d.ts +0 -456
- package/dist/scanners/types.d.ts.map +0 -1
- package/dist/scanners/types.js +0 -16
- package/dist/scanners/unused-security-imports.d.ts +0 -34
- package/dist/scanners/unused-security-imports.d.ts.map +0 -1
- package/dist/scanners/unused-security-imports.js +0 -206
- package/dist/scanners/uploads/index.d.ts +0 -5
- package/dist/scanners/uploads/index.d.ts.map +0 -1
- package/dist/scanners/uploads/index.js +0 -9
- package/dist/scanners/uploads/missing-constraints.d.ts +0 -15
- package/dist/scanners/uploads/missing-constraints.d.ts.map +0 -1
- package/dist/scanners/uploads/missing-constraints.js +0 -109
- package/dist/scanners/uploads/public-path.d.ts +0 -11
- package/dist/scanners/uploads/public-path.d.ts.map +0 -1
- package/dist/scanners/uploads/public-path.js +0 -87
- package/dist/scanners/validation/client-side-only.d.ts +0 -14
- package/dist/scanners/validation/client-side-only.d.ts.map +0 -1
- package/dist/scanners/validation/client-side-only.js +0 -140
- package/dist/scanners/validation/ignored-validation.d.ts +0 -12
- package/dist/scanners/validation/ignored-validation.d.ts.map +0 -1
- package/dist/scanners/validation/ignored-validation.js +0 -119
- package/dist/scanners/validation/index.d.ts +0 -5
- package/dist/scanners/validation/index.d.ts.map +0 -1
- package/dist/scanners/validation/index.js +0 -9
- package/dist/utils/exclude-patterns.d.ts +0 -35
- package/dist/utils/exclude-patterns.d.ts.map +0 -1
- package/dist/utils/exclude-patterns.js +0 -78
- package/dist/utils/file-utils.d.ts +0 -37
- package/dist/utils/file-utils.d.ts.map +0 -1
- package/dist/utils/file-utils.js +0 -77
- package/dist/utils/fingerprint.d.ts +0 -25
- package/dist/utils/fingerprint.d.ts.map +0 -1
- package/dist/utils/fingerprint.js +0 -28
- package/dist/utils/git-info.d.ts +0 -14
- package/dist/utils/git-info.d.ts.map +0 -1
- package/dist/utils/git-info.js +0 -55
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -3
- package/dist/utils/progress.d.ts +0 -42
- package/dist/utils/progress.d.ts.map +0 -1
- package/dist/utils/progress.js +0 -165
- package/dist/utils/sarif-formatter.d.ts +0 -92
- package/dist/utils/sarif-formatter.d.ts.map +0 -1
- 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
|
-
}
|