docs: enterprise refactor plan with ralph specs
This commit is contained in:
137
dist/core/AnomalyDetector.js
vendored
Normal file
137
dist/core/AnomalyDetector.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
"use strict";
|
||||
/**
|
||||
* AnomalyDetector — heuristic rules to detect anomalies from observations.
|
||||
* Each rule is independent and testable in isolation.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AnomalyDetector = void 0;
|
||||
let anomalyCounter = 0;
|
||||
function makeId() {
|
||||
anomalyCounter += 1;
|
||||
return `anom_${Date.now()}_${anomalyCounter.toString().padStart(4, '0')}`;
|
||||
}
|
||||
class AnomalyDetector {
|
||||
detect(observation, actionTrace) {
|
||||
const anomalies = [];
|
||||
const httpAnomaly = this.checkHttpErrors(observation, actionTrace);
|
||||
if (httpAnomaly)
|
||||
anomalies.push(httpAnomaly);
|
||||
const jsAnomaly = this.checkJsExceptions(observation, actionTrace);
|
||||
if (jsAnomaly)
|
||||
anomalies.push(jsAnomaly);
|
||||
const consoleAnomaly = this.checkConsoleErrors(observation, actionTrace);
|
||||
if (consoleAnomaly)
|
||||
anomalies.push(consoleAnomaly);
|
||||
return anomalies;
|
||||
}
|
||||
/** Rule: HTTP 4xx or 5xx responses */
|
||||
checkHttpErrors(observation, actionTrace) {
|
||||
const errorResponses = observation.httpResponses.filter((r) => r.status >= 400);
|
||||
if (errorResponses.length === 0)
|
||||
return null;
|
||||
const hasServerError = errorResponses.some((r) => r.status >= 500);
|
||||
const severity = hasServerError ? 'high' : 'medium';
|
||||
const statusCodes = errorResponses.map((r) => r.status).join(', ');
|
||||
return this.buildAnomaly({
|
||||
type: 'http_error',
|
||||
severity,
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `HTTP error responses detected: ${statusCodes}`,
|
||||
evidence: {
|
||||
httpLog: errorResponses,
|
||||
rawErrors: errorResponses.map((r) => `${r.method} ${r.url} → ${r.status} (${r.durationMs}ms)`),
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Rule: uncaught JS exceptions */
|
||||
checkJsExceptions(observation, actionTrace) {
|
||||
if (observation.jsExceptions.length === 0)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'js_exception',
|
||||
severity: 'high',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `Uncaught JS exception: ${observation.jsExceptions[0]}`,
|
||||
evidence: {
|
||||
rawErrors: observation.jsExceptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Rule: console.error messages */
|
||||
checkConsoleErrors(observation, actionTrace) {
|
||||
if (observation.consoleErrors.length === 0)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'console_error',
|
||||
severity: 'low',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `Console error detected: ${observation.consoleErrors[0]}`,
|
||||
evidence: {
|
||||
rawErrors: observation.consoleErrors,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Rule: server accepted clearly invalid/empty fuzz input (got 2xx).
|
||||
* fuzzedValue is the value that was submitted; responseStatus is the HTTP response.
|
||||
*/
|
||||
checkValidationBypass(observation, actionTrace, fuzzedValue) {
|
||||
const has2xx = observation.httpResponses.some((r) => r.status >= 200 && r.status < 300);
|
||||
if (!has2xx)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'validation_bypass',
|
||||
severity: 'high',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: `Server accepted invalid input without error (value: ${JSON.stringify(fuzzedValue).substring(0, 50)})`,
|
||||
evidence: { httpLog: observation.httpResponses, rawErrors: [`Fuzzed with: ${fuzzedValue}`] },
|
||||
});
|
||||
}
|
||||
/** Rule: server returned 500 on a fuzzed input */
|
||||
checkServerErrorOnFuzz(observation, actionTrace) {
|
||||
const has5xx = observation.httpResponses.some((r) => r.status >= 500);
|
||||
if (!has5xx)
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'server_error_on_fuzz',
|
||||
severity: 'high',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: 'Server returned 5xx on fuzzed input',
|
||||
evidence: {
|
||||
httpLog: observation.httpResponses.filter((r) => r.status >= 500),
|
||||
rawErrors: observation.jsExceptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
/** Rule: fuzzed script tag appears in response body (XSS reflection) */
|
||||
checkXssReflection(observation, actionTrace, domSnapshot) {
|
||||
if (!domSnapshot.includes('<script>alert(1)</script>'))
|
||||
return null;
|
||||
return this.buildAnomaly({
|
||||
type: 'xss_reflection',
|
||||
severity: 'critical',
|
||||
observationId: observation.id,
|
||||
actionTrace,
|
||||
description: 'XSS reflection detected: fuzzed script tag appeared in DOM',
|
||||
evidence: { rawErrors: ['XSS payload reflected in DOM'] },
|
||||
});
|
||||
}
|
||||
buildAnomaly(params) {
|
||||
return {
|
||||
id: makeId(),
|
||||
type: params.type,
|
||||
severity: params.severity,
|
||||
observationId: params.observationId,
|
||||
actionTrace: params.actionTrace,
|
||||
description: params.description,
|
||||
evidence: params.evidence,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.AnomalyDetector = AnomalyDetector;
|
||||
Reference in New Issue
Block a user