@qodfy/core 0.1.5 → 0.1.6
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.js +77 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -236,26 +236,14 @@ async function scanProject(projectPath) {
|
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const isClerkWebhook = isClerkWebhookRoute(relativeFile, content);
|
|
242
|
-
const hasClerkSignatureVerification = hasClerkWebhookSignatureVerification(content);
|
|
243
|
-
if (isStripeWebhook && !hasStripeSignatureVerification) {
|
|
239
|
+
const webhookRouteInfo = apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
|
|
240
|
+
if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
|
|
244
241
|
issues.push({
|
|
245
|
-
severity: "critical",
|
|
246
|
-
title: "
|
|
247
|
-
message: "This
|
|
242
|
+
severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
|
|
243
|
+
title: "Webhook route may be missing signature verification",
|
|
244
|
+
message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
|
|
248
245
|
file: relativeFile,
|
|
249
|
-
suggestion:
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
if (isClerkWebhook && !hasClerkSignatureVerification) {
|
|
253
|
-
issues.push({
|
|
254
|
-
severity: "critical",
|
|
255
|
-
title: "Clerk webhook may be missing signature verification",
|
|
256
|
-
message: "This Clerk webhook route does not appear to verify the webhook signature before handling the event.",
|
|
257
|
-
file: relativeFile,
|
|
258
|
-
suggestion: "Use svix Webhook(...).verify(...) with CLERK_WEBHOOK_SECRET and Clerk webhook headers."
|
|
246
|
+
suggestion: getWebhookSignatureSuggestion(webhookRouteInfo.provider)
|
|
259
247
|
});
|
|
260
248
|
}
|
|
261
249
|
for (const secretMatch of getHardcodedSecretMatches(content)) {
|
|
@@ -274,7 +262,7 @@ async function scanProject(projectPath) {
|
|
|
274
262
|
}
|
|
275
263
|
if (apiRouteSet.has(file)) {
|
|
276
264
|
const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
|
|
277
|
-
if (!hasAuth && !
|
|
265
|
+
if (!hasAuth && !webhookRouteInfo) {
|
|
278
266
|
issues.push({
|
|
279
267
|
severity: "warning",
|
|
280
268
|
title: "API route may be missing authentication",
|
|
@@ -536,23 +524,83 @@ function isClientSideFile(relativeFile, content) {
|
|
|
536
524
|
return fileName.includes(".client.") || /(^|\n)\s*["']use client["'];?/.test(content);
|
|
537
525
|
}
|
|
538
526
|
function isApiRoute(filePath) {
|
|
539
|
-
|
|
527
|
+
const normalizedFile = normalizePath(filePath);
|
|
528
|
+
const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
|
|
529
|
+
return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
|
|
540
530
|
}
|
|
541
|
-
function
|
|
531
|
+
function getWebhookRouteInfo(relativeFile, content) {
|
|
542
532
|
const normalizedFile = relativeFile.toLowerCase();
|
|
543
533
|
const normalizedContent = content.toLowerCase();
|
|
544
|
-
|
|
534
|
+
const normalizedRouteContext = `${normalizedFile}
|
|
535
|
+
${normalizedContent}`;
|
|
536
|
+
const pathLooksLikeWebhook = normalizedFile.includes("webhook") || normalizedFile.includes("callback");
|
|
537
|
+
const contentStronglySuggestsWebhook = normalizedContent.includes("stripe.webhooks") || normalizedContent.includes("constructevent(") || normalizedContent.includes("stripe-signature") || normalizedContent.includes("stripe_webhook_secret") || normalizedContent.includes("clerk_webhook_secret") || normalizedContent.includes("svix") || normalizedContent.includes("x-github-event") || normalizedContent.includes("x-hub-signature") || normalizedContent.includes("x-shopify-hmac-sha256") || normalizedContent.includes("resend") && normalizedContent.includes("webhook") || normalizedContent.includes("webhook_secret") || normalizedContent.includes("webhooksecret") || normalizedContent.includes("webhook") && (normalizedContent.includes("signature") || normalizedContent.includes("secret") || normalizedContent.includes("event"));
|
|
538
|
+
if (!pathLooksLikeWebhook && !contentStronglySuggestsWebhook) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const provider = getWebhookProvider(normalizedRouteContext);
|
|
542
|
+
return {
|
|
543
|
+
provider,
|
|
544
|
+
confidence: provider === "unknown" ? "likely" : "high"
|
|
545
|
+
};
|
|
545
546
|
}
|
|
546
|
-
function
|
|
547
|
-
|
|
547
|
+
function getWebhookProvider(normalizedRouteContext) {
|
|
548
|
+
if (normalizedRouteContext.includes("stripe-signature") || normalizedRouteContext.includes("stripe_webhook_secret") || normalizedRouteContext.includes("stripe.webhooks") || normalizedRouteContext.includes("constructevent(") || normalizedRouteContext.includes("stripe") && normalizedRouteContext.includes("webhook")) {
|
|
549
|
+
return "stripe";
|
|
550
|
+
}
|
|
551
|
+
if (normalizedRouteContext.includes("resend") && normalizedRouteContext.includes("webhook")) {
|
|
552
|
+
return "resend";
|
|
553
|
+
}
|
|
554
|
+
if (normalizedRouteContext.includes("clerk_webhook_secret") || normalizedRouteContext.includes("clerk") && normalizedRouteContext.includes("webhook")) {
|
|
555
|
+
return "clerk";
|
|
556
|
+
}
|
|
557
|
+
if (normalizedRouteContext.includes("x-github-event") || normalizedRouteContext.includes("x-hub-signature")) {
|
|
558
|
+
return "github";
|
|
559
|
+
}
|
|
560
|
+
if (normalizedRouteContext.includes("x-shopify-hmac-sha256")) {
|
|
561
|
+
return "shopify";
|
|
562
|
+
}
|
|
563
|
+
return "unknown";
|
|
548
564
|
}
|
|
549
|
-
function
|
|
550
|
-
const normalizedFile = relativeFile.toLowerCase();
|
|
565
|
+
function hasWebhookSignatureVerification(content, provider) {
|
|
551
566
|
const normalizedContent = content.toLowerCase();
|
|
552
|
-
|
|
567
|
+
if (provider === "stripe") {
|
|
568
|
+
return normalizedContent.includes("stripe.webhooks.constructevent") || normalizedContent.includes("webhooks.constructevent") || normalizedContent.includes("constructevent(");
|
|
569
|
+
}
|
|
570
|
+
if (provider === "clerk") {
|
|
571
|
+
return (normalizedContent.includes("new webhook(") || normalizedContent.includes("webhook(") || normalizedContent.includes("svix")) && (normalizedContent.includes(".verify(") || normalizedContent.includes("verify(") || normalizedContent.includes("verifywebhook"));
|
|
572
|
+
}
|
|
573
|
+
if (provider === "github") {
|
|
574
|
+
return (normalizedContent.includes("x-hub-signature") || normalizedContent.includes("x-hub-signature-256")) && hasHmacOrVerifyCall(normalizedContent);
|
|
575
|
+
}
|
|
576
|
+
if (provider === "shopify") {
|
|
577
|
+
return normalizedContent.includes("x-shopify-hmac-sha256") && hasHmacOrVerifyCall(normalizedContent);
|
|
578
|
+
}
|
|
579
|
+
if (provider === "resend") {
|
|
580
|
+
return normalizedContent.includes("verifywebhook") || (normalizedContent.includes("new webhook(") || normalizedContent.includes("webhook(") || normalizedContent.includes("svix")) && hasHmacOrVerifyCall(normalizedContent);
|
|
581
|
+
}
|
|
582
|
+
return normalizedContent.includes("constructevent(") || normalizedContent.includes("verifywebhook") || normalizedContent.includes("signature") && hasHmacOrVerifyCall(normalizedContent) || normalizedContent.includes("webhook") && normalizedContent.includes("verify(");
|
|
553
583
|
}
|
|
554
|
-
function
|
|
555
|
-
return
|
|
584
|
+
function hasHmacOrVerifyCall(normalizedContent) {
|
|
585
|
+
return normalizedContent.includes("verify(") || normalizedContent.includes(".verify(") || normalizedContent.includes("verifywebhook") || normalizedContent.includes("createhmac") || normalizedContent.includes("timingsafeequal") || normalizedContent.includes("subtle.verify");
|
|
586
|
+
}
|
|
587
|
+
function getWebhookSignatureSuggestion(provider) {
|
|
588
|
+
if (provider === "stripe") {
|
|
589
|
+
return "Use stripe.webhooks.constructEvent(...) with the Stripe signature header before handling the event.";
|
|
590
|
+
}
|
|
591
|
+
if (provider === "clerk") {
|
|
592
|
+
return "Verify the event with Svix before handling it.";
|
|
593
|
+
}
|
|
594
|
+
if (provider === "github") {
|
|
595
|
+
return "Verify the GitHub signature using the raw request body, X-Hub-Signature-256 header, and webhook secret.";
|
|
596
|
+
}
|
|
597
|
+
if (provider === "shopify") {
|
|
598
|
+
return "Verify the Shopify HMAC using the raw request body, X-Shopify-Hmac-Sha256 header, and webhook secret.";
|
|
599
|
+
}
|
|
600
|
+
if (provider === "resend") {
|
|
601
|
+
return "Verify the Resend webhook signature before handling the event.";
|
|
602
|
+
}
|
|
603
|
+
return "Verify the provider signature using the raw request body and signature header before trusting the event.";
|
|
556
604
|
}
|
|
557
605
|
function isPackageJsonObject(data) {
|
|
558
606
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|