@anomira/node-sdk 0.2.3 → 0.2.5
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/README.md +42 -11
- package/dist/index.cjs +301 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -3
- package/dist/index.d.ts +31 -3
- package/dist/index.js +301 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,41 +168,70 @@ Once registered, the middleware records **every request** with zero additional c
|
|
|
168
168
|
|
|
169
169
|
Use these when the middleware can't infer the event on its own — e.g., application-level failures.
|
|
170
170
|
|
|
171
|
+
### Getting the real client IP
|
|
172
|
+
|
|
173
|
+
> **Important — do not use `req.ip` for the `ip` field.**
|
|
174
|
+
>
|
|
175
|
+
> Behind a reverse proxy (Nginx, AWS ALB, Cloudflare — which every production app uses), `req.ip` in Express without `trust proxy` configured returns `127.0.0.1` — the proxy's address, not the user's. This breaks geo-detection, IP blocking, and attack correlation.
|
|
176
|
+
>
|
|
177
|
+
> Always use `anomira.getClientIp(req)` instead. It reads `X-Forwarded-For`, `CF-Connecting-IP`, `X-Real-IP`, and other forwarded headers in the correct priority order automatically.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// ✅ Correct — works behind Nginx, Cloudflare, AWS ALB, any reverse proxy
|
|
181
|
+
const ip = anomira.getClientIp(req);
|
|
182
|
+
|
|
183
|
+
// ❌ Wrong behind a proxy — returns 127.0.0.1 unless trust proxy is configured
|
|
184
|
+
const ip = req.ip;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
If you use `anomira.express()` or `anomira.fastify()` middleware and call `track()` **inside a request handler**, you can omit the `ip` field entirely — the SDK reads it from the active request context automatically.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// ✅ Also correct — IP is resolved from middleware context, no need to pass it
|
|
191
|
+
anomira.track("auth.otp.failed", { userId: req.body.phone });
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Examples
|
|
195
|
+
|
|
171
196
|
```ts
|
|
172
197
|
// Credential stuffing / failed login attempt
|
|
173
198
|
await anomira.trackLogin({
|
|
174
|
-
ip: req
|
|
199
|
+
ip: anomira.getClientIp(req),
|
|
175
200
|
userId: req.body.email, // email or user ID
|
|
176
201
|
success: false, // false = failed attempt
|
|
177
202
|
});
|
|
178
203
|
|
|
179
204
|
// Successful login (enables geo-velocity tracking)
|
|
180
205
|
await anomira.trackLogin({
|
|
181
|
-
ip:
|
|
182
|
-
userId:
|
|
206
|
+
ip: anomira.getClientIp(req),
|
|
207
|
+
userId: user.id,
|
|
183
208
|
success: true,
|
|
184
209
|
});
|
|
185
210
|
|
|
186
211
|
// Failed OTP — otp_flood detection
|
|
187
212
|
anomira.track("auth.otp.failed", {
|
|
188
|
-
ip: req
|
|
213
|
+
ip: anomira.getClientIp(req),
|
|
189
214
|
userId: req.body.phone,
|
|
190
215
|
meta: { endpoint: "/api/verify-otp" },
|
|
191
216
|
});
|
|
192
217
|
|
|
193
218
|
// SIM swap detection — call after any phone-based auth
|
|
194
|
-
anomira.trackPhoneAuth({
|
|
219
|
+
anomira.trackPhoneAuth({
|
|
220
|
+
ip: anomira.getClientIp(req),
|
|
221
|
+
userId: user.id,
|
|
222
|
+
phone: user.phone,
|
|
223
|
+
});
|
|
195
224
|
|
|
196
225
|
// Account takeover signal — e.g., password changed from new IP
|
|
197
226
|
anomira.track("auth.account.takeover", {
|
|
198
|
-
ip: req
|
|
227
|
+
ip: anomira.getClientIp(req),
|
|
199
228
|
userId: user.id,
|
|
200
229
|
meta: { reason: "password_changed_new_ip" },
|
|
201
230
|
});
|
|
202
231
|
|
|
203
232
|
// Webhook replay — call when you detect a replayed webhook signature
|
|
204
233
|
anomira.track("webhook.replay.detected", {
|
|
205
|
-
ip: req
|
|
234
|
+
ip: anomira.getClientIp(req),
|
|
206
235
|
meta: { webhookId: req.headers["x-webhook-id"] },
|
|
207
236
|
});
|
|
208
237
|
```
|
|
@@ -230,8 +259,10 @@ The SDK syncs your dashboard's blocked IPs and firewall rules every 60 seconds.
|
|
|
230
259
|
|
|
231
260
|
```ts
|
|
232
261
|
app.use((req, res, next) => {
|
|
262
|
+
const ip = anomira.getClientIp(req); // always use this, not req.ip
|
|
263
|
+
|
|
233
264
|
// Check if IP is manually blocked in the dashboard
|
|
234
|
-
if (anomira.isBlocked(
|
|
265
|
+
if (anomira.isBlocked(ip)) {
|
|
235
266
|
return res.status(403).json({ error: "Forbidden" });
|
|
236
267
|
}
|
|
237
268
|
|
|
@@ -241,7 +272,7 @@ app.use((req, res, next) => {
|
|
|
241
272
|
method: req.method,
|
|
242
273
|
body: req.body,
|
|
243
274
|
headers: req.headers,
|
|
244
|
-
ip
|
|
275
|
+
ip,
|
|
245
276
|
});
|
|
246
277
|
|
|
247
278
|
if (match) {
|
|
@@ -379,8 +410,8 @@ app.addHook("onClose", async () => {
|
|
|
379
410
|
3. Confirm the middleware is registered **before** your routes and **after** body parsers.
|
|
380
411
|
4. Make sure you're not blocking outbound HTTPS traffic to `api.anomira.io`.
|
|
381
412
|
|
|
382
|
-
**TypeScript errors on
|
|
383
|
-
|
|
413
|
+
**TypeScript errors on IP extraction?**
|
|
414
|
+
Use `anomira.getClientIp(req)` — it handles all proxy headers and TypeScript types correctly. Do not use `req.ip` or `req.socket.remoteAddress` directly in production; both return the proxy address behind Nginx.
|
|
384
415
|
|
|
385
416
|
**`flush()` taking too long on shutdown?**
|
|
386
417
|
The flush waits for in-flight HTTP requests to complete. If your process needs to exit fast, you can add a timeout:
|
package/dist/index.cjs
CHANGED
|
@@ -404,7 +404,13 @@ var EventName = {
|
|
|
404
404
|
SQL_ERROR: "db.sql.error",
|
|
405
405
|
// Firewall (emitted when a custom request filtering rule fires)
|
|
406
406
|
FIREWALL_BLOCK: "http.firewall.block",
|
|
407
|
-
FIREWALL_FLAG: "http.firewall.flag"
|
|
407
|
+
FIREWALL_FLAG: "http.firewall.flag",
|
|
408
|
+
FIREWALL_RATE_LIMIT: "http.firewall.rate_limit",
|
|
409
|
+
FIREWALL_REDIRECT_HONEYPOT: "http.firewall.redirect_honeypot",
|
|
410
|
+
FIREWALL_TAG: "http.firewall.tag",
|
|
411
|
+
FIREWALL_MONITOR: "http.firewall.monitor",
|
|
412
|
+
FIREWALL_CHALLENGE: "http.firewall.challenge",
|
|
413
|
+
FIREWALL_TEMP_BLOCK: "http.firewall.temporary_block"
|
|
408
414
|
};
|
|
409
415
|
|
|
410
416
|
// src/agent-detection.ts
|
|
@@ -521,6 +527,9 @@ var KNOWN_BOT_UA = [
|
|
|
521
527
|
[/^Ruby$/i, "Ruby"]
|
|
522
528
|
];
|
|
523
529
|
var AXIOS_ACCEPT = "application/json, text/plain, */*";
|
|
530
|
+
function isBrowserUa(ua) {
|
|
531
|
+
return /Mozilla\/5\.0/.test(ua) && /(?:Chrome|Firefox|Safari|OPR|Edg(?:e|HTML)?|Trident)\/[\d.]+/.test(ua);
|
|
532
|
+
}
|
|
524
533
|
function computeBrowserFingerprint(headers, ua) {
|
|
525
534
|
const signals = [];
|
|
526
535
|
let score = 0;
|
|
@@ -537,10 +546,15 @@ function computeBrowserFingerprint(headers, ua) {
|
|
|
537
546
|
}
|
|
538
547
|
const accept = str(headers["accept"]);
|
|
539
548
|
if (!isDefiniteBot && accept === AXIOS_ACCEPT) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
549
|
+
if (!isBrowserUa(ua)) {
|
|
550
|
+
isDefiniteBot = true;
|
|
551
|
+
knownClient = "axios";
|
|
552
|
+
score = 100;
|
|
553
|
+
signals.push("known_accept:axios");
|
|
554
|
+
} else {
|
|
555
|
+
score += 15;
|
|
556
|
+
signals.push("browser_axios_accept");
|
|
557
|
+
}
|
|
544
558
|
}
|
|
545
559
|
if (isDefiniteBot) return { score, signals, isDefiniteBot, knownClient };
|
|
546
560
|
const hasSecFetchSite = "sec-fetch-site" in headers;
|
|
@@ -1147,6 +1161,108 @@ var DEFAULT_INGEST_URL = "https://ingest.anomira.io/v1/events";
|
|
|
1147
1161
|
var DEFAULT_BATCH_SIZE = 100;
|
|
1148
1162
|
var DEFAULT_FLUSH_MS = 5e3;
|
|
1149
1163
|
var DEFAULT_MAX_RETRIES = 3;
|
|
1164
|
+
var SENSITIVE_FIELDS = {
|
|
1165
|
+
identity: [
|
|
1166
|
+
"first_name",
|
|
1167
|
+
"last_name",
|
|
1168
|
+
"full_name",
|
|
1169
|
+
"name",
|
|
1170
|
+
"bvn",
|
|
1171
|
+
"nin",
|
|
1172
|
+
"ssn",
|
|
1173
|
+
"national_id",
|
|
1174
|
+
"tin",
|
|
1175
|
+
"passport",
|
|
1176
|
+
"passport_number",
|
|
1177
|
+
"drivers_license",
|
|
1178
|
+
"license_number",
|
|
1179
|
+
"voter_id",
|
|
1180
|
+
"dob",
|
|
1181
|
+
"date_of_birth",
|
|
1182
|
+
"birth_date",
|
|
1183
|
+
"birthdate",
|
|
1184
|
+
"gender",
|
|
1185
|
+
"marital_status",
|
|
1186
|
+
"nationality"
|
|
1187
|
+
],
|
|
1188
|
+
contact: [
|
|
1189
|
+
"email",
|
|
1190
|
+
"phone",
|
|
1191
|
+
"phone_number",
|
|
1192
|
+
"mobile",
|
|
1193
|
+
"mobile_number",
|
|
1194
|
+
"address",
|
|
1195
|
+
"street",
|
|
1196
|
+
"city",
|
|
1197
|
+
"state",
|
|
1198
|
+
"postal_code",
|
|
1199
|
+
"zip"
|
|
1200
|
+
],
|
|
1201
|
+
financial: [
|
|
1202
|
+
"card_number",
|
|
1203
|
+
"credit_card",
|
|
1204
|
+
"debit_card",
|
|
1205
|
+
"cvv",
|
|
1206
|
+
"account_number",
|
|
1207
|
+
"bank_account",
|
|
1208
|
+
"routing_number",
|
|
1209
|
+
"sort_code",
|
|
1210
|
+
"iban",
|
|
1211
|
+
"swift",
|
|
1212
|
+
"wallet_id"
|
|
1213
|
+
],
|
|
1214
|
+
authentication: [
|
|
1215
|
+
"password",
|
|
1216
|
+
"pin",
|
|
1217
|
+
"otp",
|
|
1218
|
+
"secret",
|
|
1219
|
+
"private_key",
|
|
1220
|
+
"api_key",
|
|
1221
|
+
"access_token",
|
|
1222
|
+
"refresh_token",
|
|
1223
|
+
"jwt",
|
|
1224
|
+
"bearer_token",
|
|
1225
|
+
"security_answer",
|
|
1226
|
+
"mother_maiden_name"
|
|
1227
|
+
],
|
|
1228
|
+
biometric: [
|
|
1229
|
+
"fingerprint",
|
|
1230
|
+
"face_id",
|
|
1231
|
+
"iris",
|
|
1232
|
+
"voiceprint",
|
|
1233
|
+
"biometric"
|
|
1234
|
+
],
|
|
1235
|
+
health: [
|
|
1236
|
+
"medical_record",
|
|
1237
|
+
"diagnosis",
|
|
1238
|
+
"blood_group",
|
|
1239
|
+
"insurance_id"
|
|
1240
|
+
],
|
|
1241
|
+
fintech: [
|
|
1242
|
+
"kyc_id",
|
|
1243
|
+
"customer_id",
|
|
1244
|
+
"beneficiary_account",
|
|
1245
|
+
"transaction_pin",
|
|
1246
|
+
"wallet_balance",
|
|
1247
|
+
"bank_verification_number",
|
|
1248
|
+
"virtual_account",
|
|
1249
|
+
"monnify_account",
|
|
1250
|
+
"paystack_customer",
|
|
1251
|
+
"flutterwave_customer"
|
|
1252
|
+
]
|
|
1253
|
+
};
|
|
1254
|
+
var FIELD_CATEGORY_MAP = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const [cat, fields] of Object.entries(SENSITIVE_FIELDS)) {
|
|
1256
|
+
for (const f of fields) FIELD_CATEGORY_MAP.set(f, cat);
|
|
1257
|
+
}
|
|
1258
|
+
var VALUE_PATTERNS = [
|
|
1259
|
+
{ name: "email", category: "contact", re: /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/ },
|
|
1260
|
+
{ name: "bvn_nin", category: "identity", re: /^\d{11}$/ },
|
|
1261
|
+
{ name: "card_number", category: "financial", re: /^\d{13,19}$/ },
|
|
1262
|
+
{ name: "phone_ng", category: "contact", re: /^(?:\+234|0)[789][01]\d{8}$/ },
|
|
1263
|
+
{ name: "jwt", category: "authentication", re: /^eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\./ },
|
|
1264
|
+
{ name: "iban", category: "financial", re: /^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/ }
|
|
1265
|
+
];
|
|
1150
1266
|
var AnomiraClient = class {
|
|
1151
1267
|
constructor(config) {
|
|
1152
1268
|
this.logBuffer = [];
|
|
@@ -1479,13 +1595,35 @@ var AnomiraClient = class {
|
|
|
1479
1595
|
}
|
|
1480
1596
|
}
|
|
1481
1597
|
// ─── Public API ────────────────────────────────────────────────────────────
|
|
1598
|
+
/**
|
|
1599
|
+
* Extract the real client IP from a request object, reading forwarded headers
|
|
1600
|
+
* in the correct priority order (Cloudflare → XFF → X-Real-IP → socket).
|
|
1601
|
+
*
|
|
1602
|
+
* Use this instead of `req.ip` when tracking events manually. `req.ip` in
|
|
1603
|
+
* Express without `trust proxy` set returns the proxy's address (127.0.0.1
|
|
1604
|
+
* behind Nginx), which breaks geo-detection and IP blocking.
|
|
1605
|
+
*
|
|
1606
|
+
* ```ts
|
|
1607
|
+
* // ✅ Correct — reads X-Forwarded-For / CF-Connecting-IP automatically
|
|
1608
|
+
* sentinel.track(EventName.OTP_FAILED, {
|
|
1609
|
+
* ip: sentinel.getClientIp(req),
|
|
1610
|
+
* userId: req.body.phone,
|
|
1611
|
+
* meta: { endpoint: "/api/verify-otp" },
|
|
1612
|
+
* });
|
|
1613
|
+
*
|
|
1614
|
+
* // ❌ Wrong behind a proxy — req.ip is 127.0.0.1 without trust proxy configured
|
|
1615
|
+
* sentinel.track(EventName.OTP_FAILED, { ip: req.ip, ... });
|
|
1616
|
+
* ```
|
|
1617
|
+
*/
|
|
1618
|
+
getClientIp(req) {
|
|
1619
|
+
return this.config.getIp(req);
|
|
1620
|
+
}
|
|
1482
1621
|
/**
|
|
1483
1622
|
* Track a custom security event.
|
|
1484
1623
|
*
|
|
1485
1624
|
* ```ts
|
|
1486
|
-
* // Track a failed OTP attempt
|
|
1487
1625
|
* sentinel.track(EventName.OTP_FAILED, {
|
|
1488
|
-
* ip: req.ip
|
|
1626
|
+
* ip: sentinel.getClientIp(req), // use getClientIp, not req.ip
|
|
1489
1627
|
* userId: req.body.phone,
|
|
1490
1628
|
* meta: { endpoint: "/api/verify-otp", attempts: 3 },
|
|
1491
1629
|
* });
|
|
@@ -1558,6 +1696,11 @@ var AnomiraClient = class {
|
|
|
1558
1696
|
if (this.disabled) return;
|
|
1559
1697
|
const ctx = requestContext.getStore();
|
|
1560
1698
|
const resolvedIp = data.ip && data.ip !== "0.0.0.0" && data.ip !== "" ? data.ip : ctx?.ip ?? "0.0.0.0";
|
|
1699
|
+
if (this.config.debug && data.ip && isPrivateIp2(data.ip)) {
|
|
1700
|
+
this._origWarn(
|
|
1701
|
+
`[Anomira] Warning: track() received a private/loopback IP "${data.ip}" for event "${eventName}". Behind a reverse proxy, req.ip is the proxy's address \u2014 use sentinel.getClientIp(req) instead.`
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1561
1704
|
const resolvedMeta = ctx?.endpoint && !data.meta?.["endpoint"] ? { endpoint: ctx.endpoint, method: ctx.method, ...data.meta } : data.meta;
|
|
1562
1705
|
const event = {
|
|
1563
1706
|
name: eventName,
|
|
@@ -1798,6 +1941,9 @@ function normalizeIp(raw) {
|
|
|
1798
1941
|
if (raw.startsWith("::ffff:")) return raw.slice(7);
|
|
1799
1942
|
return raw;
|
|
1800
1943
|
}
|
|
1944
|
+
function isPrivateIp2(ip) {
|
|
1945
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0" || ip.startsWith("10.") || ip.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[01])\./.test(ip);
|
|
1946
|
+
}
|
|
1801
1947
|
function defaultGetIp(req) {
|
|
1802
1948
|
const r = req;
|
|
1803
1949
|
const fwd = r["headers"];
|
|
@@ -1898,6 +2044,27 @@ function createExpressMiddleware(client) {
|
|
|
1898
2044
|
const fingerprint = computeBrowserFingerprint(headers ?? {}, ua);
|
|
1899
2045
|
const h2settings = extractHttp2Settings(req);
|
|
1900
2046
|
const upstreamTls = extractUpstreamTls(headers ?? {});
|
|
2047
|
+
const cookieHeader = headers?.["cookie"];
|
|
2048
|
+
const cookieToken = cookieHeader ? cookieHeader.split(";").map((c) => c.trim()).find((c) => c.startsWith("anomira_fp="))?.slice("anomira_fp=".length) ?? "" : "";
|
|
2049
|
+
const browserFpRaw = cookieToken || headers?.["x-anomira-fp"] || "";
|
|
2050
|
+
let browserFp = null;
|
|
2051
|
+
if (browserFpRaw) {
|
|
2052
|
+
try {
|
|
2053
|
+
const raw = JSON.parse(Buffer.from(browserFpRaw, "base64").toString("utf8"));
|
|
2054
|
+
if (typeof raw["v"] === "number" && typeof raw["fp"] === "string") {
|
|
2055
|
+
browserFp = {
|
|
2056
|
+
fp: raw["fp"],
|
|
2057
|
+
bot: raw["bot"] ?? 0,
|
|
2058
|
+
sigs: (raw["sigs"] ?? []).join(","),
|
|
2059
|
+
uid: typeof raw["uid"] === "string" && raw["uid"] ? raw["uid"] : void 0,
|
|
2060
|
+
pst: raw["frm"]?.pst,
|
|
2061
|
+
ttf: raw["frm"]?.ttf,
|
|
2062
|
+
tts: raw["frm"]?.tts
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1901
2068
|
if (client["canaryTokenCache"] && client["canaryTokenCache"].size > 0) {
|
|
1902
2069
|
const authHeader = (typeof headers?.["authorization"] === "string" ? headers["authorization"] : "") ?? "";
|
|
1903
2070
|
if (authHeader.startsWith("Bearer ")) {
|
|
@@ -1992,11 +2159,39 @@ function createExpressMiddleware(client) {
|
|
|
1992
2159
|
userId,
|
|
1993
2160
|
meta: { url, method, ruleId: fwMatch.rule.id, attackType: fwMatch.rule.attackType }
|
|
1994
2161
|
});
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
if (typeof res_["status"] === "function") res_["status"](
|
|
1998
|
-
else res_["statusCode"] =
|
|
1999
|
-
|
|
2162
|
+
const res_ = res;
|
|
2163
|
+
const setStatus = (code) => {
|
|
2164
|
+
if (typeof res_["status"] === "function") res_["status"](code);
|
|
2165
|
+
else res_["statusCode"] = code;
|
|
2166
|
+
};
|
|
2167
|
+
const sendBody = (body) => {
|
|
2168
|
+
if (typeof res_["end"] === "function") res_["end"](body);
|
|
2169
|
+
};
|
|
2170
|
+
const setHeader = (k, v) => {
|
|
2171
|
+
if (typeof res_["setHeader"] === "function") res_["setHeader"](k, v);
|
|
2172
|
+
};
|
|
2173
|
+
const action = fwMatch.rule.action;
|
|
2174
|
+
if (action === "block" || action === "temporary_block") {
|
|
2175
|
+
setStatus(403);
|
|
2176
|
+
sendBody('{"error":"Blocked by firewall rule"}');
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (action === "rate_limit") {
|
|
2180
|
+
setStatus(429);
|
|
2181
|
+
setHeader("Retry-After", "60");
|
|
2182
|
+
sendBody('{"error":"Rate limit exceeded","retryAfter":60}');
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
if (action === "redirect_honeypot") {
|
|
2186
|
+
const target = fwMatch.rule.redirectTarget ?? "/.env";
|
|
2187
|
+
setStatus(302);
|
|
2188
|
+
setHeader("Location", target);
|
|
2189
|
+
sendBody("");
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (action === "challenge") {
|
|
2193
|
+
setStatus(403);
|
|
2194
|
+
sendBody('{"error":"Request challenge required"}');
|
|
2000
2195
|
return;
|
|
2001
2196
|
}
|
|
2002
2197
|
}
|
|
@@ -2010,7 +2205,7 @@ function createExpressMiddleware(client) {
|
|
|
2010
2205
|
}
|
|
2011
2206
|
}
|
|
2012
2207
|
const onFinish = () => {
|
|
2013
|
-
const lateUserId = client.config.getUserId(req);
|
|
2208
|
+
const lateUserId = client.config.getUserId(req) || browserFp?.uid || "";
|
|
2014
2209
|
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
2015
2210
|
userIdCheckCount++;
|
|
2016
2211
|
if (!lateUserId) userIdMissCount++;
|
|
@@ -2025,6 +2220,30 @@ function createExpressMiddleware(client) {
|
|
|
2025
2220
|
const latencyMs = Date.now() - startMs;
|
|
2026
2221
|
const getHeader = res["getHeader"];
|
|
2027
2222
|
const bytes = typeof getHeader === "function" ? parseInt(getHeader.call(res, "content-length") ?? "0", 10) || 0 : 0;
|
|
2223
|
+
const rawBody = req["body"];
|
|
2224
|
+
const piiFields = [];
|
|
2225
|
+
const piiPatterns = [];
|
|
2226
|
+
const piiCatSet = /* @__PURE__ */ new Set();
|
|
2227
|
+
if (rawBody != null && typeof rawBody === "object" && !Array.isArray(rawBody)) {
|
|
2228
|
+
const body = rawBody;
|
|
2229
|
+
for (const key of Object.keys(body)) {
|
|
2230
|
+
const cat = FIELD_CATEGORY_MAP.get(key.toLowerCase());
|
|
2231
|
+
if (cat) {
|
|
2232
|
+
piiFields.push(key.toLowerCase());
|
|
2233
|
+
piiCatSet.add(cat);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
for (const val of Object.values(body)) {
|
|
2237
|
+
if (typeof val !== "string" || val.length > 200) continue;
|
|
2238
|
+
for (const { name, category, re } of VALUE_PATTERNS) {
|
|
2239
|
+
if (!piiPatterns.includes(name) && re.test(val)) {
|
|
2240
|
+
piiPatterns.push(name);
|
|
2241
|
+
piiCatSet.add(category);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const piiCategories = piiCatSet.size > 0 ? [...piiCatSet] : void 0;
|
|
2028
2247
|
client.track(EventName.REQUEST, {
|
|
2029
2248
|
ip,
|
|
2030
2249
|
userId: lateUserId,
|
|
@@ -2035,9 +2254,20 @@ function createExpressMiddleware(client) {
|
|
|
2035
2254
|
latencyMs,
|
|
2036
2255
|
userAgent: ua,
|
|
2037
2256
|
bytes,
|
|
2257
|
+
...piiFields.length > 0 ? { piiFields } : {},
|
|
2258
|
+
...piiPatterns.length > 0 ? { piiPatterns } : {},
|
|
2259
|
+
...piiCategories ? { piiCategories } : {},
|
|
2038
2260
|
_fp: fingerprint.score,
|
|
2039
2261
|
_fpSig: fingerprint.signals.join(","),
|
|
2040
2262
|
...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
|
|
2263
|
+
...browserFp ? {
|
|
2264
|
+
_bfp: browserFp.fp,
|
|
2265
|
+
_bbot: browserFp.bot,
|
|
2266
|
+
_bsigs: browserFp.sigs,
|
|
2267
|
+
...browserFp.pst !== void 0 ? { _bpaste: browserFp.pst } : {},
|
|
2268
|
+
...browserFp.ttf !== void 0 && browserFp.ttf >= 0 ? { _bttf: browserFp.ttf } : {},
|
|
2269
|
+
...browserFp.tts !== void 0 && browserFp.tts >= 0 ? { _btts: browserFp.tts } : {}
|
|
2270
|
+
} : {},
|
|
2041
2271
|
...h2settings ? {
|
|
2042
2272
|
_h2Window: h2settings.initialWindowSize,
|
|
2043
2273
|
_h2HdrTable: h2settings.headerTableSize,
|
|
@@ -2165,14 +2395,32 @@ function createFastifyPlugin(client) {
|
|
|
2165
2395
|
userId,
|
|
2166
2396
|
meta: { url, method, ruleId: fwMatch.rule.id, attackType: fwMatch.rule.attackType }
|
|
2167
2397
|
});
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2398
|
+
const rep = reply;
|
|
2399
|
+
const replyCode = (code) => typeof rep["code"] === "function" ? rep["code"](code) : rep;
|
|
2400
|
+
const replyHeader = (k, v) => {
|
|
2401
|
+
if (typeof rep["header"] === "function") rep["header"](k, v);
|
|
2402
|
+
};
|
|
2403
|
+
const replySend = (chained, body) => {
|
|
2404
|
+
if (chained && typeof chained["send"] === "function") chained["send"](body);
|
|
2405
|
+
};
|
|
2406
|
+
const action = fwMatch.rule.action;
|
|
2407
|
+
if (action === "block" || action === "temporary_block") {
|
|
2408
|
+
replySend(replyCode(403), { error: "Blocked by firewall rule" });
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (action === "rate_limit") {
|
|
2412
|
+
replyHeader("Retry-After", "60");
|
|
2413
|
+
replySend(replyCode(429), { error: "Rate limit exceeded", retryAfter: 60 });
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
if (action === "redirect_honeypot") {
|
|
2417
|
+
const target = fwMatch.rule.redirectTarget ?? "/.env";
|
|
2418
|
+
replyHeader("Location", target);
|
|
2419
|
+
replySend(replyCode(302), "");
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
if (action === "challenge") {
|
|
2423
|
+
replySend(replyCode(403), { error: "Request challenge required" });
|
|
2176
2424
|
return;
|
|
2177
2425
|
}
|
|
2178
2426
|
}
|
|
@@ -2187,11 +2435,11 @@ function createFastifyPlugin(client) {
|
|
|
2187
2435
|
const ip = client.config.getIp(req);
|
|
2188
2436
|
const url = req["url"] ?? "/";
|
|
2189
2437
|
const method = req["method"]?.toUpperCase() ?? "GET";
|
|
2190
|
-
const
|
|
2438
|
+
const serverUserId = client.config.getUserId(req);
|
|
2191
2439
|
const status = reply["statusCode"] ?? 0;
|
|
2192
2440
|
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
2193
2441
|
userIdCheckCount++;
|
|
2194
|
-
if (!
|
|
2442
|
+
if (!serverUserId) userIdMissCount++;
|
|
2195
2443
|
if (userIdCheckCount === 20 && userIdMissCount >= 18) {
|
|
2196
2444
|
userIdWarnFired = true;
|
|
2197
2445
|
console.warn(
|
|
@@ -2206,6 +2454,28 @@ function createFastifyPlugin(client) {
|
|
|
2206
2454
|
const fingerprint = computeBrowserFingerprint(headers ?? {}, ua);
|
|
2207
2455
|
const h2settings = extractHttp2Settings(req);
|
|
2208
2456
|
const upstreamTls = extractUpstreamTls(headers ?? {});
|
|
2457
|
+
const cookieHdrF = headers?.["cookie"];
|
|
2458
|
+
const cookieTokF = cookieHdrF ? cookieHdrF.split(";").map((c) => c.trim()).find((c) => c.startsWith("anomira_fp="))?.slice("anomira_fp=".length) ?? "" : "";
|
|
2459
|
+
const bfpRawF = cookieTokF || headers?.["x-anomira-fp"] || "";
|
|
2460
|
+
let browserFpF = null;
|
|
2461
|
+
if (bfpRawF) {
|
|
2462
|
+
try {
|
|
2463
|
+
const raw = JSON.parse(Buffer.from(bfpRawF, "base64").toString("utf8"));
|
|
2464
|
+
if (typeof raw["v"] === "number" && typeof raw["fp"] === "string") {
|
|
2465
|
+
browserFpF = {
|
|
2466
|
+
fp: raw["fp"],
|
|
2467
|
+
bot: raw["bot"] ?? 0,
|
|
2468
|
+
sigs: (raw["sigs"] ?? []).join(","),
|
|
2469
|
+
uid: typeof raw["uid"] === "string" && raw["uid"] ? raw["uid"] : void 0,
|
|
2470
|
+
pst: raw["frm"]?.pst,
|
|
2471
|
+
ttf: raw["frm"]?.ttf,
|
|
2472
|
+
tts: raw["frm"]?.tts
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
} catch {
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
const userId = serverUserId || browserFpF?.uid || "";
|
|
2209
2479
|
client.track(EventName.REQUEST, {
|
|
2210
2480
|
ip,
|
|
2211
2481
|
userId,
|
|
@@ -2219,6 +2489,14 @@ function createFastifyPlugin(client) {
|
|
|
2219
2489
|
_fp: fingerprint.score,
|
|
2220
2490
|
_fpSig: fingerprint.signals.join(","),
|
|
2221
2491
|
...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
|
|
2492
|
+
...browserFpF ? {
|
|
2493
|
+
_bfp: browserFpF.fp,
|
|
2494
|
+
_bbot: browserFpF.bot,
|
|
2495
|
+
_bsigs: browserFpF.sigs,
|
|
2496
|
+
...browserFpF.pst !== void 0 ? { _bpaste: browserFpF.pst } : {},
|
|
2497
|
+
...browserFpF.ttf !== void 0 && browserFpF.ttf >= 0 ? { _bttf: browserFpF.ttf } : {},
|
|
2498
|
+
...browserFpF.tts !== void 0 && browserFpF.tts >= 0 ? { _btts: browserFpF.tts } : {}
|
|
2499
|
+
} : {},
|
|
2222
2500
|
...h2settings ? {
|
|
2223
2501
|
_h2Window: h2settings.initialWindowSize,
|
|
2224
2502
|
_h2HdrTable: h2settings.headerTableSize,
|