@stacksjs/ts-cloud 0.7.108 → 0.7.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alerts/delivery.d.ts +10 -0
- package/dist/alerts/model.d.ts +1 -1
- package/dist/api/client.d.ts +53 -0
- package/dist/api/handler.d.ts +10 -0
- package/dist/api/service.d.ts +1 -0
- package/dist/aws/index.js +10 -10
- package/dist/aws/lambda.d.ts +19 -0
- package/dist/bin/cli.js +1520 -1204
- package/dist/bin/dashboard-server.js +935 -621
- package/dist/chunk-19bs955w.js +12203 -0
- package/dist/{chunk-fmt40tpw.js → chunk-53jatzaw.js} +2 -2
- package/dist/{chunk-ef2dwfrg.js → chunk-8kh577qh.js} +5 -5
- package/dist/{chunk-s9c2x5jv.js → chunk-9v2pwxv9.js} +2 -2
- package/dist/chunk-aa2dkn7b.js +205 -0
- package/dist/{chunk-33sy72r8.js → chunk-d3vx2ekv.js} +71 -25
- package/dist/chunk-d48vtdj4.js +433 -0
- package/dist/{chunk-c6db5m63.js → chunk-t2s3gdq6.js} +253 -8
- package/dist/chunk-va2yd85b.js +200 -0
- package/dist/{chunk-3g4r09dd.js → chunk-wmbep3b5.js} +1 -1
- package/dist/{chunk-eg8ck25v.js → chunk-wmz8x3cy.js} +6935 -15274
- package/dist/{chunk-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
- package/dist/control-plane/types.d.ts +1 -1
- package/dist/deploy/dashboard-protection.test.d.ts +1 -0
- package/dist/deploy/dashboard-spend.test.d.ts +1 -0
- package/dist/deploy/index.js +11 -8
- package/dist/deploy/local-dashboard-server.d.ts +2 -0
- package/dist/drivers/index.js +4 -2
- package/dist/drivers/shared/protection.d.ts +43 -0
- package/dist/drivers/shared/protection.test.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +364 -102
- package/dist/protection/controls.d.ts +127 -0
- package/dist/protection/controls.test.d.ts +1 -0
- package/dist/protection/ddos.d.ts +130 -0
- package/dist/protection/ddos.test.d.ts +1 -0
- package/dist/protection/index.d.ts +11 -0
- package/dist/protection/index.js +89 -0
- package/dist/protection/ratelimit.d.ts +154 -0
- package/dist/protection/ratelimit.test.d.ts +1 -0
- package/dist/protection/waf.d.ts +143 -0
- package/dist/protection/waf.test.d.ts +1 -0
- package/dist/spend/anomaly-config.d.ts +130 -0
- package/dist/spend/anomaly-config.test.d.ts +1 -0
- package/dist/spend/anomaly.d.ts +114 -0
- package/dist/spend/anomaly.test.d.ts +1 -0
- package/dist/spend/api.d.ts +42 -0
- package/dist/spend/api.test.d.ts +1 -0
- package/dist/spend/appliers.d.ts +126 -0
- package/dist/spend/docs.test.d.ts +1 -0
- package/dist/spend/enforcement.d.ts +111 -0
- package/dist/spend/enforcement.integration.test.d.ts +1 -0
- package/dist/spend/evaluator.d.ts +70 -0
- package/dist/spend/evaluator.test.d.ts +1 -0
- package/dist/spend/focus.d.ts +93 -0
- package/dist/spend/focus.test.d.ts +1 -0
- package/dist/spend/gate.d.ts +96 -0
- package/dist/spend/index.d.ts +20 -0
- package/dist/spend/index.js +173 -0
- package/dist/spend/lease.d.ts +54 -0
- package/dist/spend/lease.test.d.ts +1 -0
- package/dist/spend/meter.d.ts +70 -0
- package/dist/spend/meter.test.d.ts +1 -0
- package/dist/spend/model.d.ts +228 -0
- package/dist/spend/notifications.d.ts +58 -0
- package/dist/spend/pricing.d.ts +109 -0
- package/dist/spend/pricing.test.d.ts +1 -0
- package/dist/spend/projection.d.ts +36 -0
- package/dist/spend/runner.d.ts +130 -0
- package/dist/spend/service.d.ts +129 -0
- package/dist/spend/signals.d.ts +131 -0
- package/dist/spend/signals.test.d.ts +1 -0
- package/dist/spend/sms-sdk.test.d.ts +1 -0
- package/dist/spend/spend.integration.test.d.ts +1 -0
- package/dist/spend/store.d.ts +166 -0
- package/dist/spend/transports.d.ts +169 -0
- package/dist/spend/transports.test.d.ts +1 -0
- package/dist/spend/window.d.ts +52 -0
- package/dist/spend/window.test.d.ts +1 -0
- package/dist/ui/404.html +1 -0
- package/dist/ui/access-denied.html +1 -1
- package/dist/ui/account/automation.html +3 -3
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +4 -4
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +4 -4
- package/dist/ui/index.html +4 -4
- package/dist/ui/infrastructure/topology.html +4 -4
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +3 -3
- package/dist/ui/operations/maintenance.html +4 -4
- package/dist/ui/operations/observability.html +4 -4
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +3 -3
- package/dist/ui/operations/regions.html +4 -4
- package/dist/ui/operations/releases.html +4 -4
- package/dist/ui/operations/spend.html +1634 -0
- package/dist/ui/operations/workloads.html +4 -4
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +4 -4
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +100 -161
- package/dist/ui/server/fleet.html +4 -4
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +4 -4
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +4 -4
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/deployments.html +1 -1
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +3 -3
- package/dist/ui-src/pages/operations/spend.stx +225 -0
- package/dist/ui-src/pages/partials/nav.stx +2 -0
- package/dist/ui-src/pages/server/firewall.stx +89 -0
- package/package.json +11 -3
|
@@ -31,6 +31,8 @@ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, rmSync as r
|
|
|
31
31
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
32
32
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
33
33
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
34
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
35
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
34
36
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
35
37
|
import { dirname as dirname4, join as join9 } from "node:path";
|
|
36
38
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -39,7 +41,7 @@ import { existsSync as existsSync5, mkdirSync as mkdirSync2, mkdtempSync as mkdt
|
|
|
39
41
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
40
42
|
import { join as join10, relative as relative3 } from "node:path";
|
|
41
43
|
import { execSync as execSync2 } from "node:child_process";
|
|
42
|
-
import { createHash as
|
|
44
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
43
45
|
import { readdirSync as readdirSync6, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
44
46
|
import { join as join11, relative as relative4, resolve as resolve2 } from "node:path";
|
|
45
47
|
var __defProp = Object.defineProperty;
|
|
@@ -44379,8 +44381,9 @@ export const queue = __handlers.queue
|
|
|
44379
44381
|
export const cli = __handlers.cli
|
|
44380
44382
|
`;
|
|
44381
44383
|
}
|
|
44382
|
-
|
|
44383
|
-
|
|
44384
|
+
var RUNTIME_SOURCES = ["adapter.ts", "recursion.ts", "auto-recursion.ts"];
|
|
44385
|
+
function runtimeSourceDir() {
|
|
44386
|
+
return join7(dirname2(fileURLToPath(import.meta.url)), "runtime");
|
|
44384
44387
|
}
|
|
44385
44388
|
function runBuildHooks(hooks, cwd, onStep) {
|
|
44386
44389
|
for (const hook of hooks ?? []) {
|
|
@@ -44405,7 +44408,8 @@ async function packageServerlessApp(opts) {
|
|
|
44405
44408
|
const entryPath = isAbsolute2(entry) ? entry : join7(projectRoot, entry);
|
|
44406
44409
|
const stage = mkdtempSync(join7(tmpdir(), "tscloud-pkg-"));
|
|
44407
44410
|
try {
|
|
44408
|
-
|
|
44411
|
+
for (const source of RUNTIME_SOURCES)
|
|
44412
|
+
cpSync(join7(runtimeSourceDir(), source), join7(stage, source));
|
|
44409
44413
|
const bootstrapPath = join7(stage, "bootstrap.ts");
|
|
44410
44414
|
writeFileSync3(bootstrapPath, generateBootstrap({ entryImport: entryPath, adapterImport: "./adapter" }));
|
|
44411
44415
|
opts.onStep?.("bundling application");
|
|
@@ -45460,6 +45464,231 @@ function resolveLatestNode(major) {
|
|
|
45460
45464
|
throw new Error(`No Node release found for major version ${major}`);
|
|
45461
45465
|
return match.version.replace(/^v/, "");
|
|
45462
45466
|
}
|
|
45467
|
+
var DEPTH_HEADER = "x-ts-cloud-invoke-depth";
|
|
45468
|
+
var CHAIN_HEADER = "x-ts-cloud-invoke-chain";
|
|
45469
|
+
var TRACE_HEADER = "x-ts-cloud-trace-id";
|
|
45470
|
+
function functionFingerprint(functionId) {
|
|
45471
|
+
return createHash5("sha256").update(functionId).digest("hex").slice(0, 8);
|
|
45472
|
+
}
|
|
45473
|
+
var DEFAULT_RECURSION_LIMITS = {
|
|
45474
|
+
maxDepth: 10,
|
|
45475
|
+
maxRepeats: 3,
|
|
45476
|
+
maxInvocationsPerTrace: 100,
|
|
45477
|
+
traceWindowMs: 60000,
|
|
45478
|
+
breakerThreshold: 5,
|
|
45479
|
+
breakerCooldownMs: 60000
|
|
45480
|
+
};
|
|
45481
|
+
function readHeader(headers, name) {
|
|
45482
|
+
if (typeof headers.get === "function")
|
|
45483
|
+
return headers.get(name) ?? undefined;
|
|
45484
|
+
const record = headers;
|
|
45485
|
+
return record[name] ?? record[name.toLowerCase()] ?? record[name.toUpperCase()];
|
|
45486
|
+
}
|
|
45487
|
+
function parseChain(value, maxEntries = 64) {
|
|
45488
|
+
if (!value)
|
|
45489
|
+
return [];
|
|
45490
|
+
return value.split(".").map((entry) => entry.trim()).filter((entry) => /^[0-9a-f]{8}$/.test(entry)).slice(-maxEntries);
|
|
45491
|
+
}
|
|
45492
|
+
function inspectInvocation(context, limits = DEFAULT_RECURSION_LIMITS) {
|
|
45493
|
+
const fingerprint = functionFingerprint(context.functionId);
|
|
45494
|
+
const incoming = parseChain(readHeader(context.headers, CHAIN_HEADER));
|
|
45495
|
+
const traceId = context.traceId ?? readHeader(context.headers, TRACE_HEADER) ?? crypto.randomUUID();
|
|
45496
|
+
const chain = [...incoming, fingerprint];
|
|
45497
|
+
const repeats = incoming.filter((entry) => entry === fingerprint).length;
|
|
45498
|
+
const selfInvocation = incoming[incoming.length - 1] === fingerprint;
|
|
45499
|
+
const headerDepth = Number.parseInt(readHeader(context.headers, DEPTH_HEADER) ?? "", 10);
|
|
45500
|
+
const depth = Math.max(chain.length, Number.isFinite(headerDepth) ? headerDepth + 1 : 0);
|
|
45501
|
+
let reason = "ok";
|
|
45502
|
+
if (depth > limits.maxDepth)
|
|
45503
|
+
reason = "depth_exceeded";
|
|
45504
|
+
else if (repeats >= limits.maxRepeats)
|
|
45505
|
+
reason = "cycle_detected";
|
|
45506
|
+
else if (selfInvocation && limits.maxRepeats <= 1)
|
|
45507
|
+
reason = "self_invocation";
|
|
45508
|
+
return {
|
|
45509
|
+
reason,
|
|
45510
|
+
depth,
|
|
45511
|
+
chain,
|
|
45512
|
+
traceId,
|
|
45513
|
+
selfInvocation,
|
|
45514
|
+
repeats,
|
|
45515
|
+
propagate: {
|
|
45516
|
+
[DEPTH_HEADER]: String(depth),
|
|
45517
|
+
[CHAIN_HEADER]: chain.join("."),
|
|
45518
|
+
[TRACE_HEADER]: traceId
|
|
45519
|
+
}
|
|
45520
|
+
};
|
|
45521
|
+
}
|
|
45522
|
+
var MESSAGES = {
|
|
45523
|
+
ok: "",
|
|
45524
|
+
depth_exceeded: "Invocation chain is deeper than the configured limit; the call was refused to stop a runaway loop.",
|
|
45525
|
+
cycle_detected: "This function already appears in the invocation chain; the call was refused as a recursion loop.",
|
|
45526
|
+
self_invocation: "This function invoked itself directly; the call was refused.",
|
|
45527
|
+
trace_budget_exceeded: "This request has already made more invocations than its budget allows.",
|
|
45528
|
+
breaker_open: "Recursion protection is open for this function after repeated loop detections."
|
|
45529
|
+
};
|
|
45530
|
+
|
|
45531
|
+
class RecursionGuard {
|
|
45532
|
+
limits;
|
|
45533
|
+
clock;
|
|
45534
|
+
traces = new Map;
|
|
45535
|
+
breakers = new Map;
|
|
45536
|
+
constructor(limits = DEFAULT_RECURSION_LIMITS, clock = () => Date.now()) {
|
|
45537
|
+
this.limits = limits;
|
|
45538
|
+
this.clock = clock;
|
|
45539
|
+
}
|
|
45540
|
+
check(context) {
|
|
45541
|
+
const now = this.clock();
|
|
45542
|
+
this.expireTraces(now);
|
|
45543
|
+
const inspected = inspectInvocation(context, this.limits);
|
|
45544
|
+
const breaker = this.breakers.get(context.functionId);
|
|
45545
|
+
if (breaker?.openUntil != null && breaker.openUntil > now)
|
|
45546
|
+
return this.deny(context.functionId, inspected, "breaker_open", now, false);
|
|
45547
|
+
if (inspected.reason !== "ok")
|
|
45548
|
+
return this.deny(context.functionId, inspected, inspected.reason, now, true);
|
|
45549
|
+
const tally = this.traces.get(inspected.traceId) ?? { count: 0, firstAt: now };
|
|
45550
|
+
if (tally.count >= this.limits.maxInvocationsPerTrace) {
|
|
45551
|
+
this.traces.set(inspected.traceId, tally);
|
|
45552
|
+
return this.deny(context.functionId, inspected, "trace_budget_exceeded", now, true);
|
|
45553
|
+
}
|
|
45554
|
+
this.traces.set(inspected.traceId, { count: tally.count + 1, firstAt: tally.firstAt });
|
|
45555
|
+
if (breaker)
|
|
45556
|
+
this.breakers.set(context.functionId, { consecutiveBlocks: 0 });
|
|
45557
|
+
return { ...inspected, allowed: true, reason: "ok", propagate: inspected.propagate };
|
|
45558
|
+
}
|
|
45559
|
+
deny(functionId, inspected, reason, now, countTowardBreaker) {
|
|
45560
|
+
if (countTowardBreaker) {
|
|
45561
|
+
const state = this.breakers.get(functionId) ?? { consecutiveBlocks: 0 };
|
|
45562
|
+
const consecutiveBlocks = state.consecutiveBlocks + 1;
|
|
45563
|
+
this.breakers.set(functionId, {
|
|
45564
|
+
consecutiveBlocks,
|
|
45565
|
+
openUntil: consecutiveBlocks >= this.limits.breakerThreshold ? now + this.limits.breakerCooldownMs : state.openUntil
|
|
45566
|
+
});
|
|
45567
|
+
}
|
|
45568
|
+
return { ...inspected, allowed: false, reason, message: MESSAGES[reason] };
|
|
45569
|
+
}
|
|
45570
|
+
breakerOpen(functionId) {
|
|
45571
|
+
const state = this.breakers.get(functionId);
|
|
45572
|
+
return state?.openUntil != null && state.openUntil > this.clock();
|
|
45573
|
+
}
|
|
45574
|
+
reset(functionId) {
|
|
45575
|
+
if (functionId) {
|
|
45576
|
+
this.breakers.delete(functionId);
|
|
45577
|
+
return;
|
|
45578
|
+
}
|
|
45579
|
+
this.breakers.clear();
|
|
45580
|
+
this.traces.clear();
|
|
45581
|
+
}
|
|
45582
|
+
expireTraces(now) {
|
|
45583
|
+
if (this.traces.size === 0)
|
|
45584
|
+
return;
|
|
45585
|
+
for (const [traceId, tally] of this.traces)
|
|
45586
|
+
if (now - tally.firstAt > this.limits.traceWindowMs)
|
|
45587
|
+
this.traces.delete(traceId);
|
|
45588
|
+
}
|
|
45589
|
+
get trackedTraces() {
|
|
45590
|
+
return this.traces.size;
|
|
45591
|
+
}
|
|
45592
|
+
}
|
|
45593
|
+
function propagationHeaders(verdict) {
|
|
45594
|
+
return { ...verdict.propagate };
|
|
45595
|
+
}
|
|
45596
|
+
function recursionBlockedResponse(verdict) {
|
|
45597
|
+
return {
|
|
45598
|
+
status: 508,
|
|
45599
|
+
headers: { "content-type": "application/json", "x-ts-cloud-recursion-blocked": verdict.reason },
|
|
45600
|
+
body: {
|
|
45601
|
+
error: "recursion_blocked",
|
|
45602
|
+
reason: verdict.reason,
|
|
45603
|
+
message: verdict.message ?? MESSAGES[verdict.reason],
|
|
45604
|
+
depth: verdict.depth,
|
|
45605
|
+
traceId: verdict.traceId
|
|
45606
|
+
}
|
|
45607
|
+
};
|
|
45608
|
+
}
|
|
45609
|
+
var invocationContext = new AsyncLocalStorage;
|
|
45610
|
+
function envFlag(name) {
|
|
45611
|
+
const raw = globalThis.process?.env?.[name];
|
|
45612
|
+
if (raw == null || raw === "")
|
|
45613
|
+
return;
|
|
45614
|
+
return raw !== "0" && raw.toLowerCase() !== "false";
|
|
45615
|
+
}
|
|
45616
|
+
function defaultFunctionId() {
|
|
45617
|
+
const env = globalThis.process?.env ?? {};
|
|
45618
|
+
return env.TS_CLOUD_FUNCTION_ID || env.AWS_LAMBDA_FUNCTION_NAME || env.FUNCTION_NAME || "function";
|
|
45619
|
+
}
|
|
45620
|
+
function resolveRecursionConfig(config2) {
|
|
45621
|
+
const explicit = config2 === false ? { enabled: false } : config2 ?? {};
|
|
45622
|
+
const envEnabled = envFlag("TS_CLOUD_RECURSION_PROTECTION");
|
|
45623
|
+
const envDetectionOnly = envFlag("TS_CLOUD_RECURSION_DETECTION_ONLY");
|
|
45624
|
+
return {
|
|
45625
|
+
enabled: envEnabled ?? explicit.enabled ?? true,
|
|
45626
|
+
detectionOnly: envDetectionOnly ?? explicit.detectionOnly ?? false,
|
|
45627
|
+
limits: { ...DEFAULT_RECURSION_LIMITS, ...explicit.limits },
|
|
45628
|
+
functionId: explicit.functionId ?? defaultFunctionId()
|
|
45629
|
+
};
|
|
45630
|
+
}
|
|
45631
|
+
var installedFetch;
|
|
45632
|
+
var originalFetch;
|
|
45633
|
+
function installRecursionFetch() {
|
|
45634
|
+
const current = globalThis.fetch;
|
|
45635
|
+
if (!current || installedFetch === current)
|
|
45636
|
+
return () => {};
|
|
45637
|
+
originalFetch = current;
|
|
45638
|
+
const wrapped = async (input, init) => {
|
|
45639
|
+
const verdict = invocationContext.getStore();
|
|
45640
|
+
if (!verdict)
|
|
45641
|
+
return originalFetch(input, init);
|
|
45642
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
45643
|
+
for (const [key, value] of Object.entries(propagationHeaders(verdict)))
|
|
45644
|
+
headers.set(key, value);
|
|
45645
|
+
if (input instanceof Request)
|
|
45646
|
+
return originalFetch(new Request(input, { headers }), init);
|
|
45647
|
+
return originalFetch(input, { ...init, headers });
|
|
45648
|
+
};
|
|
45649
|
+
Object.assign(wrapped, current);
|
|
45650
|
+
globalThis.fetch = wrapped;
|
|
45651
|
+
installedFetch = wrapped;
|
|
45652
|
+
return () => {
|
|
45653
|
+
if (globalThis.fetch === installedFetch && originalFetch)
|
|
45654
|
+
globalThis.fetch = originalFetch;
|
|
45655
|
+
installedFetch = undefined;
|
|
45656
|
+
};
|
|
45657
|
+
}
|
|
45658
|
+
var sharedGuard;
|
|
45659
|
+
var sharedLimits;
|
|
45660
|
+
function sharedRecursionGuard(limits) {
|
|
45661
|
+
if (!sharedGuard || JSON.stringify(sharedLimits) !== JSON.stringify(limits)) {
|
|
45662
|
+
sharedGuard = new RecursionGuard(limits);
|
|
45663
|
+
sharedLimits = limits;
|
|
45664
|
+
}
|
|
45665
|
+
return sharedGuard;
|
|
45666
|
+
}
|
|
45667
|
+
function resetRecursionRuntime() {
|
|
45668
|
+
sharedGuard = undefined;
|
|
45669
|
+
sharedLimits = undefined;
|
|
45670
|
+
if (installedFetch && originalFetch && globalThis.fetch === installedFetch)
|
|
45671
|
+
globalThis.fetch = originalFetch;
|
|
45672
|
+
installedFetch = undefined;
|
|
45673
|
+
originalFetch = undefined;
|
|
45674
|
+
}
|
|
45675
|
+
function checkInvocation(headers, config2) {
|
|
45676
|
+
const resolved = resolveRecursionConfig(config2);
|
|
45677
|
+
if (!resolved.enabled)
|
|
45678
|
+
return;
|
|
45679
|
+
const guard = sharedRecursionGuard(resolved.limits);
|
|
45680
|
+
const context = { functionId: resolved.functionId, headers };
|
|
45681
|
+
const verdict = guard.check(context);
|
|
45682
|
+
return {
|
|
45683
|
+
verdict,
|
|
45684
|
+
blocked: !verdict.allowed && !resolved.detectionOnly,
|
|
45685
|
+
observed: !verdict.allowed && resolved.detectionOnly
|
|
45686
|
+
};
|
|
45687
|
+
}
|
|
45688
|
+
function withInvocation(verdict, work) {
|
|
45689
|
+
installRecursionFetch();
|
|
45690
|
+
return invocationContext.run(verdict, work);
|
|
45691
|
+
}
|
|
45463
45692
|
function resolveApp(mod) {
|
|
45464
45693
|
const m = mod;
|
|
45465
45694
|
const def = m?.default ?? m;
|
|
@@ -45550,9 +45779,25 @@ function createHttpHandler(handler8, opts) {
|
|
|
45550
45779
|
};
|
|
45551
45780
|
}
|
|
45552
45781
|
}
|
|
45782
|
+
const recursion = checkInvocation(event.headers ?? {}, opts?.recursionProtection);
|
|
45783
|
+
if (recursion?.blocked) {
|
|
45784
|
+
const blocked = recursionBlockedResponse(recursion.verdict);
|
|
45785
|
+
return {
|
|
45786
|
+
statusCode: blocked.status,
|
|
45787
|
+
headers: { ...blocked.headers, "retry-after": "60" },
|
|
45788
|
+
body: JSON.stringify(blocked.body),
|
|
45789
|
+
isBase64Encoded: false
|
|
45790
|
+
};
|
|
45791
|
+
}
|
|
45553
45792
|
const request = eventToRequest(event);
|
|
45554
|
-
|
|
45555
|
-
|
|
45793
|
+
if (!recursion) {
|
|
45794
|
+
const response = await handler8(request);
|
|
45795
|
+
return responseToResult(response);
|
|
45796
|
+
}
|
|
45797
|
+
if (recursion.observed) {
|
|
45798
|
+
console.warn(`[ts-cloud] recursion detected (${recursion.verdict.reason}) at depth ${recursion.verdict.depth}; detection-only mode let it run`);
|
|
45799
|
+
}
|
|
45800
|
+
return withInvocation(recursion.verdict, async () => responseToResult(await handler8(request)));
|
|
45556
45801
|
};
|
|
45557
45802
|
}
|
|
45558
45803
|
function parseRecordBody(body) {
|
|
@@ -45769,7 +46014,7 @@ function packagePhpApp(opts) {
|
|
|
45769
46014
|
const handler8 = opts.app.handlers?.http ?? "public/index.php";
|
|
45770
46015
|
return {
|
|
45771
46016
|
zip: zip2,
|
|
45772
|
-
sha256:
|
|
46017
|
+
sha256: createHash6("sha256").update(zip2).digest("hex"),
|
|
45773
46018
|
handlers: {
|
|
45774
46019
|
http: handler8,
|
|
45775
46020
|
queue: opts.app.handlers?.queue ?? handler8,
|
|
@@ -46217,4 +46462,4 @@ class ResourceManagementManager {
|
|
|
46217
46462
|
}
|
|
46218
46463
|
var resourceManagementManager = new ResourceManagementManager;
|
|
46219
46464
|
|
|
46220
|
-
export { detectServiceRegion, signRequest, signRequestAsync, createPresignedUrl, createPresignedUrlAsync, makeAWSRequest, makeAWSRequestOnce, makeAWSRequestAsync, parseXMLResponse, parseJSONResponse, clearSigningKeyCache, getSigningKeyCacheSize, isNodeCryptoAvailable, isWebCryptoAvailable, QueuePresets, RealtimePresets, DEFAULT_STATE_DIR, STATE_DIR_ENV_VAR, setStateDir, stateDir, statePath, resolveStatePath, isStatePath, detectDeploymentTargets, resolveDeploymentMode, deploymentCoexistenceError, resolveAppDatabase, resolveCloudProvider, TemplateBuilder, Pseudo, generateResourceName, generateLogicalId, getTimestamp, sanitizeName, resolveProjectStackName, resolveSiteStackName, resolveSiteResourceName, resolveSiteBucketName, resolveStorageBucketName, resolveDeployBucketName, resolveServerlessAppStackName, resolveServerlessArtifactBucketName, resolveServerlessAssetBucketName, DependencyGraph, analyzeStackDiff, formatDiff, Storage, Registry, CDN, DNS, Security, Compute, Network, FileSystem, Email, exports_incoming_call, Phone, Queue, JobLoader, StacksIntegration, exports_send, SMS, Sftp, AI, Database, Cache, Permissions, ApiGateway, Messaging, Workflow, Monitoring, Auth, Deployment, AssetHasher, Secrets, ParameterStore, Search, Redirects, Communication, Fn2, Arn, AWS_PSEUDO_PARAMETERS, CloudFormationBuilder, buildCloudFormationTemplate, createStaticSitePreset, createNodeJsServerPreset, createNodeJsServerlessPreset, createServerlessNodePreset, createExistingStaticFullStackPreset, createFullStackAppPreset, createApiBackendPreset, createWordPressPreset, createJamstackPreset, createMicroservicesPreset, createRealtimeAppPreset, createDataPipelinePreset, createMLApiPreset, createTraditionalWebAppPreset, createLaravelPreset, createServerlessLaravelPreset, createDashboardSite, DASHBOARD_STATE_DIR, DASHBOARD_ENTRY, DASHBOARD_PORT_BASE, DASHBOARD_PORT_SPAN, deriveManagementDashboardPort, resolveDashboardDomain, resolveDashboardDomains, hasManagementDashboardSite, MANAGEMENT_DASHBOARD_SITE_PREFIX, managementDashboardSiteName, isManagementDashboardSiteName, resolveManagementDashboardSites, resolveManagementDashboardSite, extendPreset, composePresets, createPreset, mergeInfrastructure, withMonitoring, withSecurity, withDatabase, withCache, withCDN, withQueue, fromEnvironment, fromSharedCredentials, fromEC2Metadata, fromECSMetadata, fromWebIdentity, getCredentials, createCredentialProvider, resolveCredentials, resolveRegion, getAccountId, S3Error, createS3Client, CloudError, ConfigurationError, CredentialError, DeploymentError, ValidationError, AWSAPIError, ErrorCodes, getErrorDetails, createError, DebugLogger, validateConfiguration, detectMisconfigurations, validateCredentials, checkIAMPermissions, getRequiredPermissions, suggestIAMPolicy, DEFAULT_SERVICE_LIMITS, checkServiceQuotas, getQuotaUsageSummary, suggestQuotaIncrease, FileCache, TemplateCache, templateCache, hashFile, hashString, hashBuffer, hashDirectory, hashManifest, quickHash, findChangedFiles, HashCache, parallel, batch, parallelMap, parallelWithRetry, sequence, withTimeout, RateLimiter, chunk, processInChunks, diffTemplates, getDiffSummary, requiresReplacement, categorizeChanges, getDeploymentStrategy, getDiffStats, cloud_config_schema_default, validateAgainstSchema, defaultLocalConfig, getLocalConfig, isLocalDevelopment, getLocalEndpoint, getLocalCredentials, getLocalEnvVars, MockCloudFormation, MockS3, MockDynamoDB, createMockAWS, PreviewEnvironmentManager, previewManager, generatePreviewWorkflow, generateCleanupWorkflow, generateCostReportWorkflow, PreviewNotificationService, previewNotifications, formatTable, formatTree, formatProgressBar, formatBytes, formatDuration, formatList, formatKeyValue, ProgressBar, MultiStepProgress, Spinner, TaskList, suggestCommand, formatSuggestion, isLikelyTypo, getContextualHelp, searchCommands, autocomplete, suggestFlags, formatFlagSuggestions, getCommandUsage, validateCommand, REPL, REPLContext, REPLCommandBuilder, CommandHistory, formatHistory, formatHistoryStats, MultiRegionManager, multiRegionManager, CrossRegionReferenceManager, GlobalResourceManager, RegionPairManager, StackDependencyManager, crossRegionReferenceManager, globalResourceManager, regionPairManager, stackDependencyManager, AWS_REGIONS, getRegion, getAllRegions, getRegionsByLocation, getRegionsByCompliance, getRegionsByPricingTier, isValidRegion, getClosestRegion, suggestRegions, suggestRegionPairs, formatRegion, formatRegionList, getRegionStats, MultiAccountManager, OrganizationManager, multiAccountManager, organizationManager, RECOMMENDED_ACCOUNT_STRUCTURES, RECOMMENDED_SCPS, COMMON_CROSS_ACCOUNT_ROLES, getRecommendedStructure, generateCrossAccountRoleCF, validateAccountStructure, formatAccountStructure, generateDeploymentWorkflow, generateMultiEnvWorkflow, generatePRPreviewWorkflow, generateScheduledWorkflow, generateMatrixWorkflow, generateDeploymentPipeline, generateMultiEnvPipeline, generatePreviewPipeline, generateScheduledPipeline, generateManualPipeline, generateDeploymentConfig, generateMultiEnvConfig, generateScheduledConfig, generateApprovalConfig, generateParallelConfig, BackupManager, backupManager, DisasterRecoveryManager, drManager, AWSConfigManager, awsConfigManager, CloudTrailManager, cloudTrailManager, GuardDutyManager, guardDutyManager, SecurityHubManager, securityHubManager, BlueGreenManager, blueGreenManager, CanaryManager, canaryManager, ABTestManager, abTestManager, ProgressiveDeploymentManager, progressiveDeploymentManager, XRayManager, xrayManager, MetricsManager, metricsManager, LogsManager, logsManager, SyntheticsManager, syntheticsManager, MigrationManager, migrationManager, ReplicaManager, replicaManager, PerformanceManager, performanceManager, DatabaseUserManager, databaseUserManager, SecretsRotationManager, secretsRotationManager, SecretsManager, secretsManager, CertificateManager, certificateManager, SecurityScanningManager, securityScanningManager, ImageScanningManager, imageScanningManager, BuildOptimizationManager, buildOptimizationManager, ContainerRegistryManager, containerRegistryManager, ServiceMeshManager, serviceMeshManager, LambdaLayersManager, lambdaLayersManager, LambdaVersionsManager, lambdaVersionsManager, LambdaConcurrencyManager, lambdaConcurrencyManager, LambdaDestinationsManager, lambdaDestinationsManager, LambdaVPCManager, lambdaVPCManager, LambdaDLQManager, lambdaDLQManager, Route53RoutingManager, route53RoutingManager, DNSSECManager, dnssecManager, Route53ResolverManager, route53ResolverManager, BounceComplaintHandler, bounceComplaintHandler, EmailAnalyticsManager, emailAnalyticsManager, SenderReputationManager, senderReputationManager, EmailTemplateManager, emailTemplateManager, exports_advanced, exports_inbound, exports_advanced2, exports_advanced3, FIFOQueueManager, fifoQueueManager, DLQMonitoringManager, dlqMonitoringManager, BatchProcessingManager, batchProcessingManager, QueueManagementManager, queueManagementManager, createZip, generateBootstrap, runBuildHooks, sha256, artifactKey, packageServerlessApp, MANAGED_NODE_VERSIONS, resolveServerlessRuntime, resolveQueues, resolveQueueNames, composeServerlessAppTemplate, PHP_LAYER_EXTENSIONS, phpLayerPackages, phpLayerBuildStage, generatePhpLayerDockerfile, generateAppImageDockerfile, sharedRuntimeLoop, buildNodeRuntimeLayerZip, buildBunRuntimeLayerZip, resolveApp, eventToRequest, responseToResult, createHttpHandler, createQueueHandler, createCliHandler, createHandlers, generatePhpFpmConfig, phpRuntimeLayerAssets, laravelServerlessEnvDefaults, LARAVEL_SERVERLESS_BUILD_STEPS, buildPhpRuntimeLayerZip, PHP_DEFAULT_EXCLUDES, runPhpBuildHooks, collectPhpAppEntries, packagePhpApp, StaticSiteManager, staticSiteManager, StorageAdvancedManager, storageAdvancedManager, HealthCheckManager, healthCheckManager, NetworkSecurityManager, networkSecurityManager, ResourceManagementManager, resourceManagementManager };
|
|
46465
|
+
export { detectServiceRegion, signRequest, signRequestAsync, createPresignedUrl, createPresignedUrlAsync, makeAWSRequest, makeAWSRequestOnce, makeAWSRequestAsync, parseXMLResponse, parseJSONResponse, clearSigningKeyCache, getSigningKeyCacheSize, isNodeCryptoAvailable, isWebCryptoAvailable, QueuePresets, RealtimePresets, DEFAULT_STATE_DIR, STATE_DIR_ENV_VAR, setStateDir, stateDir, statePath, resolveStatePath, isStatePath, detectDeploymentTargets, resolveDeploymentMode, deploymentCoexistenceError, resolveAppDatabase, resolveCloudProvider, TemplateBuilder, Pseudo, generateResourceName, generateLogicalId, getTimestamp, sanitizeName, resolveProjectStackName, resolveSiteStackName, resolveSiteResourceName, resolveSiteBucketName, resolveStorageBucketName, resolveDeployBucketName, resolveServerlessAppStackName, resolveServerlessArtifactBucketName, resolveServerlessAssetBucketName, DependencyGraph, analyzeStackDiff, formatDiff, Storage, Registry, CDN, DNS, Security, Compute, Network, FileSystem, Email, exports_incoming_call, Phone, Queue, JobLoader, StacksIntegration, exports_send, SMS, Sftp, AI, Database, Cache, Permissions, ApiGateway, Messaging, Workflow, Monitoring, Auth, Deployment, AssetHasher, Secrets, ParameterStore, Search, Redirects, Communication, Fn2, Arn, AWS_PSEUDO_PARAMETERS, CloudFormationBuilder, buildCloudFormationTemplate, createStaticSitePreset, createNodeJsServerPreset, createNodeJsServerlessPreset, createServerlessNodePreset, createExistingStaticFullStackPreset, createFullStackAppPreset, createApiBackendPreset, createWordPressPreset, createJamstackPreset, createMicroservicesPreset, createRealtimeAppPreset, createDataPipelinePreset, createMLApiPreset, createTraditionalWebAppPreset, createLaravelPreset, createServerlessLaravelPreset, createDashboardSite, DASHBOARD_STATE_DIR, DASHBOARD_ENTRY, DASHBOARD_PORT_BASE, DASHBOARD_PORT_SPAN, deriveManagementDashboardPort, resolveDashboardDomain, resolveDashboardDomains, hasManagementDashboardSite, MANAGEMENT_DASHBOARD_SITE_PREFIX, managementDashboardSiteName, isManagementDashboardSiteName, resolveManagementDashboardSites, resolveManagementDashboardSite, extendPreset, composePresets, createPreset, mergeInfrastructure, withMonitoring, withSecurity, withDatabase, withCache, withCDN, withQueue, fromEnvironment, fromSharedCredentials, fromEC2Metadata, fromECSMetadata, fromWebIdentity, getCredentials, createCredentialProvider, resolveCredentials, resolveRegion, getAccountId, S3Error, createS3Client, CloudError, ConfigurationError, CredentialError, DeploymentError, ValidationError, AWSAPIError, ErrorCodes, getErrorDetails, createError, DebugLogger, validateConfiguration, detectMisconfigurations, validateCredentials, checkIAMPermissions, getRequiredPermissions, suggestIAMPolicy, DEFAULT_SERVICE_LIMITS, checkServiceQuotas, getQuotaUsageSummary, suggestQuotaIncrease, FileCache, TemplateCache, templateCache, hashFile, hashString, hashBuffer, hashDirectory, hashManifest, quickHash, findChangedFiles, HashCache, parallel, batch, parallelMap, parallelWithRetry, sequence, withTimeout, RateLimiter, chunk, processInChunks, diffTemplates, getDiffSummary, requiresReplacement, categorizeChanges, getDeploymentStrategy, getDiffStats, cloud_config_schema_default, validateAgainstSchema, defaultLocalConfig, getLocalConfig, isLocalDevelopment, getLocalEndpoint, getLocalCredentials, getLocalEnvVars, MockCloudFormation, MockS3, MockDynamoDB, createMockAWS, PreviewEnvironmentManager, previewManager, generatePreviewWorkflow, generateCleanupWorkflow, generateCostReportWorkflow, PreviewNotificationService, previewNotifications, formatTable, formatTree, formatProgressBar, formatBytes, formatDuration, formatList, formatKeyValue, ProgressBar, MultiStepProgress, Spinner, TaskList, suggestCommand, formatSuggestion, isLikelyTypo, getContextualHelp, searchCommands, autocomplete, suggestFlags, formatFlagSuggestions, getCommandUsage, validateCommand, REPL, REPLContext, REPLCommandBuilder, CommandHistory, formatHistory, formatHistoryStats, MultiRegionManager, multiRegionManager, CrossRegionReferenceManager, GlobalResourceManager, RegionPairManager, StackDependencyManager, crossRegionReferenceManager, globalResourceManager, regionPairManager, stackDependencyManager, AWS_REGIONS, getRegion, getAllRegions, getRegionsByLocation, getRegionsByCompliance, getRegionsByPricingTier, isValidRegion, getClosestRegion, suggestRegions, suggestRegionPairs, formatRegion, formatRegionList, getRegionStats, MultiAccountManager, OrganizationManager, multiAccountManager, organizationManager, RECOMMENDED_ACCOUNT_STRUCTURES, RECOMMENDED_SCPS, COMMON_CROSS_ACCOUNT_ROLES, getRecommendedStructure, generateCrossAccountRoleCF, validateAccountStructure, formatAccountStructure, generateDeploymentWorkflow, generateMultiEnvWorkflow, generatePRPreviewWorkflow, generateScheduledWorkflow, generateMatrixWorkflow, generateDeploymentPipeline, generateMultiEnvPipeline, generatePreviewPipeline, generateScheduledPipeline, generateManualPipeline, generateDeploymentConfig, generateMultiEnvConfig, generateScheduledConfig, generateApprovalConfig, generateParallelConfig, BackupManager, backupManager, DisasterRecoveryManager, drManager, AWSConfigManager, awsConfigManager, CloudTrailManager, cloudTrailManager, GuardDutyManager, guardDutyManager, SecurityHubManager, securityHubManager, BlueGreenManager, blueGreenManager, CanaryManager, canaryManager, ABTestManager, abTestManager, ProgressiveDeploymentManager, progressiveDeploymentManager, XRayManager, xrayManager, MetricsManager, metricsManager, LogsManager, logsManager, SyntheticsManager, syntheticsManager, MigrationManager, migrationManager, ReplicaManager, replicaManager, PerformanceManager, performanceManager, DatabaseUserManager, databaseUserManager, SecretsRotationManager, secretsRotationManager, SecretsManager, secretsManager, CertificateManager, certificateManager, SecurityScanningManager, securityScanningManager, ImageScanningManager, imageScanningManager, BuildOptimizationManager, buildOptimizationManager, ContainerRegistryManager, containerRegistryManager, ServiceMeshManager, serviceMeshManager, LambdaLayersManager, lambdaLayersManager, LambdaVersionsManager, lambdaVersionsManager, LambdaConcurrencyManager, lambdaConcurrencyManager, LambdaDestinationsManager, lambdaDestinationsManager, LambdaVPCManager, lambdaVPCManager, LambdaDLQManager, lambdaDLQManager, Route53RoutingManager, route53RoutingManager, DNSSECManager, dnssecManager, Route53ResolverManager, route53ResolverManager, BounceComplaintHandler, bounceComplaintHandler, EmailAnalyticsManager, emailAnalyticsManager, SenderReputationManager, senderReputationManager, EmailTemplateManager, emailTemplateManager, exports_advanced, exports_inbound, exports_advanced2, exports_advanced3, FIFOQueueManager, fifoQueueManager, DLQMonitoringManager, dlqMonitoringManager, BatchProcessingManager, batchProcessingManager, QueueManagementManager, queueManagementManager, createZip, generateBootstrap, runBuildHooks, sha256, artifactKey, packageServerlessApp, MANAGED_NODE_VERSIONS, resolveServerlessRuntime, resolveQueues, resolveQueueNames, composeServerlessAppTemplate, PHP_LAYER_EXTENSIONS, phpLayerPackages, phpLayerBuildStage, generatePhpLayerDockerfile, generateAppImageDockerfile, sharedRuntimeLoop, buildNodeRuntimeLayerZip, buildBunRuntimeLayerZip, DEPTH_HEADER, CHAIN_HEADER, TRACE_HEADER, functionFingerprint, DEFAULT_RECURSION_LIMITS, parseChain, inspectInvocation, RecursionGuard, propagationHeaders, recursionBlockedResponse, invocationContext, resolveRecursionConfig, installRecursionFetch, sharedRecursionGuard, resetRecursionRuntime, checkInvocation, withInvocation, resolveApp, eventToRequest, responseToResult, createHttpHandler, createQueueHandler, createCliHandler, createHandlers, generatePhpFpmConfig, phpRuntimeLayerAssets, laravelServerlessEnvDefaults, LARAVEL_SERVERLESS_BUILD_STEPS, buildPhpRuntimeLayerZip, PHP_DEFAULT_EXCLUDES, runPhpBuildHooks, collectPhpAppEntries, packagePhpApp, StaticSiteManager, staticSiteManager, StorageAdvancedManager, storageAdvancedManager, HealthCheckManager, healthCheckManager, NetworkSecurityManager, networkSecurityManager, ResourceManagementManager, resourceManagementManager };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/protection/waf.ts
|
|
2
|
+
var DEFAULTS = {
|
|
3
|
+
mode: "detection",
|
|
4
|
+
paranoiaLevel: 1,
|
|
5
|
+
inboundThreshold: 5,
|
|
6
|
+
outboundThreshold: 4,
|
|
7
|
+
maxBodyBytes: 13107200,
|
|
8
|
+
rejectOversizedBody: true,
|
|
9
|
+
auditLogPath: "/var/log/ts-cloud/waf-audit.log",
|
|
10
|
+
blockStatus: 403
|
|
11
|
+
};
|
|
12
|
+
var RULE_IDS = {
|
|
13
|
+
bypassBase: 1e5,
|
|
14
|
+
exclusionBase: 101000,
|
|
15
|
+
rateLimitBase: 102000,
|
|
16
|
+
customBase: 109000
|
|
17
|
+
};
|
|
18
|
+
function escapeSecLang(value) {
|
|
19
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ");
|
|
20
|
+
}
|
|
21
|
+
function isSafePattern(pattern) {
|
|
22
|
+
if (pattern.length > 1000)
|
|
23
|
+
return false;
|
|
24
|
+
if (/\([^)]*[+*]\)[+*]/.test(pattern))
|
|
25
|
+
return false;
|
|
26
|
+
if (/\[[^\]]*\][+*]\{/.test(pattern))
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
new RegExp(pattern);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function rateLimitMarkers(rules, startId) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
const unmapped = [];
|
|
38
|
+
let id = startId;
|
|
39
|
+
for (const rule of rules) {
|
|
40
|
+
if (rule.enabled === false)
|
|
41
|
+
continue;
|
|
42
|
+
if (rule.key && (rule.key.source === "cookie" || rule.key.source === "header") && !rule.key.name) {
|
|
43
|
+
unmapped.push({ ruleId: rule.id, reason: "The rule keys on a header or cookie but names none." });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const pattern = rule.path ? globToRegex(rule.path) : ".*";
|
|
47
|
+
if (!isSafePattern(pattern)) {
|
|
48
|
+
unmapped.push({ ruleId: rule.id, reason: "The path pattern compiles to an unsafe regular expression." });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const conditions = [`SecRule REQUEST_URI "@rx ${escapeSecLang(pattern)}"`];
|
|
52
|
+
if (rule.methods?.length)
|
|
53
|
+
conditions.push(` "chain,id:${id},phase:1,pass,nolog,setvar:'tx.ts_cloud_ratelimit=${escapeSecLang(rule.id)}'"`);
|
|
54
|
+
else
|
|
55
|
+
conditions.push(` "id:${id},phase:1,pass,nolog,setvar:'tx.ts_cloud_ratelimit=${escapeSecLang(rule.id)}'"`);
|
|
56
|
+
lines.push(`# Marks requests governed by the ts-cloud rate limit "${escapeSecLang(rule.id)}".`);
|
|
57
|
+
lines.push(conditions.join(" \\\n"));
|
|
58
|
+
if (rule.methods?.length)
|
|
59
|
+
lines.push(` SecRule REQUEST_METHOD "@within ${escapeSecLang(rule.methods.join(" ").toUpperCase())}" "t:none"`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
id++;
|
|
62
|
+
}
|
|
63
|
+
return { lines, unmapped };
|
|
64
|
+
}
|
|
65
|
+
function globToRegex(glob) {
|
|
66
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x01").replace(/\*/g, "[^/]*").replace(/\u0001/g, ".*");
|
|
67
|
+
return `^${escaped}$`;
|
|
68
|
+
}
|
|
69
|
+
function renderWafConfig(config = {}, rateLimits = []) {
|
|
70
|
+
const mode = config.mode ?? DEFAULTS.mode;
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const paranoia = config.paranoiaLevel ?? DEFAULTS.paranoiaLevel;
|
|
73
|
+
const inbound = config.inboundThreshold ?? DEFAULTS.inboundThreshold;
|
|
74
|
+
const outbound = config.outboundThreshold ?? DEFAULTS.outboundThreshold;
|
|
75
|
+
const maxBody = config.maxBodyBytes ?? DEFAULTS.maxBodyBytes;
|
|
76
|
+
const rejectOversized = config.rejectOversizedBody !== false;
|
|
77
|
+
if (mode === "blocking" && paranoia >= 3)
|
|
78
|
+
warnings.push(`Paranoia level ${paranoia} blocks legitimate traffic on most applications until it is tuned. Run it in detection mode first.`);
|
|
79
|
+
if (mode === "blocking")
|
|
80
|
+
warnings.push("zig-waf is pre-alpha. Blocking mode makes it a failure path for every request.");
|
|
81
|
+
if (!rejectOversized)
|
|
82
|
+
warnings.push("Oversized bodies are passed through uninspected, so padding a request bypasses every rule.");
|
|
83
|
+
if (config.bypassPaths?.length)
|
|
84
|
+
warnings.push(`${config.bypassPaths.length} path(s) bypass inspection entirely.`);
|
|
85
|
+
const lines = [
|
|
86
|
+
"# Managed by ts-cloud. Edits are overwritten on the next deploy.",
|
|
87
|
+
`# Mode: ${mode}. Paranoia level: ${paranoia}.`,
|
|
88
|
+
""
|
|
89
|
+
];
|
|
90
|
+
if (mode === "off") {
|
|
91
|
+
lines.push("SecRuleEngine Off", "");
|
|
92
|
+
return { config: `${lines.join(`
|
|
93
|
+
`)}
|
|
94
|
+
`, unmappedRateLimits: [], warnings, mode };
|
|
95
|
+
}
|
|
96
|
+
lines.push(`SecRuleEngine ${mode === "blocking" ? "On" : "DetectionOnly"}`, "SecRequestBodyAccess On", "SecResponseBodyAccess Off", `SecRequestBodyLimit ${maxBody}`, `SecRequestBodyNoFilesLimit ${Math.min(maxBody, 131072)}`, `SecRequestBodyLimitAction ${rejectOversized ? "Reject" : "ProcessPartial"}`, "SecRequestBodyInMemoryLimit 131072", `SecAuditEngine ${mode === "blocking" ? "RelevantOnly" : "On"}`, "SecAuditLogParts ABIJDEFHZ", `SecAuditLog ${escapeSecLang(config.auditLogPath ?? DEFAULTS.auditLogPath)}`, "SecAuditLogType Serial", `SecDefaultAction "phase:1,log,auditlog,${mode === "blocking" ? `deny,status:${config.blockStatus ?? DEFAULTS.blockStatus}` : "pass"}"`, "", "# CRS anomaly scoring setup.", `SecAction "id:900000,phase:1,pass,nolog,t:none,setvar:tx.paranoia_level=${paranoia}"`, `SecAction "id:900001,phase:1,pass,nolog,t:none,setvar:tx.inbound_anomaly_score_threshold=${inbound}"`, `SecAction "id:900002,phase:1,pass,nolog,t:none,setvar:tx.outbound_anomaly_score_threshold=${outbound}"`, 'SecAction "id:900003,phase:1,pass,nolog,t:none,setvar:tx.critical_anomaly_score=5,setvar:tx.error_anomaly_score=4,setvar:tx.warning_anomaly_score=3,setvar:tx.notice_anomaly_score=2"', 'SecAction "id:900004,phase:1,pass,nolog,t:none,setvar:tx.anomaly_score=0"', "");
|
|
97
|
+
let bypassId = RULE_IDS.bypassBase;
|
|
98
|
+
for (const path of config.bypassPaths ?? []) {
|
|
99
|
+
const pattern = globToRegex(path);
|
|
100
|
+
if (!isSafePattern(pattern)) {
|
|
101
|
+
warnings.push(`Bypass path "${path}" was dropped: it compiles to an unsafe regular expression.`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
lines.push(`# Inspection bypass for ${escapeSecLang(path)}. Every bypass is an unguarded route.`, `SecRule REQUEST_URI "@rx ${escapeSecLang(pattern)}" "id:${bypassId},phase:1,pass,nolog,ctl:ruleEngine=Off"`, "");
|
|
105
|
+
bypassId++;
|
|
106
|
+
}
|
|
107
|
+
let exclusionId = RULE_IDS.exclusionBase;
|
|
108
|
+
for (const exclusion of config.exclusions ?? []) {
|
|
109
|
+
if (!Number.isInteger(exclusion.ruleId) || exclusion.ruleId <= 0) {
|
|
110
|
+
warnings.push(`Exclusion with rule id "${exclusion.ruleId}" was dropped: the id is not a positive integer.`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
lines.push(`# ${escapeSecLang(exclusion.reason)}`);
|
|
114
|
+
if (exclusion.pathPattern) {
|
|
115
|
+
if (!isSafePattern(exclusion.pathPattern)) {
|
|
116
|
+
warnings.push(`Exclusion for rule ${exclusion.ruleId} was dropped: its path pattern is unsafe.`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
lines.push(`SecRule REQUEST_URI "@rx ${escapeSecLang(exclusion.pathPattern)}" "id:${exclusionId},phase:1,pass,nolog,ctl:ruleRemoveById=${exclusion.ruleId}"`);
|
|
120
|
+
exclusionId++;
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(`SecRuleRemoveById ${exclusion.ruleId}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
const markers = rateLimitMarkers(rateLimits, RULE_IDS.rateLimitBase);
|
|
127
|
+
if (markers.lines.length > 0) {
|
|
128
|
+
lines.push("# Rate-limit route markers. SecLang has no counters, so the tag is set", "# here and the counting stays in the ts-cloud limiter.", ...markers.lines);
|
|
129
|
+
}
|
|
130
|
+
let customId = RULE_IDS.customBase;
|
|
131
|
+
for (const rule of config.customRules ?? []) {
|
|
132
|
+
lines.push(`# Operator-supplied rule ${customId}.`, rule.trim(), "");
|
|
133
|
+
customId++;
|
|
134
|
+
}
|
|
135
|
+
if (mode === "blocking")
|
|
136
|
+
lines.push("# Blocking decision: one rule, at the end, on the accumulated score.", `SecRule TX:ANOMALY_SCORE "@ge %{tx.inbound_anomaly_score_threshold}" "id:949110,phase:2,deny,status:${config.blockStatus ?? DEFAULTS.blockStatus},log,msg:'Inbound anomaly score exceeded (total: %{tx.anomaly_score})'"`, "");
|
|
137
|
+
return { config: `${lines.join(`
|
|
138
|
+
`)}
|
|
139
|
+
`, unmappedRateLimits: markers.unmapped, warnings, mode };
|
|
140
|
+
}
|
|
141
|
+
function renderWafInstallScript(config = {}, rateLimits = [], options = {}) {
|
|
142
|
+
const binary = options.binary ?? "/usr/local/bin/zig-waf";
|
|
143
|
+
const configPath = options.configPath ?? "/etc/ts-cloud/waf.conf";
|
|
144
|
+
const rendered = renderWafConfig(config, rateLimits);
|
|
145
|
+
return `#!/usr/bin/env bash
|
|
146
|
+
# Managed by ts-cloud. Installs the zig-waf ruleset.
|
|
147
|
+
set -euo pipefail
|
|
148
|
+
|
|
149
|
+
if [ ! -x "${binary}" ]; then
|
|
150
|
+
echo "ts-cloud: ${binary} is not installed; skipping WAF configuration" >&2
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
install -d -m 0755 "$(dirname "${configPath}")"
|
|
155
|
+
install -d -m 0750 /var/log/ts-cloud
|
|
156
|
+
|
|
157
|
+
cat > "${configPath}.new" <<'WAF_EOF'
|
|
158
|
+
${rendered.config}WAF_EOF
|
|
159
|
+
|
|
160
|
+
# Compile the config before it goes live. A config that does not validate
|
|
161
|
+
# would leave the engine with no rules while still reporting itself enabled.
|
|
162
|
+
if ! "${binary}" validate "${configPath}.new"; then
|
|
163
|
+
echo "ts-cloud: generated WAF config failed validation, keeping the previous one" >&2
|
|
164
|
+
rm -f "${configPath}.new"
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
if [ -f "${configPath}" ] && cmp -s "${configPath}.new" "${configPath}"; then
|
|
169
|
+
rm -f "${configPath}.new"
|
|
170
|
+
echo "ts-cloud: WAF config unchanged"
|
|
171
|
+
exit 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
mv "${configPath}.new" "${configPath}"
|
|
175
|
+
echo "ts-cloud: WAF config installed (mode: ${rendered.mode})"
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
function wafEventTelemetry(events, scope) {
|
|
179
|
+
return events.map((event) => ({
|
|
180
|
+
projectId: scope.projectId,
|
|
181
|
+
environmentId: scope.environmentId,
|
|
182
|
+
resourceId: scope.resourceId,
|
|
183
|
+
kind: "event",
|
|
184
|
+
source: "zig-waf",
|
|
185
|
+
name: event.action === "blocked" ? "waf.blocked" : "waf.matched",
|
|
186
|
+
timestamp: event.timestamp,
|
|
187
|
+
value: 1,
|
|
188
|
+
host: event.host,
|
|
189
|
+
pathTemplate: event.path,
|
|
190
|
+
method: event.method,
|
|
191
|
+
attributes: {
|
|
192
|
+
ruleId: event.ruleId ?? null,
|
|
193
|
+
anomalyScore: event.anomalyScore,
|
|
194
|
+
action: event.action,
|
|
195
|
+
messages: event.messages.slice(0, 5)
|
|
196
|
+
}
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { escapeSecLang, isSafePattern, globToRegex, renderWafConfig, renderWafInstallScript, wafEventTelemetry };
|