@stacksjs/ts-cloud 0.7.108 → 0.7.109
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 +1677 -1369
- package/dist/bin/dashboard-server.js +927 -621
- 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-eg8ck25v.js → chunk-cktwk9hy.js} +6935 -15274
- package/dist/{chunk-33sy72r8.js → chunk-d3vx2ekv.js} +71 -25
- package/dist/chunk-grx1rp09.js +12163 -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-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
- package/dist/chunk-xrtwv3v2.js +420 -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 +113 -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 +87 -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 +110 -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 +2 -2
- package/dist/ui/account/automation.html +3 -3
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +3 -3
- 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 +1 -1
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +4 -4
- 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 +4 -4
- 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 +3 -3
- 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 +4 -4
- 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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator controls for edge protection.
|
|
3
|
+
*
|
|
4
|
+
* The generators in `ddos.ts` and `waf.ts` decide what a box does in the
|
|
5
|
+
* ordinary case. This is the set of levers someone reaches for during an
|
|
6
|
+
* incident, when the ordinary case is not what is happening:
|
|
7
|
+
*
|
|
8
|
+
* - **Attack mode** challenges every visitor, not only the suspicious ones.
|
|
9
|
+
* - **IP rules** allow or block specific ranges outright.
|
|
10
|
+
* - **Pausing mitigations** turns automatic blocking off for a project whose
|
|
11
|
+
* legitimate traffic is being caught by it.
|
|
12
|
+
*
|
|
13
|
+
* Two of these are dangerous in opposite directions, and both are time-boxed
|
|
14
|
+
* for that reason. Attack mode challenges real users, so leaving it on
|
|
15
|
+
* indefinitely is a slow outage. Pausing mitigations means paying for whatever
|
|
16
|
+
* arrives, so leaving *that* on indefinitely is a slow invoice. Neither
|
|
17
|
+
* defaults to permanent, and both record who did it.
|
|
18
|
+
*/
|
|
19
|
+
import type { ControlPlaneStore } from '../control-plane';
|
|
20
|
+
import type { RateLimitRule } from './ratelimit';
|
|
21
|
+
import type { DdosConfig } from './ddos';
|
|
22
|
+
export declare const PROTECTION_SETTING = "protection.controls";
|
|
23
|
+
/** The hardest ceiling on a temporary control, matching what operators expect. */
|
|
24
|
+
export declare const MAX_CONTROL_HOURS = 24;
|
|
25
|
+
/** Attack mode defaults short: it challenges real users while it is on. */
|
|
26
|
+
export declare const DEFAULT_ATTACK_MODE_HOURS = 4;
|
|
27
|
+
export interface TimeBoxedControl {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
/** ISO timestamp. The control is inert past it, with no action needed. */
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
startedAt: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
actorId?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface IpRules {
|
|
36
|
+
/** CIDRs that bypass every limit. */
|
|
37
|
+
allow: string[];
|
|
38
|
+
/** CIDRs refused outright. */
|
|
39
|
+
block: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface ProtectionControls {
|
|
42
|
+
attackMode?: TimeBoxedControl;
|
|
43
|
+
/** Automatic mitigation suspended. You pay for what arrives while it is on. */
|
|
44
|
+
mitigationPause?: TimeBoxedControl;
|
|
45
|
+
ipRules: IpRules;
|
|
46
|
+
updatedAt?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare class ProtectionControlStore {
|
|
49
|
+
private readonly controlPlane;
|
|
50
|
+
private readonly options;
|
|
51
|
+
constructor(controlPlane: ControlPlaneStore, options?: {
|
|
52
|
+
now?: () => Date;
|
|
53
|
+
});
|
|
54
|
+
private now;
|
|
55
|
+
private read;
|
|
56
|
+
private write;
|
|
57
|
+
/** Everything as stored, expired controls included. For an audit view. */
|
|
58
|
+
raw(): ProtectionControls;
|
|
59
|
+
/** Only what is in force right now. What the enforcement path should read. */
|
|
60
|
+
current(): {
|
|
61
|
+
attackMode?: TimeBoxedControl;
|
|
62
|
+
mitigationPause?: TimeBoxedControl;
|
|
63
|
+
ipRules: IpRules;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Challenge every visitor for a bounded window.
|
|
67
|
+
*
|
|
68
|
+
* The expiry is the point. An operator who enables this at 2am must not have
|
|
69
|
+
* to remember to turn it off, because the failure mode of forgetting is that
|
|
70
|
+
* every real user keeps getting challenged and nobody connects the two.
|
|
71
|
+
*/
|
|
72
|
+
enableAttackMode(input: {
|
|
73
|
+
hours?: number;
|
|
74
|
+
reason: string;
|
|
75
|
+
actorId?: string;
|
|
76
|
+
}): TimeBoxedControl;
|
|
77
|
+
disableAttackMode(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Suspend automatic mitigation for a project being caught by it.
|
|
80
|
+
*
|
|
81
|
+
* A reason is required, not optional. This is the control that costs money
|
|
82
|
+
* while it is on, and "why is our bill up" should be answerable from the
|
|
83
|
+
* record rather than from memory.
|
|
84
|
+
*/
|
|
85
|
+
pauseMitigations(input: {
|
|
86
|
+
hours?: number;
|
|
87
|
+
reason: string;
|
|
88
|
+
actorId?: string;
|
|
89
|
+
}): TimeBoxedControl;
|
|
90
|
+
resumeMitigations(): boolean;
|
|
91
|
+
/** Add a CIDR to the allow or block list. Validated before it is stored. */
|
|
92
|
+
addIpRule(list: 'allow' | 'block', cidr: string): IpRules;
|
|
93
|
+
removeIpRule(list: 'allow' | 'block', cidr: string): IpRules;
|
|
94
|
+
}
|
|
95
|
+
export interface ProtectionPosture {
|
|
96
|
+
attackMode: boolean;
|
|
97
|
+
mitigationPaused: boolean;
|
|
98
|
+
/** Human-readable, for a dashboard banner and a CLI line. */
|
|
99
|
+
summary: string;
|
|
100
|
+
expiresAt?: string;
|
|
101
|
+
}
|
|
102
|
+
export declare function describePosture(controls: ReturnType<ProtectionControlStore['current']>): ProtectionPosture;
|
|
103
|
+
/**
|
|
104
|
+
* Apply the controls to a rate-limit rule set.
|
|
105
|
+
*
|
|
106
|
+
* Attack mode does not tighten the numbers - it changes the *action*. A limit
|
|
107
|
+
* low enough to stop an attack by counting would also stop a real user, whereas
|
|
108
|
+
* a challenge lets a browser through and a script not. Pausing mitigations
|
|
109
|
+
* disables the rules outright, which is the honest reading of "paused".
|
|
110
|
+
*/
|
|
111
|
+
export declare function applyControlsToRateLimits(rules: readonly RateLimitRule[], controls: ReturnType<ProtectionControlStore['current']>): RateLimitRule[];
|
|
112
|
+
/** Apply the controls to a firewall config before it is rendered. */
|
|
113
|
+
export declare function applyControlsToDdos(config: DdosConfig, controls: ReturnType<ProtectionControlStore['current']>): DdosConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3/4 DDoS mitigation for a box we control.
|
|
3
|
+
*
|
|
4
|
+
* ts-cloud has no anycast scrubbing network, and pretending otherwise would be
|
|
5
|
+
* dishonest - a 500 Gbps flood saturates the uplink before any of this runs.
|
|
6
|
+
* What this does cover is the attack traffic that actually reaches most
|
|
7
|
+
* self-hosted infrastructure: SYN floods, connection exhaustion, slow-loris,
|
|
8
|
+
* amplified UDP, and single-source hammering. Those are stopped in the kernel,
|
|
9
|
+
* for free, and stopping them there is the difference between a box that
|
|
10
|
+
* degrades and a box that falls over.
|
|
11
|
+
*
|
|
12
|
+
* Everything here *generates configuration* rather than executing it, which is
|
|
13
|
+
* the pattern the rest of the driver layer already uses (see `ufw.ts` and
|
|
14
|
+
* `image-recipe.ts`): the deploy renders it, ships it, and reconciles it, so a
|
|
15
|
+
* rule survives a reprovision instead of living only in a live `iptables`
|
|
16
|
+
* table that the next boot discards.
|
|
17
|
+
*/
|
|
18
|
+
export interface DdosThresholds {
|
|
19
|
+
/** New connections per second, per source IP, before the source is dropped. */
|
|
20
|
+
newConnectionsPerSecond: number;
|
|
21
|
+
/** Concurrent established connections allowed from one source IP. */
|
|
22
|
+
concurrentPerSource: number;
|
|
23
|
+
/** SYN packets per second accepted platform-wide before SYN cookies do the work. */
|
|
24
|
+
synPerSecond: number;
|
|
25
|
+
/** Burst allowance on top of the per-second rates. */
|
|
26
|
+
burst: number;
|
|
27
|
+
/** ICMP echo requests per second. Ping stays useful; ping floods do not. */
|
|
28
|
+
icmpPerSecond: number;
|
|
29
|
+
/** Seconds a source stays in the drop set once it trips a limit. */
|
|
30
|
+
banSeconds: number;
|
|
31
|
+
}
|
|
32
|
+
export declare const DEFAULT_DDOS_THRESHOLDS: DdosThresholds;
|
|
33
|
+
export interface DdosConfig {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
thresholds?: Partial<DdosThresholds>;
|
|
36
|
+
/** Ports the ruleset protects. 80 and 443 unless told otherwise. */
|
|
37
|
+
ports?: number[];
|
|
38
|
+
/** CIDRs that bypass every limit: monitoring, office IPs, a load balancer. */
|
|
39
|
+
allowlist?: string[];
|
|
40
|
+
/** CIDRs dropped outright. */
|
|
41
|
+
blocklist?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Count without dropping. The way to deploy a ruleset on a live box and see
|
|
44
|
+
* what it *would* have blocked before it blocks anything.
|
|
45
|
+
*/
|
|
46
|
+
monitorOnly?: boolean;
|
|
47
|
+
/** Also drop traffic that reaches the box bypassing the CDN, when one is used. */
|
|
48
|
+
originProtection?: {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
cdnRanges: string[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate a CIDR before it reaches a generated ruleset.
|
|
55
|
+
*
|
|
56
|
+
* The list is operator-supplied and goes straight into a file the kernel
|
|
57
|
+
* parses; an unvalidated entry is both a syntax error that bricks the ruleset
|
|
58
|
+
* and an injection vector into the generated script.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isValidCidr(value: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Kernel tunables that make a box survive a flood.
|
|
63
|
+
*
|
|
64
|
+
* `syncookies` is the important one: without it, a SYN flood fills the backlog
|
|
65
|
+
* with half-open connections and the box stops accepting anything, at a packet
|
|
66
|
+
* rate a single host can produce. The rest bound how much memory an attacker
|
|
67
|
+
* can make the kernel hold on their behalf.
|
|
68
|
+
*/
|
|
69
|
+
export declare function sysctlHardening(config?: DdosConfig): Record<string, string>;
|
|
70
|
+
/** Render the tunables as a drop-in sysctl file. */
|
|
71
|
+
export declare function renderSysctlFile(config?: DdosConfig): string;
|
|
72
|
+
/**
|
|
73
|
+
* Render an nftables ruleset.
|
|
74
|
+
*
|
|
75
|
+
* nftables rather than iptables: sets and rate meters are first-class, so
|
|
76
|
+
* per-source limiting is one rule against a hash table instead of a chain that
|
|
77
|
+
* grows with the attack. The generated table is named and flushed atomically,
|
|
78
|
+
* which is what makes a redeploy idempotent instead of additive.
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderNftablesRuleset(config?: DdosConfig): string;
|
|
81
|
+
/**
|
|
82
|
+
* A script that installs the ruleset and the tunables.
|
|
83
|
+
*
|
|
84
|
+
* The validation step is not optional: an nftables file with one bad line
|
|
85
|
+
* leaves the box with no rules at all, and finding that out during an attack
|
|
86
|
+
* is the worst possible time. `nft -c` checks before anything is applied.
|
|
87
|
+
*/
|
|
88
|
+
export declare function renderDdosInstallScript(config?: DdosConfig): string;
|
|
89
|
+
export type MitigationLevel = 'off' | 'monitor' | 'rate_limit' | 'challenge' | 'lockdown';
|
|
90
|
+
export interface TrafficSignals {
|
|
91
|
+
/** Requests per second at the edge right now. */
|
|
92
|
+
requestsPerSecond: number;
|
|
93
|
+
/** The same figure under normal conditions, for comparison. */
|
|
94
|
+
baselineRequestsPerSecond: number;
|
|
95
|
+
/** Share of requests currently failing, 0-1. */
|
|
96
|
+
errorRate: number;
|
|
97
|
+
/** Distinct source IPs seen in the sampling window. */
|
|
98
|
+
uniqueSources: number;
|
|
99
|
+
/** Share of traffic from the single busiest source, 0-1. */
|
|
100
|
+
topSourceShare: number;
|
|
101
|
+
/** Established connections on the box. */
|
|
102
|
+
concurrentConnections: number;
|
|
103
|
+
/** Share of requests with no or an obviously scripted user agent, 0-1. */
|
|
104
|
+
suspiciousAgentShare?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface MitigationPlan {
|
|
107
|
+
level: MitigationLevel;
|
|
108
|
+
reasons: string[];
|
|
109
|
+
/** Rate-limit multiplier to apply to normal limits; 1 means unchanged. */
|
|
110
|
+
rateLimitFactor: number;
|
|
111
|
+
/** Ask suspicious clients to prove they are a browser. */
|
|
112
|
+
challengeEnabled: boolean;
|
|
113
|
+
/** Only serve cached and static responses; skip dynamic rendering. */
|
|
114
|
+
staticOnly: boolean;
|
|
115
|
+
/** Sources worth adding to the drop set. */
|
|
116
|
+
banCandidates: string[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Choose a mitigation level from live signals.
|
|
120
|
+
*
|
|
121
|
+
* Traffic volume alone is a bad trigger: a launch and an attack look identical
|
|
122
|
+
* on a request-rate graph, and mitigating a launch is a self-inflicted outage.
|
|
123
|
+
* The distinguishing signals are *shape* - a real surge arrives from many
|
|
124
|
+
* sources with a normal error rate, an attack concentrates on few sources, or
|
|
125
|
+
* drives errors, or both. Escalation needs volume plus at least one shape
|
|
126
|
+
* signal; volume by itself only ever reaches `monitor`.
|
|
127
|
+
*/
|
|
128
|
+
export declare function planMitigation(signals: TrafficSignals, topSources?: readonly string[]): MitigationPlan;
|
|
129
|
+
/** Scale a rate limit by a mitigation plan, never below 1. */
|
|
130
|
+
export declare function applyMitigationFactor(limit: number, plan: MitigationPlan): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursion protection lives in `@ts-cloud/core` because the function runtime
|
|
3
|
+
* has to run it, and the runtime cannot depend on this package. Re-exported
|
|
4
|
+
* here so `ts-cloud/protection` stays the one place to find edge protection.
|
|
5
|
+
*/
|
|
6
|
+
export { CHAIN_HEADER, DEFAULT_RECURSION_LIMITS, DEPTH_HEADER, functionFingerprint, inspectInvocation, parseChain, propagationHeaders, RecursionGuard, recursionBlockedResponse, TRACE_HEADER, } from '@ts-cloud/core';
|
|
7
|
+
export type { InvocationContext, RecursionLimits, RecursionReason, RecursionVerdict, } from '@ts-cloud/core';
|
|
8
|
+
export * from './ratelimit';
|
|
9
|
+
export * from './ddos';
|
|
10
|
+
export * from './controls';
|
|
11
|
+
export * from './waf';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ATTACK_MODE_HOURS,
|
|
3
|
+
MAX_CONTROL_HOURS,
|
|
4
|
+
PROTECTION_SETTING,
|
|
5
|
+
ProtectionControlStore,
|
|
6
|
+
RATE_LIMIT_KEY_SEPARATOR,
|
|
7
|
+
RateLimiter,
|
|
8
|
+
applyControlsToDdos,
|
|
9
|
+
applyControlsToRateLimits,
|
|
10
|
+
defaultRateLimitRules,
|
|
11
|
+
describePosture,
|
|
12
|
+
globMatches,
|
|
13
|
+
rateLimitHeaders,
|
|
14
|
+
rateLimitKey,
|
|
15
|
+
ruleMatches
|
|
16
|
+
} from "../chunk-xrtwv3v2.js";
|
|
17
|
+
import {
|
|
18
|
+
escapeSecLang,
|
|
19
|
+
globToRegex,
|
|
20
|
+
isSafePattern,
|
|
21
|
+
renderWafConfig,
|
|
22
|
+
renderWafInstallScript,
|
|
23
|
+
wafEventTelemetry
|
|
24
|
+
} from "../chunk-va2yd85b.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_DDOS_THRESHOLDS,
|
|
27
|
+
applyMitigationFactor,
|
|
28
|
+
isValidCidr,
|
|
29
|
+
planMitigation,
|
|
30
|
+
renderDdosInstallScript,
|
|
31
|
+
renderNftablesRuleset,
|
|
32
|
+
renderSysctlFile,
|
|
33
|
+
sysctlHardening
|
|
34
|
+
} from "../chunk-aa2dkn7b.js";
|
|
35
|
+
import {
|
|
36
|
+
CHAIN_HEADER,
|
|
37
|
+
DEFAULT_RECURSION_LIMITS,
|
|
38
|
+
DEPTH_HEADER,
|
|
39
|
+
RecursionGuard,
|
|
40
|
+
TRACE_HEADER,
|
|
41
|
+
functionFingerprint,
|
|
42
|
+
inspectInvocation,
|
|
43
|
+
parseChain,
|
|
44
|
+
propagationHeaders,
|
|
45
|
+
recursionBlockedResponse
|
|
46
|
+
} from "../chunk-t2s3gdq6.js";
|
|
47
|
+
import"../chunk-v0bahtg2.js";
|
|
48
|
+
export {
|
|
49
|
+
wafEventTelemetry,
|
|
50
|
+
sysctlHardening,
|
|
51
|
+
ruleMatches,
|
|
52
|
+
renderWafInstallScript,
|
|
53
|
+
renderWafConfig,
|
|
54
|
+
renderSysctlFile,
|
|
55
|
+
renderNftablesRuleset,
|
|
56
|
+
renderDdosInstallScript,
|
|
57
|
+
recursionBlockedResponse,
|
|
58
|
+
rateLimitKey,
|
|
59
|
+
rateLimitHeaders,
|
|
60
|
+
propagationHeaders,
|
|
61
|
+
planMitigation,
|
|
62
|
+
parseChain,
|
|
63
|
+
isValidCidr,
|
|
64
|
+
isSafePattern,
|
|
65
|
+
inspectInvocation,
|
|
66
|
+
globToRegex,
|
|
67
|
+
globMatches,
|
|
68
|
+
functionFingerprint,
|
|
69
|
+
escapeSecLang,
|
|
70
|
+
describePosture,
|
|
71
|
+
defaultRateLimitRules,
|
|
72
|
+
applyMitigationFactor,
|
|
73
|
+
applyControlsToRateLimits,
|
|
74
|
+
applyControlsToDdos,
|
|
75
|
+
TRACE_HEADER,
|
|
76
|
+
RecursionGuard,
|
|
77
|
+
RateLimiter,
|
|
78
|
+
RATE_LIMIT_KEY_SEPARATOR,
|
|
79
|
+
ProtectionControlStore,
|
|
80
|
+
PROTECTION_SETTING,
|
|
81
|
+
MAX_CONTROL_HOURS,
|
|
82
|
+
DEPTH_HEADER,
|
|
83
|
+
DEFAULT_RECURSION_LIMITS,
|
|
84
|
+
DEFAULT_DDOS_THRESHOLDS,
|
|
85
|
+
DEFAULT_ATTACK_MODE_HOURS,
|
|
86
|
+
CHAIN_HEADER
|
|
87
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 7 rate limiting - the application-aware half of DDoS mitigation.
|
|
3
|
+
*
|
|
4
|
+
* L3/L4 filtering (see `ddos.ts`) stops packet floods, but the expensive
|
|
5
|
+
* attacks now are perfectly valid HTTP: a few thousand requests a second to a
|
|
6
|
+
* route that renders a page, hits a database, or invokes a function. Those
|
|
7
|
+
* cost real money per request and no packet filter can tell them from traffic.
|
|
8
|
+
*
|
|
9
|
+
* Two algorithms, because they answer different questions:
|
|
10
|
+
*
|
|
11
|
+
* - **Token bucket** allows a burst and then a steady rate. Right for
|
|
12
|
+
* interactive traffic, where a page load legitimately fires twenty
|
|
13
|
+
* requests at once and then goes quiet.
|
|
14
|
+
* - **Sliding window** counts precisely over a period. Right for quotas
|
|
15
|
+
* ("100 signups an hour") where a burst is exactly what you want to stop.
|
|
16
|
+
*
|
|
17
|
+
* Everything is clock-injected and in-memory. A limiter that consults a shared
|
|
18
|
+
* store on every request adds a network hop to the hot path of the thing it is
|
|
19
|
+
* protecting, which is how rate limiting becomes the outage.
|
|
20
|
+
*/
|
|
21
|
+
export type RateLimitAlgorithm = 'token_bucket' | 'sliding_window';
|
|
22
|
+
export type RateLimitAction = 'allow' | 'log' | 'throttle' | 'challenge' | 'deny';
|
|
23
|
+
/** Where the identity being limited comes from. */
|
|
24
|
+
export type RateLimitKeySource = 'ip' | 'header' | 'cookie' | 'path' | 'host' | 'global';
|
|
25
|
+
export interface RateLimitKeySpec {
|
|
26
|
+
source: RateLimitKeySource;
|
|
27
|
+
/** Header or cookie name when the source needs one. */
|
|
28
|
+
name?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Fall back to the client IP when the named header or cookie is absent.
|
|
31
|
+
*
|
|
32
|
+
* On by default: without it, an attacker omits the header and becomes
|
|
33
|
+
* unlimited, which is worse than no rule at all.
|
|
34
|
+
*/
|
|
35
|
+
fallbackToIp?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface RateLimitRule {
|
|
38
|
+
id: string;
|
|
39
|
+
/** Requests allowed per window (sliding) or refill target (bucket). */
|
|
40
|
+
limit: number;
|
|
41
|
+
windowMs: number;
|
|
42
|
+
algorithm?: RateLimitAlgorithm;
|
|
43
|
+
/** Token-bucket burst ceiling. Defaults to `limit`. */
|
|
44
|
+
burst?: number;
|
|
45
|
+
key?: RateLimitKeySpec;
|
|
46
|
+
/** Glob against the path, e.g. `/api/*`. Omit to match every path. */
|
|
47
|
+
path?: string;
|
|
48
|
+
/** HTTP methods this rule covers. Omit for all. */
|
|
49
|
+
methods?: string[];
|
|
50
|
+
/** Host to match, exact or `*.example.com`. */
|
|
51
|
+
host?: string;
|
|
52
|
+
action?: RateLimitAction;
|
|
53
|
+
/** Rules are evaluated low-to-high; the first blocking match wins. */
|
|
54
|
+
priority?: number;
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface RequestDescriptor {
|
|
58
|
+
ip: string;
|
|
59
|
+
method: string;
|
|
60
|
+
path: string;
|
|
61
|
+
host?: string;
|
|
62
|
+
headers?: Record<string, string | undefined>;
|
|
63
|
+
cookies?: Record<string, string | undefined>;
|
|
64
|
+
}
|
|
65
|
+
export interface RateLimitDecision {
|
|
66
|
+
action: RateLimitAction;
|
|
67
|
+
allowed: boolean;
|
|
68
|
+
ruleId?: string;
|
|
69
|
+
limit?: number;
|
|
70
|
+
remaining?: number;
|
|
71
|
+
/** Milliseconds until the caller may retry. */
|
|
72
|
+
retryAfterMs?: number;
|
|
73
|
+
/** When the current window or bucket is fully replenished. */
|
|
74
|
+
resetAt?: number;
|
|
75
|
+
key?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Separates the rule id from the identity in a counter key.
|
|
79
|
+
*
|
|
80
|
+
* A NUL rather than a space or a colon: rule ids and header values are both
|
|
81
|
+
* caller-supplied, and any printable separator lets one key be forged into
|
|
82
|
+
* another's bucket.
|
|
83
|
+
*/
|
|
84
|
+
export declare const RATE_LIMIT_KEY_SEPARATOR = "\0";
|
|
85
|
+
/**
|
|
86
|
+
* Glob matcher for paths and hosts.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately tiny: `*` within a segment, `**` across segments, and a leading
|
|
89
|
+
* `*.` for host wildcards. A full regex surface in a rule that runs on every
|
|
90
|
+
* request is an invitation to catastrophic backtracking.
|
|
91
|
+
*/
|
|
92
|
+
export declare function globMatches(pattern: string, value: string): boolean;
|
|
93
|
+
export declare function ruleMatches(rule: RateLimitRule, request: RequestDescriptor): boolean;
|
|
94
|
+
/** Build the counter key for a request under a rule. */
|
|
95
|
+
export declare function rateLimitKey(rule: RateLimitRule, request: RequestDescriptor): string;
|
|
96
|
+
/**
|
|
97
|
+
* Rate limiter over a set of rules.
|
|
98
|
+
*
|
|
99
|
+
* `check` is side-effecting - it consumes quota - so a caller that only wants
|
|
100
|
+
* to inspect state should use `peek`.
|
|
101
|
+
*/
|
|
102
|
+
export declare class RateLimiter {
|
|
103
|
+
private readonly clock;
|
|
104
|
+
private readonly maxTrackedKeys;
|
|
105
|
+
private readonly buckets;
|
|
106
|
+
private readonly windows;
|
|
107
|
+
private readonly rules;
|
|
108
|
+
constructor(rules: readonly RateLimitRule[], clock?: () => number, maxTrackedKeys?: number);
|
|
109
|
+
/**
|
|
110
|
+
* Evaluate every matching rule and return the strictest outcome.
|
|
111
|
+
*
|
|
112
|
+
* Two phases, and the order matters. First every matching rule is *tested*;
|
|
113
|
+
* only if they all have room does anything get consumed.
|
|
114
|
+
*
|
|
115
|
+
* Testing first is what keeps overlapping rules honest. Consuming as you go
|
|
116
|
+
* charges a request to the global counter even when a per-IP rule already
|
|
117
|
+
* refused it - so a single hammering client silently burns the budget that
|
|
118
|
+
* protects everyone else, and the global limit fires against innocent
|
|
119
|
+
* traffic. Consuming all rules for a request that is *allowed* is equally
|
|
120
|
+
* necessary: stop at the first match and the lower-priority counters never
|
|
121
|
+
* advance, and those rules quietly stop working.
|
|
122
|
+
*/
|
|
123
|
+
check(request: RequestDescriptor): RateLimitDecision;
|
|
124
|
+
/** Non-mutating availability check for one rule. */
|
|
125
|
+
private test;
|
|
126
|
+
/** Inspect without consuming quota. */
|
|
127
|
+
peek(request: RequestDescriptor): RateLimitDecision;
|
|
128
|
+
private msPerToken;
|
|
129
|
+
private refill;
|
|
130
|
+
private blocked;
|
|
131
|
+
private consume;
|
|
132
|
+
/**
|
|
133
|
+
* Bound memory under an attack that rotates keys.
|
|
134
|
+
*
|
|
135
|
+
* A source IP per request is exactly what a botnet produces, so an unbounded
|
|
136
|
+
* map turns a rate limiter into an OOM. Dropping the oldest insertion is
|
|
137
|
+
* enough: the entries that matter are the ones seeing repeat traffic.
|
|
138
|
+
*/
|
|
139
|
+
private evictIfNeeded;
|
|
140
|
+
/** Drop state for keys that have gone quiet. Cheap to call on a timer. */
|
|
141
|
+
sweep(): number;
|
|
142
|
+
get trackedKeys(): number;
|
|
143
|
+
}
|
|
144
|
+
/** Standard headers for a decision, so clients can back off intelligently. */
|
|
145
|
+
export declare function rateLimitHeaders(decision: RateLimitDecision): Record<string, string>;
|
|
146
|
+
/**
|
|
147
|
+
* Baseline rules every site gets.
|
|
148
|
+
*
|
|
149
|
+
* Chosen to be invisible to a human and expensive for a script: 600 requests a
|
|
150
|
+
* minute per IP is ten a second, far above real browsing and far below what a
|
|
151
|
+
* scraper wants. The per-route rules protect the endpoints that cost the most
|
|
152
|
+
* per call rather than the ones that get the most traffic.
|
|
153
|
+
*/
|
|
154
|
+
export declare function defaultRateLimitRules(): RateLimitRule[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web application firewall: generating zig-waf configuration.
|
|
3
|
+
*
|
|
4
|
+
* ts-cloud does not implement request inspection itself. `zig-waf` already is
|
|
5
|
+
* that engine - a ModSecurity/Coraza-compatible SecLang implementation with
|
|
6
|
+
* OWASP CRS anomaly scoring, native SQLi detection, and a `validate`
|
|
7
|
+
* subcommand - and a second, worse regex-based inspector living here would be
|
|
8
|
+
* a liability rather than defence in depth.
|
|
9
|
+
*
|
|
10
|
+
* The three layers divide cleanly, and none of them substitutes for another:
|
|
11
|
+
*
|
|
12
|
+
* - `ddos.ts` L3/L4. Packet floods, SYN storms, connection exhaustion.
|
|
13
|
+
* Kernel-level, no idea what HTTP is.
|
|
14
|
+
* - this file L7 *content*. Is this request an attack? Injection, traversal,
|
|
15
|
+
* scanners, protocol abuse. Delegated to zig-waf.
|
|
16
|
+
* - `ratelimit.ts` L7 *volume*. Is this caller asking for too much? zig-waf
|
|
17
|
+
* has no rate limiting at all, so this stays in-process.
|
|
18
|
+
*
|
|
19
|
+
* **zig-waf is pre-alpha and says so.** Generated configs therefore default to
|
|
20
|
+
* detection-only: rules evaluate, matches are logged and scored, nothing is
|
|
21
|
+
* blocked. Turning that into enforcement is a deliberate act by an operator
|
|
22
|
+
* who has read their own detection log, not something a deploy does quietly.
|
|
23
|
+
*/
|
|
24
|
+
import type { RateLimitRule } from './ratelimit';
|
|
25
|
+
export type WafMode = 'off' | 'detection' | 'blocking';
|
|
26
|
+
/**
|
|
27
|
+
* OWASP CRS paranoia level.
|
|
28
|
+
*
|
|
29
|
+
* 1 is the only level safe to enable unattended: it is tuned for near-zero
|
|
30
|
+
* false positives. Each level above trades precision for recall, and 3-4 will
|
|
31
|
+
* block legitimate traffic on almost any real application until tuned.
|
|
32
|
+
*/
|
|
33
|
+
export type ParanoiaLevel = 1 | 2 | 3 | 4;
|
|
34
|
+
export interface WafRuleExclusion {
|
|
35
|
+
/** CRS rule id to disable. */
|
|
36
|
+
ruleId: number;
|
|
37
|
+
/** Only for requests to paths matching this regex. Omit to disable globally. */
|
|
38
|
+
pathPattern?: string;
|
|
39
|
+
/** Why, so the exclusion can be reviewed rather than inherited forever. */
|
|
40
|
+
reason: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WafConfig {
|
|
43
|
+
mode?: WafMode;
|
|
44
|
+
paranoiaLevel?: ParanoiaLevel;
|
|
45
|
+
/**
|
|
46
|
+
* Inbound anomaly score at which a request is blocked, when mode is
|
|
47
|
+
* `blocking`. The CRS default is 5, which one critical-severity match
|
|
48
|
+
* reaches on its own.
|
|
49
|
+
*/
|
|
50
|
+
inboundThreshold?: number;
|
|
51
|
+
/** Outbound score threshold, guarding against data leaking back out. */
|
|
52
|
+
outboundThreshold?: number;
|
|
53
|
+
/** Largest request body inspected, in bytes. Beyond it, see `rejectOversizedBody`. */
|
|
54
|
+
maxBodyBytes?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Reject a body too large to inspect, rather than passing it through
|
|
57
|
+
* unexamined. Defaults to true: an attacker who can opt out of inspection by
|
|
58
|
+
* padding a request has defeated the WAF.
|
|
59
|
+
*/
|
|
60
|
+
rejectOversizedBody?: boolean;
|
|
61
|
+
/** Paths never inspected, e.g. a large upload endpoint. Each one is a hole. */
|
|
62
|
+
bypassPaths?: string[];
|
|
63
|
+
exclusions?: WafRuleExclusion[];
|
|
64
|
+
/** Extra SecLang appended verbatim, for rules ts-cloud has no model for. */
|
|
65
|
+
customRules?: string[];
|
|
66
|
+
/** Where the engine writes its audit log. */
|
|
67
|
+
auditLogPath?: string;
|
|
68
|
+
/** Status returned to a blocked request. */
|
|
69
|
+
blockStatus?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Escape a value for a SecLang double-quoted argument.
|
|
73
|
+
*
|
|
74
|
+
* Config text is assembled from operator input (paths, patterns, reasons); an
|
|
75
|
+
* unescaped quote or newline ends the argument early and everything after it
|
|
76
|
+
* becomes directives. That is config injection, and it is the one way a WAF
|
|
77
|
+
* config file can make a system less safe than having none.
|
|
78
|
+
*/
|
|
79
|
+
export declare function escapeSecLang(value: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Whether a regex is safe to hand to the engine.
|
|
82
|
+
*
|
|
83
|
+
* Rejects nested quantifiers, the classic catastrophic-backtracking shape.
|
|
84
|
+
* A rule that hangs on one crafted request is an outage with extra steps.
|
|
85
|
+
*/
|
|
86
|
+
export declare function isSafePattern(pattern: string): boolean;
|
|
87
|
+
export interface WafGenerationResult {
|
|
88
|
+
config: string;
|
|
89
|
+
/** Rate-limit rules that could not be expressed in SecLang, with the reason. */
|
|
90
|
+
unmappedRateLimits: Array<{
|
|
91
|
+
ruleId: string;
|
|
92
|
+
reason: string;
|
|
93
|
+
}>;
|
|
94
|
+
warnings: string[];
|
|
95
|
+
mode: WafMode;
|
|
96
|
+
}
|
|
97
|
+
/** Convert a ratelimit glob into an anchored regex for SecLang's `@rx`. */
|
|
98
|
+
export declare function globToRegex(glob: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Render a complete SecLang configuration.
|
|
101
|
+
*
|
|
102
|
+
* The output is deterministic - same input, same bytes - so a deploy can
|
|
103
|
+
* compare it against what is already on the box and skip a reload when
|
|
104
|
+
* nothing changed.
|
|
105
|
+
*/
|
|
106
|
+
export declare function renderWafConfig(config?: WafConfig, rateLimits?: readonly RateLimitRule[]): WafGenerationResult;
|
|
107
|
+
/**
|
|
108
|
+
* Install script for the generated config.
|
|
109
|
+
*
|
|
110
|
+
* `zig-waf validate` runs before anything is swapped in, for the same reason
|
|
111
|
+
* `nft -c` does in `ddos.ts`: a config that fails to compile would otherwise
|
|
112
|
+
* leave the engine with no rules, and a WAF that silently stopped inspecting
|
|
113
|
+
* is worse than one that was never installed - the dashboard still says it is on.
|
|
114
|
+
*/
|
|
115
|
+
export declare function renderWafInstallScript(config?: WafConfig, rateLimits?: readonly RateLimitRule[], options?: {
|
|
116
|
+
binary?: string;
|
|
117
|
+
configPath?: string;
|
|
118
|
+
}): string;
|
|
119
|
+
/**
|
|
120
|
+
* A WAF audit event, normalized for the telemetry pipeline.
|
|
121
|
+
*
|
|
122
|
+
* Blocked requests are a spend signal as much as a security one: a scraper
|
|
123
|
+
* that the WAF stops still consumed the bandwidth to be stopped, and a sudden
|
|
124
|
+
* rise in blocks is often the first visible symptom of an attack that is about
|
|
125
|
+
* to show up on the bill.
|
|
126
|
+
*/
|
|
127
|
+
export interface WafEvent {
|
|
128
|
+
timestamp: string;
|
|
129
|
+
ruleId?: number;
|
|
130
|
+
anomalyScore: number;
|
|
131
|
+
action: 'passed' | 'logged' | 'blocked';
|
|
132
|
+
clientIp: string;
|
|
133
|
+
method: string;
|
|
134
|
+
path: string;
|
|
135
|
+
host?: string;
|
|
136
|
+
messages: string[];
|
|
137
|
+
}
|
|
138
|
+
/** Telemetry records for a batch of WAF events, ready for the spend meter. */
|
|
139
|
+
export declare function wafEventTelemetry(events: readonly WafEvent[], scope: {
|
|
140
|
+
projectId: string;
|
|
141
|
+
environmentId?: string;
|
|
142
|
+
resourceId?: string;
|
|
143
|
+
}): Array<Record<string, unknown>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|