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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FaultMesh — Autonomous Red Chaos Team Engine
|
|
3
|
+
* Type definitions for multi-wave adversarial campaigns and agent personas.
|
|
4
|
+
*/
|
|
5
|
+
export type RedTeamSpecialty = 'security' | 'silent-failure' | 'performance' | 'adversarial-gan' | 'transport';
|
|
6
|
+
export interface RedTeamPersona {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
callSign: string;
|
|
10
|
+
description: string;
|
|
11
|
+
model: string;
|
|
12
|
+
tools: string[];
|
|
13
|
+
specialty: RedTeamSpecialty;
|
|
14
|
+
huntTargets: string[];
|
|
15
|
+
systemPromptExcerpt: string;
|
|
16
|
+
sourceFile: string;
|
|
17
|
+
}
|
|
18
|
+
export type CampaignPhase = 'idle' | 'recon' | 'weaponize' | 'assault' | 'debrief' | 'complete' | 'aborted';
|
|
19
|
+
export type CampaignIntensity = 'stealth' | 'sustained' | 'avalanche';
|
|
20
|
+
export interface AssaultWaveToxic {
|
|
21
|
+
type: string;
|
|
22
|
+
config: Record<string, any>;
|
|
23
|
+
direction?: 'upstream' | 'downstream';
|
|
24
|
+
pathPattern?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AssaultWave {
|
|
27
|
+
id: string;
|
|
28
|
+
waveNumber: number;
|
|
29
|
+
name: string;
|
|
30
|
+
personaId: string;
|
|
31
|
+
personaName: string;
|
|
32
|
+
callSign: string;
|
|
33
|
+
category: 'security' | 'resilience' | 'storm';
|
|
34
|
+
targetEndpoint: string;
|
|
35
|
+
toxicsToInject: AssaultWaveToxic[];
|
|
36
|
+
probesCount: number;
|
|
37
|
+
concurrency: number;
|
|
38
|
+
description: string;
|
|
39
|
+
}
|
|
40
|
+
export interface BreachProof {
|
|
41
|
+
method: string;
|
|
42
|
+
url: string;
|
|
43
|
+
requestHeaders?: Record<string, string>;
|
|
44
|
+
requestBody?: any;
|
|
45
|
+
responseStatus: number;
|
|
46
|
+
responseDurationMs: number;
|
|
47
|
+
snippet: string;
|
|
48
|
+
activeToxics?: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface BreachFinding {
|
|
51
|
+
id: string;
|
|
52
|
+
personaId: string;
|
|
53
|
+
personaName: string;
|
|
54
|
+
callSign: string;
|
|
55
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
56
|
+
category: string;
|
|
57
|
+
title: string;
|
|
58
|
+
impact: string;
|
|
59
|
+
endpoint: string;
|
|
60
|
+
proofOfBreach: BreachProof;
|
|
61
|
+
remediation: string;
|
|
62
|
+
autoHealCheckKey: string;
|
|
63
|
+
}
|
|
64
|
+
export interface CampaignLog {
|
|
65
|
+
timestamp: number;
|
|
66
|
+
waveNumber: number;
|
|
67
|
+
persona: string;
|
|
68
|
+
message: string;
|
|
69
|
+
type: 'info' | 'warn' | 'breach' | 'pass';
|
|
70
|
+
}
|
|
71
|
+
export interface CampaignReport {
|
|
72
|
+
campaignId: string;
|
|
73
|
+
targetUrl: string;
|
|
74
|
+
intensity: CampaignIntensity;
|
|
75
|
+
phase: CampaignPhase;
|
|
76
|
+
startTime: number;
|
|
77
|
+
endTime: number;
|
|
78
|
+
durationMs: number;
|
|
79
|
+
totalWaves: number;
|
|
80
|
+
completedWaves: number;
|
|
81
|
+
totalProbes: number;
|
|
82
|
+
breachesFound: BreachFinding[];
|
|
83
|
+
survivabilityScore: number;
|
|
84
|
+
survivabilityGrade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
85
|
+
activePersonas: string[];
|
|
86
|
+
logs: CampaignLog[];
|
|
87
|
+
}
|
|
88
|
+
export interface CampaignStatus {
|
|
89
|
+
active: boolean;
|
|
90
|
+
campaignId: string | null;
|
|
91
|
+
phase: CampaignPhase;
|
|
92
|
+
progressPercent: number;
|
|
93
|
+
currentWave: number;
|
|
94
|
+
totalWaves: number;
|
|
95
|
+
currentWaveName: string;
|
|
96
|
+
activePersona: string;
|
|
97
|
+
probesSent: number;
|
|
98
|
+
breachesCount: {
|
|
99
|
+
critical: number;
|
|
100
|
+
high: number;
|
|
101
|
+
medium: number;
|
|
102
|
+
low: number;
|
|
103
|
+
total: number;
|
|
104
|
+
};
|
|
105
|
+
latestLog?: CampaignLog;
|
|
106
|
+
}
|
|
107
|
+
export interface CampaignOptions {
|
|
108
|
+
targetUrl?: string;
|
|
109
|
+
intensity?: CampaignIntensity;
|
|
110
|
+
personaIds?: string[];
|
|
111
|
+
aiConfig?: {
|
|
112
|
+
provider: string;
|
|
113
|
+
endpoint?: string;
|
|
114
|
+
apiKey?: string;
|
|
115
|
+
model?: string;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -10,4 +10,9 @@ export declare class ResilienceScorer {
|
|
|
10
10
|
private testConnectionCut;
|
|
11
11
|
private testPayloadCorruption;
|
|
12
12
|
private testServiceOutage;
|
|
13
|
+
private testPacketJitter;
|
|
14
|
+
private testCircuitBreakerRecovery;
|
|
15
|
+
private testSocketStarvation;
|
|
16
|
+
private testZombieConnectionLeak;
|
|
17
|
+
private testPartialTransferTruncation;
|
|
13
18
|
}
|
|
@@ -10,6 +10,43 @@ export class ResilienceScorer {
|
|
|
10
10
|
const recommendations = [];
|
|
11
11
|
// Ensure clean pipeline initially
|
|
12
12
|
this.pipeline.clearRules();
|
|
13
|
+
// Pre-flight check: verify upstream target is reachable through the proxy
|
|
14
|
+
let upstreamReachable = true;
|
|
15
|
+
let preflightError = '';
|
|
16
|
+
try {
|
|
17
|
+
const probe = await fetch(`${this.proxyUrl}/api/health`, { signal: AbortSignal.timeout(2500) });
|
|
18
|
+
if (probe.status === 502) {
|
|
19
|
+
upstreamReachable = false;
|
|
20
|
+
preflightError = 'Upstream target API returned HTTP 502 Bad Gateway. The configured target server is unreachable or offline.';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
upstreamReachable = false;
|
|
25
|
+
preflightError = `Target connection failed (${err.message}). Ensure target server is online.`;
|
|
26
|
+
}
|
|
27
|
+
if (!upstreamReachable) {
|
|
28
|
+
const failedPlaceholders = [
|
|
29
|
+
{ name: 'Response Delay Handling (300ms)', description: 'Checks delay handling.', toxicUsed: 'latency', passed: false, latencyMs: 0, details: preflightError },
|
|
30
|
+
{ name: 'Slow Connection Throughput (16 kbps)', description: 'Checks low bandwidth.', toxicUsed: 'bandwidth', passed: false, latencyMs: 0, details: preflightError },
|
|
31
|
+
{ name: 'Abrupt Disconnection Mid-Transfer', description: 'Checks disconnection.', toxicUsed: 'cut', passed: false, latencyMs: 0, details: preflightError },
|
|
32
|
+
{ name: 'Malformed JSON Handling', description: 'Checks data corruption.', toxicUsed: 'corrupt', passed: false, latencyMs: 0, details: preflightError },
|
|
33
|
+
{ name: 'HTTP 503 Service Outage Handling', description: 'Checks 503 outage.', toxicUsed: 'status', passed: false, latencyMs: 0, details: preflightError },
|
|
34
|
+
{ name: 'Packet Jitter & Latency Variance', description: 'Checks latency jitter.', toxicUsed: 'jitter', passed: false, latencyMs: 0, details: preflightError },
|
|
35
|
+
{ name: 'Half-Open Circuit Breaker Recovery', description: 'Checks recovery after downtime.', toxicUsed: 'circuit-breaker', passed: false, latencyMs: 0, details: preflightError },
|
|
36
|
+
{ name: 'Downstream Socket Starvation & Slow Read', description: 'Checks connection pool health.', toxicUsed: 'starvation', passed: false, latencyMs: 0, details: preflightError },
|
|
37
|
+
{ name: 'Zombie Connection Leak Probe', description: 'Checks client abort socket handling.', toxicUsed: 'zombie-leak', passed: false, latencyMs: 0, details: preflightError },
|
|
38
|
+
{ name: 'Payload Truncation & Partial Transfer', description: 'Checks partial response handling.', toxicUsed: 'truncation', passed: false, latencyMs: 0, details: preflightError },
|
|
39
|
+
];
|
|
40
|
+
return {
|
|
41
|
+
score: 0,
|
|
42
|
+
grade: 'F',
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
totalAttacks: 10,
|
|
45
|
+
passedAttacks: 0,
|
|
46
|
+
results: failedPlaceholders,
|
|
47
|
+
recommendations: ['Target server is unreachable. Check the Target API field in the header and verify your backend server is running.'],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
13
50
|
// 1. Test 1: Latency Spike
|
|
14
51
|
const r1 = await this.testLatencySpike(profile);
|
|
15
52
|
results.push(r1);
|
|
@@ -40,10 +77,40 @@ export class ResilienceScorer {
|
|
|
40
77
|
if (!r5.passed) {
|
|
41
78
|
recommendations.push('Handle 5xx server errors gracefully and display a friendly retry prompt to the user.');
|
|
42
79
|
}
|
|
80
|
+
// 6. Test 6: Packet Jitter & Latency Variance
|
|
81
|
+
const r6 = await this.testPacketJitter(profile);
|
|
82
|
+
results.push(r6);
|
|
83
|
+
if (!r6.passed) {
|
|
84
|
+
recommendations.push('Configure dynamic client timeouts with jitter buffers to absorb fluctuating network delay spikes.');
|
|
85
|
+
}
|
|
86
|
+
// 7. Test 7: Half-Open Circuit Breaker Recovery
|
|
87
|
+
const r7 = await this.testCircuitBreakerRecovery(profile);
|
|
88
|
+
results.push(r7);
|
|
89
|
+
if (!r7.passed) {
|
|
90
|
+
recommendations.push('Implement a half-open state transition in your circuit breaker to allow canary probes through after transient outages.');
|
|
91
|
+
}
|
|
92
|
+
// 8. Test 8: Downstream Socket Starvation & Slow Read
|
|
93
|
+
const r8 = await this.testSocketStarvation(profile);
|
|
94
|
+
results.push(r8);
|
|
95
|
+
if (!r8.passed) {
|
|
96
|
+
recommendations.push('Tune connection pool sizes and enforce request write timeouts to prevent slow clients from exhausting worker threads.');
|
|
97
|
+
}
|
|
98
|
+
// 9. Test 9: Zombie Connection Leak Probe
|
|
99
|
+
const r9 = await this.testZombieConnectionLeak(profile);
|
|
100
|
+
results.push(r9);
|
|
101
|
+
if (!r9.passed) {
|
|
102
|
+
recommendations.push('Listen for request "close" events server-side and abort downstream database queries when clients disconnect prematurely.');
|
|
103
|
+
}
|
|
104
|
+
// 10. Test 10: Payload Truncation & Partial Transfer
|
|
105
|
+
const r10 = await this.testPartialTransferTruncation(profile);
|
|
106
|
+
results.push(r10);
|
|
107
|
+
if (!r10.passed) {
|
|
108
|
+
recommendations.push('Verify Content-Length or chunked stream terminator before parsing payloads to prevent partial data corruption.');
|
|
109
|
+
}
|
|
43
110
|
// Cleanup pipeline after tests
|
|
44
111
|
this.pipeline.clearRules();
|
|
45
|
-
// Calculate weighted score
|
|
46
|
-
const weights = [
|
|
112
|
+
// Calculate weighted score (10 checks, 10 points each = 100 points)
|
|
113
|
+
const weights = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10];
|
|
47
114
|
let score = 0;
|
|
48
115
|
let passedCount = 0;
|
|
49
116
|
results.forEach((r, idx) => {
|
|
@@ -62,7 +129,7 @@ export class ResilienceScorer {
|
|
|
62
129
|
else if (score >= 45)
|
|
63
130
|
grade = 'D';
|
|
64
131
|
if (recommendations.length === 0) {
|
|
65
|
-
recommendations.push('Your application handled all
|
|
132
|
+
recommendations.push('Your application handled all 10 simulated network failure and resilience scenarios smoothly.');
|
|
66
133
|
}
|
|
67
134
|
return {
|
|
68
135
|
score,
|
|
@@ -101,7 +168,7 @@ export class ResilienceScorer {
|
|
|
101
168
|
const res = await fetch(`${this.proxyUrl}/api/health`, { signal: controller.signal });
|
|
102
169
|
clearTimeout(timeout);
|
|
103
170
|
const duration = Date.now() - start;
|
|
104
|
-
const passed = res.status === 200 && duration >= 180 && duration <=
|
|
171
|
+
const passed = res.status === 200 && duration >= 180 && duration <= 650;
|
|
105
172
|
return {
|
|
106
173
|
name: 'Response Delay Handling (300ms)',
|
|
107
174
|
description: 'Checks whether the client handles delayed network responses without freezing or hanging.',
|
|
@@ -282,7 +349,7 @@ export class ResilienceScorer {
|
|
|
282
349
|
async testServiceOutage(profile) {
|
|
283
350
|
if (profile === 'fragile') {
|
|
284
351
|
return {
|
|
285
|
-
name: 'HTTP 503 Service
|
|
352
|
+
name: 'HTTP 503 Service Outage Handling',
|
|
286
353
|
description: 'Tests if the application properly receives and reacts to temporary server downtime.',
|
|
287
354
|
toxicUsed: 'status',
|
|
288
355
|
passed: false,
|
|
@@ -304,7 +371,7 @@ export class ResilienceScorer {
|
|
|
304
371
|
const res = await fetch(`${this.proxyUrl}/api/data`);
|
|
305
372
|
const passed = res.status === 503;
|
|
306
373
|
return {
|
|
307
|
-
name: 'HTTP 503 Service
|
|
374
|
+
name: 'HTTP 503 Service Outage Handling',
|
|
308
375
|
description: 'Tests if the application properly receives and reacts to temporary server downtime.',
|
|
309
376
|
toxicUsed: 'status',
|
|
310
377
|
passed,
|
|
@@ -314,7 +381,7 @@ export class ResilienceScorer {
|
|
|
314
381
|
}
|
|
315
382
|
catch (err) {
|
|
316
383
|
return {
|
|
317
|
-
name: 'HTTP 503 Service
|
|
384
|
+
name: 'HTTP 503 Service Outage Handling',
|
|
318
385
|
description: 'Tests if the application properly receives and reacts to temporary server downtime.',
|
|
319
386
|
toxicUsed: 'status',
|
|
320
387
|
passed: false,
|
|
@@ -327,4 +394,253 @@ export class ResilienceScorer {
|
|
|
327
394
|
this.pipeline.removeRule('test_rule_503');
|
|
328
395
|
}
|
|
329
396
|
}
|
|
397
|
+
async testPacketJitter(profile) {
|
|
398
|
+
if (profile === 'fragile') {
|
|
399
|
+
return {
|
|
400
|
+
name: 'Packet Jitter & Latency Variance',
|
|
401
|
+
description: 'Injects high latency variance (100ms-350ms) to test jitter buffering.',
|
|
402
|
+
toxicUsed: 'jitter',
|
|
403
|
+
passed: false,
|
|
404
|
+
latencyMs: 380,
|
|
405
|
+
errorCaught: 'PacketOrderingException: stream arrival out of sequence',
|
|
406
|
+
details: 'Client experienced connection timeout or buffer stall under high network jitter',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
this.pipeline.addRule({
|
|
410
|
+
id: 'test_rule_jitter',
|
|
411
|
+
name: 'Simulated High Jitter',
|
|
412
|
+
type: 'latency',
|
|
413
|
+
direction: 'downstream',
|
|
414
|
+
enabled: true,
|
|
415
|
+
config: { latencyMs: 100, jitterMs: 70 },
|
|
416
|
+
});
|
|
417
|
+
const start = Date.now();
|
|
418
|
+
try {
|
|
419
|
+
const res = await fetch(`${this.proxyUrl}/api/health`, { signal: AbortSignal.timeout(1200) });
|
|
420
|
+
const duration = Date.now() - start;
|
|
421
|
+
const passed = res.status === 200 && duration >= 30;
|
|
422
|
+
return {
|
|
423
|
+
name: 'Packet Jitter & Latency Variance',
|
|
424
|
+
description: 'Injects high latency variance (100ms-350ms) to test jitter buffering.',
|
|
425
|
+
toxicUsed: 'jitter',
|
|
426
|
+
passed,
|
|
427
|
+
latencyMs: duration,
|
|
428
|
+
details: passed ? `Handled packet jitter smoothly in ${duration}ms` : `Unstable response latency (${duration}ms)`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
return {
|
|
433
|
+
name: 'Packet Jitter & Latency Variance',
|
|
434
|
+
description: 'Injects high latency variance (100ms-350ms) to test jitter buffering.',
|
|
435
|
+
toxicUsed: 'jitter',
|
|
436
|
+
passed: false,
|
|
437
|
+
latencyMs: Date.now() - start,
|
|
438
|
+
errorCaught: err.message,
|
|
439
|
+
details: `Jitter probe failed: ${err.message}`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
this.pipeline.removeRule('test_rule_jitter');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async testCircuitBreakerRecovery(profile) {
|
|
447
|
+
if (profile === 'fragile') {
|
|
448
|
+
return {
|
|
449
|
+
name: 'Half-Open Circuit Breaker Recovery',
|
|
450
|
+
description: 'Tests if service recovers immediately after a transient 503 outage.',
|
|
451
|
+
toxicUsed: 'circuit-breaker',
|
|
452
|
+
passed: false,
|
|
453
|
+
latencyMs: 12,
|
|
454
|
+
errorCaught: 'CircuitBreakerOpenError: service remained latched open after outage cleared',
|
|
455
|
+
details: 'Subsequent requests failed because circuit breaker failed to transition to half-open state',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// Step 1: inject transient 503 outage
|
|
459
|
+
this.pipeline.addRule({
|
|
460
|
+
id: 'test_rule_transient_503',
|
|
461
|
+
name: 'Transient Outage',
|
|
462
|
+
type: 'status',
|
|
463
|
+
direction: 'downstream',
|
|
464
|
+
enabled: true,
|
|
465
|
+
config: { statusCode: 503, statusMessage: 'Transient Down' },
|
|
466
|
+
});
|
|
467
|
+
const start = Date.now();
|
|
468
|
+
try {
|
|
469
|
+
await fetch(`${this.proxyUrl}/api/health`);
|
|
470
|
+
// Step 2: clear outage
|
|
471
|
+
this.pipeline.removeRule('test_rule_transient_503');
|
|
472
|
+
// Step 3: verify recovery request succeeds
|
|
473
|
+
const recoveryRes = await fetch(`${this.proxyUrl}/api/health`, { signal: AbortSignal.timeout(1500) });
|
|
474
|
+
const duration = Date.now() - start;
|
|
475
|
+
const passed = recoveryRes.status === 200;
|
|
476
|
+
return {
|
|
477
|
+
name: 'Half-Open Circuit Breaker Recovery',
|
|
478
|
+
description: 'Tests if service recovers immediately after a transient 503 outage.',
|
|
479
|
+
toxicUsed: 'circuit-breaker',
|
|
480
|
+
passed,
|
|
481
|
+
latencyMs: duration,
|
|
482
|
+
details: passed ? 'Service cleanly resumed serving traffic immediately after outage lifted' : 'Service failed to recover after transient 503',
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
return {
|
|
487
|
+
name: 'Half-Open Circuit Breaker Recovery',
|
|
488
|
+
description: 'Tests if service recovers immediately after a transient 503 outage.',
|
|
489
|
+
toxicUsed: 'circuit-breaker',
|
|
490
|
+
passed: false,
|
|
491
|
+
latencyMs: Date.now() - start,
|
|
492
|
+
errorCaught: err.message,
|
|
493
|
+
details: `Recovery probe failed: ${err.message}`,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
this.pipeline.removeRule('test_rule_transient_503');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async testSocketStarvation(profile) {
|
|
501
|
+
if (profile === 'fragile') {
|
|
502
|
+
return {
|
|
503
|
+
name: 'Downstream Socket Starvation & Slow Read',
|
|
504
|
+
description: 'Tests if connection pool continues serving healthy clients when one connection is throttled.',
|
|
505
|
+
toxicUsed: 'starvation',
|
|
506
|
+
passed: false,
|
|
507
|
+
latencyMs: 950,
|
|
508
|
+
errorCaught: 'ConnectionPoolExhausted: worker pool starved by slow connection',
|
|
509
|
+
details: 'Server blocked all incoming requests while servicing a single throttled downstream client',
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
this.pipeline.addRule({
|
|
513
|
+
id: 'test_rule_slow_client',
|
|
514
|
+
name: 'Throttled Downstream Client',
|
|
515
|
+
type: 'bandwidth',
|
|
516
|
+
direction: 'downstream',
|
|
517
|
+
enabled: true,
|
|
518
|
+
config: { rateKbps: 8 },
|
|
519
|
+
pathPattern: '/api/data',
|
|
520
|
+
});
|
|
521
|
+
const start = Date.now();
|
|
522
|
+
try {
|
|
523
|
+
// Start slow download in background
|
|
524
|
+
const slowPromise = fetch(`${this.proxyUrl}/api/data`).catch(() => { });
|
|
525
|
+
// Concurrent probe to /api/health should not be blocked or starved
|
|
526
|
+
const healthRes = await fetch(`${this.proxyUrl}/api/health`, { signal: AbortSignal.timeout(1200) });
|
|
527
|
+
const duration = Date.now() - start;
|
|
528
|
+
const passed = healthRes.status === 200 && duration < 800;
|
|
529
|
+
await slowPromise;
|
|
530
|
+
return {
|
|
531
|
+
name: 'Downstream Socket Starvation & Slow Read',
|
|
532
|
+
description: 'Tests if connection pool continues serving healthy clients when one connection is throttled.',
|
|
533
|
+
toxicUsed: 'starvation',
|
|
534
|
+
passed,
|
|
535
|
+
latencyMs: duration,
|
|
536
|
+
details: passed ? `Concurrent probe completed in ${duration}ms without pool starvation` : `Health probe stalled (${duration}ms)`,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
return {
|
|
541
|
+
name: 'Downstream Socket Starvation & Slow Read',
|
|
542
|
+
description: 'Tests if connection pool continues serving healthy clients when one connection is throttled.',
|
|
543
|
+
toxicUsed: 'starvation',
|
|
544
|
+
passed: false,
|
|
545
|
+
latencyMs: Date.now() - start,
|
|
546
|
+
errorCaught: err.message,
|
|
547
|
+
details: `Starvation test failed: ${err.message}`,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
this.pipeline.removeRule('test_rule_slow_client');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async testZombieConnectionLeak(profile) {
|
|
555
|
+
if (profile === 'fragile') {
|
|
556
|
+
return {
|
|
557
|
+
name: 'Zombie Connection Leak Probe',
|
|
558
|
+
description: 'Tests if backend frees sockets and server resources when client aborts prematurely.',
|
|
559
|
+
toxicUsed: 'zombie-leak',
|
|
560
|
+
passed: false,
|
|
561
|
+
latencyMs: 15,
|
|
562
|
+
errorCaught: 'SocketDescriptorLeak: unreleased TCP handle retained in LISTEN queue',
|
|
563
|
+
details: 'Aborted client connections remained open as zombie handles and leaked system file descriptors',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const start = Date.now();
|
|
567
|
+
try {
|
|
568
|
+
// Abort connection quickly
|
|
569
|
+
const controller = new AbortController();
|
|
570
|
+
const abortedFetch = fetch(`${this.proxyUrl}/api/data`, { signal: controller.signal }).catch(() => { });
|
|
571
|
+
setTimeout(() => controller.abort(), 10);
|
|
572
|
+
await abortedFetch;
|
|
573
|
+
// Ensure target is immediately responsive for new connection
|
|
574
|
+
const checkRes = await fetch(`${this.proxyUrl}/api/health`, { signal: AbortSignal.timeout(1000) });
|
|
575
|
+
const duration = Date.now() - start;
|
|
576
|
+
const passed = checkRes.status === 200;
|
|
577
|
+
return {
|
|
578
|
+
name: 'Zombie Connection Leak Probe',
|
|
579
|
+
description: 'Tests if backend frees sockets and server resources when client aborts prematurely.',
|
|
580
|
+
toxicUsed: 'zombie-leak',
|
|
581
|
+
passed,
|
|
582
|
+
latencyMs: duration,
|
|
583
|
+
details: passed ? 'Aborted request was cleanly recycled without leaking socket resources' : 'Target unresponsive after abort',
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
return {
|
|
588
|
+
name: 'Zombie Connection Leak Probe',
|
|
589
|
+
description: 'Tests if backend frees sockets and server resources when client aborts prematurely.',
|
|
590
|
+
toxicUsed: 'zombie-leak',
|
|
591
|
+
passed: false,
|
|
592
|
+
latencyMs: Date.now() - start,
|
|
593
|
+
errorCaught: err.message,
|
|
594
|
+
details: `Zombie leak probe failed: ${err.message}`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async testPartialTransferTruncation(profile) {
|
|
599
|
+
if (profile === 'fragile') {
|
|
600
|
+
return {
|
|
601
|
+
name: 'Payload Truncation & Partial Transfer',
|
|
602
|
+
description: 'Evaluates client detection when server terminates transmission mid-body.',
|
|
603
|
+
toxicUsed: 'truncation',
|
|
604
|
+
passed: false,
|
|
605
|
+
latencyMs: 22,
|
|
606
|
+
errorCaught: 'CorruptStateError: client processed incomplete byte stream without boundary check',
|
|
607
|
+
details: 'Client accepted truncated data stream as complete response without validating payload integrity',
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
this.pipeline.addRule({
|
|
611
|
+
id: 'test_rule_partial_cut',
|
|
612
|
+
name: 'Partial Body Cut',
|
|
613
|
+
type: 'cut',
|
|
614
|
+
direction: 'downstream',
|
|
615
|
+
enabled: true,
|
|
616
|
+
config: { cutAfterBytes: 30 },
|
|
617
|
+
});
|
|
618
|
+
const start = Date.now();
|
|
619
|
+
try {
|
|
620
|
+
const res = await fetch(`${this.proxyUrl}/api/data`);
|
|
621
|
+
await res.text();
|
|
622
|
+
return {
|
|
623
|
+
name: 'Payload Truncation & Partial Transfer',
|
|
624
|
+
description: 'Evaluates client detection when server terminates transmission mid-body.',
|
|
625
|
+
toxicUsed: 'truncation',
|
|
626
|
+
passed: false,
|
|
627
|
+
latencyMs: Date.now() - start,
|
|
628
|
+
details: 'Stream did not truncate as expected',
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
return {
|
|
633
|
+
name: 'Payload Truncation & Partial Transfer',
|
|
634
|
+
description: 'Evaluates client detection when server terminates transmission mid-body.',
|
|
635
|
+
toxicUsed: 'truncation',
|
|
636
|
+
passed: true,
|
|
637
|
+
latencyMs: Date.now() - start,
|
|
638
|
+
errorCaught: err.message,
|
|
639
|
+
details: `Partial stream termination detected safely: ${err.message}`,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
this.pipeline.removeRule('test_rule_partial_cut');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
330
646
|
}
|
|
@@ -2,6 +2,7 @@ import { SecurityScorecard } from '../types.js';
|
|
|
2
2
|
export declare class SecurityAuditor {
|
|
3
3
|
private targetUrl;
|
|
4
4
|
constructor(targetUrl: string);
|
|
5
|
+
setTargetUrl(url: string): void;
|
|
5
6
|
runAudit(profile?: 'secure' | 'vulnerable'): Promise<SecurityScorecard>;
|
|
6
7
|
private auditDefensiveHeaders;
|
|
7
8
|
private auditCorsConfiguration;
|
|
@@ -10,4 +11,11 @@ export declare class SecurityAuditor {
|
|
|
10
11
|
private auditErrorDisclosure;
|
|
11
12
|
private auditPathTraversal;
|
|
12
13
|
private auditInertCanaryInjection;
|
|
14
|
+
private auditHostHeaderPoisoning;
|
|
15
|
+
private auditClientIpSpoofing;
|
|
16
|
+
private auditParameterPollution;
|
|
17
|
+
private auditCacheControlHeaders;
|
|
18
|
+
private auditBrokenAuthHeaders;
|
|
19
|
+
private auditTimingAttacks;
|
|
20
|
+
private auditMetadataLeakage;
|
|
13
21
|
}
|