@fourteensystems/prodcheck 0.3.0
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 +252 -0
- package/bin/prodcheck.mjs +2 -0
- package/dist/cli/commands/baseline.d.ts +7 -0
- package/dist/cli/commands/baseline.d.ts.map +1 -0
- package/dist/cli/commands/baseline.js +22 -0
- package/dist/cli/commands/baseline.js.map +1 -0
- package/dist/cli/commands/ci.d.ts +14 -0
- package/dist/cli/commands/ci.d.ts.map +1 -0
- package/dist/cli/commands/ci.js +104 -0
- package/dist/cli/commands/ci.js.map +1 -0
- package/dist/cli/commands/explain.d.ts +2 -0
- package/dist/cli/commands/explain.d.ts.map +1 -0
- package/dist/cli/commands/explain.js +20 -0
- package/dist/cli/commands/explain.js.map +1 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +127 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +2 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +13 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +10 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +65 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/waive.d.ts +8 -0
- package/dist/cli/commands/waive.d.ts.map +1 -0
- package/dist/cli/commands/waive.js +34 -0
- package/dist/cli/commands/waive.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/engine/baseline.d.ts +11 -0
- package/dist/engine/baseline.d.ts.map +1 -0
- package/dist/engine/baseline.js +39 -0
- package/dist/engine/baseline.js.map +1 -0
- package/dist/engine/baseline.test.d.ts +2 -0
- package/dist/engine/baseline.test.d.ts.map +1 -0
- package/dist/engine/baseline.test.js +135 -0
- package/dist/engine/baseline.test.js.map +1 -0
- package/dist/engine/config.d.ts +8 -0
- package/dist/engine/config.d.ts.map +1 -0
- package/dist/engine/config.js +134 -0
- package/dist/engine/config.js.map +1 -0
- package/dist/engine/config.test.d.ts +2 -0
- package/dist/engine/config.test.d.ts.map +1 -0
- package/dist/engine/config.test.js +107 -0
- package/dist/engine/config.test.js.map +1 -0
- package/dist/engine/extensions/load.d.ts +11 -0
- package/dist/engine/extensions/load.d.ts.map +1 -0
- package/dist/engine/extensions/load.js +26 -0
- package/dist/engine/extensions/load.js.map +1 -0
- package/dist/engine/extensions/registry.d.ts +5 -0
- package/dist/engine/extensions/registry.d.ts.map +1 -0
- package/dist/engine/extensions/registry.js +11 -0
- package/dist/engine/extensions/registry.js.map +1 -0
- package/dist/engine/extensions/types.d.ts +51 -0
- package/dist/engine/extensions/types.d.ts.map +1 -0
- package/dist/engine/extensions/types.js +2 -0
- package/dist/engine/extensions/types.js.map +1 -0
- package/dist/engine/license.d.ts +40 -0
- package/dist/engine/license.d.ts.map +1 -0
- package/dist/engine/license.js +104 -0
- package/dist/engine/license.js.map +1 -0
- package/dist/engine/report.d.ts +5 -0
- package/dist/engine/report.d.ts.map +1 -0
- package/dist/engine/report.js +115 -0
- package/dist/engine/report.js.map +1 -0
- package/dist/engine/run.d.ts +11 -0
- package/dist/engine/run.d.ts.map +1 -0
- package/dist/engine/run.js +105 -0
- package/dist/engine/run.js.map +1 -0
- package/dist/engine/sarif.d.ts +3 -0
- package/dist/engine/sarif.d.ts.map +1 -0
- package/dist/engine/sarif.js +58 -0
- package/dist/engine/sarif.js.map +1 -0
- package/dist/engine/sarif.test.d.ts +2 -0
- package/dist/engine/sarif.test.d.ts.map +1 -0
- package/dist/engine/sarif.test.js +152 -0
- package/dist/engine/sarif.test.js.map +1 -0
- package/dist/engine/score.d.ts +13 -0
- package/dist/engine/score.d.ts.map +1 -0
- package/dist/engine/score.js +116 -0
- package/dist/engine/score.js.map +1 -0
- package/dist/engine/score.test.d.ts +2 -0
- package/dist/engine/score.test.d.ts.map +1 -0
- package/dist/engine/score.test.js +227 -0
- package/dist/engine/score.test.js.map +1 -0
- package/dist/engine/types.d.ts +123 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +2 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/engine/version.d.ts +5 -0
- package/dist/engine/version.d.ts.map +1 -0
- package/dist/engine/version.js +15 -0
- package/dist/engine/version.js.map +1 -0
- package/dist/engine/waivers.d.ts +9 -0
- package/dist/engine/waivers.d.ts.map +1 -0
- package/dist/engine/waivers.js +55 -0
- package/dist/engine/waivers.js.map +1 -0
- package/dist/engine/waivers.test.d.ts +2 -0
- package/dist/engine/waivers.test.d.ts.map +1 -0
- package/dist/engine/waivers.test.js +147 -0
- package/dist/engine/waivers.test.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/next/deps.d.ts +4 -0
- package/dist/next/deps.d.ts.map +1 -0
- package/dist/next/deps.js +118 -0
- package/dist/next/deps.js.map +1 -0
- package/dist/next/deps.test.d.ts +2 -0
- package/dist/next/deps.test.d.ts.map +1 -0
- package/dist/next/deps.test.js +249 -0
- package/dist/next/deps.test.js.map +1 -0
- package/dist/next/detect.d.ts +10 -0
- package/dist/next/detect.d.ts.map +1 -0
- package/dist/next/detect.js +57 -0
- package/dist/next/detect.js.map +1 -0
- package/dist/next/detect.test.d.ts +2 -0
- package/dist/next/detect.test.d.ts.map +1 -0
- package/dist/next/detect.test.js +74 -0
- package/dist/next/detect.test.js.map +1 -0
- package/dist/next/index.d.ts +5 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +59 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/middleware.d.ts +3 -0
- package/dist/next/middleware.d.ts.map +1 -0
- package/dist/next/middleware.js +48 -0
- package/dist/next/middleware.js.map +1 -0
- package/dist/next/middleware.test.d.ts +2 -0
- package/dist/next/middleware.test.d.ts.map +1 -0
- package/dist/next/middleware.test.js +203 -0
- package/dist/next/middleware.test.js.map +1 -0
- package/dist/next/routes.d.ts +10 -0
- package/dist/next/routes.d.ts.map +1 -0
- package/dist/next/routes.js +172 -0
- package/dist/next/routes.js.map +1 -0
- package/dist/next/routes.test.d.ts +2 -0
- package/dist/next/routes.test.d.ts.map +1 -0
- package/dist/next/routes.test.js +175 -0
- package/dist/next/routes.test.js.map +1 -0
- package/dist/next/server-actions.d.ts +4 -0
- package/dist/next/server-actions.d.ts.map +1 -0
- package/dist/next/server-actions.js +107 -0
- package/dist/next/server-actions.js.map +1 -0
- package/dist/next/server-actions.test.d.ts +2 -0
- package/dist/next/server-actions.test.d.ts.map +1 -0
- package/dist/next/server-actions.test.js +138 -0
- package/dist/next/server-actions.test.js.map +1 -0
- package/dist/next/trpc.d.ts +3 -0
- package/dist/next/trpc.d.ts.map +1 -0
- package/dist/next/trpc.js +312 -0
- package/dist/next/trpc.js.map +1 -0
- package/dist/next/types.d.ts +144 -0
- package/dist/next/types.d.ts.map +1 -0
- package/dist/next/types.js +2 -0
- package/dist/next/types.js.map +1 -0
- package/dist/next/wrappers.d.ts +10 -0
- package/dist/next/wrappers.d.ts.map +1 -0
- package/dist/next/wrappers.js +536 -0
- package/dist/next/wrappers.js.map +1 -0
- package/dist/next/wrappers.test.d.ts +2 -0
- package/dist/next/wrappers.test.d.ts.map +1 -0
- package/dist/next/wrappers.test.js +361 -0
- package/dist/next/wrappers.test.js.map +1 -0
- package/dist/rules/auth-boundary-missing.d.ts +5 -0
- package/dist/rules/auth-boundary-missing.d.ts.map +1 -0
- package/dist/rules/auth-boundary-missing.js +463 -0
- package/dist/rules/auth-boundary-missing.js.map +1 -0
- package/dist/rules/auth-boundary-missing.test.d.ts +2 -0
- package/dist/rules/auth-boundary-missing.test.d.ts.map +1 -0
- package/dist/rules/auth-boundary-missing.test.js +492 -0
- package/dist/rules/auth-boundary-missing.test.js.map +1 -0
- package/dist/rules/index.d.ts +12 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +95 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/input-validation-missing.d.ts +5 -0
- package/dist/rules/input-validation-missing.d.ts.map +1 -0
- package/dist/rules/input-validation-missing.js +272 -0
- package/dist/rules/input-validation-missing.js.map +1 -0
- package/dist/rules/input-validation-missing.test.d.ts +2 -0
- package/dist/rules/input-validation-missing.test.d.ts.map +1 -0
- package/dist/rules/input-validation-missing.test.js +449 -0
- package/dist/rules/input-validation-missing.test.js.map +1 -0
- package/dist/rules/rate-limit-missing.d.ts +5 -0
- package/dist/rules/rate-limit-missing.d.ts.map +1 -0
- package/dist/rules/rate-limit-missing.js +316 -0
- package/dist/rules/rate-limit-missing.js.map +1 -0
- package/dist/rules/rate-limit-missing.test.d.ts +2 -0
- package/dist/rules/rate-limit-missing.test.d.ts.map +1 -0
- package/dist/rules/rate-limit-missing.test.js +381 -0
- package/dist/rules/rate-limit-missing.test.js.map +1 -0
- package/dist/rules/tenancy-scope-missing.d.ts +5 -0
- package/dist/rules/tenancy-scope-missing.d.ts.map +1 -0
- package/dist/rules/tenancy-scope-missing.js +149 -0
- package/dist/rules/tenancy-scope-missing.js.map +1 -0
- package/dist/rules/wrapper-unrecognized.d.ts +5 -0
- package/dist/rules/wrapper-unrecognized.d.ts.map +1 -0
- package/dist/rules/wrapper-unrecognized.js +81 -0
- package/dist/rules/wrapper-unrecognized.js.map +1 -0
- package/dist/util/hof.d.ts +22 -0
- package/dist/util/hof.d.ts.map +1 -0
- package/dist/util/hof.js +99 -0
- package/dist/util/hof.js.map +1 -0
- package/dist/util/hof.test.d.ts +2 -0
- package/dist/util/hof.test.d.ts.map +1 -0
- package/dist/util/hof.test.js +79 -0
- package/dist/util/hof.test.js.map +1 -0
- package/dist/util/monorepo.d.ts +6 -0
- package/dist/util/monorepo.d.ts.map +1 -0
- package/dist/util/monorepo.js +29 -0
- package/dist/util/monorepo.js.map +1 -0
- package/dist/util/outbound-fetch.d.ts +14 -0
- package/dist/util/outbound-fetch.d.ts.map +1 -0
- package/dist/util/outbound-fetch.js +59 -0
- package/dist/util/outbound-fetch.js.map +1 -0
- package/dist/util/outbound-fetch.test.d.ts +2 -0
- package/dist/util/outbound-fetch.test.d.ts.map +1 -0
- package/dist/util/outbound-fetch.test.js +83 -0
- package/dist/util/outbound-fetch.test.js.map +1 -0
- package/dist/util/paths.d.ts +6 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +18 -0
- package/dist/util/paths.js.map +1 -0
- package/dist/util/resolve.d.ts +30 -0
- package/dist/util/resolve.d.ts.map +1 -0
- package/dist/util/resolve.js +306 -0
- package/dist/util/resolve.js.map +1 -0
- package/dist/util/resolve.test.d.ts +2 -0
- package/dist/util/resolve.test.d.ts.map +1 -0
- package/dist/util/resolve.test.js +186 -0
- package/dist/util/resolve.test.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const RULE_ID = "WRAPPER-UNRECOGNIZED";
|
|
2
|
+
const SEVERITY_RANK = { critical: 4, high: 3, med: 2, low: 1 };
|
|
3
|
+
function capSeverity(computed, max) {
|
|
4
|
+
const maxRank = SEVERITY_RANK[max] ?? 4;
|
|
5
|
+
const computedRank = SEVERITY_RANK[computed] ?? 2;
|
|
6
|
+
return computedRank > maxRank ? max : computed;
|
|
7
|
+
}
|
|
8
|
+
export function run(index, config) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
const maxSeverity = config.rules[RULE_ID]?.severity ?? "high";
|
|
11
|
+
for (const [name, wrapper] of index.wrappers.wrappers) {
|
|
12
|
+
// Skip fully resolved wrappers where both auth AND rate-limit are enforced
|
|
13
|
+
if (wrapper.resolved && wrapper.evidence.authEnforced && wrapper.evidence.rateLimitEnforced) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
// Determine what this wrapper WOULD have triggered
|
|
17
|
+
const wouldTrigger = [];
|
|
18
|
+
// Check if any wrapped routes are mutation routes (need auth)
|
|
19
|
+
const mutationFileSet = new Set(index.routes.mutationRoutes.map((r) => r.file));
|
|
20
|
+
const wrappedMutationFiles = wrapper.usageFiles.filter((f) => mutationFileSet.has(f));
|
|
21
|
+
if (wrappedMutationFiles.length > 0) {
|
|
22
|
+
if (!wrapper.resolved || !wrapper.evidence.authEnforced) {
|
|
23
|
+
wouldTrigger.push("AUTH-BOUNDARY-MISSING");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Check if any wrapped routes are API routes (need rate limiting)
|
|
27
|
+
// Exclude routes that are already exempt from rate-limit (cron, webhook, etc.)
|
|
28
|
+
const apiFileSet = new Set(index.routes.all.filter((r) => r.isApi).map((r) => r.file));
|
|
29
|
+
const wrappedApiFiles = wrapper.usageFiles.filter((f) => apiFileSet.has(f) && !isRateLimitExemptPath(f));
|
|
30
|
+
if (wrappedApiFiles.length > 0) {
|
|
31
|
+
if (!wrapper.resolved || !wrapper.evidence.rateLimitEnforced) {
|
|
32
|
+
wouldTrigger.push("RATE-LIMIT-MISSING");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (wouldTrigger.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
// Severity = high if wrapping mutation routes, med otherwise
|
|
38
|
+
const computedSeverity = wrappedMutationFiles.length > 0 ? "high" : "med";
|
|
39
|
+
const status = !wrapper.resolved
|
|
40
|
+
? "could not be resolved"
|
|
41
|
+
: wrapper.evidence.authCallPresent && !wrapper.evidence.authEnforced
|
|
42
|
+
? "calls auth but enforcement not proven"
|
|
43
|
+
: wrapper.evidence.rateLimitCallPresent && !wrapper.evidence.rateLimitEnforced
|
|
44
|
+
? "calls rate limiter but enforcement not proven"
|
|
45
|
+
: "missing protections";
|
|
46
|
+
const evidence = [
|
|
47
|
+
`${name}() wraps ${wrapper.usageCount} route handler(s) (${wrapper.mutationRouteCount} mutation)`,
|
|
48
|
+
`Would have triggered: ${wouldTrigger.join(", ")}`,
|
|
49
|
+
`Top routes: ${wrapper.usageFiles.slice(0, 5).join(", ")}${wrapper.usageCount > 5 ? ` (+${wrapper.usageCount - 5} more)` : ""}`,
|
|
50
|
+
];
|
|
51
|
+
if (wrapper.evidence.authCallPresent) {
|
|
52
|
+
evidence.push(`Auth call detected: ${wrapper.evidence.authDetails.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
if (wrapper.evidence.rateLimitCallPresent) {
|
|
55
|
+
evidence.push(`Rate-limit call detected: ${wrapper.evidence.rateLimitDetails.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
findings.push({
|
|
58
|
+
ruleId: RULE_ID,
|
|
59
|
+
severity: capSeverity(computedSeverity, maxSeverity),
|
|
60
|
+
confidence: "high",
|
|
61
|
+
message: `Wrapper "${name}" wraps ${wrapper.usageCount} handler(s); ${status}`,
|
|
62
|
+
file: wrapper.usageFiles[0],
|
|
63
|
+
evidence,
|
|
64
|
+
confidenceRationale: "High: wrapper usage is certain, but protection cannot be verified",
|
|
65
|
+
remediation: [
|
|
66
|
+
`If ${name} enforces auth: add "${name}" to hints.auth.functions`,
|
|
67
|
+
`If ${name} enforces rate limiting: add "${name}" to hints.rateLimit.wrappers`,
|
|
68
|
+
...(wrapper.definitionFile
|
|
69
|
+
? [`Verify wrapper implementation at ${wrapper.definitionFile}`]
|
|
70
|
+
: [`Wrapper definition could not be found — check import paths`]),
|
|
71
|
+
],
|
|
72
|
+
tags: ["wrapper", "config"],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return findings;
|
|
76
|
+
}
|
|
77
|
+
/** Paths exempt from rate-limit — mirrors EXEMPT_PATH_PATTERNS + WEBHOOK patterns in rate-limit-missing. */
|
|
78
|
+
function isRateLimitExemptPath(file) {
|
|
79
|
+
return /\/cron\//.test(file) || /webhook/i.test(file) || /\/tasks\//.test(file);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=wrapper-unrecognized.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrapper-unrecognized.js","sourceRoot":"","sources":["../../src/rules/wrapper-unrecognized.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Higher-Order Function (HOF) detection utilities.
|
|
3
|
+
* Used by wrapper analysis and rules.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract the ordered chain of HOF wrapper names from route source.
|
|
7
|
+
* E.g., `export const POST = withWorkspace(withErrorBoundary(handler))` → ["withWorkspace", "withErrorBoundary"]
|
|
8
|
+
* E.g., `export default withAuth(handler)` → ["withAuth"]
|
|
9
|
+
* Returns empty array if no HOF wrapper detected.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractHofWrapperChain(src: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Check if route source is exported via a specific known function (HOF pattern).
|
|
14
|
+
* E.g., `export const POST = withAuth(handler)` with functionName="withAuth" → true
|
|
15
|
+
*/
|
|
16
|
+
export declare function isWrappedByFunction(src: string, functionName: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Find the import source for a given identifier in the source.
|
|
19
|
+
* Returns the module specifier or undefined if not imported (same-file definition).
|
|
20
|
+
*/
|
|
21
|
+
export declare function findImportSource(src: string, identifierName: string): string | undefined;
|
|
22
|
+
//# sourceMappingURL=hof.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hof.d.ts","sourceRoot":"","sources":["../../src/util/hof.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAsB5D;AA2CD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAY9E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoBxF"}
|
package/dist/util/hof.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Higher-Order Function (HOF) detection utilities.
|
|
3
|
+
* Used by wrapper analysis and rules.
|
|
4
|
+
*/
|
|
5
|
+
const HTTP_METHODS = "GET|POST|PUT|PATCH|DELETE";
|
|
6
|
+
/**
|
|
7
|
+
* Extract the ordered chain of HOF wrapper names from route source.
|
|
8
|
+
* E.g., `export const POST = withWorkspace(withErrorBoundary(handler))` → ["withWorkspace", "withErrorBoundary"]
|
|
9
|
+
* E.g., `export default withAuth(handler)` → ["withAuth"]
|
|
10
|
+
* Returns empty array if no HOF wrapper detected.
|
|
11
|
+
*/
|
|
12
|
+
export function extractHofWrapperChain(src) {
|
|
13
|
+
const chains = [];
|
|
14
|
+
// Pattern 1: export const METHOD = wrapper(...)
|
|
15
|
+
const constPattern = new RegExp(`export\\s+(?:const|let|var)\\s+(?:${HTTP_METHODS})\\s*=\\s*(.+)`, "gm");
|
|
16
|
+
for (const m of src.matchAll(constPattern)) {
|
|
17
|
+
chains.push(...extractCallChain(m[1]));
|
|
18
|
+
}
|
|
19
|
+
// Pattern 2: export default wrapper(...)
|
|
20
|
+
const defaultPattern = /export\s+default\s+([a-zA-Z_]\w*)\s*\(/gm;
|
|
21
|
+
for (const m of src.matchAll(defaultPattern)) {
|
|
22
|
+
// Only add if not already captured
|
|
23
|
+
if (!chains.includes(m[1])) {
|
|
24
|
+
chains.push(m[1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...new Set(chains)];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract function names from a call chain expression.
|
|
31
|
+
* E.g., "withWorkspace(withErrorBoundary(handler))" → ["withWorkspace", "withErrorBoundary"]
|
|
32
|
+
* E.g., "withAuth(async (req) => { ... })" → ["withAuth"]
|
|
33
|
+
*
|
|
34
|
+
* Only extracts the leading nested call chain — stops at the first
|
|
35
|
+
* non-wrapper token (e.g., `async`, handler body) to avoid picking
|
|
36
|
+
* up identifiers deep inside the handler.
|
|
37
|
+
*/
|
|
38
|
+
function extractCallChain(expr) {
|
|
39
|
+
const names = [];
|
|
40
|
+
let pos = 0;
|
|
41
|
+
while (pos < expr.length) {
|
|
42
|
+
// Skip whitespace
|
|
43
|
+
while (pos < expr.length && /\s/.test(expr[pos]))
|
|
44
|
+
pos++;
|
|
45
|
+
// Try to match identifier(
|
|
46
|
+
const remaining = expr.slice(pos);
|
|
47
|
+
const match = remaining.match(/^([a-zA-Z_]\w*)\s*\(/);
|
|
48
|
+
if (!match)
|
|
49
|
+
break;
|
|
50
|
+
const name = match[1];
|
|
51
|
+
if (SKIP_IDENTIFIERS.has(name))
|
|
52
|
+
break; // Not a wrapper, stop
|
|
53
|
+
names.push(name);
|
|
54
|
+
pos += match[0].length;
|
|
55
|
+
}
|
|
56
|
+
return names;
|
|
57
|
+
}
|
|
58
|
+
const SKIP_IDENTIFIERS = new Set([
|
|
59
|
+
"async", "await", "function", "return", "new", "typeof", "void",
|
|
60
|
+
"if", "else", "for", "while", "switch", "case", "try", "catch",
|
|
61
|
+
"throw", "const", "let", "var", "class", "import", "export",
|
|
62
|
+
"console", "Error", "Promise", "Array", "Object", "String", "Number",
|
|
63
|
+
"Boolean", "JSON", "Math", "Date", "RegExp", "Map", "Set",
|
|
64
|
+
"Response", "Request", "Headers", "NextResponse", "NextRequest",
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Check if route source is exported via a specific known function (HOF pattern).
|
|
68
|
+
* E.g., `export const POST = withAuth(handler)` with functionName="withAuth" → true
|
|
69
|
+
*/
|
|
70
|
+
export function isWrappedByFunction(src, functionName) {
|
|
71
|
+
const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
// export const METHOD = fn(...)
|
|
73
|
+
const hofPattern = new RegExp(`export\\s+(?:const|let|var)\\s+(?:${HTTP_METHODS})\\s*=\\s*${escaped}\\s*\\(`, "m");
|
|
74
|
+
if (hofPattern.test(src))
|
|
75
|
+
return true;
|
|
76
|
+
// export default fn(...)
|
|
77
|
+
const defaultPattern = new RegExp(`export\\s+default\\s+${escaped}\\s*\\(`, "m");
|
|
78
|
+
return defaultPattern.test(src);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find the import source for a given identifier in the source.
|
|
82
|
+
* Returns the module specifier or undefined if not imported (same-file definition).
|
|
83
|
+
*/
|
|
84
|
+
export function findImportSource(src, identifierName) {
|
|
85
|
+
const escaped = identifierName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86
|
+
// Named import: import { name } from "source" or import { other as name } from "source"
|
|
87
|
+
const namedPattern = new RegExp(`import\\s*\\{[^}]*\\b(?:${escaped}|\\w+\\s+as\\s+${escaped})\\b[^}]*\\}\\s*from\\s*["']([^"']+)["']`);
|
|
88
|
+
const namedMatch = src.match(namedPattern);
|
|
89
|
+
if (namedMatch)
|
|
90
|
+
return namedMatch[1];
|
|
91
|
+
// Default import: import name from "source"
|
|
92
|
+
const defaultPattern = new RegExp(`import\\s+${escaped}\\s+from\\s*["']([^"']+)["']`);
|
|
93
|
+
const defaultMatch = src.match(defaultPattern);
|
|
94
|
+
if (defaultMatch)
|
|
95
|
+
return defaultMatch[1];
|
|
96
|
+
// Namespace import + property access won't match here — that's fine for v1
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=hof.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hof.js","sourceRoot":"","sources":["../../src/util/hof.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hof.test.d.ts","sourceRoot":"","sources":["../../src/util/hof.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractHofWrapperChain, isWrappedByFunction, findImportSource } from "./hof.js";
|
|
3
|
+
describe("extractHofWrapperChain", () => {
|
|
4
|
+
it("extracts single wrapper from const export", () => {
|
|
5
|
+
const src = `export const POST = withWorkspace(async (req) => { return Response.json({}); });`;
|
|
6
|
+
expect(extractHofWrapperChain(src)).toEqual(["withWorkspace"]);
|
|
7
|
+
});
|
|
8
|
+
it("extracts chained wrappers", () => {
|
|
9
|
+
const src = `export const POST = withWorkspace(withErrorBoundary(async (req) => { return Response.json({}); }));`;
|
|
10
|
+
expect(extractHofWrapperChain(src)).toEqual(["withWorkspace", "withErrorBoundary"]);
|
|
11
|
+
});
|
|
12
|
+
it("extracts from export default", () => {
|
|
13
|
+
const src = `export default withAuth(handler);`;
|
|
14
|
+
expect(extractHofWrapperChain(src)).toEqual(["withAuth"]);
|
|
15
|
+
});
|
|
16
|
+
it("extracts from multiple method exports", () => {
|
|
17
|
+
const src = `
|
|
18
|
+
export const GET = withWorkspace(getHandler);
|
|
19
|
+
export const POST = withWorkspace(postHandler);
|
|
20
|
+
`;
|
|
21
|
+
// Should deduplicate
|
|
22
|
+
expect(extractHofWrapperChain(src)).toEqual(["withWorkspace"]);
|
|
23
|
+
});
|
|
24
|
+
it("returns empty for regular function exports", () => {
|
|
25
|
+
const src = `export async function POST(req: Request) { return Response.json({}); }`;
|
|
26
|
+
expect(extractHofWrapperChain(src)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it("skips JavaScript keywords", () => {
|
|
29
|
+
const src = `export const POST = async function(req) { return new Response(); };`;
|
|
30
|
+
// "async" and "Response" should be filtered out
|
|
31
|
+
const chain = extractHofWrapperChain(src);
|
|
32
|
+
expect(chain).not.toContain("async");
|
|
33
|
+
expect(chain).not.toContain("Response");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("isWrappedByFunction", () => {
|
|
37
|
+
it("matches const export pattern", () => {
|
|
38
|
+
const src = `export const POST = withAuth(async (req) => {});`;
|
|
39
|
+
expect(isWrappedByFunction(src, "withAuth")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it("matches default export pattern", () => {
|
|
42
|
+
const src = `export default withAuth(handler);`;
|
|
43
|
+
expect(isWrappedByFunction(src, "withAuth")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it("does not match different function", () => {
|
|
46
|
+
const src = `export const POST = withAuth(handler);`;
|
|
47
|
+
expect(isWrappedByFunction(src, "withRateLimit")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("does not match function call inside handler", () => {
|
|
50
|
+
const src = `export async function POST(req: Request) { withAuth(); }`;
|
|
51
|
+
expect(isWrappedByFunction(src, "withAuth")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("findImportSource", () => {
|
|
55
|
+
it("finds named import source", () => {
|
|
56
|
+
const src = `import { withWorkspace } from "@/lib/auth";`;
|
|
57
|
+
expect(findImportSource(src, "withWorkspace")).toBe("@/lib/auth");
|
|
58
|
+
});
|
|
59
|
+
it("finds default import source", () => {
|
|
60
|
+
const src = `import withAuth from "@/lib/auth";`;
|
|
61
|
+
expect(findImportSource(src, "withAuth")).toBe("@/lib/auth");
|
|
62
|
+
});
|
|
63
|
+
it("returns undefined for same-file definition", () => {
|
|
64
|
+
const src = `
|
|
65
|
+
function withAuth(handler: any) { return handler; }
|
|
66
|
+
export const POST = withAuth(handler);
|
|
67
|
+
`;
|
|
68
|
+
expect(findImportSource(src, "withAuth")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it("handles aliased imports", () => {
|
|
71
|
+
const src = `import { myAuth as withAuth } from "@/lib/auth";`;
|
|
72
|
+
expect(findImportSource(src, "withAuth")).toBe("@/lib/auth");
|
|
73
|
+
});
|
|
74
|
+
it("handles multiple named imports", () => {
|
|
75
|
+
const src = `import { foo, withWorkspace, bar } from "@/lib/auth";`;
|
|
76
|
+
expect(findImportSource(src, "withWorkspace")).toBe("@/lib/auth");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=hof.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hof.test.js","sourceRoot":"","sources":["../../src/util/hof.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monorepo.d.ts","sourceRoot":"","sources":["../../src/util/monorepo.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Walk up from startDir to find a monorepo workspace root.
|
|
5
|
+
* Returns null if no workspace root is found (i.e., not a monorepo).
|
|
6
|
+
*/
|
|
7
|
+
export function findWorkspaceRoot(startDir) {
|
|
8
|
+
let dir = path.dirname(startDir);
|
|
9
|
+
while (dir !== path.dirname(dir)) {
|
|
10
|
+
if (existsSync(path.join(dir, "pnpm-workspace.yaml")))
|
|
11
|
+
return dir;
|
|
12
|
+
if (existsSync(path.join(dir, "turbo.json")))
|
|
13
|
+
return dir;
|
|
14
|
+
const pkgPath = path.join(dir, "package.json");
|
|
15
|
+
if (existsSync(pkgPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
18
|
+
if (pkg.workspaces)
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Ignore parse errors in parent package.json
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
dir = path.dirname(dir);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=monorepo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monorepo.js","sourceRoot":"","sources":["../../src/util/monorepo.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEnD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjC,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QAClE,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAC/C,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;gBACtD,IAAI,GAAG,CAAC,UAAU;oBAAE,OAAO,GAAG,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,6CAA6C;YAC/C,CAAC;QACH,CAAC;QACD,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect outbound HTTP fetch calls with user-influenced URLs.
|
|
3
|
+
* Used by RL and INPUT-VALIDATION rules to identify SSRF surface
|
|
4
|
+
* on public-intent endpoints.
|
|
5
|
+
*/
|
|
6
|
+
export interface OutboundFetchResult {
|
|
7
|
+
hasOutboundFetch: boolean;
|
|
8
|
+
hasUserInfluencedUrl: boolean;
|
|
9
|
+
/** True when both outbound fetch AND user-influenced URL are present */
|
|
10
|
+
isRisky: boolean;
|
|
11
|
+
evidence: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function detectOutboundFetcher(src: string): OutboundFetchResult;
|
|
14
|
+
//# sourceMappingURL=outbound-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-fetch.d.ts","sourceRoot":"","sources":["../../src/util/outbound-fetch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA8BD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CA6BtE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect outbound HTTP fetch calls with user-influenced URLs.
|
|
3
|
+
* Used by RL and INPUT-VALIDATION rules to identify SSRF surface
|
|
4
|
+
* on public-intent endpoints.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Outbound fetch patterns — HTTP client calls that make external requests.
|
|
8
|
+
* Excludes false positives like fetchUser(), fetchData() by requiring
|
|
9
|
+
* non-word char or start-of-line before "fetch".
|
|
10
|
+
*/
|
|
11
|
+
const OUTBOUND_FETCH_PATTERNS = [
|
|
12
|
+
{ pattern: /(?:^|[^.\w])fetch\s*\(/, label: "fetch()" },
|
|
13
|
+
{ pattern: /axios\s*[.(]/, label: "axios" },
|
|
14
|
+
{ pattern: /(?:^|[^.\w])got\s*[.(]/, label: "got()" },
|
|
15
|
+
{ pattern: /undici\.request\s*\(/, label: "undici.request()" },
|
|
16
|
+
{ pattern: /https?\.(?:get|request)\s*\(/, label: "http.get/request()" },
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* User-influenced URL patterns — evidence that the fetch target
|
|
20
|
+
* is constructed from user-supplied request data.
|
|
21
|
+
*/
|
|
22
|
+
const USER_INPUT_PATTERNS = [
|
|
23
|
+
{ pattern: /searchParams\.get\s*\(/, label: "reads searchParams" },
|
|
24
|
+
{ pattern: /searchParams\.\w/, label: "accesses searchParams" },
|
|
25
|
+
{ pattern: /new\s+URL\s*\(\s*(?:request|req)\.url/, label: "parses request URL" },
|
|
26
|
+
{ pattern: /(?:request|req)\.url\b/, label: "reads request.url" },
|
|
27
|
+
{ pattern: /(?:request|req)\.json\s*\(/, label: "reads request body" },
|
|
28
|
+
{ pattern: /req\.body\b/, label: "reads req.body" },
|
|
29
|
+
{ pattern: /req\.query\b/, label: "reads req.query" },
|
|
30
|
+
{ pattern: /params\.\w/, label: "reads route params" },
|
|
31
|
+
];
|
|
32
|
+
export function detectOutboundFetcher(src) {
|
|
33
|
+
const evidence = [];
|
|
34
|
+
let hasOutboundFetch = false;
|
|
35
|
+
let hasUserInfluencedUrl = false;
|
|
36
|
+
for (const { pattern, label } of OUTBOUND_FETCH_PATTERNS) {
|
|
37
|
+
if (pattern.test(src)) {
|
|
38
|
+
hasOutboundFetch = true;
|
|
39
|
+
evidence.push(`outbound HTTP call: ${label}`);
|
|
40
|
+
break; // one is enough
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (hasOutboundFetch) {
|
|
44
|
+
for (const { pattern, label } of USER_INPUT_PATTERNS) {
|
|
45
|
+
if (pattern.test(src)) {
|
|
46
|
+
hasUserInfluencedUrl = true;
|
|
47
|
+
evidence.push(`user-controlled input: ${label}`);
|
|
48
|
+
break; // one is enough
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
hasOutboundFetch,
|
|
54
|
+
hasUserInfluencedUrl,
|
|
55
|
+
isRisky: hasOutboundFetch && hasUserInfluencedUrl,
|
|
56
|
+
evidence,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=outbound-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-fetch.js","sourceRoot":"","sources":["../../src/util/outbound-fetch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH;;;;GAIG;AACH,MAAM,uBAAuB,GAAyC;IACpE,EAAE,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,SAAS,EAAE;IACvD,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE;IAC3C,EAAE,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,OAAO,EAAE;IACrD,EAAE,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC9D,EAAE,OAAO,EAAE,8BAA8B,EAAE,KAAK,EAAE,oBAAoB,EAAE;CACzE,CAAC;AAEF;;;GAGG;AACH,MAAM,mBAAmB,GAAyC;IAChE,EAAE,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAClE,EAAE,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,uBAAuB,EAAE;IAC/D,EAAE,OAAO,EAAE,uCAAuC,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACjF,EAAE,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IACjE,EAAE,OAAO,EAAE,4BAA4B,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACtE,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACnD,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACrD,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,oBAAoB,EAAE;CACvD,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAC7B,IAAI,oBAAoB,GAAG,KAAK,CAAC;IAEjC,KAAK,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,uBAAuB,EAAE,CAAC;QACzD,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,gBAAgB,GAAG,IAAI,CAAC;YACxB,QAAQ,CAAC,IAAI,CAAC,uBAAuB,KAAK,EAAE,CAAC,CAAC;YAC9C,MAAM,CAAC,gBAAgB;QACzB,CAAC;IACH,CAAC;IAED,IAAI,gBAAgB,EAAE,CAAC;QACrB,KAAK,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,mBAAmB,EAAE,CAAC;YACrD,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtB,oBAAoB,GAAG,IAAI,CAAC;gBAC5B,QAAQ,CAAC,IAAI,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;gBACjD,MAAM,CAAC,gBAAgB;YACzB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,gBAAgB;QAChB,oBAAoB;QACpB,OAAO,EAAE,gBAAgB,IAAI,oBAAoB;QACjD,QAAQ;KACT,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-fetch.test.d.ts","sourceRoot":"","sources":["../../src/util/outbound-fetch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { detectOutboundFetcher } from "./outbound-fetch.js";
|
|
3
|
+
describe("detectOutboundFetcher", () => {
|
|
4
|
+
it("detects fetch() with user-influenced URL", () => {
|
|
5
|
+
const src = `
|
|
6
|
+
export async function GET(request: Request) {
|
|
7
|
+
const url = new URL(request.url).searchParams.get("target");
|
|
8
|
+
const response = await fetch(url);
|
|
9
|
+
return Response.json(await response.json());
|
|
10
|
+
}`;
|
|
11
|
+
const result = detectOutboundFetcher(src);
|
|
12
|
+
expect(result.hasOutboundFetch).toBe(true);
|
|
13
|
+
expect(result.hasUserInfluencedUrl).toBe(true);
|
|
14
|
+
expect(result.isRisky).toBe(true);
|
|
15
|
+
expect(result.evidence.length).toBeGreaterThanOrEqual(2);
|
|
16
|
+
});
|
|
17
|
+
it("detects axios with user-influenced URL", () => {
|
|
18
|
+
const src = `
|
|
19
|
+
export async function POST(request: Request) {
|
|
20
|
+
const body = await request.json();
|
|
21
|
+
const response = await axios.get(body.url);
|
|
22
|
+
return Response.json(response.data);
|
|
23
|
+
}`;
|
|
24
|
+
const result = detectOutboundFetcher(src);
|
|
25
|
+
expect(result.isRisky).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it("does NOT flag fetch with hardcoded URL", () => {
|
|
28
|
+
const src = `
|
|
29
|
+
export async function GET() {
|
|
30
|
+
const response = await fetch("https://api.example.com/data");
|
|
31
|
+
return Response.json(await response.json());
|
|
32
|
+
}`;
|
|
33
|
+
const result = detectOutboundFetcher(src);
|
|
34
|
+
expect(result.hasOutboundFetch).toBe(true);
|
|
35
|
+
expect(result.hasUserInfluencedUrl).toBe(false);
|
|
36
|
+
expect(result.isRisky).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it("does NOT flag when no fetch present", () => {
|
|
39
|
+
const src = `
|
|
40
|
+
export async function POST(request: Request) {
|
|
41
|
+
const body = await request.json();
|
|
42
|
+
await prisma.user.create({ data: body });
|
|
43
|
+
return Response.json({ ok: true });
|
|
44
|
+
}`;
|
|
45
|
+
const result = detectOutboundFetcher(src);
|
|
46
|
+
expect(result.hasOutboundFetch).toBe(false);
|
|
47
|
+
expect(result.isRisky).toBe(false);
|
|
48
|
+
expect(result.evidence).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
it("does NOT match fetchUser() as outbound fetch", () => {
|
|
51
|
+
const src = `
|
|
52
|
+
export async function GET(request: Request) {
|
|
53
|
+
const url = new URL(request.url);
|
|
54
|
+
const user = await fetchUser(url.searchParams.get("id"));
|
|
55
|
+
return Response.json(user);
|
|
56
|
+
}`;
|
|
57
|
+
const result = detectOutboundFetcher(src);
|
|
58
|
+
expect(result.hasOutboundFetch).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it("detects got() with user input", () => {
|
|
61
|
+
const src = `
|
|
62
|
+
import got from "got";
|
|
63
|
+
export async function GET(request: Request) {
|
|
64
|
+
const target = new URL(request.url).searchParams.get("url");
|
|
65
|
+
const response = await got(target);
|
|
66
|
+
return Response.json(response.body);
|
|
67
|
+
}`;
|
|
68
|
+
const result = detectOutboundFetcher(src);
|
|
69
|
+
expect(result.isRisky).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("detects undici.request with user input", () => {
|
|
72
|
+
const src = `
|
|
73
|
+
import { request as undiciRequest } from "undici";
|
|
74
|
+
export async function POST(req: Request) {
|
|
75
|
+
const body = await req.json();
|
|
76
|
+
const { body: responseBody } = await undici.request(body.endpoint);
|
|
77
|
+
return Response.json(responseBody);
|
|
78
|
+
}`;
|
|
79
|
+
const result = detectOutboundFetcher(src);
|
|
80
|
+
expect(result.isRisky).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=outbound-fetch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-fetch.test.js","sourceRoot":"","sources":["../../src/util/outbound-fetch.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a file path matches any of the allowlist glob patterns.
|
|
3
|
+
* Used by rules to skip files/routes that the user has marked as exempt.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isAllowlisted(filePath: string, allowlistPaths: string[]): boolean;
|
|
6
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/util/paths.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAGjF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a file path matches any of the allowlist glob patterns.
|
|
3
|
+
* Used by rules to skip files/routes that the user has marked as exempt.
|
|
4
|
+
*/
|
|
5
|
+
export function isAllowlisted(filePath, allowlistPaths) {
|
|
6
|
+
if (allowlistPaths.length === 0)
|
|
7
|
+
return false;
|
|
8
|
+
return allowlistPaths.some((pattern) => matchGlob(filePath, pattern));
|
|
9
|
+
}
|
|
10
|
+
function matchGlob(filePath, pattern) {
|
|
11
|
+
const regexStr = pattern
|
|
12
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
13
|
+
.replace(/\*\*/g, "{{GLOBSTAR}}")
|
|
14
|
+
.replace(/\*/g, "[^/]*")
|
|
15
|
+
.replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
16
|
+
return new RegExp(`^${regexStr}$`).test(filePath);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/util/paths.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,cAAwB;IACtE,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,OAAe;IAClD,MAAM,QAAQ,GAAG,OAAO;SACrB,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC;SACpC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC;SAChC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC;SACvB,OAAO,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;IACtC,OAAO,IAAI,MAAM,CAAC,IAAI,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface TsconfigPaths {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
paths: Record<string, string[]>;
|
|
4
|
+
}
|
|
5
|
+
export interface ResolveOptions {
|
|
6
|
+
rootDir: string;
|
|
7
|
+
tsconfigPaths?: TsconfigPaths;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Load tsconfig.json (with extends chain) and extract compilerOptions.paths + baseUrl.
|
|
11
|
+
* Falls back to tsconfig.app.json if tsconfig.json not found.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadTsconfigPaths(rootDir: string): TsconfigPaths | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve an import specifier to a relative file path within rootDir.
|
|
16
|
+
* Returns the relative path (e.g., "src/lib/auth.ts") or undefined if unresolvable.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveImportPath(fromFile: string, importPath: string, opts: ResolveOptions): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Follow barrel re-exports to find the actual definition file.
|
|
21
|
+
* E.g., index.ts → export { withWorkspace } from "./workspace" → workspace.ts
|
|
22
|
+
*
|
|
23
|
+
* Returns the final file path and source, or undefined if not found.
|
|
24
|
+
* Follows up to maxHops (default 5) with cycle detection.
|
|
25
|
+
*/
|
|
26
|
+
export declare function followReExport(symbolName: string, startFile: string, opts: ResolveOptions, maxHops?: number): {
|
|
27
|
+
file: string;
|
|
28
|
+
src: string;
|
|
29
|
+
} | undefined;
|
|
30
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/util/resolve.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAS5E;AAyID;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,cAAc,GACnB,MAAM,GAAG,SAAS,CA6BpB;AA4ED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,EACpB,OAAO,GAAE,MAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAuD3C"}
|