@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.js
CHANGED
|
@@ -620,7 +620,7 @@ function detectHoneypotType(path) {
|
|
|
620
620
|
function generateHoneypotResponse(type, canaryToken, callbackBase, orgId) {
|
|
621
621
|
switch (type) {
|
|
622
622
|
case "env_file":
|
|
623
|
-
return makeEnvFile(canaryToken
|
|
623
|
+
return makeEnvFile(canaryToken);
|
|
624
624
|
case "aws_credentials":
|
|
625
625
|
return makeAwsCredentials(canaryToken);
|
|
626
626
|
case "git_config":
|
|
@@ -641,14 +641,16 @@ function generateHoneypotResponse(type, canaryToken, callbackBase, orgId) {
|
|
|
641
641
|
return makeGeneric(canaryToken);
|
|
642
642
|
}
|
|
643
643
|
}
|
|
644
|
-
function makeEnvFile(canaryToken,
|
|
644
|
+
function makeEnvFile(canaryToken, _callbackBase, _orgId) {
|
|
645
645
|
const jwtSecret = genHex(32);
|
|
646
646
|
const dbPassword = genAlphanumeric(20);
|
|
647
647
|
const redisPassword = genAlphanumeric(16);
|
|
648
648
|
const apiKey = genHex(32);
|
|
649
|
-
const webhookUrl = `${callbackBase}/v1/canary/${orgId}/${canaryToken}`;
|
|
650
649
|
const dnsDb = `db.${canaryToken}.srv.anomira.io`;
|
|
651
650
|
const dnsCache = `cache.${canaryToken}.srv.anomira.io`;
|
|
651
|
+
const dnsMonitoring = `hooks.monitoring.${canaryToken}.srv.anomira.io`;
|
|
652
|
+
const dnsAlerts = `hooks.alerts.${canaryToken}.srv.anomira.io`;
|
|
653
|
+
const dnsDeploy = `deploy.internal.${canaryToken}.srv.anomira.io`;
|
|
652
654
|
const fakeJwt = generateCanaryJwt(canaryToken, jwtSecret);
|
|
653
655
|
const body = `# Production Environment Configuration
|
|
654
656
|
# Last updated: ${new Date(Date.now() - 7 * 864e5).toISOString().split("T")[0]}
|
|
@@ -679,9 +681,9 @@ AWS_REGION=eu-west-1
|
|
|
679
681
|
AWS_S3_BUCKET=app-production-${genHex(4)}
|
|
680
682
|
|
|
681
683
|
# \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
|
|
682
|
-
MONITORING_WEBHOOK
|
|
683
|
-
ALERT_WEBHOOK
|
|
684
|
-
DEPLOY_HOOK
|
|
684
|
+
MONITORING_WEBHOOK=https://${dnsMonitoring}/v1/events/ingest
|
|
685
|
+
ALERT_WEBHOOK=https://${dnsAlerts}/services/${genHex(7).toUpperCase()}/notify
|
|
686
|
+
DEPLOY_HOOK=https://${dnsDeploy}/hooks/deploy/${genHex(6)}
|
|
685
687
|
|
|
686
688
|
# \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
|
|
687
689
|
INTERNAL_API_KEY=${apiKey}
|
|
@@ -1093,7 +1095,8 @@ function makeGeneric(canaryToken) {
|
|
|
1093
1095
|
}
|
|
1094
1096
|
function generateCanaryJwt(canaryToken, secret) {
|
|
1095
1097
|
const now = Math.floor(Date.now() / 1e3);
|
|
1096
|
-
const
|
|
1098
|
+
const kid = `${canaryToken.slice(0, 8)}-${canaryToken.slice(8, 12)}-4${canaryToken.slice(13, 16)}-${canaryToken.slice(16, 20)}-${canaryToken.slice(20, 32)}`;
|
|
1099
|
+
const header = { alg: "HS256", typ: "JWT", kid };
|
|
1097
1100
|
const payload = {
|
|
1098
1101
|
sub: "svc_internal_7482",
|
|
1099
1102
|
name: "service-account",
|
|
@@ -1103,7 +1106,8 @@ function generateCanaryJwt(canaryToken, secret) {
|
|
|
1103
1106
|
// issued 48h ago (realistic stale credential)
|
|
1104
1107
|
exp: now - 86400,
|
|
1105
1108
|
// expired 24h ago
|
|
1106
|
-
jti:
|
|
1109
|
+
jti: canaryToken
|
|
1110
|
+
// raw hex — no "canary-" prefix to leak purpose
|
|
1107
1111
|
};
|
|
1108
1112
|
const h = Buffer.from(JSON.stringify(header)).toString("base64url");
|
|
1109
1113
|
const p = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
@@ -1765,6 +1769,12 @@ function defaultGetUserId(req) {
|
|
|
1765
1769
|
);
|
|
1766
1770
|
const jwtId = pickId(payload);
|
|
1767
1771
|
if (jwtId) return jwtId;
|
|
1772
|
+
for (const val of Object.values(payload)) {
|
|
1773
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1774
|
+
const nested = pickId(val);
|
|
1775
|
+
if (nested) return nested;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1768
1778
|
}
|
|
1769
1779
|
} catch {
|
|
1770
1780
|
}
|
|
@@ -1787,15 +1797,21 @@ function normalizeIp(raw) {
|
|
|
1787
1797
|
function defaultGetIp(req) {
|
|
1788
1798
|
const r = req;
|
|
1789
1799
|
const fwd = r["headers"];
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1800
|
+
function firstHdr(name) {
|
|
1801
|
+
const v = fwd?.[name];
|
|
1802
|
+
if (!v) return void 0;
|
|
1803
|
+
const s = (Array.isArray(v) ? v[0] : v.split(",")[0])?.trim();
|
|
1804
|
+
return s || void 0;
|
|
1794
1805
|
}
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1806
|
+
const ip = firstHdr("cf-connecting-ip") ?? // Cloudflare
|
|
1807
|
+
firstHdr("true-client-ip") ?? // Cloudflare Enterprise / Akamai
|
|
1808
|
+
firstHdr("x-forwarded-for") ?? // Nginx, AWS ALB, GCP LB, most proxies
|
|
1809
|
+
firstHdr("x-real-ip") ?? // Nginx (single-IP alternative to XFF)
|
|
1810
|
+
firstHdr("fastly-client-ip") ?? // Fastly CDN
|
|
1811
|
+
firstHdr("x-client-ip") ?? // Generic reverse proxies
|
|
1812
|
+
firstHdr("x-cluster-client-ip") ?? // Cluster / k8s ingress
|
|
1813
|
+
r["socket"]?.remoteAddress ?? "0.0.0.0";
|
|
1814
|
+
return normalizeIp(ip);
|
|
1799
1815
|
}
|
|
1800
1816
|
function sendFakeResponse(res, fakeResponse) {
|
|
1801
1817
|
const allHeaders = {
|
|
@@ -1818,10 +1834,29 @@ function sendFakeResponse(res, fakeResponse) {
|
|
|
1818
1834
|
res["end"](fakeResponse.body);
|
|
1819
1835
|
}
|
|
1820
1836
|
}
|
|
1837
|
+
function isLoopbackIp(ip) {
|
|
1838
|
+
return ip === "127.0.0.1" || ip === "0.0.0.0" || ip === "::1";
|
|
1839
|
+
}
|
|
1821
1840
|
function createExpressMiddleware(client) {
|
|
1841
|
+
let ipCheckCount = 0;
|
|
1842
|
+
let ipLoopCount = 0;
|
|
1843
|
+
let ipWarnFired = false;
|
|
1844
|
+
let userIdCheckCount = 0;
|
|
1845
|
+
let userIdMissCount = 0;
|
|
1846
|
+
let userIdWarnFired = false;
|
|
1822
1847
|
return async function sentinelMiddleware(req, res, next) {
|
|
1823
1848
|
const startMs = Date.now();
|
|
1824
1849
|
const ip = client.config.getIp(req);
|
|
1850
|
+
if (!ipWarnFired && ipCheckCount < 20) {
|
|
1851
|
+
ipCheckCount++;
|
|
1852
|
+
if (isLoopbackIp(ip)) ipLoopCount++;
|
|
1853
|
+
if (ipCheckCount === 20 && ipLoopCount >= 16) {
|
|
1854
|
+
ipWarnFired = true;
|
|
1855
|
+
console.warn(
|
|
1856
|
+
"[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"
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1825
1860
|
if (client.isBlocked(ip)) {
|
|
1826
1861
|
const method_ = req["method"]?.toUpperCase() ?? "GET";
|
|
1827
1862
|
const url_ = req["originalUrl"] ?? req["url"] ?? "/";
|
|
@@ -1878,12 +1913,12 @@ function createExpressMiddleware(client) {
|
|
|
1878
1913
|
const b64p = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
1879
1914
|
const pay = JSON.parse(Buffer.from(b64p + "==", "base64").toString());
|
|
1880
1915
|
const jti = pay["jti"] ?? "";
|
|
1881
|
-
const
|
|
1882
|
-
if (jti
|
|
1916
|
+
const cache = client["canaryTokenCache"];
|
|
1917
|
+
if (jti && cache.has(jti)) {
|
|
1883
1918
|
client.track("http.canary.triggered", {
|
|
1884
1919
|
ip,
|
|
1885
1920
|
userId,
|
|
1886
|
-
meta: { endpoint: url, method, token: jti.slice(
|
|
1921
|
+
meta: { endpoint: url, method, token: jti.slice(0, 16), source: "canary_jwt" }
|
|
1887
1922
|
});
|
|
1888
1923
|
}
|
|
1889
1924
|
}
|
|
@@ -1971,13 +2006,24 @@ function createExpressMiddleware(client) {
|
|
|
1971
2006
|
}
|
|
1972
2007
|
}
|
|
1973
2008
|
const onFinish = () => {
|
|
2009
|
+
const lateUserId = client.config.getUserId(req);
|
|
2010
|
+
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
2011
|
+
userIdCheckCount++;
|
|
2012
|
+
if (!lateUserId) userIdMissCount++;
|
|
2013
|
+
if (userIdCheckCount === 20 && userIdMissCount >= 18) {
|
|
2014
|
+
userIdWarnFired = true;
|
|
2015
|
+
console.warn(
|
|
2016
|
+
"[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 })"
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
1974
2020
|
const status = res["statusCode"] ?? 0;
|
|
1975
2021
|
const latencyMs = Date.now() - startMs;
|
|
1976
2022
|
const getHeader = res["getHeader"];
|
|
1977
2023
|
const bytes = typeof getHeader === "function" ? parseInt(getHeader.call(res, "content-length") ?? "0", 10) || 0 : 0;
|
|
1978
2024
|
client.track(EventName.REQUEST, {
|
|
1979
2025
|
ip,
|
|
1980
|
-
userId,
|
|
2026
|
+
userId: lateUserId,
|
|
1981
2027
|
meta: {
|
|
1982
2028
|
method,
|
|
1983
2029
|
endpoint: url,
|
|
@@ -1988,14 +2034,12 @@ function createExpressMiddleware(client) {
|
|
|
1988
2034
|
_fp: fingerprint.score,
|
|
1989
2035
|
_fpSig: fingerprint.signals.join(","),
|
|
1990
2036
|
...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
|
|
1991
|
-
// HTTP/2 SETTINGS from client connection preface (when Node.js is TLS endpoint)
|
|
1992
2037
|
...h2settings ? {
|
|
1993
2038
|
_h2Window: h2settings.initialWindowSize,
|
|
1994
2039
|
_h2HdrTable: h2settings.headerTableSize,
|
|
1995
2040
|
_h2Score: h2settings.score,
|
|
1996
2041
|
_h2Signals: h2settings.signals.join(",")
|
|
1997
2042
|
} : {},
|
|
1998
|
-
// JA3/JA4 from upstream proxy (Nginx with ngx_ssl_fingerprint_module)
|
|
1999
2043
|
...upstreamTls.ja3 ? { _ja3: upstreamTls.ja3 } : {},
|
|
2000
2044
|
...upstreamTls.ja4 ? { _ja4: upstreamTls.ja4 } : {},
|
|
2001
2045
|
...agentInfo.isAgent ? {
|
|
@@ -2012,7 +2056,7 @@ function createExpressMiddleware(client) {
|
|
|
2012
2056
|
if (agentInfo.isAgent) {
|
|
2013
2057
|
client.track("http.agent_detected", {
|
|
2014
2058
|
ip,
|
|
2015
|
-
userId,
|
|
2059
|
+
userId: lateUserId,
|
|
2016
2060
|
meta: {
|
|
2017
2061
|
endpoint: url,
|
|
2018
2062
|
method,
|
|
@@ -2028,17 +2072,17 @@ function createExpressMiddleware(client) {
|
|
|
2028
2072
|
});
|
|
2029
2073
|
}
|
|
2030
2074
|
if (client.config.detect.rateAbuse && status === 429) {
|
|
2031
|
-
client.track(EventName.RATE_LIMIT, { ip, userId, meta: { url, method, statusCode: status } });
|
|
2075
|
+
client.track(EventName.RATE_LIMIT, { ip, userId: lateUserId, meta: { url, method, statusCode: status } });
|
|
2032
2076
|
}
|
|
2033
2077
|
if (client.config.detect.bruteForce && status === 401) {
|
|
2034
2078
|
if (/\/(login|signin|auth|token|session)/i.test(url)) {
|
|
2035
|
-
client.track(EventName.LOGIN_FAILED, { ip, userId, meta: { url, method, statusCode: status } });
|
|
2079
|
+
client.track(EventName.LOGIN_FAILED, { ip, userId: lateUserId, meta: { url, method, statusCode: status } });
|
|
2036
2080
|
}
|
|
2037
2081
|
}
|
|
2038
2082
|
if (client.config.detect.scanDetection && status === 404) {
|
|
2039
2083
|
const looksLikeScanner = !ua || /curl|wget|python|go-http|nuclei|sqlmap|nikto/i.test(ua);
|
|
2040
2084
|
if (looksLikeScanner) {
|
|
2041
|
-
client.track(EventName.SCAN_DETECTED, { ip, userId, meta: { url, method, userAgent: ua } });
|
|
2085
|
+
client.track(EventName.SCAN_DETECTED, { ip, userId: lateUserId, meta: { url, method, userAgent: ua } });
|
|
2042
2086
|
}
|
|
2043
2087
|
}
|
|
2044
2088
|
cleanup();
|
|
@@ -2051,9 +2095,25 @@ function createExpressMiddleware(client) {
|
|
|
2051
2095
|
};
|
|
2052
2096
|
}
|
|
2053
2097
|
function createFastifyPlugin(client) {
|
|
2098
|
+
let ipCheckCount = 0;
|
|
2099
|
+
let ipLoopCount = 0;
|
|
2100
|
+
let ipWarnFired = false;
|
|
2101
|
+
let userIdCheckCount = 0;
|
|
2102
|
+
let userIdMissCount = 0;
|
|
2103
|
+
let userIdWarnFired = false;
|
|
2054
2104
|
return async function sentinelFastifyPlugin(fastify) {
|
|
2055
2105
|
fastify.addHook("onRequest", (req, reply) => {
|
|
2056
2106
|
const ip = client.config.getIp(req);
|
|
2107
|
+
if (!ipWarnFired && ipCheckCount < 20) {
|
|
2108
|
+
ipCheckCount++;
|
|
2109
|
+
if (isLoopbackIp(ip)) ipLoopCount++;
|
|
2110
|
+
if (ipCheckCount === 20 && ipLoopCount >= 16) {
|
|
2111
|
+
ipWarnFired = true;
|
|
2112
|
+
console.warn(
|
|
2113
|
+
"[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"
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2057
2117
|
const url_ = req["url"] ?? "/";
|
|
2058
2118
|
const method_ = req["method"]?.toUpperCase() ?? "GET";
|
|
2059
2119
|
const hdrs_ = req["headers"];
|
|
@@ -2125,6 +2185,16 @@ function createFastifyPlugin(client) {
|
|
|
2125
2185
|
const method = req["method"]?.toUpperCase() ?? "GET";
|
|
2126
2186
|
const userId = client.config.getUserId(req);
|
|
2127
2187
|
const status = reply["statusCode"] ?? 0;
|
|
2188
|
+
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
2189
|
+
userIdCheckCount++;
|
|
2190
|
+
if (!userId) userIdMissCount++;
|
|
2191
|
+
if (userIdCheckCount === 20 && userIdMissCount >= 18) {
|
|
2192
|
+
userIdWarnFired = true;
|
|
2193
|
+
console.warn(
|
|
2194
|
+
"[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 })"
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2128
2198
|
const headers = req["headers"];
|
|
2129
2199
|
const ua = (typeof headers?.["user-agent"] === "string" ? headers["user-agent"] : "") ?? "";
|
|
2130
2200
|
const latencyMs = reply["elapsedTime"] ?? 0;
|
|
@@ -2208,6 +2278,11 @@ function createExpressMiddleware2(client) {
|
|
|
2208
2278
|
const ip = client.config.getIp(req);
|
|
2209
2279
|
const userId = client.config.getUserId(req);
|
|
2210
2280
|
const { method, originalUrl: url } = req;
|
|
2281
|
+
if (client.isBlocked(ip)) {
|
|
2282
|
+
client.reportBlockedHit(ip, { method, url, userAgent: req.headers["user-agent"] ?? "" });
|
|
2283
|
+
res.status(403).json({ error: "Forbidden" });
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2211
2286
|
if (client.config.detect.pathTraversal && (url.includes("../") || url.includes("..%2F"))) {
|
|
2212
2287
|
client.track(EventName.PATH_TRAVERSAL, {
|
|
2213
2288
|
ip,
|
|
@@ -2272,11 +2347,15 @@ function createExpressMiddleware2(client) {
|
|
|
2272
2347
|
// src/middleware/fastify.ts
|
|
2273
2348
|
function createFastifyPlugin2(client) {
|
|
2274
2349
|
return async function sentinelPlugin(fastify) {
|
|
2275
|
-
fastify.addHook("onRequest", async (req,
|
|
2350
|
+
fastify.addHook("onRequest", async (req, reply) => {
|
|
2276
2351
|
const ip = client.config.getIp(req);
|
|
2277
2352
|
const userId = client.config.getUserId(req);
|
|
2278
2353
|
const url = req.url;
|
|
2279
2354
|
const method = req.method.toUpperCase();
|
|
2355
|
+
if (client.isBlocked(ip)) {
|
|
2356
|
+
client.reportBlockedHit(ip, { method, url, userAgent: req.headers["user-agent"] ?? "" });
|
|
2357
|
+
return reply.code(403).send({ error: "Forbidden" });
|
|
2358
|
+
}
|
|
2280
2359
|
if (client.config.detect.pathTraversal && (url.includes("../") || url.includes("..%2F"))) {
|
|
2281
2360
|
client.track(EventName.PATH_TRAVERSAL, { ip, userId, meta: { url, method } });
|
|
2282
2361
|
}
|