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
package/src/identity.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash, randomUUID } = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build a rich client context from the incoming request.
|
|
7
|
+
* Supports testing overrides via headers or req.body.__iri when config.testing.enabled = true.
|
|
8
|
+
*/
|
|
9
|
+
function buildClientContext(req, res, config) {
|
|
10
|
+
const testingEnabled = config.testing?.enabled === true;
|
|
11
|
+
const override = testingEnabled ? getTestingOverride(req, config) : {};
|
|
12
|
+
|
|
13
|
+
// --- IP Resolution ---
|
|
14
|
+
const realIp = getClientIp(req);
|
|
15
|
+
const ip = override.ip || realIp;
|
|
16
|
+
|
|
17
|
+
// --- User Agent ---
|
|
18
|
+
const realUserAgent = req.headers['user-agent'] || '';
|
|
19
|
+
const userAgent = override.userAgent || realUserAgent;
|
|
20
|
+
|
|
21
|
+
// --- Cookie ---
|
|
22
|
+
const cookie = override.cookie || req.headers.cookie || '';
|
|
23
|
+
|
|
24
|
+
// --- Session ---
|
|
25
|
+
const sessionId =
|
|
26
|
+
override.sessionId ||
|
|
27
|
+
req.headers['x-session-id'] ||
|
|
28
|
+
readCookie(cookie, 'connect.sid') ||
|
|
29
|
+
readCookie(cookie, 'express.sid') ||
|
|
30
|
+
'';
|
|
31
|
+
|
|
32
|
+
// --- Declared User ---
|
|
33
|
+
const declaredUserId =
|
|
34
|
+
override.userId ||
|
|
35
|
+
req.headers['x-user-id'] ||
|
|
36
|
+
req.body?.userId ||
|
|
37
|
+
req.query?.userId ||
|
|
38
|
+
'';
|
|
39
|
+
|
|
40
|
+
// --- Device ID ---
|
|
41
|
+
const deviceId = override.deviceId || req.headers['x-device-id'] || '';
|
|
42
|
+
|
|
43
|
+
// --- Client ID (persistent cookie-based) ---
|
|
44
|
+
const existingClientId =
|
|
45
|
+
override.clientId ||
|
|
46
|
+
req.headers['x-iri-client-id'] ||
|
|
47
|
+
readCookie(cookie, 'iri_shield_uid');
|
|
48
|
+
const clientId = existingClientId || randomUUID();
|
|
49
|
+
|
|
50
|
+
// Set persistent client cookie if new
|
|
51
|
+
if (!existingClientId && res && !res.headersSent) {
|
|
52
|
+
res.setHeader(
|
|
53
|
+
'Set-Cookie',
|
|
54
|
+
`iri_shield_uid=${clientId}; HttpOnly; SameSite=Lax; Path=/; Max-Age=31536000`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Browser Signal Headers ---
|
|
59
|
+
const acceptLanguage = req.headers['accept-language'] || '';
|
|
60
|
+
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
61
|
+
const accept = req.headers['accept'] || '';
|
|
62
|
+
const dnt = req.headers['dnt'] || req.headers['sec-gpc'] || '';
|
|
63
|
+
const connection = req.headers['connection'] || '';
|
|
64
|
+
const referer = req.headers['referer'] || req.headers['referrer'] || '';
|
|
65
|
+
|
|
66
|
+
// --- Client Hints (modern browsers) ---
|
|
67
|
+
const secChUa = req.headers['sec-ch-ua'] || '';
|
|
68
|
+
const secChUaMobile = req.headers['sec-ch-ua-mobile'] || '';
|
|
69
|
+
const secChUaPlatform = req.headers['sec-ch-ua-platform'] || '';
|
|
70
|
+
const secChUaArch = req.headers['sec-ch-ua-arch'] || '';
|
|
71
|
+
|
|
72
|
+
// --- Fetch Metadata ---
|
|
73
|
+
const secFetchSite = req.headers['sec-fetch-site'] || '';
|
|
74
|
+
const secFetchMode = req.headers['sec-fetch-mode'] || '';
|
|
75
|
+
const secFetchDest = req.headers['sec-fetch-dest'] || '';
|
|
76
|
+
const secFetchUser = req.headers['sec-fetch-user'] || '';
|
|
77
|
+
|
|
78
|
+
// --- Network info ---
|
|
79
|
+
const xForwardedProto = req.headers['x-forwarded-proto'] || '';
|
|
80
|
+
const xRealIp = req.headers['x-real-ip'] || '';
|
|
81
|
+
const cfRay = req.headers['cf-ray'] || ''; // Cloudflare
|
|
82
|
+
const cfConnectingIp = req.headers['cf-connecting-ip'] || '';
|
|
83
|
+
|
|
84
|
+
// --- Header presence anomaly signals ---
|
|
85
|
+
const headerSignals = buildHeaderSignals(req, userAgent);
|
|
86
|
+
|
|
87
|
+
// --- Fingerprint (stable multi-signal hash) ---
|
|
88
|
+
const fingerprintSource = [
|
|
89
|
+
declaredUserId,
|
|
90
|
+
deviceId,
|
|
91
|
+
sessionId,
|
|
92
|
+
normalizeUa(userAgent),
|
|
93
|
+
normalizeIpFamily(ip),
|
|
94
|
+
acceptLanguage.slice(0, 20),
|
|
95
|
+
secChUaPlatform,
|
|
96
|
+
secChUaMobile
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join('|');
|
|
100
|
+
|
|
101
|
+
// --- Browser fingerprint (volatile signals for anomaly, not for stable ID) ---
|
|
102
|
+
const browserFingerprint = buildBrowserFingerprint({
|
|
103
|
+
userAgent,
|
|
104
|
+
acceptLanguage,
|
|
105
|
+
acceptEncoding,
|
|
106
|
+
accept,
|
|
107
|
+
dnt,
|
|
108
|
+
connection,
|
|
109
|
+
secChUa,
|
|
110
|
+
secChUaMobile,
|
|
111
|
+
secChUaPlatform,
|
|
112
|
+
secFetchSite,
|
|
113
|
+
secFetchMode
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
clientId,
|
|
118
|
+
userId: declaredUserId || clientId,
|
|
119
|
+
ip,
|
|
120
|
+
realIp,
|
|
121
|
+
userAgent,
|
|
122
|
+
cookie,
|
|
123
|
+
sessionId,
|
|
124
|
+
deviceId,
|
|
125
|
+
referer,
|
|
126
|
+
fingerprint: sha256(fingerprintSource || clientId),
|
|
127
|
+
browserFingerprint,
|
|
128
|
+
acceptLanguage,
|
|
129
|
+
secChUa,
|
|
130
|
+
secChUaPlatform,
|
|
131
|
+
secFetchSite,
|
|
132
|
+
secFetchMode,
|
|
133
|
+
secFetchDest,
|
|
134
|
+
headerSignals,
|
|
135
|
+
isTestingOverride: Boolean(override.used),
|
|
136
|
+
timestamp: new Date().toISOString()
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Detect suspicious identity changes for a returning client.
|
|
142
|
+
*/
|
|
143
|
+
function detectIdentityChange(client, existing) {
|
|
144
|
+
if (!existing || existing.requestCount === 0) {
|
|
145
|
+
return { score: 0, reasons: [], threats: [] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const reasons = [];
|
|
149
|
+
const threats = [];
|
|
150
|
+
let score = 0;
|
|
151
|
+
|
|
152
|
+
// IP change
|
|
153
|
+
if (client.ip && existing.ips?.length && !existing.ips.includes(client.ip)) {
|
|
154
|
+
score += 15;
|
|
155
|
+
threats.push('identity_ip_change');
|
|
156
|
+
reasons.push(`new_ip_for_client_${client.ip}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// User-Agent change
|
|
160
|
+
if (client.userAgent && existing.userAgents?.length && !existing.userAgents.includes(client.userAgent)) {
|
|
161
|
+
score += 20;
|
|
162
|
+
threats.push('identity_user_agent_change');
|
|
163
|
+
reasons.push('new_user_agent_for_client');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fingerprint change (strong signal)
|
|
167
|
+
if (client.fingerprint && existing.fingerprints?.length && !existing.fingerprints.includes(client.fingerprint)) {
|
|
168
|
+
score += 25;
|
|
169
|
+
threats.push('identity_fingerprint_change');
|
|
170
|
+
reasons.push('new_fingerprint_for_known_client');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Platform change (Client Hints — very reliable in modern browsers)
|
|
174
|
+
if (
|
|
175
|
+
client.secChUaPlatform &&
|
|
176
|
+
existing.platforms?.length &&
|
|
177
|
+
!existing.platforms.includes(client.secChUaPlatform)
|
|
178
|
+
) {
|
|
179
|
+
score += 10;
|
|
180
|
+
threats.push('identity_platform_change');
|
|
181
|
+
reasons.push(`new_platform_${client.secChUaPlatform}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { score, reasons, threats };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Helpers
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
function getTestingOverride(req, config) {
|
|
192
|
+
if (!config.testing?.allowClientOverrides) return {};
|
|
193
|
+
const body = req.body?.__iri || req.body?.iriShieldTest || {};
|
|
194
|
+
const output = {
|
|
195
|
+
ip: req.headers['x-iri-test-ip'] || body.ip,
|
|
196
|
+
userAgent: req.headers['x-iri-test-user-agent'] || body.userAgent,
|
|
197
|
+
cookie: req.headers['x-iri-test-cookie'] || body.cookie,
|
|
198
|
+
sessionId: req.headers['x-iri-test-session-id'] || body.sessionId,
|
|
199
|
+
userId: req.headers['x-iri-test-user-id'] || body.userId,
|
|
200
|
+
deviceId: req.headers['x-iri-test-device-id'] || body.deviceId,
|
|
201
|
+
clientId: req.headers['x-iri-test-client-id'] || body.clientId
|
|
202
|
+
};
|
|
203
|
+
output.used = Object.values(output).some(Boolean);
|
|
204
|
+
return output;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getClientIp(req) {
|
|
208
|
+
// Trust proxy chain
|
|
209
|
+
const cfIp = req.headers['cf-connecting-ip'];
|
|
210
|
+
if (cfIp) return cfIp.trim();
|
|
211
|
+
|
|
212
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
213
|
+
if (typeof forwarded === 'string' && forwarded.trim()) {
|
|
214
|
+
return forwarded.split(',')[0].trim();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const realIp = req.headers['x-real-ip'];
|
|
218
|
+
if (realIp) return realIp.trim();
|
|
219
|
+
|
|
220
|
+
return req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || 'unknown';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readCookie(cookieHeader, name) {
|
|
224
|
+
return String(cookieHeader || '')
|
|
225
|
+
.split(';')
|
|
226
|
+
.map((item) => item.trim())
|
|
227
|
+
.reduce((value, item) => {
|
|
228
|
+
const splitAt = item.indexOf('=');
|
|
229
|
+
if (splitAt === -1) return value;
|
|
230
|
+
const key = decodeURIComponent(item.slice(0, splitAt));
|
|
231
|
+
return key === name ? decodeURIComponent(item.slice(splitAt + 1)) : value;
|
|
232
|
+
}, '');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeIpFamily(ip) {
|
|
236
|
+
const value = String(ip || '');
|
|
237
|
+
// IPv6: keep first 4 groups (network prefix)
|
|
238
|
+
if (value.includes(':')) return value.split(':').slice(0, 4).join(':');
|
|
239
|
+
// IPv4: keep first 3 octets (subnet)
|
|
240
|
+
return value.split('.').slice(0, 3).join('.');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeUa(ua) {
|
|
244
|
+
// Strip version numbers for stable comparison
|
|
245
|
+
return String(ua || '')
|
|
246
|
+
.replace(/[\d.]+/g, 'X')
|
|
247
|
+
.slice(0, 80);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Build a secondary browser-signal fingerprint for anomaly detection.
|
|
252
|
+
* This is more volatile and used to detect spoofing.
|
|
253
|
+
*/
|
|
254
|
+
function buildBrowserFingerprint(signals) {
|
|
255
|
+
const parts = Object.values(signals).filter(Boolean).join('|');
|
|
256
|
+
return sha256(parts || 'empty');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Analyze header presence for anomaly detection.
|
|
261
|
+
* Real browsers send specific combinations of headers.
|
|
262
|
+
*/
|
|
263
|
+
function buildHeaderSignals(req, userAgent) {
|
|
264
|
+
const ua = String(userAgent || '').toLowerCase();
|
|
265
|
+
const isBrowserLike =
|
|
266
|
+
ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari') || ua.includes('firefox');
|
|
267
|
+
|
|
268
|
+
const hasAccept = Boolean(req.headers['accept']);
|
|
269
|
+
const hasAcceptLang = Boolean(req.headers['accept-language']);
|
|
270
|
+
const hasAcceptEncoding = Boolean(req.headers['accept-encoding']);
|
|
271
|
+
const hasSecFetch = Boolean(req.headers['sec-fetch-site']);
|
|
272
|
+
const hasSecChUa = Boolean(req.headers['sec-ch-ua']);
|
|
273
|
+
|
|
274
|
+
// Real browsers (Chrome/Edge) always send sec-fetch-* and sec-ch-ua
|
|
275
|
+
const missingBrowserHeaders =
|
|
276
|
+
isBrowserLike && (!hasAccept || !hasAcceptLang || !hasAcceptEncoding);
|
|
277
|
+
|
|
278
|
+
// Modern browsers almost always send sec-fetch headers
|
|
279
|
+
const claimsModernBrowser =
|
|
280
|
+
isBrowserLike && (ua.includes('chrome') || ua.includes('edge'));
|
|
281
|
+
const missingModernHeaders = claimsModernBrowser && !hasSecFetch && !hasSecChUa;
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
hasAccept,
|
|
285
|
+
hasAcceptLang,
|
|
286
|
+
hasAcceptEncoding,
|
|
287
|
+
hasSecFetch,
|
|
288
|
+
hasSecChUa,
|
|
289
|
+
isBrowserLike,
|
|
290
|
+
missingBrowserHeaders,
|
|
291
|
+
missingModernHeaders
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sha256(value) {
|
|
296
|
+
return createHash('sha256').update(String(value)).digest('hex').slice(0, 24);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = { buildClientContext, detectIdentityChange, getClientIp };
|