- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
228 lines
7.4 KiB
JavaScript
228 lines
7.4 KiB
JavaScript
// -----------------------------------------------------------------------------
|
|
// spike-test.js
|
|
// Sprint: SPRINT_5100_0005_0001_router_chaos_suite
|
|
// Task: T1 - Load Test Harness
|
|
// Description: k6 load test for router spike testing and backpressure validation.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
import { Rate, Trend, Counter } from 'k6/metrics';
|
|
|
|
// Custom metrics for throttle behavior
|
|
const throttledRate = new Rate('throttled_requests');
|
|
const retryAfterTrend = new Trend('retry_after_seconds');
|
|
const recoveryTime = new Trend('recovery_time_ms');
|
|
const throttle429Count = new Counter('throttle_429_count');
|
|
const throttle503Count = new Counter('throttle_503_count');
|
|
const successCount = new Counter('success_count');
|
|
|
|
export const options = {
|
|
scenarios: {
|
|
// Phase 1: Baseline load (normal operation)
|
|
baseline: {
|
|
executor: 'constant-arrival-rate',
|
|
rate: 100,
|
|
timeUnit: '1s',
|
|
duration: '1m',
|
|
preAllocatedVUs: 50,
|
|
maxVUs: 100,
|
|
},
|
|
// Phase 2: 10x spike
|
|
spike_10x: {
|
|
executor: 'constant-arrival-rate',
|
|
rate: 1000,
|
|
timeUnit: '1s',
|
|
duration: '30s',
|
|
startTime: '1m',
|
|
preAllocatedVUs: 500,
|
|
maxVUs: 1000,
|
|
},
|
|
// Phase 3: 50x spike
|
|
spike_50x: {
|
|
executor: 'constant-arrival-rate',
|
|
rate: 5000,
|
|
timeUnit: '1s',
|
|
duration: '30s',
|
|
startTime: '2m',
|
|
preAllocatedVUs: 2000,
|
|
maxVUs: 5000,
|
|
},
|
|
// Phase 4: Recovery observation
|
|
recovery: {
|
|
executor: 'constant-arrival-rate',
|
|
rate: 100,
|
|
timeUnit: '1s',
|
|
duration: '2m',
|
|
startTime: '3m',
|
|
preAllocatedVUs: 50,
|
|
maxVUs: 100,
|
|
},
|
|
},
|
|
thresholds: {
|
|
// At least 95% of requests should succeed OR return proper throttle response
|
|
'http_req_failed{expected_response:true}': ['rate<0.05'],
|
|
// Throttled requests should have Retry-After header
|
|
'throttled_requests': ['rate>0'], // We expect some throttling during spike
|
|
// Recovery should happen within reasonable time
|
|
'recovery_time_ms': ['p(95)<30000'], // 95% recover within 30s
|
|
// Response time should be bounded even under load
|
|
'http_req_duration{expected_response:true}': ['p(95)<5000'],
|
|
},
|
|
};
|
|
|
|
const ROUTER_URL = __ENV.ROUTER_URL || 'http://localhost:8080';
|
|
const API_ENDPOINT = __ENV.API_ENDPOINT || '/api/v1/scan';
|
|
|
|
export function setup() {
|
|
console.log(`Testing router at: ${ROUTER_URL}${API_ENDPOINT}`);
|
|
|
|
// Verify router is reachable
|
|
const healthCheck = http.get(`${ROUTER_URL}/health`);
|
|
if (healthCheck.status !== 200) {
|
|
console.warn(`Router health check returned ${healthCheck.status}`);
|
|
}
|
|
|
|
return {
|
|
startTime: new Date().toISOString(),
|
|
routerUrl: ROUTER_URL,
|
|
};
|
|
}
|
|
|
|
export default function () {
|
|
const payload = JSON.stringify({
|
|
image: 'alpine:latest',
|
|
requestId: `spike-test-${__VU}-${__ITER}`,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
const params = {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': `${__VU}-${__ITER}`,
|
|
},
|
|
tags: { expected_response: 'true' },
|
|
timeout: '10s',
|
|
};
|
|
|
|
const response = http.post(`${ROUTER_URL}${API_ENDPOINT}`, payload, params);
|
|
|
|
// Handle throttle responses (429 Too Many Requests)
|
|
if (response.status === 429) {
|
|
throttledRate.add(1);
|
|
throttle429Count.add(1);
|
|
|
|
// Verify Retry-After header
|
|
const retryAfter = response.headers['Retry-After'];
|
|
check(response, {
|
|
'429 has Retry-After header': (r) => r.headers['Retry-After'] !== undefined,
|
|
'Retry-After is valid number': (r) => {
|
|
const val = r.headers['Retry-After'];
|
|
return val && !isNaN(parseInt(val));
|
|
},
|
|
'Retry-After is reasonable (1-300s)': (r) => {
|
|
const val = parseInt(r.headers['Retry-After']);
|
|
return val >= 1 && val <= 300;
|
|
},
|
|
});
|
|
|
|
if (retryAfter) {
|
|
retryAfterTrend.add(parseInt(retryAfter));
|
|
}
|
|
}
|
|
// Handle overload responses (503 Service Unavailable)
|
|
else if (response.status === 503) {
|
|
throttledRate.add(1);
|
|
throttle503Count.add(1);
|
|
|
|
check(response, {
|
|
'503 has Retry-After header': (r) => r.headers['Retry-After'] !== undefined,
|
|
});
|
|
|
|
const retryAfter = response.headers['Retry-After'];
|
|
if (retryAfter) {
|
|
retryAfterTrend.add(parseInt(retryAfter));
|
|
}
|
|
}
|
|
// Handle success responses
|
|
else {
|
|
throttledRate.add(0);
|
|
successCount.add(1);
|
|
|
|
check(response, {
|
|
'status is 200 or 202': (r) => r.status === 200 || r.status === 202,
|
|
'response has body': (r) => r.body && r.body.length > 0,
|
|
'response time < 5s': (r) => r.timings.duration < 5000,
|
|
});
|
|
}
|
|
|
|
// Track any errors
|
|
if (response.status >= 500 && response.status !== 503) {
|
|
check(response, {
|
|
'no unexpected 5xx errors': () => false,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function teardown(data) {
|
|
console.log(`Test completed. Started at: ${data.startTime}`);
|
|
console.log(`Router URL: ${data.routerUrl}`);
|
|
}
|
|
|
|
export function handleSummary(data) {
|
|
const summary = {
|
|
testRun: {
|
|
startTime: new Date().toISOString(),
|
|
routerUrl: ROUTER_URL,
|
|
},
|
|
metrics: {
|
|
totalRequests: data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0,
|
|
throttled429: data.metrics.throttle_429_count ? data.metrics.throttle_429_count.values.count : 0,
|
|
throttled503: data.metrics.throttle_503_count ? data.metrics.throttle_503_count.values.count : 0,
|
|
successful: data.metrics.success_count ? data.metrics.success_count.values.count : 0,
|
|
throttleRate: data.metrics.throttled_requests ? data.metrics.throttled_requests.values.rate : 0,
|
|
retryAfterAvg: data.metrics.retry_after_seconds ? data.metrics.retry_after_seconds.values.avg : null,
|
|
retryAfterP95: data.metrics.retry_after_seconds ? data.metrics.retry_after_seconds.values['p(95)'] : null,
|
|
},
|
|
thresholds: data.thresholds,
|
|
checks: data.metrics.checks ? {
|
|
passes: data.metrics.checks.values.passes,
|
|
fails: data.metrics.checks.values.fails,
|
|
rate: data.metrics.checks.values.rate,
|
|
} : null,
|
|
};
|
|
|
|
return {
|
|
'results/spike-test-summary.json': JSON.stringify(summary, null, 2),
|
|
stdout: textSummary(data, { indent: ' ', enableColors: true }),
|
|
};
|
|
}
|
|
|
|
function textSummary(data, options) {
|
|
let output = '\n=== Router Spike Test Summary ===\n\n';
|
|
|
|
const totalReqs = data.metrics.http_reqs ? data.metrics.http_reqs.values.count : 0;
|
|
const throttled429 = data.metrics.throttle_429_count ? data.metrics.throttle_429_count.values.count : 0;
|
|
const throttled503 = data.metrics.throttle_503_count ? data.metrics.throttle_503_count.values.count : 0;
|
|
const successful = data.metrics.success_count ? data.metrics.success_count.values.count : 0;
|
|
|
|
output += `Total Requests: ${totalReqs}\n`;
|
|
output += `Successful (2xx): ${successful}\n`;
|
|
output += `Throttled (429): ${throttled429}\n`;
|
|
output += `Overloaded (503): ${throttled503}\n`;
|
|
output += `Throttle Rate: ${((throttled429 + throttled503) / totalReqs * 100).toFixed(2)}%\n`;
|
|
|
|
if (data.metrics.retry_after_seconds) {
|
|
output += `\nRetry-After Header:\n`;
|
|
output += ` Avg: ${data.metrics.retry_after_seconds.values.avg.toFixed(2)}s\n`;
|
|
output += ` P95: ${data.metrics.retry_after_seconds.values['p(95)'].toFixed(2)}s\n`;
|
|
}
|
|
|
|
output += '\nThreshold Results:\n';
|
|
for (const [name, result] of Object.entries(data.thresholds || {})) {
|
|
output += ` ${result.ok ? 'PASS' : 'FAIL'}: ${name}\n`;
|
|
}
|
|
|
|
return output;
|
|
}
|