@anomira/node-sdk 0.2.2 → 0.2.3
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/dist/index.cjs +106 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +106 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -624,7 +624,7 @@ function detectHoneypotType(path) {
|
|
|
624
624
|
function generateHoneypotResponse(type, canaryToken, callbackBase, orgId) {
|
|
625
625
|
switch (type) {
|
|
626
626
|
case "env_file":
|
|
627
|
-
return makeEnvFile(canaryToken
|
|
627
|
+
return makeEnvFile(canaryToken);
|
|
628
628
|
case "aws_credentials":
|
|
629
629
|
return makeAwsCredentials(canaryToken);
|
|
630
630
|
case "git_config":
|
|
@@ -645,14 +645,16 @@ function generateHoneypotResponse(type, canaryToken, callbackBase, orgId) {
|
|
|
645
645
|
return makeGeneric(canaryToken);
|
|
646
646
|
}
|
|
647
647
|
}
|
|
648
|
-
function makeEnvFile(canaryToken,
|
|
648
|
+
function makeEnvFile(canaryToken, _callbackBase, _orgId) {
|
|
649
649
|
const jwtSecret = genHex(32);
|
|
650
650
|
const dbPassword = genAlphanumeric(20);
|
|
651
651
|
const redisPassword = genAlphanumeric(16);
|
|
652
652
|
const apiKey = genHex(32);
|
|
653
|
-
const webhookUrl = `${callbackBase}/v1/canary/${orgId}/${canaryToken}`;
|
|
654
653
|
const dnsDb = `db.${canaryToken}.srv.anomira.io`;
|
|
655
654
|
const dnsCache = `cache.${canaryToken}.srv.anomira.io`;
|
|
655
|
+
const dnsMonitoring = `hooks.monitoring.${canaryToken}.srv.anomira.io`;
|
|
656
|
+
const dnsAlerts = `hooks.alerts.${canaryToken}.srv.anomira.io`;
|
|
657
|
+
const dnsDeploy = `deploy.internal.${canaryToken}.srv.anomira.io`;
|
|
656
658
|
const fakeJwt = generateCanaryJwt(canaryToken, jwtSecret);
|
|
657
659
|
const body = `# Production Environment Configuration
|
|
658
660
|
# Last updated: ${new Date(Date.now() - 7 * 864e5).toISOString().split("T")[0]}
|
|
@@ -683,9 +685,9 @@ AWS_REGION=eu-west-1
|
|
|
683
685
|
AWS_S3_BUCKET=app-production-${genHex(4)}
|
|
684
686
|
|
|
685
687
|
# \u2500\u2500 Monitoring / Webhooks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
686
|
-
MONITORING_WEBHOOK
|
|
687
|
-
ALERT_WEBHOOK
|
|
688
|
-
DEPLOY_HOOK
|
|
688
|
+
MONITORING_WEBHOOK=https://${dnsMonitoring}/v1/events/ingest
|
|
689
|
+
ALERT_WEBHOOK=https://${dnsAlerts}/services/${genHex(7).toUpperCase()}/notify
|
|
690
|
+
DEPLOY_HOOK=https://${dnsDeploy}/hooks/deploy/${genHex(6)}
|
|
689
691
|
|
|
690
692
|
# \u2500\u2500 External APIs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
691
693
|
INTERNAL_API_KEY=${apiKey}
|
|
@@ -1097,7 +1099,8 @@ function makeGeneric(canaryToken) {
|
|
|
1097
1099
|
}
|
|
1098
1100
|
function generateCanaryJwt(canaryToken, secret) {
|
|
1099
1101
|
const now = Math.floor(Date.now() / 1e3);
|
|
1100
|
-
const
|
|
1102
|
+
const kid = `${canaryToken.slice(0, 8)}-${canaryToken.slice(8, 12)}-4${canaryToken.slice(13, 16)}-${canaryToken.slice(16, 20)}-${canaryToken.slice(20, 32)}`;
|
|
1103
|
+
const header = { alg: "HS256", typ: "JWT", kid };
|
|
1101
1104
|
const payload = {
|
|
1102
1105
|
sub: "svc_internal_7482",
|
|
1103
1106
|
name: "service-account",
|
|
@@ -1107,7 +1110,8 @@ function generateCanaryJwt(canaryToken, secret) {
|
|
|
1107
1110
|
// issued 48h ago (realistic stale credential)
|
|
1108
1111
|
exp: now - 86400,
|
|
1109
1112
|
// expired 24h ago
|
|
1110
|
-
jti:
|
|
1113
|
+
jti: canaryToken
|
|
1114
|
+
// raw hex — no "canary-" prefix to leak purpose
|
|
1111
1115
|
};
|
|
1112
1116
|
const h = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
1113
1117
|
const p = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
@@ -1769,6 +1773,12 @@ function defaultGetUserId(req) {
|
|
|
1769
1773
|
);
|
|
1770
1774
|
const jwtId = pickId(payload);
|
|
1771
1775
|
if (jwtId) return jwtId;
|
|
1776
|
+
for (const val of Object.values(payload)) {
|
|
1777
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1778
|
+
const nested = pickId(val);
|
|
1779
|
+
if (nested) return nested;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1772
1782
|
}
|
|
1773
1783
|
} catch {
|
|
1774
1784
|
}
|
|
@@ -1791,15 +1801,21 @@ function normalizeIp(raw) {
|
|
|
1791
1801
|
function defaultGetIp(req) {
|
|
1792
1802
|
const r = req;
|
|
1793
1803
|
const fwd = r["headers"];
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1804
|
+
function firstHdr(name) {
|
|
1805
|
+
const v = fwd?.[name];
|
|
1806
|
+
if (!v) return void 0;
|
|
1807
|
+
const s = (Array.isArray(v) ? v[0] : v.split(",")[0])?.trim();
|
|
1808
|
+
return s || void 0;
|
|
1798
1809
|
}
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1810
|
+
const ip = firstHdr("cf-connecting-ip") ?? // Cloudflare
|
|
1811
|
+
firstHdr("true-client-ip") ?? // Cloudflare Enterprise / Akamai
|
|
1812
|
+
firstHdr("x-forwarded-for") ?? // Nginx, AWS ALB, GCP LB, most proxies
|
|
1813
|
+
firstHdr("x-real-ip") ?? // Nginx (single-IP alternative to XFF)
|
|
1814
|
+
firstHdr("fastly-client-ip") ?? // Fastly CDN
|
|
1815
|
+
firstHdr("x-client-ip") ?? // Generic reverse proxies
|
|
1816
|
+
firstHdr("x-cluster-client-ip") ?? // Cluster / k8s ingress
|
|
1817
|
+
r["socket"]?.remoteAddress ?? "0.0.0.0";
|
|
1818
|
+
return normalizeIp(ip);
|
|
1803
1819
|
}
|
|
1804
1820
|
function sendFakeResponse(res, fakeResponse) {
|
|
1805
1821
|
const allHeaders = {
|
|
@@ -1822,10 +1838,29 @@ function sendFakeResponse(res, fakeResponse) {
|
|
|
1822
1838
|
res["end"](fakeResponse.body);
|
|
1823
1839
|
}
|
|
1824
1840
|
}
|
|
1841
|
+
function isLoopbackIp(ip) {
|
|
1842
|
+
return ip === "127.0.0.1" || ip === "0.0.0.0" || ip === "::1";
|
|
1843
|
+
}
|
|
1825
1844
|
function createExpressMiddleware(client) {
|
|
1845
|
+
let ipCheckCount = 0;
|
|
1846
|
+
let ipLoopCount = 0;
|
|
1847
|
+
let ipWarnFired = false;
|
|
1848
|
+
let userIdCheckCount = 0;
|
|
1849
|
+
let userIdMissCount = 0;
|
|
1850
|
+
let userIdWarnFired = false;
|
|
1826
1851
|
return async function sentinelMiddleware(req, res, next) {
|
|
1827
1852
|
const startMs = Date.now();
|
|
1828
1853
|
const ip = client.config.getIp(req);
|
|
1854
|
+
if (!ipWarnFired && ipCheckCount < 20) {
|
|
1855
|
+
ipCheckCount++;
|
|
1856
|
+
if (isLoopbackIp(ip)) ipLoopCount++;
|
|
1857
|
+
if (ipCheckCount === 20 && ipLoopCount >= 16) {
|
|
1858
|
+
ipWarnFired = true;
|
|
1859
|
+
console.warn(
|
|
1860
|
+
"[Anomira] WARNING: client IP not captured on 80%+ of requests.\n Your app is likely behind a reverse proxy (Nginx, Cloudflare, AWS ALB)\n that is not forwarding client IP headers. Alerts will have no IP attribution.\n\n Nginx fix: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Real-IP $remote_addr;\n Express fix: app.set('trust proxy', 1);\n Fastify fix: Fastify({ trustProxy: true })\n Docs: https://docs.anomira.io/sdk/ip-capture"
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1829
1864
|
if (client.isBlocked(ip)) {
|
|
1830
1865
|
const method_ = req["method"]?.toUpperCase() ?? "GET";
|
|
1831
1866
|
const url_ = req["originalUrl"] ?? req["url"] ?? "/";
|
|
@@ -1882,12 +1917,12 @@ function createExpressMiddleware(client) {
|
|
|
1882
1917
|
const b64p = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
1883
1918
|
const pay = JSON.parse(Buffer.from(b64p + "==", "base64").toString());
|
|
1884
1919
|
const jti = pay["jti"] ?? "";
|
|
1885
|
-
const
|
|
1886
|
-
if (jti
|
|
1920
|
+
const cache = client["canaryTokenCache"];
|
|
1921
|
+
if (jti && cache.has(jti)) {
|
|
1887
1922
|
client.track("http.canary.triggered", {
|
|
1888
1923
|
ip,
|
|
1889
1924
|
userId,
|
|
1890
|
-
meta: { endpoint: url, method, token: jti.slice(
|
|
1925
|
+
meta: { endpoint: url, method, token: jti.slice(0, 16), source: "canary_jwt" }
|
|
1891
1926
|
});
|
|
1892
1927
|
}
|
|
1893
1928
|
}
|
|
@@ -1975,13 +2010,24 @@ function createExpressMiddleware(client) {
|
|
|
1975
2010
|
}
|
|
1976
2011
|
}
|
|
1977
2012
|
const onFinish = () => {
|
|
2013
|
+
const lateUserId = client.config.getUserId(req);
|
|
2014
|
+
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
2015
|
+
userIdCheckCount++;
|
|
2016
|
+
if (!lateUserId) userIdMissCount++;
|
|
2017
|
+
if (userIdCheckCount === 20 && userIdMissCount >= 18) {
|
|
2018
|
+
userIdWarnFired = true;
|
|
2019
|
+
console.warn(
|
|
2020
|
+
"[Anomira] WARNING: userId not captured on 90%+ of requests.\n EWS Evidence Package, geo-velocity, and account takeover detection\n silently stop working without it. The SDK tried 5 auto-detection tiers\n (Passport / express-jwt, req.auth, direct req.userId, session, JWT Bearer)\n \u2014 none matched your auth setup.\n\n Fix: pass a getUserId resolver that matches your auth middleware:\n new Anomira({ ..., getUserId: (req) => req.user?.id })"
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
1978
2024
|
const status = res["statusCode"] ?? 0;
|
|
1979
2025
|
const latencyMs = Date.now() - startMs;
|
|
1980
2026
|
const getHeader = res["getHeader"];
|
|
1981
2027
|
const bytes = typeof getHeader === "function" ? parseInt(getHeader.call(res, "content-length") ?? "0", 10) || 0 : 0;
|
|
1982
2028
|
client.track(EventName.REQUEST, {
|
|
1983
2029
|
ip,
|
|
1984
|
-
userId,
|
|
2030
|
+
userId: lateUserId,
|
|
1985
2031
|
meta: {
|
|
1986
2032
|
method,
|
|
1987
2033
|
endpoint: url,
|
|
@@ -1992,14 +2038,12 @@ function createExpressMiddleware(client) {
|
|
|
1992
2038
|
_fp: fingerprint.score,
|
|
1993
2039
|
_fpSig: fingerprint.signals.join(","),
|
|
1994
2040
|
...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
|
|
1995
|
-
// HTTP/2 SETTINGS from client connection preface (when Node.js is TLS endpoint)
|
|
1996
2041
|
...h2settings ? {
|
|
1997
2042
|
_h2Window: h2settings.initialWindowSize,
|
|
1998
2043
|
_h2HdrTable: h2settings.headerTableSize,
|
|
1999
2044
|
_h2Score: h2settings.score,
|
|
2000
2045
|
_h2Signals: h2settings.signals.join(",")
|
|
2001
2046
|
} : {},
|
|
2002
|
-
// JA3/JA4 from upstream proxy (Nginx with ngx_ssl_fingerprint_module)
|
|
2003
2047
|
...upstreamTls.ja3 ? { _ja3: upstreamTls.ja3 } : {},
|
|
2004
2048
|
...upstreamTls.ja4 ? { _ja4: upstreamTls.ja4 } : {},
|
|
2005
2049
|
...agentInfo.isAgent ? {
|
|
@@ -2016,7 +2060,7 @@ function createExpressMiddleware(client) {
|
|
|
2016
2060
|
if (agentInfo.isAgent) {
|
|
2017
2061
|
client.track("http.agent_detected", {
|
|
2018
2062
|
ip,
|
|
2019
|
-
userId,
|
|
2063
|
+
userId: lateUserId,
|
|
2020
2064
|
meta: {
|
|
2021
2065
|
endpoint: url,
|
|
2022
2066
|
method,
|
|
@@ -2032,17 +2076,17 @@ function createExpressMiddleware(client) {
|
|
|
2032
2076
|
});
|
|
2033
2077
|
}
|
|
2034
2078
|
if (client.config.detect.rateAbuse && status === 429) {
|
|
2035
|
-
client.track(EventName.RATE_LIMIT, { ip, userId, meta: { url, method, statusCode: status } });
|
|
2079
|
+
client.track(EventName.RATE_LIMIT, { ip, userId: lateUserId, meta: { url, method, statusCode: status } });
|
|
2036
2080
|
}
|
|
2037
2081
|
if (client.config.detect.bruteForce && status === 401) {
|
|
2038
2082
|
if (/\/(login|signin|auth|token|session)/i.test(url)) {
|
|
2039
|
-
client.track(EventName.LOGIN_FAILED, { ip, userId, meta: { url, method, statusCode: status } });
|
|
2083
|
+
client.track(EventName.LOGIN_FAILED, { ip, userId: lateUserId, meta: { url, method, statusCode: status } });
|
|
2040
2084
|
}
|
|
2041
2085
|
}
|
|
2042
2086
|
if (client.config.detect.scanDetection && status === 404) {
|
|
2043
2087
|
const looksLikeScanner = !ua || /curl|wget|python|go-http|nuclei|sqlmap|nikto/i.test(ua);
|
|
2044
2088
|
if (looksLikeScanner) {
|
|
2045
|
-
client.track(EventName.SCAN_DETECTED, { ip, userId, meta: { url, method, userAgent: ua } });
|
|
2089
|
+
client.track(EventName.SCAN_DETECTED, { ip, userId: lateUserId, meta: { url, method, userAgent: ua } });
|
|
2046
2090
|
}
|
|
2047
2091
|
}
|
|
2048
2092
|
cleanup();
|
|
@@ -2055,9 +2099,25 @@ function createExpressMiddleware(client) {
|
|
|
2055
2099
|
};
|
|
2056
2100
|
}
|
|
2057
2101
|
function createFastifyPlugin(client) {
|
|
2102
|
+
let ipCheckCount = 0;
|
|
2103
|
+
let ipLoopCount = 0;
|
|
2104
|
+
let ipWarnFired = false;
|
|
2105
|
+
let userIdCheckCount = 0;
|
|
2106
|
+
let userIdMissCount = 0;
|
|
2107
|
+
let userIdWarnFired = false;
|
|
2058
2108
|
return async function sentinelFastifyPlugin(fastify) {
|
|
2059
2109
|
fastify.addHook("onRequest", (req, reply) => {
|
|
2060
2110
|
const ip = client.config.getIp(req);
|
|
2111
|
+
if (!ipWarnFired && ipCheckCount < 20) {
|
|
2112
|
+
ipCheckCount++;
|
|
2113
|
+
if (isLoopbackIp(ip)) ipLoopCount++;
|
|
2114
|
+
if (ipCheckCount === 20 && ipLoopCount >= 16) {
|
|
2115
|
+
ipWarnFired = true;
|
|
2116
|
+
console.warn(
|
|
2117
|
+
"[Anomira] WARNING: client IP not captured on 80%+ of requests.\n Your app is likely behind a reverse proxy (Nginx, Cloudflare, AWS ALB)\n that is not forwarding client IP headers. Alerts will have no IP attribution.\n\n Nginx fix: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Real-IP $remote_addr;\n Fastify fix: Fastify({ trustProxy: true })\n Docs: https://docs.anomira.io/sdk/ip-capture"
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2061
2121
|
const url_ = req["url"] ?? "/";
|
|
2062
2122
|
const method_ = req["method"]?.toUpperCase() ?? "GET";
|
|
2063
2123
|
const hdrs_ = req["headers"];
|
|
@@ -2129,6 +2189,16 @@ function createFastifyPlugin(client) {
|
|
|
2129
2189
|
const method = req["method"]?.toUpperCase() ?? "GET";
|
|
2130
2190
|
const userId = client.config.getUserId(req);
|
|
2131
2191
|
const status = reply["statusCode"] ?? 0;
|
|
2192
|
+
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
2193
|
+
userIdCheckCount++;
|
|
2194
|
+
if (!userId) userIdMissCount++;
|
|
2195
|
+
if (userIdCheckCount === 20 && userIdMissCount >= 18) {
|
|
2196
|
+
userIdWarnFired = true;
|
|
2197
|
+
console.warn(
|
|
2198
|
+
"[Anomira] WARNING: userId not captured on 90%+ of requests.\n EWS Evidence Package, geo-velocity, and account takeover detection\n silently stop working without it. The SDK tried 5 auto-detection tiers\n (Passport / @fastify/jwt, req.auth, direct req.userId, session, JWT Bearer)\n \u2014 none matched your auth setup.\n\n Fix: pass a getUserId resolver that matches your auth middleware:\n new Anomira({ ..., getUserId: (req) => (req as any).user?.id })"
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2132
2202
|
const headers = req["headers"];
|
|
2133
2203
|
const ua = (typeof headers?.["user-agent"] === "string" ? headers["user-agent"] : "") ?? "";
|
|
2134
2204
|
const latencyMs = reply["elapsedTime"] ?? 0;
|
|
@@ -2212,6 +2282,11 @@ function createExpressMiddleware2(client) {
|
|
|
2212
2282
|
const ip = client.config.getIp(req);
|
|
2213
2283
|
const userId = client.config.getUserId(req);
|
|
2214
2284
|
const { method, originalUrl: url } = req;
|
|
2285
|
+
if (client.isBlocked(ip)) {
|
|
2286
|
+
client.reportBlockedHit(ip, { method, url, userAgent: req.headers["user-agent"] ?? "" });
|
|
2287
|
+
res.status(403).json({ error: "Forbidden" });
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2215
2290
|
if (client.config.detect.pathTraversal && (url.includes("../") || url.includes("..%2F"))) {
|
|
2216
2291
|
client.track(EventName.PATH_TRAVERSAL, {
|
|
2217
2292
|
ip,
|
|
@@ -2276,11 +2351,15 @@ function createExpressMiddleware2(client) {
|
|
|
2276
2351
|
// src/middleware/fastify.ts
|
|
2277
2352
|
function createFastifyPlugin2(client) {
|
|
2278
2353
|
return async function sentinelPlugin(fastify) {
|
|
2279
|
-
fastify.addHook("onRequest", async (req,
|
|
2354
|
+
fastify.addHook("onRequest", async (req, reply) => {
|
|
2280
2355
|
const ip = client.config.getIp(req);
|
|
2281
2356
|
const userId = client.config.getUserId(req);
|
|
2282
2357
|
const url = req.url;
|
|
2283
2358
|
const method = req.method.toUpperCase();
|
|
2359
|
+
if (client.isBlocked(ip)) {
|
|
2360
|
+
client.reportBlockedHit(ip, { method, url, userAgent: req.headers["user-agent"] ?? "" });
|
|
2361
|
+
return reply.code(403).send({ error: "Forbidden" });
|
|
2362
|
+
}
|
|
2284
2363
|
if (client.config.detect.pathTraversal && (url.includes("../") || url.includes("..%2F"))) {
|
|
2285
2364
|
client.track(EventName.PATH_TRAVERSAL, { ip, userId, meta: { url, method } });
|
|
2286
2365
|
}
|