@patchstack/connect 0.3.15 → 0.3.17
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/AGENT-INSTALL.md +27 -19
- package/README.md +14 -24
- package/dist/cli.js +817 -110
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +35 -0
- package/dist/protect/templates/demo-rules.json +238 -0
- package/dist/protect/templates/fastify-plugin.ts +48 -0
- package/dist/protect/templates/generic-guard.ts +51 -0
- package/dist/protect/templates/guard.ts +17 -3
- package/dist/protect/templates/next-middleware.ts +37 -0
- package/dist/protect/templates/sveltekit-hooks.ts +35 -0
- package/dist/protect.cjs +451 -78
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +9 -1
- package/dist/protect.js +451 -78
- package/dist/protect.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Patchstack runtime guard for Astro — middleware. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Runs the request-phase WAF (+ egress SSRF) and screens the response body/headers for every request.
|
|
3
|
+
// If you already have middleware, compose them with `sequence()` from "astro:middleware".
|
|
4
|
+
import type { MiddlewareHandler } from "astro";
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./patchstack.rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-astro (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export const onRequest: MiddlewareHandler = async (context, next) => {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const blocked = await protection.fetchGuard()(context.request);
|
|
32
|
+
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
33
|
+
return protection.screenResponse(await next());
|
|
34
|
+
};
|
|
35
|
+
// #endregion patchstack-astro
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "DEMO sample rules scaffolded by `patchstack-connect protect --demo` — a broad, many-class set (SQLi/XSS/XXE/SSRF/PII/…) for demonstrations, NOT the production corpus. In production, rules come from the Patchstack API per site (via a baked site UUID / token). Kept in sync with examples/protect/demo-rules.json (minus its _demo test vectors).",
|
|
3
|
+
"firewall": [
|
|
4
|
+
{
|
|
5
|
+
"id": "demo-prototype-pollution",
|
|
6
|
+
"title": "Prototype pollution in request body",
|
|
7
|
+
"category": "prototype-pollution",
|
|
8
|
+
"rule_v2": [
|
|
9
|
+
{
|
|
10
|
+
"parameter": "raw",
|
|
11
|
+
"mutations": [
|
|
12
|
+
"urldecode"
|
|
13
|
+
],
|
|
14
|
+
"match": {
|
|
15
|
+
"type": "contains",
|
|
16
|
+
"value": "__proto__"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"parameter": "rules",
|
|
21
|
+
"rules": [
|
|
22
|
+
{
|
|
23
|
+
"parameter": "raw",
|
|
24
|
+
"mutations": [
|
|
25
|
+
"urldecode"
|
|
26
|
+
],
|
|
27
|
+
"match": {
|
|
28
|
+
"type": "contains",
|
|
29
|
+
"value": "constructor"
|
|
30
|
+
},
|
|
31
|
+
"inclusive": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"parameter": "raw",
|
|
35
|
+
"mutations": [
|
|
36
|
+
"urldecode"
|
|
37
|
+
],
|
|
38
|
+
"match": {
|
|
39
|
+
"type": "contains",
|
|
40
|
+
"value": "prototype"
|
|
41
|
+
},
|
|
42
|
+
"inclusive": true
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "demo-path-traversal",
|
|
50
|
+
"title": "Path traversal in a file/path parameter",
|
|
51
|
+
"category": "lfi",
|
|
52
|
+
"rule_v2": [
|
|
53
|
+
{
|
|
54
|
+
"parameter": [
|
|
55
|
+
"get.file",
|
|
56
|
+
"post.file",
|
|
57
|
+
"raw.file",
|
|
58
|
+
"get.path",
|
|
59
|
+
"post.path"
|
|
60
|
+
],
|
|
61
|
+
"mutations": [
|
|
62
|
+
"urldecode"
|
|
63
|
+
],
|
|
64
|
+
"match": {
|
|
65
|
+
"type": "contains",
|
|
66
|
+
"value": ".."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "demo-sqli",
|
|
73
|
+
"title": "SQL injection in a query parameter",
|
|
74
|
+
"category": "sqli",
|
|
75
|
+
"rule_v2": [
|
|
76
|
+
{
|
|
77
|
+
"parameter": [
|
|
78
|
+
"get.q",
|
|
79
|
+
"post.q",
|
|
80
|
+
"get.id",
|
|
81
|
+
"post.id",
|
|
82
|
+
"raw"
|
|
83
|
+
],
|
|
84
|
+
"mutations": [
|
|
85
|
+
"urldecode"
|
|
86
|
+
],
|
|
87
|
+
"match": {
|
|
88
|
+
"type": "regex",
|
|
89
|
+
"value": "/union\\s+select|;\\s*drop\\s+table|\\bor\\s+1\\s*=\\s*1\\b/i"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "demo-xss",
|
|
96
|
+
"title": "Reflected/stored XSS in a text field",
|
|
97
|
+
"category": "xss",
|
|
98
|
+
"rule_v2": [
|
|
99
|
+
{
|
|
100
|
+
"parameter": [
|
|
101
|
+
"get.q",
|
|
102
|
+
"post.comment",
|
|
103
|
+
"post.title",
|
|
104
|
+
"raw"
|
|
105
|
+
],
|
|
106
|
+
"mutations": [
|
|
107
|
+
"urldecode",
|
|
108
|
+
"htmlentitydecode"
|
|
109
|
+
],
|
|
110
|
+
"match": {
|
|
111
|
+
"type": "regex",
|
|
112
|
+
"value": "/<script\\b|onerror\\s*=|javascript:/i"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "demo-command-injection",
|
|
119
|
+
"title": "OS command injection in a host/cmd parameter",
|
|
120
|
+
"category": "command-injection",
|
|
121
|
+
"rule_v2": [
|
|
122
|
+
{
|
|
123
|
+
"parameter": [
|
|
124
|
+
"get.host",
|
|
125
|
+
"post.host",
|
|
126
|
+
"get.cmd",
|
|
127
|
+
"post.cmd"
|
|
128
|
+
],
|
|
129
|
+
"mutations": [
|
|
130
|
+
"urldecode"
|
|
131
|
+
],
|
|
132
|
+
"match": {
|
|
133
|
+
"type": "regex",
|
|
134
|
+
"value": "/[;&|`$()<>]/"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "demo-nosql-injection",
|
|
141
|
+
"title": "NoSQL operator injection in request body",
|
|
142
|
+
"category": "nosqli",
|
|
143
|
+
"rule_v2": [
|
|
144
|
+
{
|
|
145
|
+
"parameter": "raw",
|
|
146
|
+
"match": {
|
|
147
|
+
"type": "regex",
|
|
148
|
+
"value": "/\"\\$(?:ne|gt|lt|gte|lte|where|regex|in|nin)\"\\s*:/i"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "demo-xxe",
|
|
155
|
+
"title": "XML external entity (XXE) in request body",
|
|
156
|
+
"category": "xxe",
|
|
157
|
+
"rule_v2": [
|
|
158
|
+
{
|
|
159
|
+
"parameter": "raw",
|
|
160
|
+
"match": {
|
|
161
|
+
"type": "regex",
|
|
162
|
+
"value": "/<!ENTITY|<!DOCTYPE[^>]*SYSTEM/i"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"id": "demo-ssrf-request",
|
|
169
|
+
"title": "SSRF via a url parameter (request-side)",
|
|
170
|
+
"category": "ssrf",
|
|
171
|
+
"rule_v2": [
|
|
172
|
+
{
|
|
173
|
+
"parameter": [
|
|
174
|
+
"get.url",
|
|
175
|
+
"post.url",
|
|
176
|
+
"raw.url"
|
|
177
|
+
],
|
|
178
|
+
"mutations": [
|
|
179
|
+
"urldecode"
|
|
180
|
+
],
|
|
181
|
+
"match": {
|
|
182
|
+
"type": "regex",
|
|
183
|
+
"value": "/localhost|127\\.0\\.0\\.1|169\\.254\\.169\\.254|::1|metadata\\.google/i"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "demo-resp-pii-email",
|
|
190
|
+
"title": "Email address leaking in response body",
|
|
191
|
+
"phase": "response",
|
|
192
|
+
"category": "pii-exposure",
|
|
193
|
+
"action": "redact",
|
|
194
|
+
"rule_v2": [
|
|
195
|
+
{
|
|
196
|
+
"parameter": "response.body",
|
|
197
|
+
"match": {
|
|
198
|
+
"type": "regex",
|
|
199
|
+
"value": "/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "demo-resp-credit-card",
|
|
206
|
+
"title": "Credit-card number leaking in response body",
|
|
207
|
+
"phase": "response",
|
|
208
|
+
"category": "pii-exposure",
|
|
209
|
+
"action": "redact",
|
|
210
|
+
"rule_v2": [
|
|
211
|
+
{
|
|
212
|
+
"parameter": "response.body",
|
|
213
|
+
"match": {
|
|
214
|
+
"type": "regex",
|
|
215
|
+
"value": "/(?:\\d[ -]?){13,16}/"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "demo-egress-blocklist-host",
|
|
222
|
+
"title": "Outbound request to a known exfiltration host",
|
|
223
|
+
"phase": "egress",
|
|
224
|
+
"category": "exfiltration",
|
|
225
|
+
"rule_v2": [
|
|
226
|
+
{
|
|
227
|
+
"parameter": "egress.host",
|
|
228
|
+
"match": {
|
|
229
|
+
"type": "equals",
|
|
230
|
+
"value": "evil.example.com"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"whitelists": [],
|
|
237
|
+
"whitelist_keys": {}
|
|
238
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Patchstack runtime guard for Fastify — plugin. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Register it once (`app.register(patchstackFastify)`); it adds a preHandler hook that runs the
|
|
3
|
+
// request-phase WAF (+ egress SSRF) on every request. Fastify's request/reply aren't Web-Fetch
|
|
4
|
+
// shaped, so we reconstruct a Request from the parsed fastify request and run the fetch guard.
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-fastify (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export async function patchstackFastify(fastify: any) {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const guard = protection.fetchGuard();
|
|
32
|
+
fastify.addHook("preHandler", async (request: any, reply: any) => {
|
|
33
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
34
|
+
const host = request.headers?.host ?? "localhost";
|
|
35
|
+
const url = `http://${host}${request.url ?? "/"}`;
|
|
36
|
+
const hasBody = method !== "GET" && method !== "HEAD" && request.body != null;
|
|
37
|
+
const body = hasBody ? (typeof request.body === "string" ? request.body : JSON.stringify(request.body)) : undefined;
|
|
38
|
+
const blocked = await guard(new Request(url, { method, headers: request.headers as HeadersInit, body }));
|
|
39
|
+
if (blocked) {
|
|
40
|
+
const contentType = blocked.headers.get("content-type");
|
|
41
|
+
reply.code(blocked.status);
|
|
42
|
+
if (contentType) reply.header("content-type", contentType);
|
|
43
|
+
reply.send(await blocked.text());
|
|
44
|
+
return reply; // stop the request here
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// #endregion patchstack-fastify
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Patchstack runtime protection — GENERIC guard (framework-agnostic).
|
|
2
|
+
//
|
|
3
|
+
// Scaffolded by `patchstack-connect protect` when no built-in adapter matched your stack. Wire
|
|
4
|
+
// whichever helper below fits your server into your request path (see the plan the CLI printed),
|
|
5
|
+
// then run `patchstack-connect protect --check` to confirm it's hooked up. The engine ships inside
|
|
6
|
+
// @patchstack/connect — nothing else to install.
|
|
7
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
8
|
+
import fallbackRules from "./rules.json";
|
|
9
|
+
|
|
10
|
+
// Baked by `patchstack-connect protect` from .patchstackrc.json when available.
|
|
11
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
12
|
+
|
|
13
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
14
|
+
|
|
15
|
+
/** One memoized protection policy. Rules come from the Patchstack API per-site (cached); the
|
|
16
|
+
* bundled rules.json is the fallback until a site UUID / token is configured. */
|
|
17
|
+
export async function getProtection() {
|
|
18
|
+
if (!_protection) {
|
|
19
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
20
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
21
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
22
|
+
const common = { mode, egress: true } as const;
|
|
23
|
+
_protection = await createProtection(
|
|
24
|
+
siteUuid
|
|
25
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
26
|
+
: token
|
|
27
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
28
|
+
: { ...common, rules: fallbackRules as never },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return _protection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Web Fetch (Cloudflare Workers, Bun, Deno, Hono, Next edge, TanStack server.ts) ---------
|
|
35
|
+
// Wrap your fetch handler: export default { fetch: protectFetch(originalFetch) }
|
|
36
|
+
export function protectFetch<H extends (request: Request, ...rest: unknown[]) => unknown>(handler: H): H {
|
|
37
|
+
return (async (request: Request, ...rest: unknown[]) => {
|
|
38
|
+
const protection = await getProtection();
|
|
39
|
+
const blocked = await protection.fetchGuard()(request);
|
|
40
|
+
if (blocked) return blocked;
|
|
41
|
+
return protection.screenResponse(await handler(request, ...rest) as Response);
|
|
42
|
+
}) as H;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Node / Express -------------------------------------------------------------------------
|
|
46
|
+
// Add before your routes: app.use(patchstackMiddleware)
|
|
47
|
+
export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: unknown) => void) {
|
|
48
|
+
getProtection()
|
|
49
|
+
.then((protection) => (protection.node() as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, next))
|
|
50
|
+
.catch(() => next()); // fail open
|
|
51
|
+
}
|
|
@@ -30,7 +30,7 @@ let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
|
30
30
|
async function getProtection() {
|
|
31
31
|
if (!_protection) {
|
|
32
32
|
// Always-on: block by default. An explicit PATCHSTACK_MODE=dry-run downgrades to log-only.
|
|
33
|
-
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
33
|
+
const mode: "block" | "dry-run" = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
34
34
|
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
35
35
|
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
36
36
|
// Egress SSRF screening: block the app's outbound calls to internal / metadata addresses,
|
|
@@ -80,12 +80,26 @@ export async function inspectServerFn(data: unknown): Promise<{ rule?: string; m
|
|
|
80
80
|
// src/start.ts (the browser→Supabase tunnel screens its own forwarded response). Only acts on a
|
|
81
81
|
// web Response (text/JSON/HTML) — anything else, or any error, passes through untouched (fail-open,
|
|
82
82
|
// never breaks a response).
|
|
83
|
-
export async function screenResponse(response:
|
|
83
|
+
export async function screenResponse<T>(response: T): Promise<T> {
|
|
84
84
|
try {
|
|
85
85
|
if (!(response instanceof Response)) return response;
|
|
86
86
|
const protection = await getProtection();
|
|
87
|
-
return protection.screenResponse ? await protection.screenResponse(response) : response;
|
|
87
|
+
return (protection.screenResponse ? await protection.screenResponse(response) : response) as T;
|
|
88
88
|
} catch {
|
|
89
89
|
return response; // fail open
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
// Optional route-level WAF: evaluate the request-phase rules on EVERY incoming request (not just
|
|
94
|
+
// the Supabase tunnel / server functions) and return a 403 on a match. Off unless the app sets
|
|
95
|
+
// PATCHSTACK_ROUTE_WAF=1 — enabling it runs the request rules on all traffic, which is a classic
|
|
96
|
+
// WAF false-positive surface, so it is opt-in. `request.clone()` so the app can still read the body.
|
|
97
|
+
let _reqGuard: ((request: Request) => Promise<Response | null>) | undefined;
|
|
98
|
+
export async function guardRequest(request: Request): Promise<Response | null> {
|
|
99
|
+
try {
|
|
100
|
+
if (!_reqGuard) _reqGuard = (await getProtection()).fetchGuard();
|
|
101
|
+
return await _reqGuard(request.clone());
|
|
102
|
+
} catch {
|
|
103
|
+
return null; // fail open
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Patchstack runtime guard for Next.js — edge middleware. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Runs the request-phase WAF (+ egress SSRF) on every request before it reaches your route.
|
|
3
|
+
// Note: response-body redaction isn't available in Next middleware (it runs before the response);
|
|
4
|
+
// wire the connect runtime in your route handlers if you also need response screening there.
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./patchstack.rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-next (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export async function middleware(request: Request) {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const blocked = await protection.fetchGuard()(request);
|
|
32
|
+
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
33
|
+
// otherwise fall through (Next continues to the route)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const config = { matcher: "/:path*" };
|
|
37
|
+
// #endregion patchstack-next
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Patchstack runtime guard for SvelteKit — server hook. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Runs the request-phase WAF (+ egress SSRF) and screens the response body/headers for every request.
|
|
3
|
+
// If you already have a `handle`, compose them with `sequence()` from "@sveltejs/kit/hooks".
|
|
4
|
+
import type { Handle } from "@sveltejs/kit";
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./patchstack.rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-sveltekit (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const blocked = await protection.fetchGuard()(event.request);
|
|
32
|
+
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
33
|
+
return protection.screenResponse(await resolve(event));
|
|
34
|
+
};
|
|
35
|
+
// #endregion patchstack-sveltekit
|