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.
@@ -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
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./src');
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
+ }
@@ -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 };