faultmesh 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -19
- package/dist/dashboard/app.js +1906 -139
- package/dist/dashboard/index.html +393 -99
- package/dist/dashboard/styles.css +1191 -23
- package/dist/engine/ControlApi.d.ts +13 -1
- package/dist/engine/ControlApi.js +304 -14
- package/dist/engine/FaultMeshProxy.d.ts +2 -0
- package/dist/engine/FaultMeshProxy.js +41 -5
- package/dist/engine/TelemetryHub.d.ts +1 -0
- package/dist/engine/TelemetryHub.js +3 -0
- package/dist/engine/ToxicPipeline.d.ts +5 -4
- package/dist/engine/ToxicPipeline.js +17 -4
- package/dist/healer/AiHealer.d.ts +30 -0
- package/dist/healer/AiHealer.js +188 -0
- package/dist/healer/AutoHealer.d.ts +24 -0
- package/dist/healer/AutoHealer.js +503 -0
- package/dist/healer/DiffGenerator.d.ts +10 -0
- package/dist/healer/DiffGenerator.js +63 -0
- package/dist/healer/DiffUtil.d.ts +6 -0
- package/dist/healer/DiffUtil.js +67 -0
- package/dist/healer/transformers/ExpressTransformers.d.ts +43 -0
- package/dist/healer/transformers/ExpressTransformers.js +228 -0
- package/dist/healer/transformers/FastApiTransformers.d.ts +19 -0
- package/dist/healer/transformers/FastApiTransformers.js +93 -0
- package/dist/healer/transformers/GoTransformers.d.ts +19 -0
- package/dist/healer/transformers/GoTransformers.js +106 -0
- package/dist/healer/types.d.ts +59 -0
- package/dist/healer/types.js +1 -0
- package/dist/redteam/EccAgentBridge.d.ts +20 -0
- package/dist/redteam/EccAgentBridge.js +303 -0
- package/dist/redteam/RedTeamEngine.d.ts +56 -0
- package/dist/redteam/RedTeamEngine.js +709 -0
- package/dist/redteam/types.d.ts +117 -0
- package/dist/redteam/types.js +5 -0
- package/dist/scorer/ResilienceScorer.d.ts +5 -0
- package/dist/scorer/ResilienceScorer.js +323 -7
- package/dist/scorer/SecurityAuditor.d.ts +8 -0
- package/dist/scorer/SecurityAuditor.js +471 -69
- package/dist/scorer/TrafficStormAuditor.d.ts +5 -0
- package/dist/scorer/TrafficStormAuditor.js +328 -69
- package/dist/server.js +17 -10
- package/dist/types.d.ts +7 -3
- package/package.json +2 -1
|
@@ -2,9 +2,14 @@ import { TrafficStormScorecard } from '../types.js';
|
|
|
2
2
|
export declare class TrafficStormAuditor {
|
|
3
3
|
private targetUrl;
|
|
4
4
|
constructor(targetUrl: string);
|
|
5
|
+
setTargetUrl(url: string): void;
|
|
5
6
|
runStormSuite(profile?: 'resilient' | 'fragile'): Promise<TrafficStormScorecard>;
|
|
6
7
|
private auditRateLimitBackoff;
|
|
7
8
|
private auditOversizedPayload;
|
|
8
9
|
private auditSlowlorisDefense;
|
|
9
10
|
private auditIdempotencyProtection;
|
|
11
|
+
private auditReDosLockup;
|
|
12
|
+
private auditConcurrentRaceCondition;
|
|
13
|
+
private auditChunkedRequestDrip;
|
|
14
|
+
private auditResourceAvalancheFlood;
|
|
10
15
|
}
|
|
@@ -3,10 +3,13 @@ export class TrafficStormAuditor {
|
|
|
3
3
|
constructor(targetUrl) {
|
|
4
4
|
this.targetUrl = targetUrl;
|
|
5
5
|
}
|
|
6
|
+
setTargetUrl(url) {
|
|
7
|
+
this.targetUrl = url;
|
|
8
|
+
}
|
|
6
9
|
async runStormSuite(profile = 'resilient') {
|
|
7
10
|
const checks = [];
|
|
8
11
|
const recommendations = [];
|
|
9
|
-
// 1. Rate Limit Back-off & Retry
|
|
12
|
+
// 1. Rate Limit Back-off & Retry Storm Handling
|
|
10
13
|
const c1 = await this.auditRateLimitBackoff(profile);
|
|
11
14
|
checks.push(c1);
|
|
12
15
|
if (!c1.passed)
|
|
@@ -21,13 +24,34 @@ export class TrafficStormAuditor {
|
|
|
21
24
|
checks.push(c3);
|
|
22
25
|
if (!c3.passed)
|
|
23
26
|
recommendations.push(c3.remediation);
|
|
24
|
-
// 4. Duplicate Request
|
|
27
|
+
// 4. Duplicate Request Idempotency Protection
|
|
25
28
|
const c4 = await this.auditIdempotencyProtection(profile);
|
|
26
29
|
checks.push(c4);
|
|
27
30
|
if (!c4.passed)
|
|
28
31
|
recommendations.push(c4.remediation);
|
|
29
|
-
//
|
|
30
|
-
const
|
|
32
|
+
// 5. ReDoS (Regex Denial of Service) Lockup Probe
|
|
33
|
+
const c5 = await this.auditReDosLockup(profile);
|
|
34
|
+
checks.push(c5);
|
|
35
|
+
if (!c5.passed)
|
|
36
|
+
recommendations.push(c5.remediation);
|
|
37
|
+
// 6. Concurrent Race Condition & Double Processing
|
|
38
|
+
const c6 = await this.auditConcurrentRaceCondition(profile);
|
|
39
|
+
checks.push(c6);
|
|
40
|
+
if (!c6.passed)
|
|
41
|
+
recommendations.push(c6.remediation);
|
|
42
|
+
// 7. Chunked Request Drip & Slow POST Defense
|
|
43
|
+
const c7 = await this.auditChunkedRequestDrip(profile);
|
|
44
|
+
checks.push(c7);
|
|
45
|
+
if (!c7.passed)
|
|
46
|
+
recommendations.push(c7.remediation);
|
|
47
|
+
// 8. Resource Avalanche & Sudden Concurrency Flood
|
|
48
|
+
const c8 = await this.auditResourceAvalancheFlood(profile);
|
|
49
|
+
checks.push(c8);
|
|
50
|
+
if (!c8.passed)
|
|
51
|
+
recommendations.push(c8.remediation);
|
|
52
|
+
// Calculate score (8 checks, weighted to 100)
|
|
53
|
+
// 4 * 15 + 4 * 10 = 60 + 40 = 100
|
|
54
|
+
const weights = [15, 15, 15, 15, 10, 10, 10, 10];
|
|
31
55
|
let score = 0;
|
|
32
56
|
let passedCount = 0;
|
|
33
57
|
checks.forEach((c, idx) => {
|
|
@@ -43,11 +67,8 @@ export class TrafficStormAuditor {
|
|
|
43
67
|
grade = 'B';
|
|
44
68
|
else if (score >= 60)
|
|
45
69
|
grade = 'C';
|
|
46
|
-
else if (score >=
|
|
70
|
+
else if (score >= 40)
|
|
47
71
|
grade = 'D';
|
|
48
|
-
if (recommendations.length === 0) {
|
|
49
|
-
recommendations.push('Your application safely handled all traffic storm, rate limit, and denial-of-service resilience tests.');
|
|
50
|
-
}
|
|
51
72
|
return {
|
|
52
73
|
score,
|
|
53
74
|
grade,
|
|
@@ -70,42 +91,46 @@ export class TrafficStormAuditor {
|
|
|
70
91
|
severity: 'high',
|
|
71
92
|
passed: false,
|
|
72
93
|
latencyMs: 45,
|
|
73
|
-
details: 'Client ignored HTTP 429 Retry-After header and triggered 12 rapid retries within 300ms, causing an unmitigated retry
|
|
94
|
+
details: 'Client ignored HTTP 429 Retry-After header and triggered 12 rapid retries within 300ms, causing an unmitigated retry stampede.',
|
|
74
95
|
remediation: 'Implement exponential backoff with jitter and respect the standard Retry-After header when receiving HTTP 429.',
|
|
75
96
|
};
|
|
76
97
|
}
|
|
77
98
|
try {
|
|
78
|
-
const res = await fetch(`${this.targetUrl}/api/ratelimit-test
|
|
99
|
+
const res = await fetch(`${this.targetUrl}/api/ratelimit-test`, { signal: AbortSignal.timeout(3000) });
|
|
79
100
|
const latency = Date.now() - start;
|
|
101
|
+
const retryAfter = res.headers.get('retry-after');
|
|
102
|
+
const isRateLimited = res.status === 429 && Boolean(retryAfter);
|
|
80
103
|
return {
|
|
81
104
|
id: 'storm_ratelimit',
|
|
82
105
|
name: 'Rate Limit Back-off & Retry Storm Handling',
|
|
83
106
|
category: 'ratelimit',
|
|
84
107
|
description,
|
|
85
108
|
severity: 'high',
|
|
86
|
-
passed:
|
|
109
|
+
passed: isRateLimited,
|
|
87
110
|
latencyMs: latency,
|
|
88
|
-
details:
|
|
89
|
-
|
|
111
|
+
details: isRateLimited
|
|
112
|
+
? `Rate limit back-off verified: server returned HTTP 429 with Retry-After: ${retryAfter}s.`
|
|
113
|
+
: 'Server did not provide standard HTTP 429 with Retry-After rate-limiting protection.',
|
|
114
|
+
remediation: 'Return HTTP 429 with standard Retry-After response headers to prevent client-driven retry avalanches.',
|
|
90
115
|
};
|
|
91
116
|
}
|
|
92
|
-
catch {
|
|
117
|
+
catch (err) {
|
|
93
118
|
return {
|
|
94
119
|
id: 'storm_ratelimit',
|
|
95
120
|
name: 'Rate Limit Back-off & Retry Storm Handling',
|
|
96
121
|
category: 'ratelimit',
|
|
97
122
|
description,
|
|
98
123
|
severity: 'high',
|
|
99
|
-
passed:
|
|
124
|
+
passed: false,
|
|
100
125
|
latencyMs: Date.now() - start,
|
|
101
|
-
details:
|
|
102
|
-
remediation: '
|
|
126
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
127
|
+
remediation: 'Configure rate limiting with Retry-After headers.',
|
|
103
128
|
};
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
async auditOversizedPayload(profile) {
|
|
107
132
|
const start = Date.now();
|
|
108
|
-
const description = '
|
|
133
|
+
const description = 'Verifies whether server enforces strict request body limits (e.g. 1MB) and rejects oversized payloads with HTTP 413 before memory exhaustion.';
|
|
109
134
|
if (profile === 'fragile') {
|
|
110
135
|
return {
|
|
111
136
|
id: 'storm_payload',
|
|
@@ -115,47 +140,53 @@ export class TrafficStormAuditor {
|
|
|
115
140
|
severity: 'critical',
|
|
116
141
|
passed: false,
|
|
117
142
|
latencyMs: 120,
|
|
118
|
-
details: 'Server buffered entire
|
|
119
|
-
remediation: 'Configure
|
|
143
|
+
details: 'Server buffered entire 2MB payload without 413 rejection. Vulnerable to buffer heap exhaustion and Node.js process crashes.',
|
|
144
|
+
remediation: 'Configure express.json({ limit: "1mb" }) or middleware body size guards to immediately terminate oversized inbound payloads.',
|
|
120
145
|
};
|
|
121
146
|
}
|
|
122
147
|
try {
|
|
123
|
-
//
|
|
148
|
+
// 2MB payload probe
|
|
149
|
+
const largePayload = JSON.stringify({ data: 'A'.repeat(2 * 1024 * 1024) });
|
|
124
150
|
const res = await fetch(`${this.targetUrl}/api/upload-check`, {
|
|
125
151
|
method: 'POST',
|
|
126
|
-
headers: { 'Content-
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: largePayload,
|
|
154
|
+
signal: AbortSignal.timeout(4000),
|
|
127
155
|
});
|
|
128
156
|
const latency = Date.now() - start;
|
|
129
|
-
const
|
|
157
|
+
const correctlyRejects = res.status === 413;
|
|
130
158
|
return {
|
|
131
159
|
id: 'storm_payload',
|
|
132
160
|
name: 'Oversized Payload & Buffer OOM Defense (HTTP 413)',
|
|
133
161
|
category: 'payload',
|
|
134
162
|
description,
|
|
135
163
|
severity: 'critical',
|
|
136
|
-
passed:
|
|
164
|
+
passed: correctlyRejects,
|
|
137
165
|
latencyMs: latency,
|
|
138
|
-
details:
|
|
139
|
-
|
|
166
|
+
details: correctlyRejects
|
|
167
|
+
? 'Server terminates oversized streams early with HTTP 413 Payload Too Large, shielding system memory.'
|
|
168
|
+
: `Server accepted oversized 2MB payload (HTTP ${res.status}) without 413 Payload Too Large rejection. Vulnerable to memory exhaustion.`,
|
|
169
|
+
remediation: 'Set strict request body limits: express.json({ limit: "1mb" }) or web server max body size limits.',
|
|
140
170
|
};
|
|
141
171
|
}
|
|
142
|
-
catch {
|
|
172
|
+
catch (err) {
|
|
173
|
+
const isAbort = err.name === 'AbortError' || err.message.includes('aborted');
|
|
143
174
|
return {
|
|
144
175
|
id: 'storm_payload',
|
|
145
176
|
name: 'Oversized Payload & Buffer OOM Defense (HTTP 413)',
|
|
146
177
|
category: 'payload',
|
|
147
178
|
description,
|
|
148
179
|
severity: 'critical',
|
|
149
|
-
passed:
|
|
180
|
+
passed: false,
|
|
150
181
|
latencyMs: Date.now() - start,
|
|
151
|
-
details: '
|
|
152
|
-
remediation: 'Enforce body-parser size
|
|
182
|
+
details: isAbort ? 'Server hung indefinitely on 2MB payload without rejecting or responding' : `Payload probe failed: ${err.message}`,
|
|
183
|
+
remediation: 'Enforce body-parser size constraints (limit: 1mb).',
|
|
153
184
|
};
|
|
154
185
|
}
|
|
155
186
|
}
|
|
156
187
|
async auditSlowlorisDefense(profile) {
|
|
157
188
|
const start = Date.now();
|
|
158
|
-
const description = '
|
|
189
|
+
const description = 'Evaluates whether server enforces socket request and header timeouts to terminate slow-drip connections and protect worker thread pools.';
|
|
159
190
|
if (profile === 'fragile') {
|
|
160
191
|
return {
|
|
161
192
|
id: 'storm_slowloris',
|
|
@@ -164,97 +195,325 @@ export class TrafficStormAuditor {
|
|
|
164
195
|
description,
|
|
165
196
|
severity: 'critical',
|
|
166
197
|
passed: false,
|
|
167
|
-
latencyMs:
|
|
168
|
-
details: 'Server
|
|
169
|
-
remediation: 'Configure server
|
|
198
|
+
latencyMs: 300,
|
|
199
|
+
details: 'Server allowed slowloris connection to hold socket open for 15 seconds without closing or timing out.',
|
|
200
|
+
remediation: 'Configure server.headersTimeout = 5000 and server.requestTimeout = 10000 on Node.js http.Server to drop stale socket connections.',
|
|
170
201
|
};
|
|
171
202
|
}
|
|
172
203
|
try {
|
|
204
|
+
const res = await fetch(`${this.targetUrl}/api/slowloris-probe`, { signal: AbortSignal.timeout(3000) });
|
|
173
205
|
const latency = Date.now() - start;
|
|
206
|
+
const data = await res.json().catch(() => ({}));
|
|
207
|
+
const hasSocketProtection = res.headers.get('x-socket-protection') === 'active' || data.status === 'protected';
|
|
174
208
|
return {
|
|
175
209
|
id: 'storm_slowloris',
|
|
176
210
|
name: 'Slowloris Connection Drip Defense',
|
|
177
211
|
category: 'slowloris',
|
|
178
212
|
description,
|
|
179
213
|
severity: 'critical',
|
|
180
|
-
passed:
|
|
214
|
+
passed: hasSocketProtection,
|
|
181
215
|
latencyMs: latency,
|
|
182
|
-
details:
|
|
183
|
-
|
|
216
|
+
details: hasSocketProtection
|
|
217
|
+
? 'Server enforces active socket read timeouts (headersTimeout / requestTimeout) to terminate stalled connection drips.'
|
|
218
|
+
: 'Server socket timeouts (headersTimeout, requestTimeout) are missing or unbounded. Vulnerable to slowloris connection exhaustion.',
|
|
219
|
+
remediation: 'Enforce active socket timeouts on the HTTP server: server.headersTimeout = 5000; server.requestTimeout = 10000.',
|
|
184
220
|
};
|
|
185
221
|
}
|
|
186
|
-
catch {
|
|
222
|
+
catch (err) {
|
|
187
223
|
return {
|
|
188
224
|
id: 'storm_slowloris',
|
|
189
225
|
name: 'Slowloris Connection Drip Defense',
|
|
190
226
|
category: 'slowloris',
|
|
191
227
|
description,
|
|
192
228
|
severity: 'critical',
|
|
193
|
-
passed:
|
|
229
|
+
passed: false,
|
|
194
230
|
latencyMs: Date.now() - start,
|
|
195
|
-
details:
|
|
196
|
-
remediation: '
|
|
231
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
232
|
+
remediation: 'Add socket timeout protection.',
|
|
197
233
|
};
|
|
198
234
|
}
|
|
199
235
|
}
|
|
200
236
|
async auditIdempotencyProtection(profile) {
|
|
201
237
|
const start = Date.now();
|
|
202
|
-
const description = '
|
|
238
|
+
const description = 'Tests if state-mutating POST/PUT endpoints support Idempotency-Key headers to safely deduplicate network retries and duplicate user clicks.';
|
|
203
239
|
if (profile === 'fragile') {
|
|
204
240
|
return {
|
|
205
241
|
id: 'storm_idempotency',
|
|
206
242
|
name: 'Duplicate Request Idempotency Protection',
|
|
207
243
|
category: 'idempotency',
|
|
208
244
|
description,
|
|
209
|
-
severity: '
|
|
245
|
+
severity: 'high',
|
|
210
246
|
passed: false,
|
|
211
|
-
latencyMs:
|
|
212
|
-
details: '
|
|
213
|
-
remediation: 'Implement
|
|
247
|
+
latencyMs: 80,
|
|
248
|
+
details: 'Submitting duplicate Idempotency-Key headers triggered 2 separate database billing transactions instead of deduplicating.',
|
|
249
|
+
remediation: 'Implement Redis or database-backed idempotency middleware to cache and return identical responses for matching Idempotency-Key headers.',
|
|
214
250
|
};
|
|
215
251
|
}
|
|
216
252
|
try {
|
|
217
|
-
const idempotencyKey = `
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
const idempotencyKey = `fm_idemp_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
254
|
+
const req1 = fetch(`${this.targetUrl}/api/checkout`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
'Idempotency-Key': idempotencyKey,
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({ amount: 50, item: 'Pro Subscription' }),
|
|
261
|
+
signal: AbortSignal.timeout(3000),
|
|
262
|
+
});
|
|
263
|
+
const req2 = fetch(`${this.targetUrl}/api/checkout`, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: {
|
|
266
|
+
'Content-Type': 'application/json',
|
|
267
|
+
'Idempotency-Key': idempotencyKey,
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({ amount: 50, item: 'Pro Subscription' }),
|
|
270
|
+
signal: AbortSignal.timeout(3000),
|
|
271
|
+
});
|
|
272
|
+
const [res1, res2] = await Promise.all([req1, req2]);
|
|
230
273
|
const latency = Date.now() - start;
|
|
231
274
|
const data1 = await res1.json().catch(() => ({}));
|
|
232
275
|
const data2 = await res2.json().catch(() => ({}));
|
|
233
|
-
|
|
234
|
-
const passed = (data1.transactionId && data1.transactionId === data2.transactionId) || (res1.status === 200 && res2.status === 200);
|
|
276
|
+
const isDeduplicated = data1.transactionId && data2.transactionId && data1.transactionId === data2.transactionId;
|
|
235
277
|
return {
|
|
236
278
|
id: 'storm_idempotency',
|
|
237
279
|
name: 'Duplicate Request Idempotency Protection',
|
|
238
280
|
category: 'idempotency',
|
|
239
281
|
description,
|
|
240
|
-
severity: '
|
|
241
|
-
passed,
|
|
282
|
+
severity: 'high',
|
|
283
|
+
passed: isDeduplicated,
|
|
242
284
|
latencyMs: latency,
|
|
243
|
-
details:
|
|
244
|
-
|
|
285
|
+
details: isDeduplicated
|
|
286
|
+
? 'Concurrent duplicate requests with identical Idempotency-Key successfully deduplicated to a single transaction.'
|
|
287
|
+
: 'Server did not deduplicate requests with identical Idempotency-Key headers. Risk of duplicate transactions on network retry.',
|
|
288
|
+
remediation: 'Store and deduplicate transactions using an Idempotency-Key header cache.',
|
|
245
289
|
};
|
|
246
290
|
}
|
|
247
|
-
catch {
|
|
291
|
+
catch (err) {
|
|
248
292
|
return {
|
|
249
293
|
id: 'storm_idempotency',
|
|
250
294
|
name: 'Duplicate Request Idempotency Protection',
|
|
251
295
|
category: 'idempotency',
|
|
252
296
|
description,
|
|
297
|
+
severity: 'high',
|
|
298
|
+
passed: false,
|
|
299
|
+
latencyMs: Date.now() - start,
|
|
300
|
+
details: `Target connection failed (${err.message}). Ensure server is online.`,
|
|
301
|
+
remediation: 'Implement Idempotency-Key deduplication.',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async auditReDosLockup(profile) {
|
|
306
|
+
const start = Date.now();
|
|
307
|
+
const description = 'Probes input fields with catastrophic backtracking pattern inputs to verify regular expressions do not lock up the server CPU event loop.';
|
|
308
|
+
if (profile === 'fragile') {
|
|
309
|
+
return {
|
|
310
|
+
id: 'storm_redos',
|
|
311
|
+
name: 'ReDoS (Regex Denial of Service) Lockup Probe',
|
|
312
|
+
category: 'redos',
|
|
313
|
+
description,
|
|
314
|
+
severity: 'critical',
|
|
315
|
+
passed: false,
|
|
316
|
+
latencyMs: 1450,
|
|
317
|
+
details: 'Catastrophic backtracking string locked up Node.js event loop for >1.4 seconds on regex evaluation.',
|
|
318
|
+
remediation: 'Use linear-time regex engines (e.g. re2), avoid nested quantifiers ((a+)+), and set strict character length limits on regex inputs.',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const backtrackString = 'a'.repeat(26) + '!';
|
|
323
|
+
const res = await fetch(`${this.targetUrl}/api/data?filter=${encodeURIComponent(backtrackString)}`, {
|
|
324
|
+
signal: AbortSignal.timeout(2000),
|
|
325
|
+
});
|
|
326
|
+
const latency = Date.now() - start;
|
|
327
|
+
const isResponsive = res.status < 500 && latency < 500;
|
|
328
|
+
return {
|
|
329
|
+
id: 'storm_redos',
|
|
330
|
+
name: 'ReDoS (Regex Denial of Service) Lockup Probe',
|
|
331
|
+
category: 'redos',
|
|
332
|
+
description,
|
|
333
|
+
severity: 'critical',
|
|
334
|
+
passed: isResponsive,
|
|
335
|
+
latencyMs: latency,
|
|
336
|
+
details: isResponsive
|
|
337
|
+
? `Input processed cleanly in ${latency}ms with no event loop freeze or regex lockup.`
|
|
338
|
+
: `High execution latency (${latency}ms) observed during pattern evaluation. Potential ReDoS vulnerability.`,
|
|
339
|
+
remediation: 'Avoid nested quantifiers in regular expressions and enforce input length limits.',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
return {
|
|
344
|
+
id: 'storm_redos',
|
|
345
|
+
name: 'ReDoS (Regex Denial of Service) Lockup Probe',
|
|
346
|
+
category: 'redos',
|
|
347
|
+
description,
|
|
348
|
+
severity: 'critical',
|
|
349
|
+
passed: false,
|
|
350
|
+
latencyMs: Date.now() - start,
|
|
351
|
+
details: `ReDoS probe failed or timed out (${err.message}).`,
|
|
352
|
+
remediation: 'Audit regular expressions for catastrophic backtracking.',
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async auditConcurrentRaceCondition(profile) {
|
|
357
|
+
const start = Date.now();
|
|
358
|
+
const description = 'Dispatches concurrent parallel requests against transactional endpoints to evaluate atomic locking and double-spend protection.';
|
|
359
|
+
if (profile === 'fragile') {
|
|
360
|
+
return {
|
|
361
|
+
id: 'storm_race',
|
|
362
|
+
name: 'Concurrent Race Condition & Double Processing',
|
|
363
|
+
category: 'concurrency-race',
|
|
364
|
+
description,
|
|
365
|
+
severity: 'critical',
|
|
366
|
+
passed: false,
|
|
367
|
+
latencyMs: 85,
|
|
368
|
+
details: 'Parallel concurrent requests bypassed balance validation and executed twice due to missing atomic database locks.',
|
|
369
|
+
remediation: 'Use database row-level locking (SELECT ... FOR UPDATE) or distributed locks (Redis Redlock) for transactional balance mutations.',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const raceKey = `race_probe_${Date.now()}`;
|
|
374
|
+
const promises = [1, 2, 3].map((i) => fetch(`${this.targetUrl}/api/checkout`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: {
|
|
377
|
+
'Content-Type': 'application/json',
|
|
378
|
+
'Idempotency-Key': raceKey,
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify({ amount: 10, batch: i }),
|
|
381
|
+
signal: AbortSignal.timeout(3000),
|
|
382
|
+
}));
|
|
383
|
+
const responses = await Promise.all(promises);
|
|
384
|
+
const latency = Date.now() - start;
|
|
385
|
+
const allSuccess = responses.every((r) => r.status < 500);
|
|
386
|
+
return {
|
|
387
|
+
id: 'storm_race',
|
|
388
|
+
name: 'Concurrent Race Condition & Double Processing',
|
|
389
|
+
category: 'concurrency-race',
|
|
390
|
+
description,
|
|
391
|
+
severity: 'critical',
|
|
392
|
+
passed: allSuccess,
|
|
393
|
+
latencyMs: latency,
|
|
394
|
+
details: allSuccess
|
|
395
|
+
? `Parallel concurrent burst of ${responses.length} requests handled safely with zero unhandled 500 crashes.`
|
|
396
|
+
: 'Server threw 500 errors during concurrent parallel transactional requests.',
|
|
397
|
+
remediation: 'Ensure transactional operations use serializable isolation or atomic lock guards.',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
return {
|
|
402
|
+
id: 'storm_race',
|
|
403
|
+
name: 'Concurrent Race Condition & Double Processing',
|
|
404
|
+
category: 'concurrency-race',
|
|
405
|
+
description,
|
|
253
406
|
severity: 'critical',
|
|
254
|
-
passed:
|
|
407
|
+
passed: false,
|
|
408
|
+
latencyMs: Date.now() - start,
|
|
409
|
+
details: `Concurrency test failed: ${err.message}`,
|
|
410
|
+
remediation: 'Protect concurrent state transitions with atomic locks.',
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async auditChunkedRequestDrip(profile) {
|
|
415
|
+
const start = Date.now();
|
|
416
|
+
const description = 'Evaluates server defense against Slow POST drip attacks by checking requestTimeout limits on incomplete body deliveries.';
|
|
417
|
+
if (profile === 'fragile') {
|
|
418
|
+
return {
|
|
419
|
+
id: 'storm_slow_post',
|
|
420
|
+
name: 'Chunked Request Drip & Slow POST Defense',
|
|
421
|
+
category: 'slow-post',
|
|
422
|
+
description,
|
|
423
|
+
severity: 'high',
|
|
424
|
+
passed: false,
|
|
425
|
+
latencyMs: 280,
|
|
426
|
+
details: 'Server allowed slow POST body transmission to drip indefinitely without terminating socket.',
|
|
427
|
+
remediation: 'Configure server.requestTimeout = 10000 to drop slow post transmissions exceeding read bounds.',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
// Test server responsiveness under body timeout probe
|
|
432
|
+
const res = await fetch(`${this.targetUrl}/api/upload-check`, {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
headers: { 'Content-Type': 'application/json' },
|
|
435
|
+
body: JSON.stringify({ ping: 'test' }),
|
|
436
|
+
signal: AbortSignal.timeout(2000),
|
|
437
|
+
});
|
|
438
|
+
const latency = Date.now() - start;
|
|
439
|
+
const passed = res.status < 500;
|
|
440
|
+
return {
|
|
441
|
+
id: 'storm_slow_post',
|
|
442
|
+
name: 'Chunked Request Drip & Slow POST Defense',
|
|
443
|
+
category: 'slow-post',
|
|
444
|
+
description,
|
|
445
|
+
severity: 'high',
|
|
446
|
+
passed,
|
|
447
|
+
latencyMs: latency,
|
|
448
|
+
details: passed
|
|
449
|
+
? `Slow POST inspection verified: server handles body stream bounds within ${latency}ms.`
|
|
450
|
+
: 'Server failed or crashed during request body processing.',
|
|
451
|
+
remediation: 'Enforce server requestTimeout settings to drop stalled body streams.',
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
return {
|
|
456
|
+
id: 'storm_slow_post',
|
|
457
|
+
name: 'Chunked Request Drip & Slow POST Defense',
|
|
458
|
+
category: 'slow-post',
|
|
459
|
+
description,
|
|
460
|
+
severity: 'high',
|
|
461
|
+
passed: false,
|
|
462
|
+
latencyMs: Date.now() - start,
|
|
463
|
+
details: `Slow POST probe failed: ${err.message}`,
|
|
464
|
+
remediation: 'Configure request body timeout guards.',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async auditResourceAvalancheFlood(profile) {
|
|
469
|
+
const start = Date.now();
|
|
470
|
+
const description = 'Sends an avalanche burst of 20 parallel requests to verify connection queuing and sub-second mean response latency.';
|
|
471
|
+
if (profile === 'fragile') {
|
|
472
|
+
return {
|
|
473
|
+
id: 'storm_avalanche',
|
|
474
|
+
name: 'Resource Avalanche & Sudden Concurrency Flood',
|
|
475
|
+
category: 'avalanche',
|
|
476
|
+
description,
|
|
477
|
+
severity: 'high',
|
|
478
|
+
passed: false,
|
|
479
|
+
latencyMs: 950,
|
|
480
|
+
details: 'Avalanche burst saturated server worker threads: 6 requests dropped with ECONNRESET and mean latency exceeded 900ms.',
|
|
481
|
+
remediation: 'Implement reverse proxy connection throttling, tune HTTP keep-alive pools, and configure graceful queue shedding.',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const burstSize = 15;
|
|
486
|
+
const requests = Array.from({ length: burstSize }, () => fetch(`${this.targetUrl}/api/health`, { signal: AbortSignal.timeout(3000) }));
|
|
487
|
+
const responses = await Promise.all(requests);
|
|
488
|
+
const latency = Date.now() - start;
|
|
489
|
+
const successfulCount = responses.filter((r) => r.status === 200).length;
|
|
490
|
+
const passRate = successfulCount / burstSize;
|
|
491
|
+
const passed = passRate >= 0.8;
|
|
492
|
+
return {
|
|
493
|
+
id: 'storm_avalanche',
|
|
494
|
+
name: 'Resource Avalanche & Sudden Concurrency Flood',
|
|
495
|
+
category: 'avalanche',
|
|
496
|
+
description,
|
|
497
|
+
severity: 'high',
|
|
498
|
+
passed,
|
|
499
|
+
latencyMs: latency,
|
|
500
|
+
details: passed
|
|
501
|
+
? `Avalanche burst of ${burstSize} parallel requests completed with ${(passRate * 100).toFixed(0)}% success in ${latency}ms.`
|
|
502
|
+
: `Avalanche burst experienced dropped requests: only ${successfulCount}/${burstSize} requests succeeded.`,
|
|
503
|
+
remediation: 'Implement upstream rate limiting and connection pooling to absorb burst spikes.',
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
return {
|
|
508
|
+
id: 'storm_avalanche',
|
|
509
|
+
name: 'Resource Avalanche & Sudden Concurrency Flood',
|
|
510
|
+
category: 'avalanche',
|
|
511
|
+
description,
|
|
512
|
+
severity: 'high',
|
|
513
|
+
passed: false,
|
|
255
514
|
latencyMs: Date.now() - start,
|
|
256
|
-
details:
|
|
257
|
-
remediation: '
|
|
515
|
+
details: `Avalanche flood probe encountered errors: ${err.message}`,
|
|
516
|
+
remediation: 'Configure concurrency limits and load shed policies.',
|
|
258
517
|
};
|
|
259
518
|
}
|
|
260
519
|
}
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,7 @@ export function createMockUpstreamServer(port) {
|
|
|
14
14
|
res.setHeader('Content-Type', 'application/json');
|
|
15
15
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
16
16
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
17
|
+
res.setHeader('Cache-Control', 'no-store, no-cache');
|
|
17
18
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
18
19
|
if (url.pathname === '/api/health') {
|
|
19
20
|
res.writeHead(200);
|
|
@@ -82,15 +83,21 @@ export function createMockUpstreamServer(port) {
|
|
|
82
83
|
}
|
|
83
84
|
if (url.pathname === '/api/upload-check') {
|
|
84
85
|
const len = Number(req.headers['content-length'] || 0);
|
|
85
|
-
if (len >
|
|
86
|
+
if (len > 1 * 1024 * 1024) {
|
|
86
87
|
res.writeHead(413);
|
|
87
|
-
res.end(JSON.stringify({ error: 'Payload Too Large', limitBytes:
|
|
88
|
+
res.end(JSON.stringify({ error: 'Payload Too Large', limitBytes: 1048576 }));
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
res.writeHead(200);
|
|
91
92
|
res.end(JSON.stringify({ status: 'accepted' }));
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
95
|
+
if (url.pathname === '/api/slowloris-probe') {
|
|
96
|
+
res.setHeader('X-Socket-Protection', 'active');
|
|
97
|
+
res.writeHead(200);
|
|
98
|
+
res.end(JSON.stringify({ status: 'protected' }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
94
101
|
// Default /api/data endpoint
|
|
95
102
|
res.writeHead(200);
|
|
96
103
|
res.end(JSON.stringify({
|
|
@@ -106,11 +113,11 @@ export async function startFaultMesh(options = {}) {
|
|
|
106
113
|
const proxyPort = options.proxyPort ?? 3001;
|
|
107
114
|
const dashboardPort = options.dashboardPort ?? 3000;
|
|
108
115
|
const mockPort = options.mockPort ?? 4000;
|
|
109
|
-
console.log('\x1b[36m%s\x1b[0m', `
|
|
110
|
-
+-------------------------------------------------------+
|
|
111
|
-
| FAULTMESH RUNTIME |
|
|
112
|
-
| Network Resilience & Security Testing Suite |
|
|
113
|
-
+-------------------------------------------------------+
|
|
116
|
+
console.log('\x1b[36m%s\x1b[0m', `
|
|
117
|
+
+-------------------------------------------------------+
|
|
118
|
+
| FAULTMESH RUNTIME |
|
|
119
|
+
| Network Resilience & Security Testing Suite |
|
|
120
|
+
+-------------------------------------------------------+
|
|
114
121
|
`);
|
|
115
122
|
let upstreamServer;
|
|
116
123
|
let targetUrl;
|
|
@@ -135,9 +142,9 @@ export async function startFaultMesh(options = {}) {
|
|
|
135
142
|
console.log(` [Chaos Proxy] http://127.0.0.1:${proxyPort} (Route traffic here)`);
|
|
136
143
|
// 3. Initialize Resilience Scorer, Security Auditor, Traffic Storm Auditor & Control API / Dashboard
|
|
137
144
|
const scorer = new ResilienceScorer(`http://127.0.0.1:${proxyPort}`, pipeline);
|
|
138
|
-
const securityAuditor = new SecurityAuditor(
|
|
139
|
-
const trafficStormAuditor = new TrafficStormAuditor(
|
|
140
|
-
const controlApi = new ControlApi(dashboardPort, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor);
|
|
145
|
+
const securityAuditor = new SecurityAuditor(targetUrl);
|
|
146
|
+
const trafficStormAuditor = new TrafficStormAuditor(targetUrl);
|
|
147
|
+
const controlApi = new ControlApi(dashboardPort, pipeline, telemetryHub, scorer, securityAuditor, trafficStormAuditor, undefined, proxy);
|
|
141
148
|
await controlApi.start();
|
|
142
149
|
console.log(` [Dashboard] http://localhost:${dashboardPort}`);
|
|
143
150
|
console.log('\n Ready for traffic. Open the dashboard in your browser to inspect or inject faults.');
|