@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 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, callbackBase, orgId);
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, callbackBase, orgId) {
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=${webhookUrl}
687
- ALERT_WEBHOOK=${webhookUrl}
688
- DEPLOY_HOOK=${callbackBase}/v1/canary/${orgId}/${canaryToken}
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 header = { alg: "HS256", typ: "JWT", kid: `canary-${canaryToken.slice(0, 12)}` };
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: `canary-${canaryToken}`
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
- const xff = fwd?.["x-forwarded-for"];
1795
- if (xff) {
1796
- const first = Array.isArray(xff) ? xff[0] : xff.split(",")[0];
1797
- if (first) return normalizeIp(first.trim());
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 xri = fwd?.["x-real-ip"];
1800
- if (typeof xri === "string") return normalizeIp(xri.trim());
1801
- const socket = r["socket"];
1802
- return normalizeIp(socket?.remoteAddress ?? "0.0.0.0");
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 kid = hdr["kid"] ?? "";
1886
- if (jti.startsWith("canary-") && client["canaryTokenCache"].has(jti.slice(7)) || kid.startsWith("canary-") && client["canaryTokenCache"].has(kid.slice(7))) {
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(7, 23), source: "canary_jwt" }
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, _reply) => {
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
  }