@qodfy/core 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +189 -24
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -9,7 +9,27 @@ var ignoredPaths = [
|
|
|
9
9
|
"dist/**",
|
|
10
10
|
"build/**",
|
|
11
11
|
".turbo/**",
|
|
12
|
-
".vercel/**"
|
|
12
|
+
".vercel/**",
|
|
13
|
+
"coverage/**",
|
|
14
|
+
"**/coverage/**",
|
|
15
|
+
".cache/**",
|
|
16
|
+
"**/.cache/**",
|
|
17
|
+
".output/**",
|
|
18
|
+
"**/.output/**",
|
|
19
|
+
".open-next/**",
|
|
20
|
+
"**/.open-next/**",
|
|
21
|
+
"storybook-static/**",
|
|
22
|
+
"**/storybook-static/**",
|
|
23
|
+
"playwright-report/**",
|
|
24
|
+
"**/playwright-report/**",
|
|
25
|
+
"test-results/**",
|
|
26
|
+
"**/test-results/**",
|
|
27
|
+
"**/*.d.ts",
|
|
28
|
+
"**/*.map",
|
|
29
|
+
"generated/**",
|
|
30
|
+
"**/generated/**",
|
|
31
|
+
"__generated__/**",
|
|
32
|
+
"**/__generated__/**"
|
|
13
33
|
];
|
|
14
34
|
var aiKeywords = [
|
|
15
35
|
"openai",
|
|
@@ -21,7 +41,44 @@ var aiKeywords = [
|
|
|
21
41
|
"streamText",
|
|
22
42
|
"useChat"
|
|
23
43
|
];
|
|
44
|
+
var hardcodedSecretPatterns = [
|
|
45
|
+
{
|
|
46
|
+
label: "OpenAI API key",
|
|
47
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Stripe secret key",
|
|
51
|
+
pattern: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "Stripe webhook secret",
|
|
55
|
+
pattern: /\bwhsec_[A-Za-z0-9]{16,}\b/g
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "GitHub token",
|
|
59
|
+
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b/g
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "GitHub fine-grained token",
|
|
63
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "Google API key",
|
|
67
|
+
pattern: /\bAIza[A-Za-z0-9_-]{20,}\b/g
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "Slack token",
|
|
71
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: "private key",
|
|
75
|
+
pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
var LARGE_FILE_WARNING_BYTES = 15e3;
|
|
79
|
+
var MAX_FILE_SIZE_BYTES = 500 * 1024;
|
|
24
80
|
async function scanProject(projectPath) {
|
|
81
|
+
const startTime = Date.now();
|
|
25
82
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
26
83
|
const issues = [];
|
|
27
84
|
const packageJsonPath = path.join(resolvedProjectPath, "package.json");
|
|
@@ -102,17 +159,38 @@ async function scanProject(projectPath) {
|
|
|
102
159
|
}
|
|
103
160
|
const files = await getSourceFiles(resolvedProjectPath, issues);
|
|
104
161
|
const apiRoutes = files.filter((file) => {
|
|
105
|
-
return
|
|
162
|
+
return isApiRoute(file);
|
|
106
163
|
});
|
|
107
164
|
const apiRouteSet = new Set(apiRoutes);
|
|
108
|
-
const readableFiles = /* @__PURE__ */ new Map();
|
|
109
165
|
const envExampleWarningKeys = /* @__PURE__ */ new Set();
|
|
110
166
|
const clientSecretWarningKeys = /* @__PURE__ */ new Set();
|
|
167
|
+
const hardcodedSecretWarningKeys = /* @__PURE__ */ new Set();
|
|
111
168
|
let aiFiles = 0;
|
|
112
169
|
let largeFiles = 0;
|
|
113
170
|
for (const file of files) {
|
|
114
|
-
const fileResult = await safeReadFile(file);
|
|
115
171
|
const relativeFile = normalizePath(path.relative(resolvedProjectPath, file));
|
|
172
|
+
const statResult = await safeStatFile(file);
|
|
173
|
+
if (!statResult.ok) {
|
|
174
|
+
issues.push({
|
|
175
|
+
severity: "info",
|
|
176
|
+
title: "File could not be checked",
|
|
177
|
+
message: statResult.reason,
|
|
178
|
+
file: relativeFile
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (statResult.size > MAX_FILE_SIZE_BYTES) {
|
|
183
|
+
largeFiles++;
|
|
184
|
+
issues.push({
|
|
185
|
+
severity: "info",
|
|
186
|
+
title: "Large file skipped from deep scan",
|
|
187
|
+
message: "This file is larger than 500KB and was skipped from deep content checks.",
|
|
188
|
+
file: relativeFile,
|
|
189
|
+
suggestion: "Review large generated or bundled files manually."
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const fileResult = await safeReadFile(file);
|
|
116
194
|
if (!fileResult.ok) {
|
|
117
195
|
issues.push({
|
|
118
196
|
severity: "info",
|
|
@@ -123,8 +201,7 @@ async function scanProject(projectPath) {
|
|
|
123
201
|
continue;
|
|
124
202
|
}
|
|
125
203
|
const content = fileResult.content;
|
|
126
|
-
|
|
127
|
-
if (content.length > 15e3) {
|
|
204
|
+
if (statResult.size > LARGE_FILE_WARNING_BYTES) {
|
|
128
205
|
largeFiles++;
|
|
129
206
|
issues.push({
|
|
130
207
|
severity: "info",
|
|
@@ -150,6 +227,43 @@ async function scanProject(projectPath) {
|
|
|
150
227
|
});
|
|
151
228
|
}
|
|
152
229
|
}
|
|
230
|
+
const isStripeWebhook = isStripeWebhookRoute(relativeFile, content);
|
|
231
|
+
const hasStripeSignatureVerification = hasStripeWebhookSignatureVerification(content);
|
|
232
|
+
if (isStripeWebhook && !hasStripeSignatureVerification) {
|
|
233
|
+
issues.push({
|
|
234
|
+
severity: "critical",
|
|
235
|
+
title: "Stripe webhook may be missing signature verification",
|
|
236
|
+
message: "This Stripe webhook route does not appear to verify the Stripe signature before handling the event.",
|
|
237
|
+
file: relativeFile,
|
|
238
|
+
suggestion: "Use stripe.webhooks.constructEvent with the raw request body, Stripe-Signature header, and STRIPE_WEBHOOK_SECRET."
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
for (const secretMatch of getHardcodedSecretMatches(content)) {
|
|
242
|
+
const warningKey = `${relativeFile}:${secretMatch.label}`;
|
|
243
|
+
if (hardcodedSecretWarningKeys.has(warningKey)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
hardcodedSecretWarningKeys.add(warningKey);
|
|
247
|
+
issues.push({
|
|
248
|
+
severity: "critical",
|
|
249
|
+
title: "Possible hardcoded secret",
|
|
250
|
+
message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
|
|
251
|
+
file: relativeFile,
|
|
252
|
+
suggestion: "Move secrets into environment variables and rotate the value if this is a real secret."
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (apiRouteSet.has(file)) {
|
|
256
|
+
const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
|
|
257
|
+
if (!hasAuth && !isStripeWebhook) {
|
|
258
|
+
issues.push({
|
|
259
|
+
severity: "warning",
|
|
260
|
+
title: "API route may be missing authentication",
|
|
261
|
+
message: "This API route does not appear to contain an auth/session check.",
|
|
262
|
+
file: relativeFile,
|
|
263
|
+
suggestion: "Confirm the route is public, or add an auth/session check before handling user data."
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
153
267
|
const usedEnvVariables = getUsedEnvVariables(content);
|
|
154
268
|
if (envExampleVariables) {
|
|
155
269
|
for (const variableName of usedEnvVariables) {
|
|
@@ -188,23 +302,6 @@ async function scanProject(projectPath) {
|
|
|
188
302
|
}
|
|
189
303
|
}
|
|
190
304
|
}
|
|
191
|
-
for (const route of apiRoutes) {
|
|
192
|
-
const content = readableFiles.get(route);
|
|
193
|
-
if (!content) {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
const relativeFile = normalizePath(path.relative(resolvedProjectPath, route));
|
|
197
|
-
const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
|
|
198
|
-
if (!hasAuth) {
|
|
199
|
-
issues.push({
|
|
200
|
-
severity: "warning",
|
|
201
|
-
title: "API route may be missing authentication",
|
|
202
|
-
message: "This API route does not appear to contain an auth/session check.",
|
|
203
|
-
file: relativeFile,
|
|
204
|
-
suggestion: "Confirm the route is public, or add an auth/session check before handling user data."
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
305
|
const criticalCount = issues.filter((issue) => issue.severity === "critical").length;
|
|
209
306
|
const warningCount = issues.filter((issue) => issue.severity === "warning").length;
|
|
210
307
|
const score = Math.max(0, 100 - criticalCount * 20 - warningCount * 8);
|
|
@@ -217,7 +314,8 @@ async function scanProject(projectPath) {
|
|
|
217
314
|
totalFiles: files.length,
|
|
218
315
|
apiRoutes: apiRoutes.length,
|
|
219
316
|
aiFiles,
|
|
220
|
-
largeFiles
|
|
317
|
+
largeFiles,
|
|
318
|
+
durationMs: Date.now() - startTime
|
|
221
319
|
}
|
|
222
320
|
};
|
|
223
321
|
}
|
|
@@ -258,6 +356,36 @@ async function safeReadFile(filePath) {
|
|
|
258
356
|
};
|
|
259
357
|
}
|
|
260
358
|
}
|
|
359
|
+
async function safeStatFile(filePath) {
|
|
360
|
+
try {
|
|
361
|
+
const stats = await fs.stat(filePath);
|
|
362
|
+
return {
|
|
363
|
+
ok: true,
|
|
364
|
+
size: stats.size
|
|
365
|
+
};
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const code = getErrorCode(error);
|
|
368
|
+
if (code === "ENOENT") {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
code,
|
|
372
|
+
reason: "The file disappeared while Qodfy was scanning it."
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
code,
|
|
379
|
+
reason: "Qodfy does not have permission to check this file."
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
code,
|
|
385
|
+
reason: "Qodfy could not check this file."
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
261
389
|
async function safeReadJson(filePath) {
|
|
262
390
|
const fileResult = await safeReadFile(filePath);
|
|
263
391
|
if (!fileResult.ok) {
|
|
@@ -312,18 +440,55 @@ function getUsedEnvVariables(content) {
|
|
|
312
440
|
const variables = /* @__PURE__ */ new Set();
|
|
313
441
|
const dotAccessPattern = /\bprocess\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
314
442
|
const bracketAccessPattern = /\bprocess\.env\[['"`]([A-Za-z_][A-Za-z0-9_]*)['"`]\]/g;
|
|
443
|
+
const destructuredEnvPattern = /\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*process\.env\b/g;
|
|
315
444
|
for (const match of content.matchAll(dotAccessPattern)) {
|
|
316
445
|
variables.add(match[1]);
|
|
317
446
|
}
|
|
318
447
|
for (const match of content.matchAll(bracketAccessPattern)) {
|
|
319
448
|
variables.add(match[1]);
|
|
320
449
|
}
|
|
450
|
+
for (const match of content.matchAll(destructuredEnvPattern)) {
|
|
451
|
+
for (const variableName of parseDestructuredEnvNames(match[1])) {
|
|
452
|
+
variables.add(variableName);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
321
455
|
return variables;
|
|
322
456
|
}
|
|
457
|
+
function parseDestructuredEnvNames(destructuredContent) {
|
|
458
|
+
const variables = [];
|
|
459
|
+
for (const part of destructuredContent.split(",")) {
|
|
460
|
+
const variableName = part.trim().split(":")[0].split("=")[0].trim();
|
|
461
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(variableName)) {
|
|
462
|
+
variables.push(variableName);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return variables;
|
|
466
|
+
}
|
|
467
|
+
function getHardcodedSecretMatches(content) {
|
|
468
|
+
const matches = [];
|
|
469
|
+
for (const secretPattern of hardcodedSecretPatterns) {
|
|
470
|
+
secretPattern.pattern.lastIndex = 0;
|
|
471
|
+
if (secretPattern.pattern.test(content)) {
|
|
472
|
+
matches.push({ label: secretPattern.label });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return matches;
|
|
476
|
+
}
|
|
323
477
|
function isClientSideFile(relativeFile, content) {
|
|
324
478
|
const fileName = path.basename(relativeFile);
|
|
325
479
|
return fileName.includes(".client.") || /(^|\n)\s*["']use client["'];?/.test(content);
|
|
326
480
|
}
|
|
481
|
+
function isApiRoute(filePath) {
|
|
482
|
+
return filePath.includes(`${path.sep}app${path.sep}api${path.sep}`) || filePath.includes(`${path.sep}pages${path.sep}api${path.sep}`);
|
|
483
|
+
}
|
|
484
|
+
function isStripeWebhookRoute(relativeFile, content) {
|
|
485
|
+
const normalizedFile = relativeFile.toLowerCase();
|
|
486
|
+
const normalizedContent = content.toLowerCase();
|
|
487
|
+
return normalizedContent.includes("stripe") && (normalizedFile.includes("webhook") || normalizedContent.includes("webhook") || normalizedContent.includes("stripe.webhooks"));
|
|
488
|
+
}
|
|
489
|
+
function hasStripeWebhookSignatureVerification(content) {
|
|
490
|
+
return content.includes("stripe.webhooks.constructEvent") || content.includes("webhooks.constructEvent") || content.includes("constructEvent(");
|
|
491
|
+
}
|
|
327
492
|
function isPackageJsonObject(data) {
|
|
328
493
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
329
494
|
}
|