@qodfy/core 0.2.3 → 0.2.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 +6 -1
- package/dist/index.js +205 -89
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
type IssueSeverity = "critical" | "warning" | "info";
|
|
2
2
|
type IssueConfidence = "high" | "medium" | "low";
|
|
3
|
+
type IssueEvidence = {
|
|
4
|
+
label: string;
|
|
5
|
+
detail?: string;
|
|
6
|
+
};
|
|
3
7
|
type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
|
|
4
8
|
declare const validScanChecks: readonly ["project", "api", "environment", "ai", "webhook", "maintainability", "security"];
|
|
5
9
|
type ScanCheck = typeof validScanChecks[number];
|
|
@@ -15,6 +19,7 @@ type Issue = {
|
|
|
15
19
|
file?: string;
|
|
16
20
|
suggestion?: string;
|
|
17
21
|
fixPrompt?: string;
|
|
22
|
+
evidence?: IssueEvidence[];
|
|
18
23
|
};
|
|
19
24
|
type ScanReport = {
|
|
20
25
|
projectPath: string;
|
|
@@ -36,4 +41,4 @@ type ScanOptions = {
|
|
|
36
41
|
};
|
|
37
42
|
declare function scanProject(input: string | ScanOptions): Promise<ScanReport>;
|
|
38
43
|
|
|
39
|
-
export { type Issue, type IssueCategory, type IssueConfidence, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
|
|
44
|
+
export { type Issue, type IssueCategory, type IssueConfidence, type IssueEvidence, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
|
package/dist/index.js
CHANGED
|
@@ -114,10 +114,11 @@ var issueIdPrefixes = {
|
|
|
114
114
|
"environment-variable-missing-from-example": "environment-variable-missing-from-example",
|
|
115
115
|
"security-client-side-secret": "security-client-side-secret",
|
|
116
116
|
"security-hardcoded-secret": "security-hardcoded-secret",
|
|
117
|
-
"api-route-missing-auth": "
|
|
117
|
+
"sensitive-api-route-missing-auth": "sensitive-api-route-missing-auth",
|
|
118
118
|
"api-public-read-route": "api-public-read-route",
|
|
119
|
-
"
|
|
120
|
-
"
|
|
119
|
+
"public-form-missing-abuse-protection": "public-form-missing-abuse-protection",
|
|
120
|
+
"internal-route-missing-protection": "internal-route-missing-protection",
|
|
121
|
+
"api-mutation-route-review-auth": "api-mutation-route-review-auth",
|
|
121
122
|
"ai-route-missing-rate-limit": "ai-route-rate-limit",
|
|
122
123
|
"maintainability-large-file": "maintainability-large-file",
|
|
123
124
|
"maintainability-large-file-skipped": "maintainability-large-file-skipped",
|
|
@@ -326,7 +327,7 @@ async function scanProject(input) {
|
|
|
326
327
|
);
|
|
327
328
|
if (runAiChecks && usesAI) {
|
|
328
329
|
aiFiles++;
|
|
329
|
-
const hasRateLimit =
|
|
330
|
+
const hasRateLimit = hasRateLimitSignal(content);
|
|
330
331
|
if (apiRouteSet.has(file) && !hasRateLimit) {
|
|
331
332
|
addIssue({
|
|
332
333
|
ruleId: "ai-route-missing-rate-limit",
|
|
@@ -341,18 +342,23 @@ async function scanProject(input) {
|
|
|
341
342
|
});
|
|
342
343
|
}
|
|
343
344
|
}
|
|
344
|
-
const
|
|
345
|
-
|
|
345
|
+
const apiRouteAnalysis = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? analyzeApiRoute({
|
|
346
|
+
file,
|
|
347
|
+
relativeFile,
|
|
348
|
+
content
|
|
349
|
+
}) : null;
|
|
350
|
+
if (runWebhookChecks && apiRouteAnalysis?.intent === "webhook" && !apiRouteAnalysis.hasWebhookVerification) {
|
|
346
351
|
addIssue({
|
|
347
352
|
ruleId: "webhook-missing-signature-verification",
|
|
348
353
|
category: "webhook",
|
|
349
|
-
severity:
|
|
350
|
-
confidence:
|
|
354
|
+
severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
|
|
355
|
+
confidence: apiRouteAnalysis.confidence,
|
|
351
356
|
title: "Webhook route may be missing signature verification",
|
|
352
357
|
message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
|
|
353
358
|
file: relativeFile,
|
|
354
|
-
suggestion: getWebhookSignatureSuggestion(
|
|
355
|
-
fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
|
|
359
|
+
suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
|
|
360
|
+
fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
|
|
361
|
+
evidence: apiRouteAnalysis.evidence
|
|
356
362
|
});
|
|
357
363
|
}
|
|
358
364
|
if (runSecurityChecks) {
|
|
@@ -375,13 +381,11 @@ async function scanProject(input) {
|
|
|
375
381
|
});
|
|
376
382
|
}
|
|
377
383
|
}
|
|
378
|
-
if (runApiChecks &&
|
|
384
|
+
if (runApiChecks && apiRouteAnalysis) {
|
|
379
385
|
addApiRouteProtectionIssues({
|
|
380
386
|
addIssue,
|
|
381
|
-
content,
|
|
382
387
|
includeLowConfidence,
|
|
383
|
-
|
|
384
|
-
webhookRouteInfo
|
|
388
|
+
analysis: apiRouteAnalysis
|
|
385
389
|
});
|
|
386
390
|
}
|
|
387
391
|
const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
|
|
@@ -686,108 +690,123 @@ function isApiRoute(filePath) {
|
|
|
686
690
|
}
|
|
687
691
|
function addApiRouteProtectionIssues({
|
|
688
692
|
addIssue,
|
|
689
|
-
content,
|
|
690
693
|
includeLowConfidence,
|
|
691
|
-
|
|
692
|
-
webhookRouteInfo
|
|
694
|
+
analysis
|
|
693
695
|
}) {
|
|
694
|
-
|
|
695
|
-
const hasAuth = hasAuthOrSessionCheck(content);
|
|
696
|
-
const methods = getHttpMethods(content);
|
|
697
|
-
if (intent === "webhook") {
|
|
696
|
+
if (analysis.intent === "webhook") {
|
|
698
697
|
return;
|
|
699
698
|
}
|
|
700
|
-
if (intent === "public-read") {
|
|
699
|
+
if (analysis.intent === "public-read") {
|
|
701
700
|
if (includeLowConfidence) {
|
|
702
701
|
addIssue({
|
|
703
702
|
ruleId: "api-public-read-route",
|
|
704
703
|
category: "api",
|
|
705
704
|
severity: "info",
|
|
706
|
-
confidence:
|
|
705
|
+
confidence: analysis.confidence,
|
|
707
706
|
title: "Public read API route detected",
|
|
708
707
|
message: "This route appears intentionally public. Authentication may not be required.",
|
|
709
|
-
file: relativeFile,
|
|
708
|
+
file: analysis.relativeFile,
|
|
710
709
|
suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
|
|
711
|
-
fixPrompt: createPublicReadRouteFixPrompt(relativeFile)
|
|
710
|
+
fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
|
|
711
|
+
evidence: analysis.evidence
|
|
712
712
|
});
|
|
713
713
|
}
|
|
714
714
|
return;
|
|
715
715
|
}
|
|
716
|
-
if (intent === "public-form") {
|
|
717
|
-
if (!
|
|
716
|
+
if (analysis.intent === "public-form") {
|
|
717
|
+
if (!analysis.hasRateLimit && !analysis.hasValidation) {
|
|
718
718
|
addIssue({
|
|
719
|
-
ruleId: "
|
|
719
|
+
ruleId: "public-form-missing-abuse-protection",
|
|
720
720
|
category: "api",
|
|
721
721
|
severity: "warning",
|
|
722
|
-
confidence:
|
|
722
|
+
confidence: analysis.confidence,
|
|
723
723
|
title: "Public form route may be missing abuse protection",
|
|
724
724
|
message: "This route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.",
|
|
725
|
-
file: relativeFile,
|
|
725
|
+
file: analysis.relativeFile,
|
|
726
726
|
suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
|
|
727
|
-
fixPrompt: createPublicFormProtectionFixPrompt(relativeFile)
|
|
727
|
+
fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile),
|
|
728
|
+
evidence: analysis.evidence
|
|
728
729
|
});
|
|
729
730
|
}
|
|
730
731
|
return;
|
|
731
732
|
}
|
|
732
|
-
if (intent === "internal") {
|
|
733
|
-
if (!
|
|
733
|
+
if (analysis.intent === "internal") {
|
|
734
|
+
if (!analysis.hasAuth && !analysis.hasSecretProtection) {
|
|
734
735
|
addIssue({
|
|
735
|
-
ruleId: "
|
|
736
|
-
category: "
|
|
736
|
+
ruleId: "internal-route-missing-protection",
|
|
737
|
+
category: "security",
|
|
737
738
|
severity: "warning",
|
|
738
|
-
confidence:
|
|
739
|
+
confidence: analysis.confidence,
|
|
739
740
|
title: "Internal API route may be missing protection",
|
|
740
741
|
message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
|
|
741
|
-
file: relativeFile,
|
|
742
|
+
file: analysis.relativeFile,
|
|
742
743
|
suggestion: "Use the project's existing auth pattern or a secret token check for operational routes such as cron, cleanup, or revalidation.",
|
|
743
|
-
fixPrompt: createInternalRouteProtectionFixPrompt(relativeFile)
|
|
744
|
+
fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile),
|
|
745
|
+
evidence: analysis.evidence
|
|
744
746
|
});
|
|
745
747
|
}
|
|
746
748
|
return;
|
|
747
749
|
}
|
|
748
|
-
if (intent === "sensitive-mutation") {
|
|
749
|
-
if (!hasAuth) {
|
|
750
|
+
if (analysis.intent === "sensitive-mutation") {
|
|
751
|
+
if (!analysis.hasAuth) {
|
|
750
752
|
addIssue({
|
|
751
|
-
ruleId: "api-route-missing-auth",
|
|
752
|
-
category: "
|
|
753
|
+
ruleId: "sensitive-api-route-missing-auth",
|
|
754
|
+
category: "security",
|
|
753
755
|
severity: "warning",
|
|
754
|
-
confidence:
|
|
756
|
+
confidence: analysis.confidence,
|
|
755
757
|
title: "Sensitive API route may be missing authentication",
|
|
756
758
|
message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
|
|
757
|
-
file: relativeFile,
|
|
759
|
+
file: analysis.relativeFile,
|
|
758
760
|
suggestion: "Review the existing project auth/session pattern and apply it if this route handles private data, uploads, payments, or account changes.",
|
|
759
|
-
fixPrompt: createApiAuthFixPrompt(relativeFile)
|
|
761
|
+
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
762
|
+
evidence: analysis.evidence
|
|
760
763
|
});
|
|
761
764
|
}
|
|
762
765
|
return;
|
|
763
766
|
}
|
|
764
|
-
if (
|
|
767
|
+
if (analysis.authExpected === "review" && !analysis.hasAuth) {
|
|
765
768
|
addIssue({
|
|
766
|
-
ruleId: "api-route-
|
|
769
|
+
ruleId: "api-mutation-route-review-auth",
|
|
767
770
|
category: "api",
|
|
768
771
|
severity: "warning",
|
|
769
|
-
confidence:
|
|
772
|
+
confidence: analysis.confidence,
|
|
770
773
|
title: "API mutation route should be reviewed for authentication",
|
|
771
|
-
message: "This route
|
|
772
|
-
file: relativeFile,
|
|
774
|
+
message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
|
|
775
|
+
file: analysis.relativeFile,
|
|
773
776
|
suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
|
|
774
|
-
fixPrompt: createApiAuthFixPrompt(relativeFile)
|
|
777
|
+
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
778
|
+
evidence: analysis.evidence
|
|
775
779
|
});
|
|
776
780
|
}
|
|
777
781
|
}
|
|
778
|
-
function
|
|
782
|
+
function analyzeApiRoute({
|
|
783
|
+
file,
|
|
784
|
+
relativeFile,
|
|
785
|
+
content
|
|
786
|
+
}) {
|
|
779
787
|
const normalizedFile = relativeFile.toLowerCase();
|
|
780
|
-
const methods =
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
const methods = getRouteHttpMethods(content);
|
|
789
|
+
const evidence = [];
|
|
790
|
+
const webhookRouteInfo = getWebhookRouteInfo(relativeFile, content);
|
|
791
|
+
const hasAuth = hasAuthOrSessionCheck(content);
|
|
792
|
+
const hasSecretProtection = hasSecretProtectionSignal(content);
|
|
793
|
+
const hasRateLimit = hasRateLimitSignal(content);
|
|
794
|
+
const hasValidation = hasValidationSignal(content);
|
|
795
|
+
const hasCacheHeaders = hasCacheHeaderSignal(content);
|
|
796
|
+
const hasMethodBlocking = hasMethodBlockingSignal(content);
|
|
797
|
+
const webhookProvider = webhookRouteInfo?.provider ?? "unknown";
|
|
798
|
+
const hasWebhookVerification = hasWebhookSignatureVerification(content, webhookProvider);
|
|
799
|
+
if (methods.length > 0) {
|
|
800
|
+
for (const method of methods) {
|
|
801
|
+
evidence.push({ label: "exports", detail: method });
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
evidence.push({ label: "no exported HTTP method detected" });
|
|
789
805
|
}
|
|
790
|
-
|
|
806
|
+
const webhookPathMatch = getRoutePathMatch(normalizedFile, ["webhook", "webhooks", "callback"]);
|
|
807
|
+
const internalPathMatch = getRoutePathMatch(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"]);
|
|
808
|
+
const formPathMatch = getRoutePathMatch(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"]);
|
|
809
|
+
const sensitivePathMatch = getRoutePathMatch(normalizedFile, [
|
|
791
810
|
"upload",
|
|
792
811
|
"checkout",
|
|
793
812
|
"order",
|
|
@@ -802,10 +821,8 @@ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
|
|
|
802
821
|
"cart",
|
|
803
822
|
"profile",
|
|
804
823
|
"settings"
|
|
805
|
-
])
|
|
806
|
-
|
|
807
|
-
}
|
|
808
|
-
if (routePathHasAny(normalizedFile, [
|
|
824
|
+
]);
|
|
825
|
+
const publicContentPathMatch = getRoutePathMatch(normalizedFile, [
|
|
809
826
|
"blog",
|
|
810
827
|
"blogs",
|
|
811
828
|
"post",
|
|
@@ -818,56 +835,155 @@ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
|
|
|
818
835
|
"categories",
|
|
819
836
|
"sitemap",
|
|
820
837
|
"rss"
|
|
821
|
-
])
|
|
822
|
-
|
|
838
|
+
]);
|
|
839
|
+
let intent = "unknown";
|
|
840
|
+
if (webhookRouteInfo || webhookPathMatch) {
|
|
841
|
+
intent = "webhook";
|
|
842
|
+
evidence.push({
|
|
843
|
+
label: webhookPathMatch ? "webhook path detected" : "webhook content detected",
|
|
844
|
+
detail: webhookPathMatch ?? webhookProvider
|
|
845
|
+
});
|
|
846
|
+
} else if (internalPathMatch) {
|
|
847
|
+
intent = "internal";
|
|
848
|
+
evidence.push({ label: "path contains", detail: internalPathMatch });
|
|
849
|
+
} else if (formPathMatch) {
|
|
850
|
+
intent = "public-form";
|
|
851
|
+
evidence.push({ label: "path contains", detail: formPathMatch });
|
|
852
|
+
} else if (hasMutationMethod(methods) && sensitivePathMatch) {
|
|
853
|
+
intent = "sensitive-mutation";
|
|
854
|
+
evidence.push({ label: "path contains", detail: sensitivePathMatch });
|
|
855
|
+
} else if (methods.includes("GET") && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
|
|
856
|
+
intent = "public-read";
|
|
857
|
+
evidence.push({ label: "public content route detected", detail: publicContentPathMatch });
|
|
823
858
|
}
|
|
824
|
-
if (
|
|
825
|
-
|
|
859
|
+
if (hasAuth) {
|
|
860
|
+
evidence.push({ label: "auth/session check detected" });
|
|
861
|
+
} else {
|
|
862
|
+
evidence.push({ label: "no auth/session check detected" });
|
|
826
863
|
}
|
|
827
|
-
if (
|
|
828
|
-
|
|
864
|
+
if (hasSecretProtection) {
|
|
865
|
+
evidence.push({ label: "secret token check detected" });
|
|
829
866
|
}
|
|
830
|
-
|
|
867
|
+
if (hasRateLimit) {
|
|
868
|
+
evidence.push({ label: "rate limit detected" });
|
|
869
|
+
} else if (intent === "public-form") {
|
|
870
|
+
evidence.push({ label: "no rate limit detected" });
|
|
871
|
+
}
|
|
872
|
+
if (hasValidation) {
|
|
873
|
+
evidence.push({ label: "validation detected" });
|
|
874
|
+
} else if (intent === "public-form") {
|
|
875
|
+
evidence.push({ label: "no validation detected" });
|
|
876
|
+
}
|
|
877
|
+
if (hasCacheHeaders) {
|
|
878
|
+
evidence.push({ label: "cache/public-read safety signal detected" });
|
|
879
|
+
}
|
|
880
|
+
if (hasMethodBlocking) {
|
|
881
|
+
evidence.push({ label: "method blocking detected" });
|
|
882
|
+
}
|
|
883
|
+
if (intent === "webhook") {
|
|
884
|
+
if (hasWebhookVerification) {
|
|
885
|
+
evidence.push({ label: "webhook signature verification detected" });
|
|
886
|
+
} else {
|
|
887
|
+
evidence.push({ label: "no webhook signature verification detected" });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
file,
|
|
892
|
+
relativeFile,
|
|
893
|
+
methods,
|
|
894
|
+
intent,
|
|
895
|
+
authExpected: getAuthExpectation(intent, methods),
|
|
896
|
+
confidence: getApiRouteConfidence(intent, methods, webhookRouteInfo),
|
|
897
|
+
evidence,
|
|
898
|
+
hasAuth,
|
|
899
|
+
hasSecretProtection,
|
|
900
|
+
hasRateLimit,
|
|
901
|
+
hasValidation,
|
|
902
|
+
hasCacheHeaders,
|
|
903
|
+
hasMethodBlocking,
|
|
904
|
+
hasWebhookVerification,
|
|
905
|
+
webhookProvider
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function getAuthExpectation(intent, methods) {
|
|
909
|
+
if (intent === "internal" || intent === "sensitive-mutation") {
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
if (intent === "unknown" && hasMutationMethod(methods)) {
|
|
913
|
+
return "review";
|
|
914
|
+
}
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
function getApiRouteConfidence(intent, methods, webhookRouteInfo) {
|
|
918
|
+
if (intent === "sensitive-mutation" || intent === "internal") {
|
|
919
|
+
return "high";
|
|
920
|
+
}
|
|
921
|
+
if (intent === "webhook") {
|
|
922
|
+
return webhookRouteInfo?.confidence === "high" ? "high" : "medium";
|
|
923
|
+
}
|
|
924
|
+
if (intent === "public-form" || intent === "unknown" && hasMutationMethod(methods)) {
|
|
925
|
+
return "medium";
|
|
926
|
+
}
|
|
927
|
+
return "low";
|
|
831
928
|
}
|
|
832
|
-
function
|
|
833
|
-
return terms.
|
|
929
|
+
function getRoutePathMatch(normalizedFile, terms) {
|
|
930
|
+
return terms.find((term) => {
|
|
834
931
|
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
835
932
|
return new RegExp(`(^|[\\/._\\[\\]-])${escapedTerm}([\\/._\\[\\]-]|$)`).test(normalizedFile);
|
|
836
933
|
});
|
|
837
934
|
}
|
|
838
|
-
function
|
|
935
|
+
function getExportedHttpMethods(content) {
|
|
839
936
|
const methods = /* @__PURE__ */ new Set();
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
|
|
937
|
+
const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
938
|
+
const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
939
|
+
for (const match of content.matchAll(functionExportPattern)) {
|
|
940
|
+
methods.add(match[1]);
|
|
941
|
+
}
|
|
942
|
+
for (const match of content.matchAll(constExportPattern)) {
|
|
844
943
|
methods.add(match[1]);
|
|
845
944
|
}
|
|
945
|
+
return [...methods];
|
|
946
|
+
}
|
|
947
|
+
function getRouteHttpMethods(content) {
|
|
948
|
+
const methods = new Set(getExportedHttpMethods(content));
|
|
949
|
+
const requestMethodPattern = /\b(?:request|req)\.method\s*(?:={2,3}|!={1,2})\s*["'](GET|POST|PUT|PATCH|DELETE)["']/g;
|
|
950
|
+
const methodCasePattern = /\bcase\s+["'](GET|POST|PUT|PATCH|DELETE)["']/g;
|
|
846
951
|
for (const match of content.matchAll(requestMethodPattern)) {
|
|
847
952
|
methods.add(match[1]);
|
|
848
953
|
}
|
|
849
954
|
for (const match of content.matchAll(methodCasePattern)) {
|
|
850
955
|
methods.add(match[1]);
|
|
851
956
|
}
|
|
852
|
-
return methods;
|
|
957
|
+
return [...methods];
|
|
853
958
|
}
|
|
854
959
|
function hasMutationMethod(methods) {
|
|
855
|
-
return ["POST", "PUT", "PATCH", "DELETE"].some(
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
return [...methods].every((method) => method === "GET" || method === "HEAD" || method === "OPTIONS");
|
|
960
|
+
return ["POST", "PUT", "PATCH", "DELETE"].some(
|
|
961
|
+
(method) => methods.includes(method)
|
|
962
|
+
);
|
|
859
963
|
}
|
|
860
964
|
function hasAuthOrSessionCheck(content) {
|
|
861
965
|
const normalizedContent = content.toLowerCase();
|
|
862
966
|
return normalizedContent.includes("auth(") || normalizedContent.includes("getserversession") || normalizedContent.includes("currentuser") || normalizedContent.includes("clerkclient") || normalizedContent.includes("session") || normalizedContent.includes("requireauth") || normalizedContent.includes("requireuser") || normalizedContent.includes("requireadmin") || normalizedContent.includes("verifysession") || normalizedContent.includes("getuser") || normalizedContent.includes("jwt") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("cookies()") || normalizedContent.includes("request.cookies") || normalizedContent.includes("middleware");
|
|
863
967
|
}
|
|
864
|
-
function
|
|
968
|
+
function hasSecretProtectionSignal(content) {
|
|
969
|
+
const normalizedContent = content.toLowerCase();
|
|
970
|
+
return /\bprocess\.env\.[A-Za-z0-9_]*SECRET\b/.test(content) || /\bprocess\.env\[['"`][A-Za-z0-9_]*SECRET['"`]\]/.test(content) || normalizedContent.includes("cron_secret") || normalizedContent.includes("revalidate_secret") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("token");
|
|
971
|
+
}
|
|
972
|
+
function hasRateLimitSignal(content) {
|
|
973
|
+
const normalizedContent = content.toLowerCase();
|
|
974
|
+
return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
|
|
975
|
+
}
|
|
976
|
+
function hasValidationSignal(content) {
|
|
977
|
+
const normalizedContent = content.toLowerCase();
|
|
978
|
+
return normalizedContent.includes("zod") || normalizedContent.includes("schema") || normalizedContent.includes("validate") || normalizedContent.includes("validation") || normalizedContent.includes("sanitize") || normalizedContent.includes("safeparse") || normalizedContent.includes("parse(") || normalizedContent.includes("slugregex") || normalizedContent.includes("isvalid") || normalizedContent.includes("captcha") || normalizedContent.includes("turnstile") || normalizedContent.includes("recaptcha") || normalizedContent.includes("hcaptcha");
|
|
979
|
+
}
|
|
980
|
+
function hasCacheHeaderSignal(content) {
|
|
865
981
|
const normalizedContent = content.toLowerCase();
|
|
866
|
-
return
|
|
982
|
+
return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published") || normalizedContent.includes("status");
|
|
867
983
|
}
|
|
868
|
-
function
|
|
984
|
+
function hasMethodBlockingSignal(content) {
|
|
869
985
|
const normalizedContent = content.toLowerCase();
|
|
870
|
-
return normalizedContent.includes("
|
|
986
|
+
return normalizedContent.includes("method not allowed") || normalizedContent.includes("status: 405") || /\b405\b/.test(normalizedContent);
|
|
871
987
|
}
|
|
872
988
|
function getWebhookRouteInfo(relativeFile, content) {
|
|
873
989
|
const normalizedFile = relativeFile.toLowerCase();
|