@patchstack/connect 0.3.27 → 0.3.29
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 +65 -2
- package/dist/cli.js +243 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +17 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +229 -59
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +17 -0
- package/dist/protect.edge.js +211 -59
- package/dist/protect.edge.js.map +4 -4
- package/dist/protect.js +212 -60
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-KZSQD2X3.js → refresh-manifest-VRBE6RH6.js} +18 -1
- package/dist/refresh-manifest-VRBE6RH6.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-KZSQD2X3.js.map +0 -1
package/dist/protect.cjs
CHANGED
|
@@ -141,6 +141,19 @@ function buildEndpointUrl(base, siteUuid) {
|
|
|
141
141
|
const trimmed = base.replace(/\/$/, "");
|
|
142
142
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
143
143
|
}
|
|
144
|
+
function authFailureMessage(status, config) {
|
|
145
|
+
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
146
|
+
if (status === 401 && !hasCredential) {
|
|
147
|
+
return "Patchstack requires an API credential for this site and none is configured. Run `npx patchstack-connect login`, or set PATCHSTACK_API_KEY.";
|
|
148
|
+
}
|
|
149
|
+
if (status === 401) {
|
|
150
|
+
return "Patchstack rejected this API credential. It may have expired, been revoked, or the site may no longer exist. Run `npx patchstack-connect login` to issue a new one.";
|
|
151
|
+
}
|
|
152
|
+
if (status === 403) {
|
|
153
|
+
return "This API credential is not permitted to act on this site. Check that siteUuid in .patchstackrc.json matches the credential (a credential is issued for one site).";
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
144
157
|
async function postManifest(config, payload) {
|
|
145
158
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
146
159
|
const timeoutMs = config.timeoutMs;
|
|
@@ -189,6 +202,10 @@ async function postManifest(config, payload) {
|
|
|
189
202
|
"VALIDATION_ERROR"
|
|
190
203
|
);
|
|
191
204
|
}
|
|
205
|
+
const refused = authFailureMessage(response.status, config);
|
|
206
|
+
if (refused !== null) {
|
|
207
|
+
throw new PatchstackError(refused, "UNAUTHORIZED");
|
|
208
|
+
}
|
|
192
209
|
if (response.status < 200 || response.status >= 300) {
|
|
193
210
|
throw new PatchstackError(
|
|
194
211
|
`Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
|
|
@@ -1785,6 +1802,35 @@ var RequestResolver = class {
|
|
|
1785
1802
|
}
|
|
1786
1803
|
};
|
|
1787
1804
|
|
|
1805
|
+
// src/protect/notify.js
|
|
1806
|
+
var reported = /* @__PURE__ */ new Set();
|
|
1807
|
+
function notify(fn, arg, label) {
|
|
1808
|
+
if (typeof fn !== "function") return false;
|
|
1809
|
+
try {
|
|
1810
|
+
const result = fn(arg);
|
|
1811
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1812
|
+
try {
|
|
1813
|
+
result.then(void 0, (err) => warnOnce2(label, err));
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return true;
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
warnOnce2(label, err);
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
function warnOnce2(label, err) {
|
|
1824
|
+
if (reported.has(label)) return;
|
|
1825
|
+
reported.add(label);
|
|
1826
|
+
try {
|
|
1827
|
+
console.warn(
|
|
1828
|
+
`Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this is reported once per process. Cause: ${err && err.message ? err.message : String(err)}`
|
|
1829
|
+
);
|
|
1830
|
+
} catch {
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1788
1834
|
// src/protect/engine/normalizer.js
|
|
1789
1835
|
var HTML_ENTITIES = {
|
|
1790
1836
|
"&": "&",
|
|
@@ -2069,7 +2115,7 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
2069
2115
|
return out;
|
|
2070
2116
|
}
|
|
2071
2117
|
var warnedKeys = /* @__PURE__ */ new Set();
|
|
2072
|
-
function
|
|
2118
|
+
function warnOnce3(key, message) {
|
|
2073
2119
|
if (warnedKeys.has(key)) return;
|
|
2074
2120
|
warnedKeys.add(key);
|
|
2075
2121
|
console.warn(message);
|
|
@@ -2411,21 +2457,20 @@ var RuleEngine = class {
|
|
|
2411
2457
|
this.#whitelistKeys = whitelist_keys;
|
|
2412
2458
|
this.#onError = onError;
|
|
2413
2459
|
if (Array.isArray(whitelists) && whitelists.some((w) => w && Array.isArray(w.rule_v2) && !w.rule_id)) {
|
|
2414
|
-
|
|
2460
|
+
warnOnce3(
|
|
2415
2461
|
"whitelist-no-rule-id",
|
|
2416
2462
|
"[patchstack] a whitelist has no `rule_id` \u2014 it suppresses ALL rules when it matches. Scope each whitelist to a specific rule_id, and key it only on values an attacker cannot set."
|
|
2417
2463
|
);
|
|
2418
2464
|
}
|
|
2419
2465
|
if (whitelist_keys && typeof whitelist_keys === "object" && Object.keys(whitelist_keys).length > 0) {
|
|
2420
|
-
|
|
2466
|
+
warnOnce3("whitelist-keys-unimplemented", "[patchstack] `whitelist_keys` is not implemented and has no effect.");
|
|
2421
2467
|
}
|
|
2422
2468
|
}
|
|
2423
2469
|
// A mitigation engine must never take down the app it protects: any error while
|
|
2424
2470
|
// evaluating a rule is reported and the request is allowed through (fail open). A
|
|
2425
2471
|
// malformed rule is skipped without aborting the rest of the ruleset.
|
|
2426
2472
|
#reportError(err) {
|
|
2427
|
-
if (this.#onError) {
|
|
2428
|
-
this.#onError(err);
|
|
2473
|
+
if (notify(this.#onError, err, "onError")) {
|
|
2429
2474
|
return;
|
|
2430
2475
|
}
|
|
2431
2476
|
const key = err && err.message ? err.message : String(err);
|
|
@@ -2571,13 +2616,15 @@ var PulseRuleClient = class {
|
|
|
2571
2616
|
#ttlEffective = 0;
|
|
2572
2617
|
#etag;
|
|
2573
2618
|
#pulseAuth;
|
|
2574
|
-
|
|
2619
|
+
#reportsDetections;
|
|
2620
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, reportsDetections } = {}) {
|
|
2575
2621
|
this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
2576
2622
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
2577
2623
|
this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
|
|
2578
2624
|
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
|
|
2579
2625
|
this.#etag = etag ?? null;
|
|
2580
2626
|
this.#pulseAuth = pulseAuth ?? null;
|
|
2627
|
+
this.#reportsDetections = reportsDetections === true;
|
|
2581
2628
|
if (!this.#siteUuid) {
|
|
2582
2629
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
2583
2630
|
}
|
|
@@ -2589,13 +2636,14 @@ var PulseRuleClient = class {
|
|
|
2589
2636
|
}
|
|
2590
2637
|
const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
|
|
2591
2638
|
try {
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2639
|
+
const auth = await pulseAuthHeader(
|
|
2640
|
+
{ pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
|
|
2641
|
+
fetch
|
|
2642
|
+
);
|
|
2643
|
+
const headers = { Accept: "application/json", ...auth };
|
|
2644
|
+
if (this.#reportsDetections && typeof auth.Authorization === "string") {
|
|
2645
|
+
headers["X-Patchstack-Detections"] = "enabled";
|
|
2646
|
+
}
|
|
2599
2647
|
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
2600
2648
|
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
2601
2649
|
if (response.status === 304) {
|
|
@@ -2877,7 +2925,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2877
2925
|
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
2878
2926
|
const block = (url, host, method) => {
|
|
2879
2927
|
if (!shouldBlock(url, host, method)) return false;
|
|
2880
|
-
onBlock
|
|
2928
|
+
notify(onBlock, { url, host, method }, "onEgressBlock");
|
|
2881
2929
|
return true;
|
|
2882
2930
|
};
|
|
2883
2931
|
let screen = null;
|
|
@@ -2892,12 +2940,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
2892
2940
|
screen = null;
|
|
2893
2941
|
}
|
|
2894
2942
|
}
|
|
2895
|
-
const skip = (reason, detail) => {
|
|
2896
|
-
try {
|
|
2897
|
-
onSkip?.({ phase: "egress", reason, detail });
|
|
2898
|
-
} catch {
|
|
2899
|
-
}
|
|
2900
|
-
};
|
|
2943
|
+
const skip = (reason, detail) => notify(onSkip, { phase: "egress", reason, detail }, "onSkip");
|
|
2901
2944
|
const resolvesToDisallowed = (url, host, method) => new Promise((resolve) => {
|
|
2902
2945
|
if (!screen) {
|
|
2903
2946
|
if (host && dnsScreen) skip("resolver-unavailable", { host });
|
|
@@ -3483,22 +3526,19 @@ function reportRejections(rejected, options, label) {
|
|
|
3483
3526
|
const report = options.onRuleRejected;
|
|
3484
3527
|
for (const r of rejected) {
|
|
3485
3528
|
if (typeof report === "function") {
|
|
3486
|
-
|
|
3487
|
-
report({ ...r, accepted: false });
|
|
3488
|
-
} catch {
|
|
3489
|
-
}
|
|
3529
|
+
notify(report, { ...r, accepted: false }, "onRuleRejected");
|
|
3490
3530
|
}
|
|
3491
3531
|
}
|
|
3492
3532
|
const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
|
|
3493
|
-
options.onError
|
|
3533
|
+
notify(options.onError, new Error(
|
|
3494
3534
|
`${label}: rejected the entire update because ${rejected.length} rule(s) failed validation \u2014 keeping the previous ruleset and NOT caching this response: ${sample}${rejected.length > 3 ? ", \u2026" : ""}`
|
|
3495
|
-
));
|
|
3535
|
+
), "onError");
|
|
3496
3536
|
}
|
|
3497
3537
|
async function resolveRules(options, store, ctx = {}) {
|
|
3498
3538
|
const timeoutMs = ctx.timeoutMs;
|
|
3499
3539
|
if (options.siteUuid) {
|
|
3500
3540
|
const prior = await store.read();
|
|
3501
|
-
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag, timeoutMs, pulseAuth: ctx.pulseAuth });
|
|
3541
|
+
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag, timeoutMs, pulseAuth: ctx.pulseAuth, reportsDetections: options.reportDetections === true });
|
|
3502
3542
|
const res = await client.getRules();
|
|
3503
3543
|
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle, options);
|
|
3504
3544
|
if (res.success && !res.notModified) {
|
|
@@ -3514,14 +3554,14 @@ async function resolveRules(options, store, ctx = {}) {
|
|
|
3514
3554
|
return bundle;
|
|
3515
3555
|
}
|
|
3516
3556
|
if (prior?.bundle) {
|
|
3517
|
-
options.onError
|
|
3557
|
+
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
3518
3558
|
return normalizeBundle(prior.bundle, options);
|
|
3519
3559
|
}
|
|
3520
3560
|
if (options.rules) {
|
|
3521
|
-
options.onError
|
|
3561
|
+
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
|
|
3522
3562
|
return normalizeBundle(options.rules, options);
|
|
3523
3563
|
}
|
|
3524
|
-
options.onError
|
|
3564
|
+
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
3525
3565
|
return emptyBundle();
|
|
3526
3566
|
}
|
|
3527
3567
|
if (options.token) {
|
|
@@ -3542,10 +3582,10 @@ async function resolveRules(options, store, ctx = {}) {
|
|
|
3542
3582
|
return bundle;
|
|
3543
3583
|
}
|
|
3544
3584
|
if (prior?.bundle) {
|
|
3545
|
-
options.onError
|
|
3585
|
+
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
3546
3586
|
return normalizeBundle(prior.bundle, options);
|
|
3547
3587
|
}
|
|
3548
|
-
options.onError
|
|
3588
|
+
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
3549
3589
|
return emptyBundle();
|
|
3550
3590
|
}
|
|
3551
3591
|
if (options.rules) {
|
|
@@ -3566,10 +3606,7 @@ function normalizeBundle(b, options = {}) {
|
|
|
3566
3606
|
const report = options.onRuleRejected;
|
|
3567
3607
|
if (typeof report === "function") {
|
|
3568
3608
|
for (const r of rejected) {
|
|
3569
|
-
|
|
3570
|
-
report(r);
|
|
3571
|
-
} catch {
|
|
3572
|
-
}
|
|
3609
|
+
notify(report, r, "onRuleRejected");
|
|
3573
3610
|
}
|
|
3574
3611
|
} else {
|
|
3575
3612
|
const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
|
|
@@ -3611,7 +3648,7 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
3611
3648
|
failures = 0;
|
|
3612
3649
|
} catch (err) {
|
|
3613
3650
|
failures++;
|
|
3614
|
-
onError
|
|
3651
|
+
notify(onError, err, "onError");
|
|
3615
3652
|
}
|
|
3616
3653
|
schedule();
|
|
3617
3654
|
};
|
|
@@ -3643,10 +3680,125 @@ function makeRefreshHandler(tick, secret) {
|
|
|
3643
3680
|
};
|
|
3644
3681
|
}
|
|
3645
3682
|
|
|
3683
|
+
// src/protect/detections.js
|
|
3684
|
+
init_pulse_token();
|
|
3685
|
+
var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
|
|
3686
|
+
var DEFAULT_FLUSH_MS = 5e3;
|
|
3687
|
+
var MAX_BATCH = 50;
|
|
3688
|
+
var MAX_QUEUE = 500;
|
|
3689
|
+
function ruleParameters(rule) {
|
|
3690
|
+
const out = /* @__PURE__ */ new Set();
|
|
3691
|
+
const walk2 = (conditions) => {
|
|
3692
|
+
if (!Array.isArray(conditions)) return;
|
|
3693
|
+
for (const condition of conditions) {
|
|
3694
|
+
if (!condition || typeof condition !== "object") continue;
|
|
3695
|
+
if (typeof condition.parameter === "string" && condition.parameter !== "rules") {
|
|
3696
|
+
out.add(condition.parameter);
|
|
3697
|
+
}
|
|
3698
|
+
if (Array.isArray(condition.rules)) walk2(condition.rules);
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
walk2(rule?.rule_v2);
|
|
3702
|
+
return [...out];
|
|
3703
|
+
}
|
|
3704
|
+
function routeOf(path7) {
|
|
3705
|
+
if (typeof path7 !== "string" || path7 === "") return null;
|
|
3706
|
+
const cut = path7.search(/[?#]/);
|
|
3707
|
+
return cut === -1 ? path7 : path7.slice(0, cut);
|
|
3708
|
+
}
|
|
3709
|
+
function createDetectionReporter(opts) {
|
|
3710
|
+
const siteUuid = opts.siteUuid ?? process.env?.PATCHSTACK_SITE_UUID;
|
|
3711
|
+
if (!siteUuid) {
|
|
3712
|
+
return { record() {
|
|
3713
|
+
}, flush() {
|
|
3714
|
+
}, stop() {
|
|
3715
|
+
}, dropped: () => 0 };
|
|
3716
|
+
}
|
|
3717
|
+
const configured = opts.baseUrl ?? process.env?.PATCHSTACK_PULSE_RULES_URL;
|
|
3718
|
+
const baseUrl = typeof configured === "string" && isSafeOrigin(configured) ? configured.replace(/\/$/, "") : DEFAULT_BASE_URL3;
|
|
3719
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
3720
|
+
const flushMs = Number.isFinite(opts.flushMs) && opts.flushMs > 0 ? opts.flushMs : DEFAULT_FLUSH_MS;
|
|
3721
|
+
const maxQueue = Number.isFinite(opts.maxQueue) && opts.maxQueue > 0 ? opts.maxQueue : MAX_QUEUE;
|
|
3722
|
+
let queue = [];
|
|
3723
|
+
let timer = null;
|
|
3724
|
+
let stopped = false;
|
|
3725
|
+
let dropped = 0;
|
|
3726
|
+
const flush = () => {
|
|
3727
|
+
if (timer) {
|
|
3728
|
+
clearTimeout(timer);
|
|
3729
|
+
timer = null;
|
|
3730
|
+
}
|
|
3731
|
+
if (queue.length === 0 || typeof fetchImpl !== "function") return;
|
|
3732
|
+
const batch = queue.splice(0, MAX_BATCH);
|
|
3733
|
+
const droppedWith = dropped;
|
|
3734
|
+
dropped = 0;
|
|
3735
|
+
void (async () => {
|
|
3736
|
+
try {
|
|
3737
|
+
const res = await fetchImpl(`${baseUrl}/detections/${encodeURIComponent(siteUuid)}`, {
|
|
3738
|
+
method: "POST",
|
|
3739
|
+
headers: {
|
|
3740
|
+
"Content-Type": "application/json",
|
|
3741
|
+
Accept: "application/json",
|
|
3742
|
+
"User-Agent": "@patchstack/connect",
|
|
3743
|
+
// Same credential path as the rules fetch, and unauthenticated when none resolves: the
|
|
3744
|
+
// server accepts the UUID, and reporting must never hinge on getting a token.
|
|
3745
|
+
...await pulseAuthHeader({ pulseAuth: opts.pulseAuth, endpoint: baseUrl }, fetchImpl)
|
|
3746
|
+
},
|
|
3747
|
+
body: JSON.stringify({ detections: batch, dropped: droppedWith })
|
|
3748
|
+
});
|
|
3749
|
+
if (res && typeof res.then === "function") res.catch(() => {
|
|
3750
|
+
});
|
|
3751
|
+
} catch {
|
|
3752
|
+
}
|
|
3753
|
+
})();
|
|
3754
|
+
};
|
|
3755
|
+
return {
|
|
3756
|
+
/**
|
|
3757
|
+
* @param {{
|
|
3758
|
+
* rule?: { id?: string, rule_v2?: unknown },
|
|
3759
|
+
* phase?: string,
|
|
3760
|
+
* mode?: string,
|
|
3761
|
+
* path?: string,
|
|
3762
|
+
* }} detection
|
|
3763
|
+
*/
|
|
3764
|
+
record(detection) {
|
|
3765
|
+
if (stopped) return;
|
|
3766
|
+
const ruleId = detection?.rule?.id;
|
|
3767
|
+
if (ruleId === void 0 || ruleId === null || ruleId === "") return;
|
|
3768
|
+
if (queue.length >= maxQueue) {
|
|
3769
|
+
queue.shift();
|
|
3770
|
+
dropped++;
|
|
3771
|
+
}
|
|
3772
|
+
queue.push({
|
|
3773
|
+
rule_id: ruleId,
|
|
3774
|
+
route: routeOf(detection.path),
|
|
3775
|
+
parameters: ruleParameters(detection.rule),
|
|
3776
|
+
phase: detection.phase ?? null,
|
|
3777
|
+
// The state this detection was handled under, which is the whole point: `false` is a rule that
|
|
3778
|
+
// saw traffic it would have stopped.
|
|
3779
|
+
enforced: detection.mode === "block",
|
|
3780
|
+
rules_etag: opts.rulesEtag ?? null,
|
|
3781
|
+
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3782
|
+
});
|
|
3783
|
+
if (queue.length >= MAX_BATCH) {
|
|
3784
|
+
flush();
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
if (!timer) timer = setTimeout(flush, flushMs);
|
|
3788
|
+
},
|
|
3789
|
+
flush,
|
|
3790
|
+
stop() {
|
|
3791
|
+
stopped = true;
|
|
3792
|
+
flush();
|
|
3793
|
+
},
|
|
3794
|
+
dropped: () => dropped
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3646
3798
|
// src/protect/firewall-log.js
|
|
3647
3799
|
var DEFAULT_API_BASE = "https://api.patchstack.com";
|
|
3648
|
-
var
|
|
3649
|
-
var
|
|
3800
|
+
var DEFAULT_FLUSH_MS2 = 1e3;
|
|
3801
|
+
var MAX_BATCH2 = 50;
|
|
3650
3802
|
var TOKEN_SKEW_MS2 = 6e4;
|
|
3651
3803
|
function parseApiKey(apiKey) {
|
|
3652
3804
|
if (typeof apiKey !== "string" || apiKey.length === 0) return null;
|
|
@@ -3691,7 +3843,7 @@ function createFirewallLogReporter(opts) {
|
|
|
3691
3843
|
}
|
|
3692
3844
|
const apiBase = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
3693
3845
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
3694
|
-
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs :
|
|
3846
|
+
const flushMs = Number.isFinite(opts.flushMs) ? opts.flushMs : DEFAULT_FLUSH_MS2;
|
|
3695
3847
|
const sourceHost = typeof opts.sourceHost === "string" ? opts.sourceHost : "";
|
|
3696
3848
|
let queue = [];
|
|
3697
3849
|
let timer = null;
|
|
@@ -3740,7 +3892,7 @@ function createFirewallLogReporter(opts) {
|
|
|
3740
3892
|
timer = null;
|
|
3741
3893
|
}
|
|
3742
3894
|
if (queue.length === 0 || typeof fetchImpl !== "function") return;
|
|
3743
|
-
const batch = queue.splice(0,
|
|
3895
|
+
const batch = queue.splice(0, MAX_BATCH2);
|
|
3744
3896
|
void (async () => {
|
|
3745
3897
|
const token = await fetchAccessToken();
|
|
3746
3898
|
if (!token) return;
|
|
@@ -3787,7 +3939,7 @@ function createFirewallLogReporter(opts) {
|
|
|
3787
3939
|
user_agent: event.userAgent ?? null,
|
|
3788
3940
|
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
3789
3941
|
});
|
|
3790
|
-
if (queue.length >=
|
|
3942
|
+
if (queue.length >= MAX_BATCH2) {
|
|
3791
3943
|
flush();
|
|
3792
3944
|
return;
|
|
3793
3945
|
}
|
|
@@ -3886,8 +4038,10 @@ async function createProtection(options = {}) {
|
|
|
3886
4038
|
sourceHost: options.sourceHost,
|
|
3887
4039
|
fetchImpl: options.fetchImpl
|
|
3888
4040
|
}) : null;
|
|
4041
|
+
let detections = null;
|
|
3889
4042
|
const onDetect = (detection) => {
|
|
3890
|
-
userOnDetect
|
|
4043
|
+
notify(userOnDetect, detection, "onDetect");
|
|
4044
|
+
if (detections) detections.record(detection);
|
|
3891
4045
|
if (firewallLog && detection?.mode === "block") {
|
|
3892
4046
|
firewallLog.record({
|
|
3893
4047
|
rule: detection.rule,
|
|
@@ -3901,7 +4055,24 @@ async function createProtection(options = {}) {
|
|
|
3901
4055
|
const store = makeStore(options);
|
|
3902
4056
|
const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
|
|
3903
4057
|
const pulseAuth = await resolvePulseAuth(options);
|
|
4058
|
+
if (options.siteUuid && !pulseAuth) {
|
|
4059
|
+
const message = "Patchstack: no API credential resolved for site " + options.siteUuid + ". Rule updates may be rejected and this guard would keep running on its cached rules. Set PATCHSTACK_API_KEY (or pass { pulseAuth }) \u2014 required on runtimes without a filesystem.";
|
|
4060
|
+
notify(onError, new Error(message), "onError");
|
|
4061
|
+
console.warn(message);
|
|
4062
|
+
}
|
|
3904
4063
|
const bundle = await resolveRules(options, store, { timeoutMs: bootTimeoutMs, pulseAuth });
|
|
4064
|
+
if (options.reportDetections === true && options.siteUuid && telemetryEnabled()) {
|
|
4065
|
+
detections = createDetectionReporter({
|
|
4066
|
+
siteUuid: options.siteUuid,
|
|
4067
|
+
baseUrl: options.pulseRulesUrl,
|
|
4068
|
+
pulseAuth,
|
|
4069
|
+
// The bundle the guard is actually running, so a hit can be attributed to the rules that produced
|
|
4070
|
+
// it rather than to whatever is current when the report is read.
|
|
4071
|
+
rulesEtag: (await store.read())?.etag ?? null,
|
|
4072
|
+
fetchImpl: options.fetchImpl,
|
|
4073
|
+
flushMs: options.detectionFlushMs
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
3905
4076
|
let mode = resolveMode(options, bundle);
|
|
3906
4077
|
let requestRules;
|
|
3907
4078
|
let responseRules;
|
|
@@ -3944,12 +4115,7 @@ async function createProtection(options = {}) {
|
|
|
3944
4115
|
const recordSkip = (phase, reason, detail) => {
|
|
3945
4116
|
const key = `${phase}:${reason}`;
|
|
3946
4117
|
skipCounts[key] = (skipCounts[key] ?? 0) + 1;
|
|
3947
|
-
|
|
3948
|
-
try {
|
|
3949
|
-
onSkip({ phase, reason, detail, count: skipCounts[key] });
|
|
3950
|
-
} catch {
|
|
3951
|
-
}
|
|
3952
|
-
}
|
|
4118
|
+
notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
|
|
3953
4119
|
};
|
|
3954
4120
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
3955
4121
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
@@ -3985,7 +4151,7 @@ async function createProtection(options = {}) {
|
|
|
3985
4151
|
try {
|
|
3986
4152
|
result = re.evaluate({ ...reqCtx || {}, _response: { ...meta, body: text } });
|
|
3987
4153
|
} catch (err) {
|
|
3988
|
-
onError
|
|
4154
|
+
notify(onError, err, "onError");
|
|
3989
4155
|
continue;
|
|
3990
4156
|
}
|
|
3991
4157
|
if (!result.blocked) continue;
|
|
@@ -4107,7 +4273,7 @@ async function createProtection(options = {}) {
|
|
|
4107
4273
|
try {
|
|
4108
4274
|
r = screenText(text, { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} }, reqCtx);
|
|
4109
4275
|
} catch (err) {
|
|
4110
|
-
onError
|
|
4276
|
+
notify(onError, err, "onError");
|
|
4111
4277
|
for (const c of chunks) origWrite(c);
|
|
4112
4278
|
return origEnd(cb);
|
|
4113
4279
|
}
|
|
@@ -4159,7 +4325,7 @@ async function createProtection(options = {}) {
|
|
|
4159
4325
|
try {
|
|
4160
4326
|
result = egressEngine.evaluate({ _egress: { url, host, method } });
|
|
4161
4327
|
} catch (err) {
|
|
4162
|
-
onError
|
|
4328
|
+
notify(onError, err, "onError");
|
|
4163
4329
|
return false;
|
|
4164
4330
|
}
|
|
4165
4331
|
if (!result.blocked) return false;
|
|
@@ -4193,7 +4359,7 @@ async function createProtection(options = {}) {
|
|
|
4193
4359
|
try {
|
|
4194
4360
|
result = engine.evaluate(await fromFetchRequest(request));
|
|
4195
4361
|
} catch (err) {
|
|
4196
|
-
onError
|
|
4362
|
+
notify(onError, err, "onError");
|
|
4197
4363
|
return null;
|
|
4198
4364
|
}
|
|
4199
4365
|
return decide("request", result, () => blockResponse(result, request), () => null, fetchRequestMeta(request));
|
|
@@ -4217,7 +4383,7 @@ async function createProtection(options = {}) {
|
|
|
4217
4383
|
try {
|
|
4218
4384
|
result = engine.evaluate(req);
|
|
4219
4385
|
} catch (err) {
|
|
4220
|
-
onError
|
|
4386
|
+
notify(onError, err, "onError");
|
|
4221
4387
|
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req));
|
|
4222
4388
|
return next();
|
|
4223
4389
|
}
|
|
@@ -4256,7 +4422,7 @@ async function createProtection(options = {}) {
|
|
|
4256
4422
|
chunks.push(chunk);
|
|
4257
4423
|
});
|
|
4258
4424
|
req.on("error", (err) => {
|
|
4259
|
-
onError
|
|
4425
|
+
notify(onError, err, "onError");
|
|
4260
4426
|
next();
|
|
4261
4427
|
});
|
|
4262
4428
|
req.on("end", () => {
|
|
@@ -4268,7 +4434,7 @@ async function createProtection(options = {}) {
|
|
|
4268
4434
|
shaped = fromNodeRequest(req, rawBody);
|
|
4269
4435
|
result = engine.evaluate(shaped);
|
|
4270
4436
|
} catch (err) {
|
|
4271
|
-
onError
|
|
4437
|
+
notify(onError, err, "onError");
|
|
4272
4438
|
return next();
|
|
4273
4439
|
}
|
|
4274
4440
|
decide(
|
|
@@ -4315,7 +4481,7 @@ async function createProtection(options = {}) {
|
|
|
4315
4481
|
try {
|
|
4316
4482
|
({ reportManifest: reporter } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
|
|
4317
4483
|
} catch (err) {
|
|
4318
|
-
onError
|
|
4484
|
+
notify(onError, err, "onError");
|
|
4319
4485
|
}
|
|
4320
4486
|
}
|
|
4321
4487
|
const runRefreshTick = async () => {
|
|
@@ -4323,7 +4489,7 @@ async function createProtection(options = {}) {
|
|
|
4323
4489
|
try {
|
|
4324
4490
|
await reporter(cwd);
|
|
4325
4491
|
} catch (err) {
|
|
4326
|
-
onError
|
|
4492
|
+
notify(onError, err, "onError");
|
|
4327
4493
|
}
|
|
4328
4494
|
}
|
|
4329
4495
|
const next = await resolveRules(options, store, { timeoutMs: options.refreshTimeoutMs, pulseAuth });
|
|
@@ -4339,9 +4505,13 @@ async function createProtection(options = {}) {
|
|
|
4339
4505
|
protection.stopRefresh = () => {
|
|
4340
4506
|
loop.stop();
|
|
4341
4507
|
firewallLog?.stop();
|
|
4508
|
+
detections?.stop();
|
|
4342
4509
|
};
|
|
4343
4510
|
} else if (firewallLog) {
|
|
4344
|
-
protection.stopRefresh = () =>
|
|
4511
|
+
protection.stopRefresh = () => {
|
|
4512
|
+
firewallLog.stop();
|
|
4513
|
+
detections?.stop();
|
|
4514
|
+
};
|
|
4345
4515
|
}
|
|
4346
4516
|
return protection;
|
|
4347
4517
|
}
|