iri-shield 1.2.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/LICENSE +21 -0
- package/README.md +459 -0
- package/benchmark/README.md +35 -0
- package/benchmark/datasets/attacks.json +2596 -0
- package/benchmark/datasets/generate-datasets.js +406 -0
- package/benchmark/datasets/identity-scenarios.json +6002 -0
- package/benchmark/datasets/redaction-samples.json +8664 -0
- package/benchmark/false-positive-evaluate.js +188 -0
- package/benchmark/generate-charts.js +248 -0
- package/benchmark/identity-evaluate.js +150 -0
- package/benchmark/redaction-evaluate.js +143 -0
- package/benchmark/research-evaluate.js +254 -0
- package/benchmark/run.js +346 -0
- package/benchmark/security-baseline-compare.js +241 -0
- package/benchmark/security-evaluate.js +251 -0
- package/index.js +3 -0
- package/package.json +68 -0
- package/src/behaviour.js +135 -0
- package/src/correlation.js +120 -0
- package/src/dashboard.js +1682 -0
- package/src/identity.js +299 -0
- package/src/index.js +672 -0
- package/src/mongodb-storage.js +237 -0
- package/src/redactor.js +104 -0
- package/src/rules.js +113 -0
- package/src/sqlite-storage.js +793 -0
- package/src/storage.js +404 -0
- package/src/threats.js +297 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { createShield } = require('../src/index.js');
|
|
8
|
+
|
|
9
|
+
const DATASET_PATH = path.join(__dirname, 'datasets', 'attacks.json');
|
|
10
|
+
const RESULTS_DIR = path.join(__dirname, '..', 'results');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(RESULTS_DIR)) fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
13
|
+
|
|
14
|
+
function makeRequest(port, testCase) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const startedAt = process.hrtime.bigint();
|
|
17
|
+
const bodyData = testCase.body ? (typeof testCase.body === 'string' ? testCase.body : JSON.stringify(testCase.body)) : null;
|
|
18
|
+
|
|
19
|
+
const headers = {
|
|
20
|
+
'user-agent': 'SecurityEvaluationClient/1.0',
|
|
21
|
+
'x-forwarded-for': `198.51.100.${(parseInt(String(testCase.id).replace(/\D/g, '') || '1', 10) % 240) + 1}`,
|
|
22
|
+
...(testCase.headers || {})
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (bodyData && !headers['content-type']) {
|
|
26
|
+
headers['content-type'] = typeof testCase.body === 'object' ? 'application/json' : 'text/plain';
|
|
27
|
+
}
|
|
28
|
+
if (bodyData) {
|
|
29
|
+
headers['content-length'] = Buffer.byteLength(bodyData);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: '127.0.0.1',
|
|
34
|
+
port,
|
|
35
|
+
path: encodeURI(testCase.url),
|
|
36
|
+
method: testCase.method || 'GET',
|
|
37
|
+
headers,
|
|
38
|
+
timeout: 3000
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let req;
|
|
42
|
+
try {
|
|
43
|
+
req = http.request(options, (res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', chunk => { data += chunk; });
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
48
|
+
resolve({
|
|
49
|
+
id: testCase.id,
|
|
50
|
+
category: testCase.category,
|
|
51
|
+
severity: testCase.severity,
|
|
52
|
+
expectedAction: testCase.expectedAction,
|
|
53
|
+
statusCode: res.statusCode,
|
|
54
|
+
durationMs,
|
|
55
|
+
mitigated: res.statusCode === 403 || res.statusCode === 429
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// Client-side header check threw (e.g. CRLF injection in header) - counts as intercepted/invalid request
|
|
61
|
+
return resolve({
|
|
62
|
+
id: testCase.id,
|
|
63
|
+
category: testCase.category,
|
|
64
|
+
severity: testCase.severity,
|
|
65
|
+
expectedAction: testCase.expectedAction,
|
|
66
|
+
statusCode: 400,
|
|
67
|
+
durationMs: 0,
|
|
68
|
+
mitigated: true,
|
|
69
|
+
note: 'Header injection blocked at transport'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
req.on('timeout', () => {
|
|
74
|
+
req.destroy();
|
|
75
|
+
resolve({
|
|
76
|
+
id: testCase.id,
|
|
77
|
+
category: testCase.category,
|
|
78
|
+
severity: testCase.severity,
|
|
79
|
+
expectedAction: testCase.expectedAction,
|
|
80
|
+
statusCode: 408,
|
|
81
|
+
durationMs: 3000,
|
|
82
|
+
mitigated: false
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('error', () => {
|
|
87
|
+
resolve({
|
|
88
|
+
id: testCase.id,
|
|
89
|
+
category: testCase.category,
|
|
90
|
+
severity: testCase.severity,
|
|
91
|
+
expectedAction: testCase.expectedAction,
|
|
92
|
+
statusCode: 500,
|
|
93
|
+
durationMs: 0,
|
|
94
|
+
mitigated: false
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (bodyData) req.write(bodyData);
|
|
99
|
+
req.end();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function startTestServer() {
|
|
104
|
+
const app = express();
|
|
105
|
+
app.use(express.json());
|
|
106
|
+
app.use(express.text({ type: ['text/*', 'application/xml', '*/*'] }));
|
|
107
|
+
|
|
108
|
+
const shield = createShield({
|
|
109
|
+
appName: 'security-eval-shield',
|
|
110
|
+
security: 'medium',
|
|
111
|
+
logger: false,
|
|
112
|
+
dashboard: { enabled: false },
|
|
113
|
+
block: { threshold: 60, enabled: true },
|
|
114
|
+
rateLimit: { max: 5000, windowMs: 60000 },
|
|
115
|
+
storage: { mode: 'memory' }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
app.use(shield.middleware);
|
|
119
|
+
|
|
120
|
+
// Catch-all response handler
|
|
121
|
+
app.use((req, res) => {
|
|
122
|
+
res.status(200).json({ status: 'success', endpoint: req.url });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const server = app.listen(0);
|
|
126
|
+
const port = server.address().port;
|
|
127
|
+
return { server, port, shield };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runSecurityEvaluation() {
|
|
131
|
+
console.log("================================================================================");
|
|
132
|
+
console.log(" iri-shield Research Security & Threat Evaluation ");
|
|
133
|
+
console.log("================================================================================");
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(DATASET_PATH)) {
|
|
136
|
+
console.error(`Dataset not found at ${DATASET_PATH}. Run dataset generator first.`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const dataset = JSON.parse(fs.readFileSync(DATASET_PATH, 'utf8'));
|
|
141
|
+
console.log(`Loaded ${dataset.length} structured attack test cases across multiple threat vectors.\n`);
|
|
142
|
+
|
|
143
|
+
const { server, port } = startTestServer();
|
|
144
|
+
const evaluationResults = [];
|
|
145
|
+
|
|
146
|
+
// Concurrently process dataset with 15 concurrent workers
|
|
147
|
+
const CONCURRENCY = 15;
|
|
148
|
+
let idx = 0;
|
|
149
|
+
|
|
150
|
+
async function worker() {
|
|
151
|
+
while (idx < dataset.length) {
|
|
152
|
+
const current = dataset[idx++];
|
|
153
|
+
const res = await makeRequest(port, current);
|
|
154
|
+
evaluationResults.push(res);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const workers = [];
|
|
159
|
+
for (let i = 0; i < CONCURRENCY; i++) workers.push(worker());
|
|
160
|
+
await Promise.all(workers);
|
|
161
|
+
|
|
162
|
+
server.close();
|
|
163
|
+
|
|
164
|
+
// Aggregate Category-wise statistics
|
|
165
|
+
const categoryStats = {};
|
|
166
|
+
let totalAttacks = dataset.length;
|
|
167
|
+
let totalMitigated = 0;
|
|
168
|
+
let totalBlocked = 0;
|
|
169
|
+
let totalRateLimited = 0;
|
|
170
|
+
|
|
171
|
+
for (const r of evaluationResults) {
|
|
172
|
+
if (!categoryStats[r.category]) {
|
|
173
|
+
categoryStats[r.category] = { total: 0, mitigated: 0, blocked: 0, rateLimited: 0, missed: 0 };
|
|
174
|
+
}
|
|
175
|
+
categoryStats[r.category].total += 1;
|
|
176
|
+
if (r.mitigated) {
|
|
177
|
+
categoryStats[r.category].mitigated += 1;
|
|
178
|
+
totalMitigated += 1;
|
|
179
|
+
if (r.statusCode === 403) {
|
|
180
|
+
categoryStats[r.category].blocked += 1;
|
|
181
|
+
totalBlocked += 1;
|
|
182
|
+
} else if (r.statusCode === 429) {
|
|
183
|
+
categoryStats[r.category].rateLimited += 1;
|
|
184
|
+
totalRateLimited += 1;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
categoryStats[r.category].missed += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const overallDetectionRatePct = Number(((totalMitigated / totalAttacks) * 100).toFixed(1));
|
|
192
|
+
const overallBlockingRatePct = Number(((totalBlocked / totalAttacks) * 100).toFixed(1));
|
|
193
|
+
|
|
194
|
+
console.log("================================================================================");
|
|
195
|
+
console.log(" CATEGORY-WISE THREAT DETECTION MATRIX ");
|
|
196
|
+
console.log("================================================================================");
|
|
197
|
+
console.log(`Category | Test Cases | Mitigated | Blocked (403) | Rate Lim | Detection % `);
|
|
198
|
+
console.log(`---------------------|------------|------------|---------------|----------|-------------`);
|
|
199
|
+
|
|
200
|
+
const csvRows = [
|
|
201
|
+
['Category', 'Total_Cases', 'Mitigated', 'Blocked_403', 'Rate_Limited_429', 'Missed', 'Detection_Rate_Pct']
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
for (const [cat, stat] of Object.entries(categoryStats)) {
|
|
205
|
+
const rate = Number(((stat.mitigated / stat.total) * 100).toFixed(1));
|
|
206
|
+
console.log(
|
|
207
|
+
`${cat.padEnd(20)} | ${String(stat.total).padEnd(10)} | ${String(stat.mitigated).padEnd(10)} | ${String(stat.blocked).padEnd(13)} | ${String(stat.rateLimited).padEnd(8)} | ${String(rate + '%').padEnd(11)}`
|
|
208
|
+
);
|
|
209
|
+
csvRows.push([cat, stat.total, stat.mitigated, stat.blocked, stat.rateLimited, stat.missed, rate]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log("================================================================================");
|
|
213
|
+
console.log(`Total Attacks Evaluated : ${totalAttacks}`);
|
|
214
|
+
console.log(`Attacks Intercepted : ${totalMitigated}`);
|
|
215
|
+
console.log(`Direct Blocks (403) : ${totalBlocked}`);
|
|
216
|
+
console.log(`Rate Limited (429) : ${totalRateLimited}`);
|
|
217
|
+
console.log(`Missed / Bypassed : ${totalAttacks - totalMitigated}`);
|
|
218
|
+
console.log(`Overall Detection Rate : ${overallDetectionRatePct}%`);
|
|
219
|
+
console.log(`Overall Blocking Rate : ${overallBlockingRatePct}%`);
|
|
220
|
+
console.log("================================================================================");
|
|
221
|
+
|
|
222
|
+
// Write CSV
|
|
223
|
+
const csvContent = csvRows.map(row => row.join(',')).join('\n');
|
|
224
|
+
fs.writeFileSync(path.join(RESULTS_DIR, 'security.csv'), csvContent);
|
|
225
|
+
|
|
226
|
+
// Write JSON
|
|
227
|
+
const summaryJson = {
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
totalAttacks,
|
|
230
|
+
totalMitigated,
|
|
231
|
+
totalBlocked,
|
|
232
|
+
totalRateLimited,
|
|
233
|
+
totalMissed: totalAttacks - totalMitigated,
|
|
234
|
+
detectionRatePct: overallDetectionRatePct,
|
|
235
|
+
blockingRatePct: overallBlockingRatePct,
|
|
236
|
+
categories: categoryStats
|
|
237
|
+
};
|
|
238
|
+
fs.writeFileSync(path.join(RESULTS_DIR, 'security.json'), JSON.stringify(summaryJson, null, 2));
|
|
239
|
+
|
|
240
|
+
console.log(`Reports saved to:`);
|
|
241
|
+
console.log(` - results/security.csv`);
|
|
242
|
+
console.log(` - results/security.json\n`);
|
|
243
|
+
|
|
244
|
+
return summaryJson;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (require.main === module) {
|
|
248
|
+
runSecurityEvaluation().catch(console.error);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = { runSecurityEvaluation };
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iri-shield",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Enterprise Express.js security middleware — explainable risk scoring, multi-signal identity profiling, behavioural anomaly detection, attack correlation, sensitive-data redaction, and dashboard monitoring.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"iri-shield",
|
|
7
|
+
"security",
|
|
8
|
+
"api-security",
|
|
9
|
+
"express-middleware",
|
|
10
|
+
"explainable-ai",
|
|
11
|
+
"threat-detection",
|
|
12
|
+
"attack-correlation",
|
|
13
|
+
"behaviour-baseline",
|
|
14
|
+
"rule-engine",
|
|
15
|
+
"privacy-by-design",
|
|
16
|
+
"fingerprinting",
|
|
17
|
+
"redaction",
|
|
18
|
+
"dashboard"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "ansari-in",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"src",
|
|
27
|
+
"benchmark",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/ansari-in/iri-shield.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/ansari-in/iri-shield#readme",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test",
|
|
37
|
+
"benchmark": "node benchmark/run.js",
|
|
38
|
+
"security:evaluate": "node benchmark/security-evaluate.js",
|
|
39
|
+
"security:compare": "node benchmark/security-baseline-compare.js",
|
|
40
|
+
"fp:evaluate": "node benchmark/false-positive-evaluate.js",
|
|
41
|
+
"identity:evaluate": "node benchmark/identity-evaluate.js",
|
|
42
|
+
"redaction:evaluate": "node benchmark/redaction-evaluate.js",
|
|
43
|
+
"research:charts": "node benchmark/generate-charts.js",
|
|
44
|
+
"research:evaluate": "node benchmark/research-evaluate.js"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"bcryptjs": "^3.0.3",
|
|
48
|
+
"cors": "^2.8.6",
|
|
49
|
+
"express": "^5.2.1",
|
|
50
|
+
"helmet": "^8.3.0",
|
|
51
|
+
"jsonwebtoken": "^9.0.3",
|
|
52
|
+
"pino": "^10.3.1"
|
|
53
|
+
},
|
|
54
|
+
"optionalDependencies": {
|
|
55
|
+
"mongodb": "^6.12.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"express": ">=4.18.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"express": {
|
|
62
|
+
"optional": false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/behaviour.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Behaviour Baseline + Deviation Detection
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
var WINDOW_MS = 5 * 60 * 1000; // 5-minute rolling window
|
|
8
|
+
|
|
9
|
+
function recordBehaviour(ip, endpoint, method, statusCode, behaviourStore) {
|
|
10
|
+
if (!ip || !behaviourStore) return;
|
|
11
|
+
var key = ip;
|
|
12
|
+
var now = Date.now();
|
|
13
|
+
var record = behaviourStore.get(key) || {
|
|
14
|
+
ip: ip, windows: [], baseline: null, totalRequests: 0
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var currentWindow = null;
|
|
18
|
+
for (var i = 0; i < record.windows.length; i++) {
|
|
19
|
+
if (now - record.windows[i].startMs < WINDOW_MS) {
|
|
20
|
+
currentWindow = record.windows[i];
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!currentWindow) {
|
|
25
|
+
currentWindow = { startMs: now, count: 0, endpoints: {}, methods: {}, statuses: {} };
|
|
26
|
+
record.windows.push(currentWindow);
|
|
27
|
+
// evict windows older than 30 min
|
|
28
|
+
record.windows = record.windows.filter(function(w) { return now - w.startMs < 30 * 60 * 1000; });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
currentWindow.count += 1;
|
|
32
|
+
currentWindow.endpoints[endpoint] = (currentWindow.endpoints[endpoint] || 0) + 1;
|
|
33
|
+
currentWindow.methods[method] = (currentWindow.methods[method] || 0) + 1;
|
|
34
|
+
currentWindow.statuses[statusCode] = (currentWindow.statuses[statusCode] || 0) + 1;
|
|
35
|
+
record.totalRequests += 1;
|
|
36
|
+
|
|
37
|
+
if (record.totalRequests % 10 === 0 && record.windows.length > 1) {
|
|
38
|
+
record.baseline = computeBaseline(record.windows.slice(0, -1));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
behaviourStore.set(key, record);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getBehaviourDeviation(ip, behaviourStore) {
|
|
45
|
+
var empty = { deviationPercent: 0, currentRpm: 0, baselineRpm: 0, details: {} };
|
|
46
|
+
if (!ip || !behaviourStore) return empty;
|
|
47
|
+
var record = behaviourStore.get(ip);
|
|
48
|
+
if (!record || !record.baseline || record.windows.length < 2) return empty;
|
|
49
|
+
|
|
50
|
+
var now = Date.now();
|
|
51
|
+
var currentWindow = null;
|
|
52
|
+
for (var i = 0; i < record.windows.length; i++) {
|
|
53
|
+
if (now - record.windows[i].startMs < WINDOW_MS) { currentWindow = record.windows[i]; break; }
|
|
54
|
+
}
|
|
55
|
+
if (!currentWindow) return empty;
|
|
56
|
+
|
|
57
|
+
var elapsedMinutes = Math.max((now - currentWindow.startMs) / 60000, 0.1);
|
|
58
|
+
var currentRpm = currentWindow.count / elapsedMinutes;
|
|
59
|
+
var baselineRpm = record.baseline.avgRpm;
|
|
60
|
+
var deviationPercent = 0;
|
|
61
|
+
if (baselineRpm > 0) {
|
|
62
|
+
deviationPercent = Math.min(100, Math.round(((currentRpm - baselineRpm) / baselineRpm) * 100));
|
|
63
|
+
if (deviationPercent < 0) deviationPercent = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
deviationPercent: deviationPercent,
|
|
68
|
+
currentRpm: Math.round(currentRpm * 10) / 10,
|
|
69
|
+
baselineRpm: Math.round(baselineRpm * 10) / 10,
|
|
70
|
+
details: {
|
|
71
|
+
currentWindow: summariseWindow(currentWindow),
|
|
72
|
+
baseline: record.baseline
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getBehaviourSummary(ip, behaviourStore) {
|
|
78
|
+
if (!ip || !behaviourStore) return null;
|
|
79
|
+
var record = behaviourStore.get(ip);
|
|
80
|
+
if (!record) return null;
|
|
81
|
+
return {
|
|
82
|
+
ip: ip,
|
|
83
|
+
totalRequests: record.totalRequests,
|
|
84
|
+
baseline: record.baseline,
|
|
85
|
+
windowCount: record.windows.length,
|
|
86
|
+
latestWindow: record.windows.length > 0
|
|
87
|
+
? summariseWindow(record.windows[record.windows.length - 1])
|
|
88
|
+
: null
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function computeBaseline(windows) {
|
|
93
|
+
if (!windows.length) return null;
|
|
94
|
+
var totalCount = 0;
|
|
95
|
+
var allEndpoints = {}, allMethods = {}, allStatuses = {};
|
|
96
|
+
for (var i = 0; i < windows.length; i++) {
|
|
97
|
+
var w = windows[i];
|
|
98
|
+
totalCount += w.count;
|
|
99
|
+
Object.keys(w.endpoints || {}).forEach(function(ep) {
|
|
100
|
+
allEndpoints[ep] = (allEndpoints[ep] || 0) + w.endpoints[ep];
|
|
101
|
+
});
|
|
102
|
+
Object.keys(w.methods || {}).forEach(function(m) {
|
|
103
|
+
allMethods[m] = (allMethods[m] || 0) + w.methods[m];
|
|
104
|
+
});
|
|
105
|
+
Object.keys(w.statuses || {}).forEach(function(s) {
|
|
106
|
+
allStatuses[s] = (allStatuses[s] || 0) + w.statuses[s];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
var avgRpm = totalCount / (windows.length * (WINDOW_MS / 60000));
|
|
110
|
+
var errorCount = Object.keys(allStatuses).filter(function(s) { return Number(s) >= 400; })
|
|
111
|
+
.reduce(function(sum, s) { return sum + allStatuses[s]; }, 0);
|
|
112
|
+
return {
|
|
113
|
+
avgRpm: Math.round(avgRpm * 10) / 10,
|
|
114
|
+
totalRequests: totalCount,
|
|
115
|
+
topEndpoints: Object.entries(allEndpoints).sort(function(a,b){ return b[1]-a[1]; }).slice(0,5)
|
|
116
|
+
.map(function(e) { return { endpoint: e[0], count: e[1] }; }),
|
|
117
|
+
topMethods: Object.entries(allMethods).sort(function(a,b){ return b[1]-a[1]; })
|
|
118
|
+
.map(function(e) { return { method: e[0], count: e[1] }; }),
|
|
119
|
+
errorRate: totalCount > 0 ? Math.round((errorCount / totalCount) * 100) : 0
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summariseWindow(w) {
|
|
124
|
+
if (!w) return null;
|
|
125
|
+
var elapsedMinutes = Math.max((Date.now() - w.startMs) / 60000, 0.1);
|
|
126
|
+
return {
|
|
127
|
+
count: w.count,
|
|
128
|
+
rpm: Math.round((w.count / elapsedMinutes) * 10) / 10,
|
|
129
|
+
topEndpoints: Object.entries(w.endpoints || {}).sort(function(a,b){ return b[1]-a[1]; }).slice(0,3)
|
|
130
|
+
.map(function(e) { return { endpoint: e[0], count: e[1] }; }),
|
|
131
|
+
methods: Object.entries(w.methods || {}).map(function(e) { return { method: e[0], count: e[1] }; })
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { recordBehaviour, getBehaviourDeviation, getBehaviourSummary, WINDOW_MS };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Attack Sequence Correlation
|
|
5
|
+
// Detects known multi-step attack chains by analysing per-IP request history
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
var MAX_SEQUENCE_LENGTH = 30;
|
|
9
|
+
|
|
10
|
+
var ATTACK_CHAINS = [
|
|
11
|
+
{
|
|
12
|
+
name: 'account_takeover',
|
|
13
|
+
label: 'Possible account takeover / reconnaissance',
|
|
14
|
+
confidence: 88,
|
|
15
|
+
riskBonus: 20,
|
|
16
|
+
detect: function(seq) {
|
|
17
|
+
var loginFails = seq.filter(function(s) { return s.threat === 'repeated_failed_auth'; }).length;
|
|
18
|
+
var postAuth = seq.some(function(s) {
|
|
19
|
+
return s.endpoint.includes('/user') || s.endpoint.includes('/admin') || s.endpoint.includes('/profile');
|
|
20
|
+
});
|
|
21
|
+
return loginFails >= 3 && postAuth;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'secret_enumeration',
|
|
26
|
+
label: 'Secret / config file enumeration chain',
|
|
27
|
+
confidence: 92,
|
|
28
|
+
riskBonus: 25,
|
|
29
|
+
detect: function(seq) {
|
|
30
|
+
return seq.filter(function(s) { return s.threat === 'secret_probe'; }).length >= 2;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'multi_vector_attack',
|
|
35
|
+
label: 'Multi-vector attack chain (injection + traversal)',
|
|
36
|
+
confidence: 90,
|
|
37
|
+
riskBonus: 30,
|
|
38
|
+
detect: function(seq) {
|
|
39
|
+
var injectionThreats = ['sql_injection','nosql_injection','command_injection','ssti_pattern','xxe_pattern'];
|
|
40
|
+
var hasInjection = seq.some(function(s) { return injectionThreats.indexOf(s.threat) !== -1; });
|
|
41
|
+
var hasTraversal = seq.some(function(s) { return s.threat === 'path_traversal'; });
|
|
42
|
+
return hasInjection && hasTraversal;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'scanner_sweep',
|
|
47
|
+
label: 'Automated scanner sweep detected',
|
|
48
|
+
confidence: 85,
|
|
49
|
+
riskBonus: 20,
|
|
50
|
+
detect: function(seq) {
|
|
51
|
+
var endpoints = {};
|
|
52
|
+
seq.forEach(function(s) { endpoints[s.endpoint] = true; });
|
|
53
|
+
var uniqueCount = Object.keys(endpoints).length;
|
|
54
|
+
var hasScanner = seq.some(function(s) { return s.threat && s.threat.indexOf('scanner_ua_') === 0; });
|
|
55
|
+
return uniqueCount >= 5 && hasScanner;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'brute_force_escalation',
|
|
60
|
+
label: 'Brute force escalating to privilege escalation',
|
|
61
|
+
confidence: 87,
|
|
62
|
+
riskBonus: 25,
|
|
63
|
+
detect: function(seq) {
|
|
64
|
+
var failedAuths = seq.filter(function(s) { return s.threat === 'repeated_failed_auth'; }).length;
|
|
65
|
+
var sensitiveAccess = seq.some(function(s) { return s.threat === 'sensitive_endpoint_access'; });
|
|
66
|
+
return failedAuths >= 2 && sensitiveAccess;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'xss_data_exfil',
|
|
71
|
+
label: 'XSS attempt followed by sensitive data access',
|
|
72
|
+
confidence: 80,
|
|
73
|
+
riskBonus: 20,
|
|
74
|
+
detect: function(seq) {
|
|
75
|
+
var hasXss = seq.some(function(s) { return s.threat === 'xss_pattern'; });
|
|
76
|
+
var hasSensitive = seq.some(function(s) {
|
|
77
|
+
return s.endpoint.includes('/cookie') || s.endpoint.includes('/token') || s.endpoint.includes('/session');
|
|
78
|
+
});
|
|
79
|
+
return hasXss && hasSensitive;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function recordSequence(ip, endpoint, threats, sequenceStore) {
|
|
85
|
+
if (!ip || !sequenceStore) return;
|
|
86
|
+
var seq = sequenceStore.get(ip) || [];
|
|
87
|
+
var list = threats && threats.length ? threats : ['none'];
|
|
88
|
+
for (var i = 0; i < list.length; i++) {
|
|
89
|
+
seq.push({ timestamp: Date.now(), endpoint: endpoint || '/', threat: list[i] || 'none' });
|
|
90
|
+
}
|
|
91
|
+
sequenceStore.set(ip, seq.slice(-MAX_SEQUENCE_LENGTH));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detectCorrelation(ip, sequenceStore) {
|
|
95
|
+
if (!ip || !sequenceStore) return null;
|
|
96
|
+
var seq = sequenceStore.get(ip);
|
|
97
|
+
if (!seq || seq.length < 3) return null;
|
|
98
|
+
for (var i = 0; i < ATTACK_CHAINS.length; i++) {
|
|
99
|
+
var chain = ATTACK_CHAINS[i];
|
|
100
|
+
try {
|
|
101
|
+
if (chain.detect(seq)) {
|
|
102
|
+
return {
|
|
103
|
+
pattern: chain.name,
|
|
104
|
+
label: chain.label,
|
|
105
|
+
confidence: chain.confidence,
|
|
106
|
+
riskBonus: chain.riskBonus,
|
|
107
|
+
sequenceLength: seq.length
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
} catch (_) { /* never crash middleware */ }
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getSequence(ip, sequenceStore) {
|
|
116
|
+
if (!ip || !sequenceStore) return [];
|
|
117
|
+
return (sequenceStore.get(ip) || []).slice(-10);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { recordSequence, detectCorrelation, getSequence, ATTACK_CHAINS };
|