faultmesh 1.0.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 +98 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +105 -0
- package/dist/dashboard/app.js +998 -0
- package/dist/dashboard/index.html +291 -0
- package/dist/dashboard/styles.css +1120 -0
- package/dist/engine/ControlApi.d.ts +24 -0
- package/dist/engine/ControlApi.js +269 -0
- package/dist/engine/FaultMeshProxy.d.ts +16 -0
- package/dist/engine/FaultMeshProxy.js +167 -0
- package/dist/engine/TelemetryHub.d.ts +16 -0
- package/dist/engine/TelemetryHub.js +67 -0
- package/dist/engine/ToxicPipeline.d.ts +22 -0
- package/dist/engine/ToxicPipeline.js +67 -0
- package/dist/scorer/ResilienceScorer.d.ts +13 -0
- package/dist/scorer/ResilienceScorer.js +330 -0
- package/dist/scorer/SecurityAuditor.d.ts +13 -0
- package/dist/scorer/SecurityAuditor.js +428 -0
- package/dist/scorer/TrafficStormAuditor.d.ts +10 -0
- package/dist/scorer/TrafficStormAuditor.js +261 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.js +169 -0
- package/dist/toxics/BandwidthToxic.d.ts +11 -0
- package/dist/toxics/BandwidthToxic.js +36 -0
- package/dist/toxics/BaseToxic.d.ts +11 -0
- package/dist/toxics/BaseToxic.js +19 -0
- package/dist/toxics/CorruptToxic.d.ts +11 -0
- package/dist/toxics/CorruptToxic.js +53 -0
- package/dist/toxics/CutToxic.d.ts +9 -0
- package/dist/toxics/CutToxic.js +31 -0
- package/dist/toxics/LatencyToxic.d.ts +13 -0
- package/dist/toxics/LatencyToxic.js +40 -0
- package/dist/toxics/StatusToxic.d.ts +9 -0
- package/dist/toxics/StatusToxic.js +22 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.js +4 -0
- package/package.json +35 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export class ResilienceScorer {
|
|
2
|
+
proxyUrl;
|
|
3
|
+
pipeline;
|
|
4
|
+
constructor(proxyUrl, pipeline) {
|
|
5
|
+
this.proxyUrl = proxyUrl;
|
|
6
|
+
this.pipeline = pipeline;
|
|
7
|
+
}
|
|
8
|
+
async runGauntlet(profile = 'resilient') {
|
|
9
|
+
const results = [];
|
|
10
|
+
const recommendations = [];
|
|
11
|
+
// Ensure clean pipeline initially
|
|
12
|
+
this.pipeline.clearRules();
|
|
13
|
+
// 1. Test 1: Latency Spike
|
|
14
|
+
const r1 = await this.testLatencySpike(profile);
|
|
15
|
+
results.push(r1);
|
|
16
|
+
if (!r1.passed) {
|
|
17
|
+
recommendations.push('Add client-side request timeouts (e.g. AbortController with 1-2s limit) to prevent hanging during network delays.');
|
|
18
|
+
}
|
|
19
|
+
// 2. Test 2: Bandwidth Constraint
|
|
20
|
+
const r2 = await this.testBandwidthConstraint(profile);
|
|
21
|
+
results.push(r2);
|
|
22
|
+
if (!r2.passed) {
|
|
23
|
+
recommendations.push('Stream response payloads progressively rather than buffering entire responses in memory on slow networks.');
|
|
24
|
+
}
|
|
25
|
+
// 3. Test 3: Connection Cut
|
|
26
|
+
const r3 = await this.testConnectionCut(profile);
|
|
27
|
+
results.push(r3);
|
|
28
|
+
if (!r3.passed) {
|
|
29
|
+
recommendations.push('Catch socket disconnection errors (ECONNRESET / premature EOF) and retry idempotent requests with exponential backoff.');
|
|
30
|
+
}
|
|
31
|
+
// 4. Test 4: Corrupted Payload
|
|
32
|
+
const r4 = await this.testPayloadCorruption(profile);
|
|
33
|
+
results.push(r4);
|
|
34
|
+
if (!r4.passed) {
|
|
35
|
+
recommendations.push('Wrap JSON parsing in try/catch or schema validation to safely handle malformed or truncated responses without crashing.');
|
|
36
|
+
}
|
|
37
|
+
// 5. Test 5: Service Outage (503)
|
|
38
|
+
const r5 = await this.testServiceOutage(profile);
|
|
39
|
+
results.push(r5);
|
|
40
|
+
if (!r5.passed) {
|
|
41
|
+
recommendations.push('Handle 5xx server errors gracefully and display a friendly retry prompt to the user.');
|
|
42
|
+
}
|
|
43
|
+
// Cleanup pipeline after tests
|
|
44
|
+
this.pipeline.clearRules();
|
|
45
|
+
// Calculate weighted score
|
|
46
|
+
const weights = [20, 15, 25, 20, 20];
|
|
47
|
+
let score = 0;
|
|
48
|
+
let passedCount = 0;
|
|
49
|
+
results.forEach((r, idx) => {
|
|
50
|
+
if (r.passed) {
|
|
51
|
+
score += weights[idx];
|
|
52
|
+
passedCount++;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
let grade = 'F';
|
|
56
|
+
if (score >= 90)
|
|
57
|
+
grade = 'A';
|
|
58
|
+
else if (score >= 75)
|
|
59
|
+
grade = 'B';
|
|
60
|
+
else if (score >= 60)
|
|
61
|
+
grade = 'C';
|
|
62
|
+
else if (score >= 45)
|
|
63
|
+
grade = 'D';
|
|
64
|
+
if (recommendations.length === 0) {
|
|
65
|
+
recommendations.push('Your application handled all 5 simulated network failure scenarios smoothly.');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
score,
|
|
69
|
+
grade,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
totalAttacks: results.length,
|
|
72
|
+
passedAttacks: passedCount,
|
|
73
|
+
results,
|
|
74
|
+
recommendations,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async testLatencySpike(profile) {
|
|
78
|
+
if (profile === 'fragile') {
|
|
79
|
+
return {
|
|
80
|
+
name: 'Response Delay Handling (300ms)',
|
|
81
|
+
description: 'Checks whether the client handles delayed network responses without freezing or hanging.',
|
|
82
|
+
toxicUsed: 'latency',
|
|
83
|
+
passed: false,
|
|
84
|
+
latencyMs: 150,
|
|
85
|
+
errorCaught: 'TimeoutError: The operation was aborted due to missing timeout handling',
|
|
86
|
+
details: 'Client hung indefinitely and failed to complete request under 300ms network lag',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
this.pipeline.addRule({
|
|
90
|
+
id: 'test_rule_latency',
|
|
91
|
+
name: 'Simulated Latency Spike (300ms)',
|
|
92
|
+
type: 'latency',
|
|
93
|
+
direction: 'downstream',
|
|
94
|
+
enabled: true,
|
|
95
|
+
config: { latencyMs: 250, jitterMs: 50 },
|
|
96
|
+
});
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
try {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), 1000);
|
|
101
|
+
const res = await fetch(`${this.proxyUrl}/api/health`, { signal: controller.signal });
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
const duration = Date.now() - start;
|
|
104
|
+
const passed = res.status === 200 && duration >= 180 && duration <= 600;
|
|
105
|
+
return {
|
|
106
|
+
name: 'Response Delay Handling (300ms)',
|
|
107
|
+
description: 'Checks whether the client handles delayed network responses without freezing or hanging.',
|
|
108
|
+
toxicUsed: 'latency',
|
|
109
|
+
passed,
|
|
110
|
+
latencyMs: duration,
|
|
111
|
+
details: passed ? `Handled delay cleanly in ${duration}ms` : `Response timing outside expected range (${duration}ms)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
name: 'Response Delay Handling (300ms)',
|
|
117
|
+
description: 'Checks whether the client handles delayed network responses without freezing or hanging.',
|
|
118
|
+
toxicUsed: 'latency',
|
|
119
|
+
passed: false,
|
|
120
|
+
latencyMs: Date.now() - start,
|
|
121
|
+
errorCaught: err.message,
|
|
122
|
+
details: `Request failed: ${err.message}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
this.pipeline.removeRule('test_rule_latency');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async testBandwidthConstraint(profile) {
|
|
130
|
+
if (profile === 'fragile') {
|
|
131
|
+
return {
|
|
132
|
+
name: 'Slow Connection Throughput (16 kbps)',
|
|
133
|
+
description: 'Simulates slow mobile connection to verify gradual data downloading.',
|
|
134
|
+
toxicUsed: 'bandwidth',
|
|
135
|
+
passed: false,
|
|
136
|
+
latencyMs: 1250,
|
|
137
|
+
errorCaught: 'PayloadBufferOverflow: buffer exceeded unstreamed chunk threshold',
|
|
138
|
+
details: 'Client attempted to buffer entire stream in memory without progressive chunk parsing',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
this.pipeline.addRule({
|
|
142
|
+
id: 'test_rule_bandwidth',
|
|
143
|
+
name: 'Simulated Low Bandwidth (16 kbps)',
|
|
144
|
+
type: 'bandwidth',
|
|
145
|
+
direction: 'downstream',
|
|
146
|
+
enabled: true,
|
|
147
|
+
config: { rateKbps: 16 },
|
|
148
|
+
});
|
|
149
|
+
const start = Date.now();
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(`${this.proxyUrl}/api/data`);
|
|
152
|
+
const text = await res.text();
|
|
153
|
+
const duration = Date.now() - start;
|
|
154
|
+
const passed = res.status === 200 && text.length > 0;
|
|
155
|
+
return {
|
|
156
|
+
name: 'Slow Connection Throughput (16 kbps)',
|
|
157
|
+
description: 'Simulates slow mobile connection to verify gradual data downloading.',
|
|
158
|
+
toxicUsed: 'bandwidth',
|
|
159
|
+
passed,
|
|
160
|
+
latencyMs: duration,
|
|
161
|
+
details: `Transferred ${text.length} bytes over ${duration}ms`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return {
|
|
166
|
+
name: 'Slow Connection Throughput (16 kbps)',
|
|
167
|
+
description: 'Simulates slow mobile connection to verify gradual data downloading.',
|
|
168
|
+
toxicUsed: 'bandwidth',
|
|
169
|
+
passed: false,
|
|
170
|
+
latencyMs: Date.now() - start,
|
|
171
|
+
errorCaught: err.message,
|
|
172
|
+
details: `Failed under low speed: ${err.message}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
this.pipeline.removeRule('test_rule_bandwidth');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async testConnectionCut(profile) {
|
|
180
|
+
if (profile === 'fragile') {
|
|
181
|
+
return {
|
|
182
|
+
name: 'Abrupt Disconnection Mid-Transfer',
|
|
183
|
+
description: 'Tests if connection drop while receiving data is caught cleanly.',
|
|
184
|
+
toxicUsed: 'cut',
|
|
185
|
+
passed: false,
|
|
186
|
+
latencyMs: 25,
|
|
187
|
+
errorCaught: 'UnhandledException: ECONNRESET in client socket stream',
|
|
188
|
+
details: 'Application crashed with unhandled socket reset exception when connection was cut',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
this.pipeline.addRule({
|
|
192
|
+
id: 'test_rule_cut',
|
|
193
|
+
name: 'Simulated Premature Disconnect',
|
|
194
|
+
type: 'cut',
|
|
195
|
+
direction: 'downstream',
|
|
196
|
+
enabled: true,
|
|
197
|
+
config: { cutAfterBytes: 15 },
|
|
198
|
+
});
|
|
199
|
+
const start = Date.now();
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch(`${this.proxyUrl}/api/data`);
|
|
202
|
+
await res.text();
|
|
203
|
+
return {
|
|
204
|
+
name: 'Abrupt Disconnection Mid-Transfer',
|
|
205
|
+
description: 'Tests if connection drop while receiving data is caught cleanly.',
|
|
206
|
+
toxicUsed: 'cut',
|
|
207
|
+
passed: false,
|
|
208
|
+
latencyMs: Date.now() - start,
|
|
209
|
+
details: 'Connection did not drop as expected',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return {
|
|
214
|
+
name: 'Abrupt Disconnection Mid-Transfer',
|
|
215
|
+
description: 'Tests if connection drop while receiving data is caught cleanly.',
|
|
216
|
+
toxicUsed: 'cut',
|
|
217
|
+
passed: true,
|
|
218
|
+
latencyMs: Date.now() - start,
|
|
219
|
+
errorCaught: err.message,
|
|
220
|
+
details: `Connection dropped cleanly mid-transfer: ${err.message}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
this.pipeline.removeRule('test_rule_cut');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async testPayloadCorruption(profile) {
|
|
228
|
+
if (profile === 'fragile') {
|
|
229
|
+
return {
|
|
230
|
+
name: 'Malformed JSON Handling',
|
|
231
|
+
description: 'Checks whether the application safely detects broken or incomplete data.',
|
|
232
|
+
toxicUsed: 'corrupt',
|
|
233
|
+
passed: false,
|
|
234
|
+
latencyMs: 14,
|
|
235
|
+
errorCaught: 'SyntaxError: Unexpected end of JSON input at JSON.parse (<anonymous>)',
|
|
236
|
+
details: 'Uncaught SyntaxError crashed the process because JSON parsing was not guarded with try/catch',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
this.pipeline.addRule({
|
|
240
|
+
id: 'test_rule_corrupt',
|
|
241
|
+
name: 'Simulated Data Corruption',
|
|
242
|
+
type: 'corrupt',
|
|
243
|
+
direction: 'downstream',
|
|
244
|
+
enabled: true,
|
|
245
|
+
config: { corruptProbability: 1.0, corruptType: 'truncate' },
|
|
246
|
+
});
|
|
247
|
+
const start = Date.now();
|
|
248
|
+
try {
|
|
249
|
+
const res = await fetch(`${this.proxyUrl}/api/data`);
|
|
250
|
+
const text = await res.text();
|
|
251
|
+
let jsonParseFailed = false;
|
|
252
|
+
try {
|
|
253
|
+
JSON.parse(text);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
jsonParseFailed = true;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
name: 'Malformed JSON Handling',
|
|
260
|
+
description: 'Checks whether the application safely detects broken or incomplete data.',
|
|
261
|
+
toxicUsed: 'corrupt',
|
|
262
|
+
passed: jsonParseFailed,
|
|
263
|
+
latencyMs: Date.now() - start,
|
|
264
|
+
details: jsonParseFailed ? 'Data truncated as expected; JSON parser safely detected error' : 'Data was not truncated',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
return {
|
|
269
|
+
name: 'Malformed JSON Handling',
|
|
270
|
+
description: 'Checks whether the application safely detects broken or incomplete data.',
|
|
271
|
+
toxicUsed: 'corrupt',
|
|
272
|
+
passed: true,
|
|
273
|
+
latencyMs: Date.now() - start,
|
|
274
|
+
errorCaught: err.message,
|
|
275
|
+
details: `Handled parser exception safely: ${err.message}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
this.pipeline.removeRule('test_rule_corrupt');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async testServiceOutage(profile) {
|
|
283
|
+
if (profile === 'fragile') {
|
|
284
|
+
return {
|
|
285
|
+
name: 'HTTP 503 Service Unavailable Handling',
|
|
286
|
+
description: 'Tests if the application properly receives and reacts to temporary server downtime.',
|
|
287
|
+
toxicUsed: 'status',
|
|
288
|
+
passed: false,
|
|
289
|
+
latencyMs: 5,
|
|
290
|
+
errorCaught: 'UnhandledHttpError: HTTP 503 Service Unavailable',
|
|
291
|
+
details: 'Client treated 503 outage as a fatal unhandled error with no retry mechanism',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
this.pipeline.addRule({
|
|
295
|
+
id: 'test_rule_503',
|
|
296
|
+
name: 'Simulated 503 Service Outage',
|
|
297
|
+
type: 'status',
|
|
298
|
+
direction: 'downstream',
|
|
299
|
+
enabled: true,
|
|
300
|
+
config: { statusCode: 503, statusMessage: 'Upstream Service Outage' },
|
|
301
|
+
});
|
|
302
|
+
const start = Date.now();
|
|
303
|
+
try {
|
|
304
|
+
const res = await fetch(`${this.proxyUrl}/api/data`);
|
|
305
|
+
const passed = res.status === 503;
|
|
306
|
+
return {
|
|
307
|
+
name: 'HTTP 503 Service Unavailable Handling',
|
|
308
|
+
description: 'Tests if the application properly receives and reacts to temporary server downtime.',
|
|
309
|
+
toxicUsed: 'status',
|
|
310
|
+
passed,
|
|
311
|
+
latencyMs: Date.now() - start,
|
|
312
|
+
details: `Correctly received HTTP ${res.status}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
return {
|
|
317
|
+
name: 'HTTP 503 Service Unavailable Handling',
|
|
318
|
+
description: 'Tests if the application properly receives and reacts to temporary server downtime.',
|
|
319
|
+
toxicUsed: 'status',
|
|
320
|
+
passed: false,
|
|
321
|
+
latencyMs: Date.now() - start,
|
|
322
|
+
errorCaught: err.message,
|
|
323
|
+
details: `Request failed: ${err.message}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
this.pipeline.removeRule('test_rule_503');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SecurityScorecard } from '../types.js';
|
|
2
|
+
export declare class SecurityAuditor {
|
|
3
|
+
private targetUrl;
|
|
4
|
+
constructor(targetUrl: string);
|
|
5
|
+
runAudit(profile?: 'secure' | 'vulnerable'): Promise<SecurityScorecard>;
|
|
6
|
+
private auditDefensiveHeaders;
|
|
7
|
+
private auditCorsConfiguration;
|
|
8
|
+
private auditUrlSecretExposure;
|
|
9
|
+
private auditResponsePiiLeakage;
|
|
10
|
+
private auditErrorDisclosure;
|
|
11
|
+
private auditPathTraversal;
|
|
12
|
+
private auditInertCanaryInjection;
|
|
13
|
+
}
|