@anomira/node-sdk 0.2.0 → 0.2.2

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.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from 'async_hooks';
2
+ import { randomBytes, createHmac } from 'crypto';
2
3
 
3
4
  // src/client.ts
4
5
 
@@ -402,7 +403,737 @@ var EventName = {
402
403
  FIREWALL_FLAG: "http.firewall.flag"
403
404
  };
404
405
 
405
- // src/client.ts
406
+ // src/agent-detection.ts
407
+ var CONFIRMED_UA_PATTERNS = [
408
+ // OpenAI — https://developers.openai.com/api/docs/bots
409
+ [/GPTBot\//i, "openai", "GPTBot"],
410
+ [/ChatGPT-User\//i, "openai", "ChatGPT-User"],
411
+ [/OAI-SearchBot\//i, "openai", "OAI-SearchBot"],
412
+ [/OAI-AdsBot\//i, "openai", "OAI-AdsBot"],
413
+ // Anthropic — https://support.claude.com/en/articles/8896518
414
+ [/ClaudeBot\//i, "anthropic", "ClaudeBot"],
415
+ [/Claude-User\//i, "anthropic", "Claude-User"],
416
+ [/Claude-SearchBot\//i, "anthropic", "Claude-SearchBot"],
417
+ // Perplexity — https://docs.perplexity.ai/guides/bots
418
+ [/PerplexityBot\//i, "perplexity", "PerplexityBot"],
419
+ [/Perplexity-User\//i, "perplexity", "Perplexity-User"]
420
+ ];
421
+ var MCP_SESSION_HEADER = "mcp-session-id";
422
+ var RFC9421_SIG_HEADER = "signature-agent";
423
+ var MCP_ACCEPT_EXACT = "application/json, text/event-stream";
424
+ function detectAgent(headers) {
425
+ const signals = [];
426
+ let agentType = null;
427
+ let agentName = null;
428
+ let sessionId = null;
429
+ let isMcp = false;
430
+ let confidence = null;
431
+ const sigAgent = headerStr(headers[RFC9421_SIG_HEADER]);
432
+ if (sigAgent) {
433
+ signals.push("rfc9421_signature_agent");
434
+ confidence = "high";
435
+ agentType = sigAgent.includes("chatgpt.com") ? "openai" : "rfc9421_agent";
436
+ agentName = `rfc9421:${sigAgent.slice(0, 60)}`;
437
+ }
438
+ const mcpSession = headerStr(headers[MCP_SESSION_HEADER]);
439
+ if (mcpSession) {
440
+ signals.push("mcp_session_id");
441
+ isMcp = true;
442
+ sessionId = mcpSession;
443
+ confidence = "high";
444
+ if (!agentType) agentType = "mcp_client";
445
+ }
446
+ const accept = headerStr(headers["accept"]);
447
+ if (accept === MCP_ACCEPT_EXACT) {
448
+ signals.push("mcp_accept_header");
449
+ isMcp = true;
450
+ if (!confidence) confidence = "high";
451
+ if (!agentType) agentType = "mcp_client";
452
+ }
453
+ const ua = headerStr(headers["user-agent"]) ?? "";
454
+ for (const [pattern, type, name] of CONFIRMED_UA_PATTERNS) {
455
+ const match = ua.match(pattern);
456
+ if (match) {
457
+ signals.push(`ua_${name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`);
458
+ confidence = "high";
459
+ const tokenStart = ua.indexOf(match[0]);
460
+ const tokenEnd = ua.indexOf(" ", tokenStart);
461
+ agentName = tokenEnd > -1 ? ua.slice(tokenStart, tokenEnd) : match[0];
462
+ agentType = type;
463
+ break;
464
+ }
465
+ }
466
+ if (!agentType && /^python-httpx\//i.test(ua) && isMcp) {
467
+ signals.push("ua_python_httpx_with_mcp");
468
+ confidence = "medium";
469
+ agentType = "mcp_client";
470
+ agentName = ua.split(" ")[0] ?? "python-httpx";
471
+ }
472
+ const isAgent = confidence !== null;
473
+ return {
474
+ isAgent,
475
+ confidence: confidence ?? "medium",
476
+ agentType: isAgent ? agentType ?? "unknown_agent" : null,
477
+ agentName: isAgent ? agentName : null,
478
+ sessionId,
479
+ isMcp,
480
+ signals
481
+ };
482
+ }
483
+ function headerStr(v) {
484
+ if (v === void 0) return void 0;
485
+ return Array.isArray(v) ? v[0] : v;
486
+ }
487
+
488
+ // src/behavioral-fingerprint.ts
489
+ var KNOWN_BOT_UA = [
490
+ // Confirmed: psf/requests utils.py (default_headers)
491
+ [/^python-requests\//i, "python-requests"],
492
+ // Confirmed: encode/httpx tests/client/test_headers.py
493
+ [/^python-httpx\//i, "python-httpx"],
494
+ // Confirmed: aiohttp docs (format: "Python/3.x aiohttp/3.x.x")
495
+ [/\baiohttp\//i, "aiohttp"],
496
+ // Confirmed: nodejs/undici issue #1305
497
+ [/^undici$/i, "undici"],
498
+ // Confirmed: everything.curl.dev
499
+ [/^curl\//i, "curl"],
500
+ // Confirmed: GNU wget docs
501
+ [/^Wget\//i, "wget"],
502
+ // Confirmed: Go net/http DefaultClient (golang.org/pkg/net/http)
503
+ [/^Go-http-client\//i, "Go-http-client"],
504
+ // Confirmed: Java HttpURLConnection default
505
+ [/^Java\//i, "Java"],
506
+ // Confirmed: Scrapy docs (scrapy.org)
507
+ [/^Scrapy\//i, "Scrapy"],
508
+ // Confirmed: OkHttp (square.github.io/okhttp)
509
+ [/\bokhttp\//i, "okhttp"],
510
+ // Confirmed: libwww-perl (metacpan.org/pod/LWP)
511
+ [/^libwww-perl\//i, "libwww-perl"],
512
+ // Confirmed: node-fetch (github.com/node-fetch/node-fetch README)
513
+ [/^node-fetch\//i, "node-fetch"],
514
+ // Confirmed: axios docs (axios-http.com/docs/config_defaults)
515
+ [/^axios\//i, "axios"],
516
+ // Confirmed: Ruby net/http default (ruby-doc.org)
517
+ [/^Ruby$/i, "Ruby"]
518
+ ];
519
+ var AXIOS_ACCEPT = "application/json, text/plain, */*";
520
+ function computeBrowserFingerprint(headers, ua) {
521
+ const signals = [];
522
+ let score = 0;
523
+ let isDefiniteBot = false;
524
+ let knownClient = null;
525
+ for (const [pattern, name] of KNOWN_BOT_UA) {
526
+ if (pattern.test(ua)) {
527
+ isDefiniteBot = true;
528
+ knownClient = name;
529
+ score = 100;
530
+ signals.push(`known_ua:${name}`);
531
+ break;
532
+ }
533
+ }
534
+ const accept = str(headers["accept"]);
535
+ if (!isDefiniteBot && accept === AXIOS_ACCEPT) {
536
+ isDefiniteBot = true;
537
+ knownClient = "axios";
538
+ score = 100;
539
+ signals.push("known_accept:axios");
540
+ }
541
+ if (isDefiniteBot) return { score, signals, isDefiniteBot, knownClient };
542
+ const hasSecFetchSite = "sec-fetch-site" in headers;
543
+ const hasSecFetchMode = "sec-fetch-mode" in headers;
544
+ if (!hasSecFetchSite && !hasSecFetchMode) {
545
+ score += 30;
546
+ signals.push("missing_sec_fetch");
547
+ }
548
+ const acceptLang = str(headers["accept-language"]);
549
+ if (!acceptLang) {
550
+ score += 25;
551
+ signals.push("missing_accept_language");
552
+ }
553
+ const acceptEnc = str(headers["accept-encoding"]);
554
+ if (!acceptEnc) {
555
+ score += 10;
556
+ signals.push("missing_accept_encoding");
557
+ }
558
+ if (hasSecFetchMode && !hasSecFetchSite) {
559
+ score += 20;
560
+ signals.push("undici_pattern:sec_fetch_mode_without_site");
561
+ knownClient = "undici (Node.js fetch)";
562
+ }
563
+ if (acceptLang === "*") {
564
+ score += 15;
565
+ signals.push("undici_pattern:accept_language_wildcard");
566
+ }
567
+ const claimsChrome = /Chrome\//i.test(ua) && !/Chromium/i.test(ua);
568
+ const hasSecChUa = "sec-ch-ua" in headers;
569
+ if (claimsChrome && !hasSecChUa) {
570
+ score += 10;
571
+ signals.push("chrome_ua_without_sec_ch_ua");
572
+ }
573
+ score = Math.min(100, score);
574
+ return { score, signals, isDefiniteBot, knownClient };
575
+ }
576
+ function extractHttp2Settings(req) {
577
+ const stream = req["stream"] ?? req["raw"]?.["stream"];
578
+ if (!stream) return null;
579
+ const session = stream["session"];
580
+ if (!session) return null;
581
+ const remote = session["remoteSettings"];
582
+ if (!remote) return null;
583
+ const initialWindowSize = remote.initialWindowSize ?? 65535;
584
+ const headerTableSize = remote.headerTableSize ?? 4096;
585
+ const enablePush = remote.enablePush ?? true;
586
+ const signals = [];
587
+ let score = 0;
588
+ if (initialWindowSize <= 131072) {
589
+ score += 20;
590
+ signals.push(`h2_window_${initialWindowSize}`);
591
+ }
592
+ if (headerTableSize <= 4096 && initialWindowSize <= 131072) {
593
+ score += 5;
594
+ signals.push("h2_header_table_default");
595
+ }
596
+ return { initialWindowSize, headerTableSize, enablePush, score, signals };
597
+ }
598
+ function extractUpstreamTls(headers) {
599
+ const ja3 = str(headers["x-ja3-hash"]) || str(headers["x-ja3"]) || null;
600
+ const ja4 = str(headers["x-ja4"]) || null;
601
+ return { ja3: ja3 || null, ja4: ja4 || null };
602
+ }
603
+ function str(v) {
604
+ if (v === void 0) return "";
605
+ return Array.isArray(v) ? v[0] ?? "" : v;
606
+ }
607
+ function detectHoneypotType(path) {
608
+ const p = path.toLowerCase().split("?")[0] ?? path.toLowerCase();
609
+ if (/\/\.env(\.[\w]+)?$/.test(p) || p.endsWith("/.env")) return "env_file";
610
+ if (p.includes(".aws/credentials") || p.includes("aws/credentials")) return "aws_credentials";
611
+ if (p.includes(".git/config") || p.endsWith("/git/config")) return "git_config";
612
+ if (p.includes("/graphql") || p.includes("/graphiql")) return "graphql";
613
+ if (p.includes("/actuator/env") || p.includes("/actuator/")) return "spring_actuator";
614
+ if (/\/(config|settings|secrets?)(\.json)?$/.test(p)) return "json_config";
615
+ if (p.endsWith("/.htpasswd") || p.endsWith("/.htaccess")) return "htpasswd";
616
+ if (p.includes("/.s3cfg") || p.endsWith("/s3") || p.includes("s3cfg")) return "s3_bucket";
617
+ if (/\/(admin|wp-admin|administrator|panel|cpanel|dashboard|manage|backend)(\/|$)/.test(p)) return "admin_portal";
618
+ return "generic";
619
+ }
620
+ function generateHoneypotResponse(type, canaryToken, callbackBase, orgId) {
621
+ switch (type) {
622
+ case "env_file":
623
+ return makeEnvFile(canaryToken, callbackBase, orgId);
624
+ case "aws_credentials":
625
+ return makeAwsCredentials(canaryToken);
626
+ case "git_config":
627
+ return makeGitConfig(canaryToken, callbackBase, orgId);
628
+ case "graphql":
629
+ return makeGraphQL();
630
+ case "spring_actuator":
631
+ return makeSpringActuator(canaryToken);
632
+ case "json_config":
633
+ return makeJsonConfig(canaryToken, callbackBase, orgId);
634
+ case "htpasswd":
635
+ return makeHtpasswd(canaryToken);
636
+ case "s3_bucket":
637
+ return makeS3Bucket(canaryToken);
638
+ case "admin_portal":
639
+ return makeAdminPortal(canaryToken);
640
+ default:
641
+ return makeGeneric(canaryToken);
642
+ }
643
+ }
644
+ function makeEnvFile(canaryToken, callbackBase, orgId) {
645
+ const jwtSecret = genHex(32);
646
+ const dbPassword = genAlphanumeric(20);
647
+ const redisPassword = genAlphanumeric(16);
648
+ const apiKey = genHex(32);
649
+ const webhookUrl = `${callbackBase}/v1/canary/${orgId}/${canaryToken}`;
650
+ const dnsDb = `db.${canaryToken}.srv.anomira.io`;
651
+ const dnsCache = `cache.${canaryToken}.srv.anomira.io`;
652
+ const fakeJwt = generateCanaryJwt(canaryToken, jwtSecret);
653
+ const body = `# Production Environment Configuration
654
+ # Last updated: ${new Date(Date.now() - 7 * 864e5).toISOString().split("T")[0]}
655
+
656
+ NODE_ENV=production
657
+ PORT=3000
658
+
659
+ # \u2500\u2500 Database \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\u2500\u2500\u2500\u2500\u2500
660
+ DATABASE_URL=postgresql://db_prod:${dbPassword}@${dnsDb}:5432/app_production
661
+ DATABASE_REPLICA_URL=postgresql://db_prod:${dbPassword}@${dnsDb}:5433/app_production
662
+ DATABASE_POOL_MIN=2
663
+ DATABASE_POOL_MAX=10
664
+
665
+ # \u2500\u2500 Cache \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
666
+ REDIS_URL=redis://:${redisPassword}@${dnsCache}:6379/0
667
+
668
+ # \u2500\u2500 Authentication \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
669
+ JWT_SECRET=${jwtSecret}
670
+ JWT_EXPIRES_IN=15m
671
+ REFRESH_TOKEN_SECRET=${genHex(32)}
672
+ SESSION_SECRET=${genHex(32)}
673
+
674
+ # \u2500\u2500 Sample authenticated token (service account) \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
675
+ SERVICE_ACCOUNT_TOKEN=${fakeJwt}
676
+
677
+ # \u2500\u2500 AWS \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
678
+ AWS_REGION=eu-west-1
679
+ AWS_S3_BUCKET=app-production-${genHex(4)}
680
+
681
+ # \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=${webhookUrl}
683
+ ALERT_WEBHOOK=${webhookUrl}
684
+ DEPLOY_HOOK=${callbackBase}/v1/canary/${orgId}/${canaryToken}
685
+
686
+ # \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
+ INTERNAL_API_KEY=${apiKey}
688
+ `;
689
+ return {
690
+ statusCode: 200,
691
+ contentType: "text/plain; charset=utf-8",
692
+ headers: {
693
+ "Cache-Control": "no-store",
694
+ "Last-Modified": new Date(Date.now() - 7 * 864e5).toUTCString(),
695
+ "ETag": `"${genHex(8)}-${genHex(4)}"`,
696
+ // Apache-style headers — most exposed .env files are on PHP/Apache stacks
697
+ "Server": "Apache/2.4.41 (Ubuntu)",
698
+ "X-Content-Type-Options": "nosniff"
699
+ },
700
+ body,
701
+ canaryToken
702
+ };
703
+ }
704
+ function makeAwsCredentials(canaryToken) {
705
+ const fakeKeyId = makeAwsKeyId();
706
+ const fakeSecret = makeAwsSecret();
707
+ const stsPrefixKey = "ASIA" + makeAwsKeyId().slice(4);
708
+ const stsSecret = makeAwsSecret();
709
+ const stsToken = genBase64(300);
710
+ const body = `[default]
711
+ aws_access_key_id=${fakeKeyId}
712
+ aws_secret_access_key=${fakeSecret}
713
+ region=eu-west-1
714
+
715
+ [production]
716
+ aws_access_key_id=${fakeKeyId}
717
+ aws_secret_access_key=${fakeSecret}
718
+ region=eu-west-1
719
+
720
+ [staging]
721
+ aws_access_key_id=${stsPrefixKey}
722
+ aws_secret_access_key=${stsSecret}
723
+ aws_session_token=${stsToken}
724
+ region=eu-west-1
725
+ `;
726
+ return {
727
+ statusCode: 200,
728
+ contentType: "text/plain; charset=utf-8",
729
+ headers: {
730
+ "Cache-Control": "no-store",
731
+ "Last-Modified": new Date(Date.now() - 14 * 864e5).toUTCString(),
732
+ // Match the Server header an AWS EC2 instance metadata endpoint might have
733
+ "Server": "EC2ws",
734
+ "X-Content-Type-Options": "nosniff"
735
+ },
736
+ body,
737
+ canaryToken
738
+ };
739
+ }
740
+ function makeGitConfig(canaryToken, callbackBase, orgId) {
741
+ const ghpToken = `ghp_${genAlphanumeric(36)}`;
742
+ const glpatToken = `glpat-${genAlphanumeric(20)}`;
743
+ const webhookUrl = `${callbackBase}/v1/canary/${orgId}/${canaryToken}`;
744
+ const dnsGit = `git.${canaryToken}.srv.anomira.io`;
745
+ const body = `[core]
746
+ repositoryformatversion = 0
747
+ filemode = true
748
+ bare = false
749
+ logallrefupdates = true
750
+
751
+ [remote "origin"]
752
+ url = https://oauth2:${ghpToken}@github.com/company/app.git
753
+ fetch = +refs/heads/*:refs/remotes/origin/*
754
+
755
+ [remote "deploy"]
756
+ url = https://deploy-token:${glpatToken}@${dnsGit}/backend/app.git
757
+ fetch = +refs/heads/*:refs/remotes/deploy/*
758
+
759
+ [remote "notify"]
760
+ url = ${webhookUrl}
761
+
762
+ [branch "main"]
763
+ remote = origin
764
+ merge = refs/heads/main
765
+
766
+ [branch "production"]
767
+ remote = deploy
768
+ merge = refs/heads/production
769
+
770
+ [credential "https://github.com"]
771
+ username = deploy-bot
772
+ `;
773
+ return {
774
+ statusCode: 200,
775
+ contentType: "text/plain; charset=utf-8",
776
+ headers: {
777
+ "Cache-Control": "no-store",
778
+ "Last-Modified": new Date(Date.now() - 30 * 864e5).toUTCString(),
779
+ "Server": "Apache/2.4.41 (Ubuntu)"
780
+ },
781
+ body,
782
+ canaryToken
783
+ };
784
+ }
785
+ function makeGraphQL() {
786
+ const body = JSON.stringify({
787
+ data: {
788
+ __schema: {
789
+ queryType: { name: "Query" },
790
+ mutationType: { name: "Mutation" },
791
+ types: [
792
+ { kind: "OBJECT", name: "Query", fields: [{ name: "user" }, { name: "users" }, { name: "posts" }] },
793
+ { kind: "OBJECT", name: "Mutation", fields: [{ name: "login" }, { name: "createUser" }, { name: "adminReset" }, { name: "exportData" }] },
794
+ { kind: "OBJECT", name: "User", fields: [{ name: "id" }, { name: "email" }, { name: "role" }, { name: "createdAt" }] },
795
+ { kind: "OBJECT", name: "AuthPayload", fields: [{ name: "token" }, { name: "user" }] },
796
+ { kind: "SCALAR", name: "String", fields: null },
797
+ { kind: "SCALAR", name: "Boolean", fields: null },
798
+ { kind: "SCALAR", name: "Int", fields: null },
799
+ { kind: "SCALAR", name: "ID", fields: null }
800
+ ]
801
+ }
802
+ }
803
+ });
804
+ return {
805
+ statusCode: 200,
806
+ contentType: "application/json; charset=utf-8",
807
+ headers: {
808
+ "X-Powered-By": "Express",
809
+ "X-Request-Id": `req_${genHex(16)}`
810
+ },
811
+ body,
812
+ canaryToken: ""
813
+ };
814
+ }
815
+ function makeSpringActuator(canaryToken) {
816
+ const dbPassword = genAlphanumeric(20);
817
+ const secretKey = genHex(32);
818
+ const body = JSON.stringify({
819
+ activeProfiles: ["production"],
820
+ defaultProfiles: ["default"],
821
+ propertySources: [
822
+ {
823
+ name: "systemProperties",
824
+ properties: {
825
+ "java.runtime.version": { value: "17.0.9+9" },
826
+ "server.port": { value: "8080" },
827
+ "user.home": { value: "/home/appuser" }
828
+ }
829
+ },
830
+ {
831
+ name: "applicationConfig: [classpath:/application-production.properties]",
832
+ properties: {
833
+ "spring.datasource.url": { origin: "class path resource [application-production.properties] - 3:1", value: "jdbc:postgresql://db.internal:5432/myapp" },
834
+ "spring.datasource.username": { origin: "class path resource [application-production.properties] - 4:1", value: "dbadmin" },
835
+ "spring.datasource.password": { origin: "class path resource [application-production.properties] - 5:1", value: dbPassword },
836
+ "app.jwt.secret": { origin: "class path resource [application-production.properties] - 8:1", value: secretKey },
837
+ "app.canary.token": { value: canaryToken },
838
+ "management.endpoints.web.exposure.include": { value: "health,info,env,metrics,loggers" }
839
+ }
840
+ }
841
+ ]
842
+ }, null, 2);
843
+ return {
844
+ statusCode: 200,
845
+ contentType: "application/vnd.spring-boot.actuator.v3+json",
846
+ headers: {
847
+ "X-Application-Context": "myapp:production:8080",
848
+ "X-Content-Type-Options": "nosniff",
849
+ "X-XSS-Protection": "1; mode=block"
850
+ },
851
+ body,
852
+ canaryToken
853
+ };
854
+ }
855
+ function makeJsonConfig(canaryToken, callbackBase, orgId) {
856
+ const webhookUrl = `${callbackBase}/v1/canary/${orgId}/${canaryToken}`;
857
+ const body = JSON.stringify({
858
+ environment: "production",
859
+ version: "2.4.1",
860
+ database: {
861
+ host: "db.internal.company.com",
862
+ port: 5432,
863
+ name: "app_production",
864
+ user: "db_prod",
865
+ password: genAlphanumeric(20)
866
+ },
867
+ jwt: {
868
+ secret: genHex(32),
869
+ expiresIn: "15m"
870
+ },
871
+ webhooks: {
872
+ events: webhookUrl,
873
+ alerts: `${callbackBase}/v1/canary/${orgId}/${canaryToken}`
874
+ },
875
+ internalApiKey: genHex(32)
876
+ }, null, 2);
877
+ return {
878
+ statusCode: 200,
879
+ contentType: "application/json; charset=utf-8",
880
+ headers: {
881
+ "Cache-Control": "no-store",
882
+ "ETag": `"${genHex(8)}"`,
883
+ "X-Powered-By": "Express"
884
+ },
885
+ body,
886
+ canaryToken
887
+ };
888
+ }
889
+ function makeHtpasswd(canaryToken) {
890
+ const apr1Salt = genAlphanumeric(8).toLowerCase();
891
+ const apr1Hash = genBase64(22).replace(
892
+ /[+=/]/g,
893
+ (c) => ({ "+": ".", "=": "/", "/": "X" })[c] ?? c
894
+ );
895
+ const bcryptHash = genBase64(53).replace(
896
+ /[+=]/g,
897
+ (c) => ({ "+": ".", "=": "/" })[c] ?? c
898
+ );
899
+ const canaryApr1 = `$apr1$${canaryToken.slice(0, 8)}$${genBase64(22).slice(0, 22)}`;
900
+ const body = `# Apache HTTP Server password file
901
+ admin:$apr1$${apr1Salt}$${apr1Hash}
902
+ deploy:$2y$10$${bcryptHash}
903
+ root:${canaryApr1}
904
+ backup-user:$apr1$${genAlphanumeric(8).toLowerCase()}$${genBase64(22).slice(0, 22)}
905
+ `;
906
+ return {
907
+ statusCode: 200,
908
+ contentType: "text/plain; charset=utf-8",
909
+ headers: {
910
+ "Cache-Control": "no-store",
911
+ "Last-Modified": new Date(Date.now() - 45 * 864e5).toUTCString(),
912
+ "Server": "Apache/2.4.41 (Ubuntu)",
913
+ "Content-Disposition": "inline"
914
+ },
915
+ body,
916
+ canaryToken
917
+ };
918
+ }
919
+ function makeS3Bucket(canaryToken) {
920
+ const ownerId = genHex(32) + genHex(32);
921
+ const now = /* @__PURE__ */ new Date();
922
+ const dateStr = (offset) => new Date(now.getTime() - offset).toISOString().replace(/\.\d{3}Z/, ".000Z");
923
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
924
+ <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
925
+ <Owner>
926
+ <ID>${ownerId}</ID>
927
+ <DisplayName>company-admin</DisplayName>
928
+ </Owner>
929
+ <Buckets>
930
+ <Bucket>
931
+ <Name>company-production-data</Name>
932
+ <CreationDate>${dateStr(120 * 864e5)}</CreationDate>
933
+ </Bucket>
934
+ <Bucket>
935
+ <Name>company-assets-${genHex(4)}</Name>
936
+ <CreationDate>${dateStr(90 * 864e5)}</CreationDate>
937
+ </Bucket>
938
+ <Bucket>
939
+ <Name>company-backups-${canaryToken.slice(0, 12)}</Name>
940
+ <CreationDate>${dateStr(30 * 864e5)}</CreationDate>
941
+ </Bucket>
942
+ <Bucket>
943
+ <Name>company-logs-archive</Name>
944
+ <CreationDate>${dateStr(180 * 864e5)}</CreationDate>
945
+ </Bucket>
946
+ </Buckets>
947
+ </ListAllMyBucketsResult>`;
948
+ return {
949
+ statusCode: 200,
950
+ contentType: "application/xml",
951
+ headers: {
952
+ "x-amz-request-id": genHex(8).toUpperCase() + genHex(8).toUpperCase(),
953
+ "x-amz-id-2": genBase64(60),
954
+ "Server": "AmazonS3"
955
+ },
956
+ body,
957
+ canaryToken
958
+ };
959
+ }
960
+ function makeAdminPortal(canaryToken) {
961
+ const body = `<!DOCTYPE html>
962
+ <html lang="en">
963
+ <head>
964
+ <meta charset="UTF-8" />
965
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
966
+ <title>Administration \u2014 Login</title>
967
+ <style>
968
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
969
+ body {
970
+ background: #0f1117;
971
+ color: #c9d1d9;
972
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
973
+ display: flex; align-items: center; justify-content: center;
974
+ min-height: 100vh;
975
+ }
976
+ .card {
977
+ background: #161b22;
978
+ border: 1px solid #30363d;
979
+ border-radius: 8px;
980
+ padding: 32px;
981
+ width: 100%;
982
+ max-width: 360px;
983
+ }
984
+ h1 { font-size: 1.1rem; font-weight: 600; color: #e6edf3; margin-bottom: 4px; }
985
+ .subtitle { font-size: 0.8rem; color: #7d8590; margin-bottom: 24px; }
986
+ label { display: block; font-size: 0.8rem; font-weight: 600; color: #c9d1d9; margin-bottom: 6px; }
987
+ input[type=text], input[type=password] {
988
+ width: 100%; padding: 8px 12px; border: 1px solid #30363d;
989
+ border-radius: 6px; background: #0d1117; color: #c9d1d9;
990
+ font-size: 0.875rem; outline: none; margin-bottom: 16px;
991
+ }
992
+ input:focus { border-color: #388bfd; box-shadow: 0 0 0 3px rgba(56,139,253,.1); }
993
+ .row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
994
+ .row label { margin-bottom: 0; font-weight: normal; display: flex; align-items: center; gap: 6px; cursor: pointer; }
995
+ button {
996
+ width: 100%; padding: 8px 16px; background: #238636; color: #fff;
997
+ border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 600;
998
+ cursor: pointer; transition: background .15s;
999
+ }
1000
+ button:hover { background: #2ea043; }
1001
+ .footer { text-align: center; font-size: 0.72rem; color: #7d8590; margin-top: 20px; }
1002
+ </style>
1003
+ </head>
1004
+ <body>
1005
+ <div class="card">
1006
+ <h1>Administration Panel</h1>
1007
+ <p class="subtitle">Sign in to continue</p>
1008
+ <form method="POST" action="./login" autocomplete="off">
1009
+ <input type="hidden" name="_token" value="${canaryToken}" />
1010
+ <div>
1011
+ <label for="username">Username</label>
1012
+ <input id="username" name="username" type="text" placeholder="admin" autocomplete="off" />
1013
+ </div>
1014
+ <div>
1015
+ <label for="password">Password</label>
1016
+ <input id="password" name="password" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" autocomplete="off" />
1017
+ </div>
1018
+ <div class="row">
1019
+ <label><input type="checkbox" name="remember" /> Remember me</label>
1020
+ <a href="#" style="font-size:.8rem;color:#388bfd;text-decoration:none;">Forgot password?</a>
1021
+ </div>
1022
+ <button type="submit">Sign in</button>
1023
+ </form>
1024
+ <p class="footer">v3.2.1 \xB7 Secure Administration Portal</p>
1025
+ </div>
1026
+ </body>
1027
+ </html>`;
1028
+ return {
1029
+ statusCode: 200,
1030
+ contentType: "text/html; charset=utf-8",
1031
+ headers: {
1032
+ "Cache-Control": "no-store, no-cache",
1033
+ "X-Frame-Options": "SAMEORIGIN",
1034
+ "Server": "nginx/1.18.0 (Ubuntu)"
1035
+ },
1036
+ body,
1037
+ canaryToken
1038
+ };
1039
+ }
1040
+ function makeAdminLoginFailed() {
1041
+ const body = `<!DOCTYPE html>
1042
+ <html lang="en">
1043
+ <head>
1044
+ <meta charset="UTF-8" />
1045
+ <title>Administration \u2014 Login</title>
1046
+ <style>
1047
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1048
+ body { background: #0f1117; color: #c9d1d9; font-family: -apple-system, sans-serif;
1049
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; }
1050
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 32px; width: 100%; max-width: 360px; }
1051
+ h1 { font-size: 1.1rem; font-weight: 600; color: #e6edf3; margin-bottom: 4px; }
1052
+ .error { background: #3d1010; border: 1px solid #f85149; color: #f85149; border-radius: 6px; padding: 10px 14px; font-size: .8rem; margin-bottom: 16px; }
1053
+ label { display: block; font-size: .8rem; font-weight: 600; color: #c9d1d9; margin-bottom: 6px; }
1054
+ input[type=text], input[type=password] { width: 100%; padding: 8px 12px; border: 1px solid #f85149; border-radius: 6px; background: #0d1117; color: #c9d1d9; font-size: .875rem; outline: none; margin-bottom: 16px; }
1055
+ button { width: 100%; padding: 8px 16px; background: #238636; color: #fff; border: none; border-radius: 6px; font-size: .875rem; font-weight: 600; cursor: pointer; }
1056
+ .footer { text-align: center; font-size: .72rem; color: #7d8590; margin-top: 20px; }
1057
+ </style>
1058
+ </head>
1059
+ <body>
1060
+ <div class="card">
1061
+ <h1>Administration Panel</h1>
1062
+ <div class="error">\u26A0 Invalid username or password. Please try again.</div>
1063
+ <form method="POST" action="" autocomplete="off">
1064
+ <div><label for="u">Username</label><input id="u" name="username" type="text" autocomplete="off" /></div>
1065
+ <div><label for="p">Password</label><input id="p" name="password" type="password" autocomplete="off" /></div>
1066
+ <button type="submit">Sign in</button>
1067
+ </form>
1068
+ <p class="footer">v3.2.1 \xB7 Secure Administration Portal</p>
1069
+ </div>
1070
+ </body>
1071
+ </html>`;
1072
+ return {
1073
+ statusCode: 401,
1074
+ contentType: "text/html; charset=utf-8",
1075
+ headers: {
1076
+ "Cache-Control": "no-store",
1077
+ "Server": "nginx/1.18.0 (Ubuntu)",
1078
+ "WWW-Authenticate": 'Form realm="Administration Panel"'
1079
+ },
1080
+ body,
1081
+ canaryToken: ""
1082
+ };
1083
+ }
1084
+ function makeGeneric(canaryToken) {
1085
+ return {
1086
+ statusCode: 200,
1087
+ contentType: "text/plain; charset=utf-8",
1088
+ headers: { "Cache-Control": "no-store" },
1089
+ body: `# ${canaryToken}
1090
+ `,
1091
+ canaryToken
1092
+ };
1093
+ }
1094
+ function generateCanaryJwt(canaryToken, secret) {
1095
+ const now = Math.floor(Date.now() / 1e3);
1096
+ const header = { alg: "HS256", typ: "JWT", kid: `canary-${canaryToken.slice(0, 12)}` };
1097
+ const payload = {
1098
+ sub: "svc_internal_7482",
1099
+ name: "service-account",
1100
+ role: "admin",
1101
+ email: "admin@internal.company.com",
1102
+ iat: now - 172800,
1103
+ // issued 48h ago (realistic stale credential)
1104
+ exp: now - 86400,
1105
+ // expired 24h ago
1106
+ jti: `canary-${canaryToken}`
1107
+ };
1108
+ const h = Buffer.from(JSON.stringify(header)).toString("base64url");
1109
+ const p = Buffer.from(JSON.stringify(payload)).toString("base64url");
1110
+ const sig = createHmac("sha256", secret).update(`${h}.${p}`).digest("base64url");
1111
+ return `${h}.${p}.${sig}`;
1112
+ }
1113
+ function makeAwsKeyId() {
1114
+ const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1115
+ const middle = Array.from(
1116
+ { length: 13 },
1117
+ () => base32Chars[randomBytes(1)[0] % base32Chars.length]
1118
+ ).join("");
1119
+ return `AKIAI${middle}A`;
1120
+ }
1121
+ function makeAwsSecret() {
1122
+ return randomBytes(30).toString("base64").slice(0, 40);
1123
+ }
1124
+ function genHex(bytes) {
1125
+ return randomBytes(bytes).toString("hex");
1126
+ }
1127
+ function genAlphanumeric(len) {
1128
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1129
+ return Array.from(
1130
+ { length: len },
1131
+ () => chars[randomBytes(1)[0] % chars.length]
1132
+ ).join("");
1133
+ }
1134
+ function genBase64(approxLen) {
1135
+ return randomBytes(Math.ceil(approxLen * 3 / 4)).toString("base64").slice(0, approxLen);
1136
+ }
406
1137
  var requestContext = new AsyncLocalStorage();
407
1138
  var DEFAULT_INGEST_URL = "https://ingest.anomira.io/v1/events";
408
1139
  var DEFAULT_BATCH_SIZE = 100;
@@ -414,10 +1145,28 @@ var AnomiraClient = class {
414
1145
  this.logFlushTimer = null;
415
1146
  this.blocklistTimer = null;
416
1147
  this.firewallTimer = null;
1148
+ this.communityThreatTimer = null;
417
1149
  /** True when credentials are missing — all operations become no-ops. */
418
1150
  this.disabled = false;
419
- /** In-process cache of blocked IPs — refreshed every 60 s from the ingest server. */
1151
+ /** In-process cache of manually blocked IPs — refreshed every 60 s. */
420
1152
  this.blockedIpCache = /* @__PURE__ */ new Set();
1153
+ /** In-process cache of community threat IPs with their confidence scores.
1154
+ * Populated from Anomira's federated threat network — IPs confirmed malicious
1155
+ * across multiple customers. Refreshed every 60 s. */
1156
+ this.communityThreatCache = /* @__PURE__ */ new Map();
1157
+ /**
1158
+ * Set of honeypot paths to intercept. When a request matches, the SDK returns
1159
+ * a fake response instead of forwarding to the customer's route handlers.
1160
+ * Synced from ingest every 60 s. Keys are lowercase path strings.
1161
+ */
1162
+ this.honeypotPaths = /* @__PURE__ */ new Set();
1163
+ /**
1164
+ * Set of active canary token strings embedded in previously-served honeypot
1165
+ * responses. When any of these appear in a request's Authorization header or
1166
+ * body, a http.canary.triggered event is fired.
1167
+ * Synced from ingest every 60 s (max 100 tokens, sliding 7-day window).
1168
+ */
1169
+ this.canaryTokenCache = /* @__PURE__ */ new Set();
421
1170
  /** In-process cache of firewall rules with pre-compiled regex — refreshed every 60 s. */
422
1171
  this.compiledRules = [];
423
1172
  // Saved originals — used by SDK internals so patched console doesn't recurse
@@ -441,7 +1190,8 @@ var AnomiraClient = class {
441
1190
  service: "app",
442
1191
  getUserId: defaultGetUserId,
443
1192
  getIp: defaultGetIp,
444
- detect: { bruteForce: true, rateAbuse: true, pathTraversal: true, xss: true, scanDetection: true, geoVelocity: true }
1193
+ detect: { bruteForce: true, rateAbuse: true, pathTraversal: true, xss: true, scanDetection: true, geoVelocity: true },
1194
+ autoBlock: { enabled: true, communityThreshold: 85 }
445
1195
  };
446
1196
  this.buffer = new EventBuffer({ appId: "", apiKey: "", ingestUrl: DEFAULT_INGEST_URL, maxBatchSize: 0, flushIntervalMs: 999999999, maxRetries: 0, debug: false });
447
1197
  return;
@@ -466,6 +1216,10 @@ var AnomiraClient = class {
466
1216
  xss: config.detect?.xss ?? true,
467
1217
  scanDetection: config.detect?.scanDetection ?? true,
468
1218
  geoVelocity: config.detect?.geoVelocity ?? true
1219
+ },
1220
+ autoBlock: {
1221
+ enabled: config.autoBlock?.enabled ?? true,
1222
+ communityThreshold: config.autoBlock?.communityThreshold ?? 85
469
1223
  }
470
1224
  };
471
1225
  this.buffer = new EventBuffer({
@@ -488,6 +1242,21 @@ var AnomiraClient = class {
488
1242
  void this.#refreshFirewallRules();
489
1243
  }, 6e4);
490
1244
  if (this.firewallTimer.unref) this.firewallTimer.unref();
1245
+ void this.#refreshHoneypotPaths();
1246
+ const honeypotTimer = setInterval(() => {
1247
+ void this.#refreshHoneypotPaths();
1248
+ }, 6e4);
1249
+ if (honeypotTimer.unref) honeypotTimer.unref();
1250
+ void this.#refreshCanaryTokens();
1251
+ const canaryTimer = setInterval(() => {
1252
+ void this.#refreshCanaryTokens();
1253
+ }, 6e4);
1254
+ if (canaryTimer.unref) canaryTimer.unref();
1255
+ void this.#refreshCommunityThreats();
1256
+ this.communityThreatTimer = setInterval(() => {
1257
+ void this.#refreshCommunityThreats();
1258
+ }, 6e4);
1259
+ if (this.communityThreatTimer.unref) this.communityThreatTimer.unref();
491
1260
  this.logFlushTimer = setInterval(() => {
492
1261
  void this.#flushLogs();
493
1262
  }, 1e4);
@@ -523,7 +1292,7 @@ var AnomiraClient = class {
523
1292
  const res = await fetch(syncUrl, {
524
1293
  headers: {
525
1294
  Authorization: `Bearer ${this.config.apiKey}`,
526
- "User-Agent": `@anomira/node-sdk/0.1.0`
1295
+ "User-Agent": `@anomira/node-sdk/0.2.2`
527
1296
  },
528
1297
  signal: AbortSignal.timeout(5e3)
529
1298
  });
@@ -536,6 +1305,58 @@ var AnomiraClient = class {
536
1305
  } catch {
537
1306
  }
538
1307
  }
1308
+ async #refreshCommunityThreats() {
1309
+ const syncUrl = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/community-threats/sync");
1310
+ try {
1311
+ const res = await fetch(syncUrl, {
1312
+ headers: {
1313
+ Authorization: `Bearer ${this.config.apiKey}`,
1314
+ "User-Agent": `@anomira/node-sdk/0.2.2`
1315
+ },
1316
+ signal: AbortSignal.timeout(5e3)
1317
+ });
1318
+ if (!res.ok) return;
1319
+ const data = await res.json();
1320
+ const newCache = /* @__PURE__ */ new Map();
1321
+ for (const t of data.threats ?? []) {
1322
+ newCache.set(t.ip, { score: t.score, topAttack: t.topAttack });
1323
+ }
1324
+ this.communityThreatCache = newCache;
1325
+ if (this.config.debug && newCache.size > 0) {
1326
+ this._origLog(`[Anomira] community threats refreshed \u2014 ${newCache.size} known threat IPs`);
1327
+ }
1328
+ } catch {
1329
+ }
1330
+ }
1331
+ async #refreshHoneypotPaths() {
1332
+ const syncUrl = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/honeypots/sync");
1333
+ try {
1334
+ const res = await fetch(syncUrl, {
1335
+ headers: { Authorization: `Bearer ${this.config.apiKey}`, "User-Agent": "@anomira/node-sdk/0.2.2" },
1336
+ signal: AbortSignal.timeout(5e3)
1337
+ });
1338
+ if (!res.ok) return;
1339
+ const data = await res.json();
1340
+ this.honeypotPaths = new Set((data.paths ?? []).map((p) => p.toLowerCase()));
1341
+ if (this.config.debug && this.honeypotPaths.size > 0) {
1342
+ this._origLog(`[Anomira] honeypot paths refreshed \u2014 ${this.honeypotPaths.size} traps active`);
1343
+ }
1344
+ } catch {
1345
+ }
1346
+ }
1347
+ async #refreshCanaryTokens() {
1348
+ const syncUrl = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/canary-tokens/sync");
1349
+ try {
1350
+ const res = await fetch(syncUrl, {
1351
+ headers: { Authorization: `Bearer ${this.config.apiKey}`, "User-Agent": "@anomira/node-sdk/0.2.2" },
1352
+ signal: AbortSignal.timeout(5e3)
1353
+ });
1354
+ if (!res.ok) return;
1355
+ const data = await res.json();
1356
+ this.canaryTokenCache = new Set(data.tokens ?? []);
1357
+ } catch {
1358
+ }
1359
+ }
539
1360
  async #refreshFirewallRules() {
540
1361
  const syncUrl = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/firewall-rules/sync");
541
1362
  try {
@@ -662,10 +1483,40 @@ var AnomiraClient = class {
662
1483
  * });
663
1484
  * ```
664
1485
  */
665
- /** Returns true if the IP is in the current blocked list. Synchronous — no network call. */
1486
+ /**
1487
+ * Returns true if the IP should be blocked. Synchronous — no network call.
1488
+ *
1489
+ * Checks two independent sources:
1490
+ * 1. Manual blocklist — IPs explicitly blocked by the customer or via playbooks.
1491
+ * 2. Community threats — IPs from Anomira's federated network whose confidence
1492
+ * score meets the autoBlock.communityThreshold (default 85).
1493
+ * Only active when autoBlock.enabled is true (the default).
1494
+ *
1495
+ * If a customer sets autoBlock.enabled = false, only the manual blocklist is checked.
1496
+ */
666
1497
  isBlocked(ip) {
667
1498
  if (this.disabled) return false;
668
- return this.blockedIpCache.has(ip);
1499
+ if (this.blockedIpCache.has(ip)) return true;
1500
+ if (this.config.autoBlock.enabled) {
1501
+ const threat = this.communityThreatCache.get(ip);
1502
+ if (threat && threat.score >= this.config.autoBlock.communityThreshold) return true;
1503
+ }
1504
+ return false;
1505
+ }
1506
+ /**
1507
+ * Returns the block reason for an IP — useful for logging or custom responses.
1508
+ * Returns null if the IP is not blocked.
1509
+ */
1510
+ blockReason(ip) {
1511
+ if (this.disabled) return null;
1512
+ if (this.blockedIpCache.has(ip)) return { source: "manual" };
1513
+ if (this.config.autoBlock.enabled) {
1514
+ const threat = this.communityThreatCache.get(ip);
1515
+ if (threat && threat.score >= this.config.autoBlock.communityThreshold) {
1516
+ return { source: "community", score: threat.score, topAttack: threat.topAttack };
1517
+ }
1518
+ }
1519
+ return null;
669
1520
  }
670
1521
  /**
671
1522
  * Fire-and-forget: report a blocked-IP attempt to the ingest server so the
@@ -697,12 +1548,15 @@ var AnomiraClient = class {
697
1548
  }
698
1549
  track(eventName, data) {
699
1550
  if (this.disabled) return;
1551
+ const ctx = requestContext.getStore();
1552
+ const resolvedIp = data.ip && data.ip !== "0.0.0.0" && data.ip !== "" ? data.ip : ctx?.ip ?? "0.0.0.0";
1553
+ const resolvedMeta = ctx?.endpoint && !data.meta?.["endpoint"] ? { endpoint: ctx.endpoint, method: ctx.method, ...data.meta } : data.meta;
700
1554
  const event = {
701
1555
  name: eventName,
702
1556
  ts: Date.now(),
703
- ip: data.ip,
1557
+ ip: resolvedIp,
704
1558
  userId: data.userId,
705
- meta: data.meta
1559
+ meta: resolvedMeta
706
1560
  };
707
1561
  this.buffer.push(event);
708
1562
  }
@@ -721,13 +1575,15 @@ var AnomiraClient = class {
721
1575
  async trackLogin(data) {
722
1576
  if (this.disabled) return;
723
1577
  const tsMs = Date.now();
724
- this.track(EventName.LOGIN_SUCCESS, { ...data, meta: { ...data.meta } });
1578
+ const ctx = requestContext.getStore();
1579
+ const resolvedIp = data.ip && data.ip !== "0.0.0.0" ? data.ip : ctx?.ip ?? "0.0.0.0";
1580
+ this.track(EventName.LOGIN_SUCCESS, { ...data, ip: resolvedIp, meta: { ...data.meta } });
725
1581
  if (!this.config.detect.geoVelocity) return;
726
1582
  try {
727
- const result = await checkGeoVelocity(data.userId, data.ip, tsMs, this.config.geoLookupUrl || void 0);
1583
+ const result = await checkGeoVelocity(data.userId, resolvedIp, tsMs, this.config.geoLookupUrl || void 0);
728
1584
  if (!result) return;
729
1585
  this.track(EventName.GEO_VELOCITY, {
730
- ip: data.ip,
1586
+ ip: resolvedIp,
731
1587
  userId: data.userId,
732
1588
  meta: {
733
1589
  distanceKm: result.distanceKm,
@@ -762,6 +1618,7 @@ var AnomiraClient = class {
762
1618
  if (this.disabled) return;
763
1619
  this.track(EventName.PHONE_AUTH, {
764
1620
  ip: data.ip,
1621
+ // track() auto-resolves from context if empty
765
1622
  userId: data.userId,
766
1623
  meta: { phone: data.phone, ...data.meta }
767
1624
  });
@@ -830,6 +1687,22 @@ var AnomiraClient = class {
830
1687
  */
831
1688
  async flush() {
832
1689
  if (this.disabled) return;
1690
+ if (this.blocklistTimer) {
1691
+ clearInterval(this.blocklistTimer);
1692
+ this.blocklistTimer = null;
1693
+ }
1694
+ if (this.firewallTimer) {
1695
+ clearInterval(this.firewallTimer);
1696
+ this.firewallTimer = null;
1697
+ }
1698
+ if (this.communityThreatTimer) {
1699
+ clearInterval(this.communityThreatTimer);
1700
+ this.communityThreatTimer = null;
1701
+ }
1702
+ if (this.logFlushTimer) {
1703
+ clearInterval(this.logFlushTimer);
1704
+ this.logFlushTimer = null;
1705
+ }
833
1706
  await Promise.all([this.buffer.flush(), this.#flushLogs()]);
834
1707
  }
835
1708
  /**
@@ -856,8 +1729,54 @@ var AnomiraClient = class {
856
1729
  function defaultGetUserId(req) {
857
1730
  const r = req;
858
1731
  const user = r["user"];
859
- if (!user) return void 0;
860
- return user["id"] ?? user["userId"] ?? user["sub"] ?? user["_id"];
1732
+ if (user && typeof user === "object") {
1733
+ const id = pickId(user);
1734
+ if (id) return id;
1735
+ }
1736
+ const auth = r["auth"];
1737
+ if (auth && typeof auth === "object") {
1738
+ const id = pickId(auth);
1739
+ if (id) return id;
1740
+ }
1741
+ const direct = r["userId"] ?? r["user_id"] ?? r["accountId"] ?? r["account_id"] ?? r["customerId"];
1742
+ if (direct && typeof direct === "string") return direct;
1743
+ const session = r["session"];
1744
+ if (session && typeof session === "object") {
1745
+ const sessionDirect = session["userId"] ?? session["user_id"];
1746
+ if (sessionDirect) return sessionDirect;
1747
+ const sessionUser = session["user"];
1748
+ if (sessionUser && typeof sessionUser === "object") {
1749
+ const id = pickId(sessionUser);
1750
+ if (id) return id;
1751
+ }
1752
+ }
1753
+ const headers = r["headers"];
1754
+ const rawAuth = headers?.["authorization"];
1755
+ const authStr = Array.isArray(rawAuth) ? rawAuth[0] : rawAuth;
1756
+ if (authStr?.startsWith("Bearer ")) {
1757
+ const token = authStr.slice(7);
1758
+ try {
1759
+ const parts = token.split(".");
1760
+ if (parts.length === 3) {
1761
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1762
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
1763
+ const payload = JSON.parse(
1764
+ Buffer.from(padded, "base64").toString("utf8")
1765
+ );
1766
+ const jwtId = pickId(payload);
1767
+ if (jwtId) return jwtId;
1768
+ }
1769
+ } catch {
1770
+ }
1771
+ }
1772
+ return void 0;
1773
+ }
1774
+ function pickId(obj) {
1775
+ const id = obj["id"] ?? obj["sub"] ?? // JWT standard claim
1776
+ obj["userId"] ?? obj["user_id"] ?? obj["uid"] ?? // Firebase
1777
+ obj["_id"] ?? // MongoDB
1778
+ obj["accountId"] ?? obj["account_id"] ?? obj["customerId"] ?? obj["customer_id"];
1779
+ return typeof id === "string" && id.length > 0 ? id : void 0;
861
1780
  }
862
1781
  function normalizeIp(raw) {
863
1782
  if (raw === "::1") return "127.0.0.1";
@@ -878,6 +1797,27 @@ function defaultGetIp(req) {
878
1797
  const socket = r["socket"];
879
1798
  return normalizeIp(socket?.remoteAddress ?? "0.0.0.0");
880
1799
  }
1800
+ function sendFakeResponse(res, fakeResponse) {
1801
+ const allHeaders = {
1802
+ "Content-Type": fakeResponse.contentType,
1803
+ ...fakeResponse.headers
1804
+ };
1805
+ if (typeof res["set"] === "function") {
1806
+ res["set"](allHeaders);
1807
+ } else if (typeof res["setHeader"] === "function") {
1808
+ for (const [k, v] of Object.entries(allHeaders)) {
1809
+ res["setHeader"](k, v);
1810
+ }
1811
+ }
1812
+ if (typeof res["status"] === "function") {
1813
+ res["status"](fakeResponse.statusCode);
1814
+ } else {
1815
+ res["statusCode"] = fakeResponse.statusCode;
1816
+ }
1817
+ if (typeof res["end"] === "function") {
1818
+ res["end"](fakeResponse.body);
1819
+ }
1820
+ }
881
1821
  function createExpressMiddleware(client) {
882
1822
  return async function sentinelMiddleware(req, res, next) {
883
1823
  const startMs = Date.now();
@@ -886,7 +1826,20 @@ function createExpressMiddleware(client) {
886
1826
  const method_ = req["method"]?.toUpperCase() ?? "GET";
887
1827
  const url_ = req["originalUrl"] ?? req["url"] ?? "/";
888
1828
  const ua_ = req["headers"]?.["user-agent"] ?? "";
1829
+ const reason_ = client.blockReason(ip);
889
1830
  client.reportBlockedHit(ip, { method: method_, url: url_, userAgent: ua_ });
1831
+ if (reason_?.source === "community") {
1832
+ client.track("http.community_threat_blocked", {
1833
+ ip,
1834
+ meta: {
1835
+ endpoint: url_,
1836
+ method: method_,
1837
+ score: reason_.score,
1838
+ topAttack: reason_.topAttack,
1839
+ source: "anomira_network"
1840
+ }
1841
+ });
1842
+ }
890
1843
  const res_ = res;
891
1844
  if (typeof res_["status"] === "function") {
892
1845
  res_["status"](403);
@@ -900,8 +1853,100 @@ function createExpressMiddleware(client) {
900
1853
  const method = req["method"]?.toUpperCase() ?? "GET";
901
1854
  const url = req["originalUrl"] ?? req["url"] ?? "/";
902
1855
  const headers = req["headers"];
903
- const ua = headers?.["user-agent"] ?? "";
904
- const fwMatch = client.matchFirewallRule({ url, body: req["body"], headers: headers ?? {}, ip });
1856
+ const headersStr = headers;
1857
+ const ua = (typeof headers?.["user-agent"] === "string" ? headers["user-agent"] : "") ?? "";
1858
+ const agentInfo = detectAgent(headers ?? {});
1859
+ const fingerprint = computeBrowserFingerprint(headers ?? {}, ua);
1860
+ const h2settings = extractHttp2Settings(req);
1861
+ const upstreamTls = extractUpstreamTls(headers ?? {});
1862
+ if (client["canaryTokenCache"] && client["canaryTokenCache"].size > 0) {
1863
+ const authHeader = (typeof headers?.["authorization"] === "string" ? headers["authorization"] : "") ?? "";
1864
+ if (authHeader.startsWith("Bearer ")) {
1865
+ const bearerToken = authHeader.slice(7);
1866
+ if (client["canaryTokenCache"].has(bearerToken)) {
1867
+ client.track("http.canary.triggered", {
1868
+ ip,
1869
+ userId,
1870
+ meta: { endpoint: url, method, token: bearerToken.slice(0, 16), source: "authorization_header" }
1871
+ });
1872
+ }
1873
+ try {
1874
+ const parts = bearerToken.split(".");
1875
+ if (parts.length === 3) {
1876
+ const b64h = (parts[0] ?? "").replace(/-/g, "+").replace(/_/g, "/");
1877
+ const hdr = JSON.parse(Buffer.from(b64h + "==", "base64").toString());
1878
+ const b64p = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
1879
+ const pay = JSON.parse(Buffer.from(b64p + "==", "base64").toString());
1880
+ const jti = pay["jti"] ?? "";
1881
+ const kid = hdr["kid"] ?? "";
1882
+ if (jti.startsWith("canary-") && client["canaryTokenCache"].has(jti.slice(7)) || kid.startsWith("canary-") && client["canaryTokenCache"].has(kid.slice(7))) {
1883
+ client.track("http.canary.triggered", {
1884
+ ip,
1885
+ userId,
1886
+ meta: { endpoint: url, method, token: jti.slice(7, 23), source: "canary_jwt" }
1887
+ });
1888
+ }
1889
+ }
1890
+ } catch {
1891
+ }
1892
+ }
1893
+ }
1894
+ const urlPath = url.split("?")[0]?.toLowerCase() ?? url.toLowerCase();
1895
+ const isHoneypot = client["honeypotPaths"].size > 0 && [...client["honeypotPaths"]].some(
1896
+ (hp) => urlPath === hp || urlPath.startsWith(hp + "/") || urlPath.startsWith(hp + ".")
1897
+ );
1898
+ if (isHoneypot) {
1899
+ const honeypotType = detectHoneypotType(url);
1900
+ const callbackBase = client.config.ingestUrl.replace(/\/v1\/events$/, "");
1901
+ const orgId = "";
1902
+ if (method === "POST" && honeypotType === "admin_portal") {
1903
+ const body_ = req["body"];
1904
+ const username = String(
1905
+ body_?.["username"] ?? body_?.["email"] ?? body_?.["user"] ?? body_?.["login"] ?? ""
1906
+ );
1907
+ const password = String(
1908
+ body_?.["password"] ?? body_?.["pass"] ?? body_?.["pwd"] ?? body_?.["passwd"] ?? ""
1909
+ );
1910
+ if (username || password) {
1911
+ const passwordHint = password.length > 0 ? `${password.slice(0, 2)}***[${password.length}]` : "(empty)";
1912
+ client.track("http.honeypot.credential_attempt", {
1913
+ ip,
1914
+ userId,
1915
+ meta: {
1916
+ endpoint: url,
1917
+ method,
1918
+ userAgent: ua,
1919
+ honeypotType,
1920
+ username,
1921
+ passwordHint,
1922
+ // Hidden _token field — if this is the canary token we issued
1923
+ // in the GET response, we know this is the same attacker session
1924
+ formToken: String(body_?.["_token"] ?? "")
1925
+ }
1926
+ });
1927
+ }
1928
+ const failed = makeAdminLoginFailed();
1929
+ sendFakeResponse(res, failed);
1930
+ return;
1931
+ }
1932
+ const canaryToken = randomBytes(16).toString("hex");
1933
+ const fakeResponse = generateHoneypotResponse(honeypotType, canaryToken, callbackBase, orgId);
1934
+ client.track("http.honeypot.hit", {
1935
+ ip,
1936
+ userId,
1937
+ meta: {
1938
+ endpoint: url,
1939
+ method,
1940
+ userAgent: ua,
1941
+ honeypotType,
1942
+ canaryToken,
1943
+ responseType: "enhanced"
1944
+ }
1945
+ });
1946
+ sendFakeResponse(res, fakeResponse);
1947
+ return;
1948
+ }
1949
+ const fwMatch = client.matchFirewallRule({ url, body: req["body"], headers: headersStr ?? {}, ip });
905
1950
  if (fwMatch) {
906
1951
  client.track("http.firewall." + fwMatch.rule.action, {
907
1952
  ip,
@@ -933,8 +1978,55 @@ function createExpressMiddleware(client) {
933
1978
  client.track(EventName.REQUEST, {
934
1979
  ip,
935
1980
  userId,
936
- meta: { method, endpoint: url, status, latencyMs, userAgent: ua, bytes }
1981
+ meta: {
1982
+ method,
1983
+ endpoint: url,
1984
+ status,
1985
+ latencyMs,
1986
+ userAgent: ua,
1987
+ bytes,
1988
+ _fp: fingerprint.score,
1989
+ _fpSig: fingerprint.signals.join(","),
1990
+ ...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
1991
+ // HTTP/2 SETTINGS from client connection preface (when Node.js is TLS endpoint)
1992
+ ...h2settings ? {
1993
+ _h2Window: h2settings.initialWindowSize,
1994
+ _h2HdrTable: h2settings.headerTableSize,
1995
+ _h2Score: h2settings.score,
1996
+ _h2Signals: h2settings.signals.join(",")
1997
+ } : {},
1998
+ // JA3/JA4 from upstream proxy (Nginx with ngx_ssl_fingerprint_module)
1999
+ ...upstreamTls.ja3 ? { _ja3: upstreamTls.ja3 } : {},
2000
+ ...upstreamTls.ja4 ? { _ja4: upstreamTls.ja4 } : {},
2001
+ ...agentInfo.isAgent ? {
2002
+ agentDetected: true,
2003
+ agentType: agentInfo.agentType,
2004
+ agentName: agentInfo.agentName,
2005
+ agentConfidence: agentInfo.confidence,
2006
+ mcpSessionId: agentInfo.sessionId,
2007
+ isMcp: agentInfo.isMcp,
2008
+ agentSignals: agentInfo.signals.join(",")
2009
+ } : {}
2010
+ }
937
2011
  });
2012
+ if (agentInfo.isAgent) {
2013
+ client.track("http.agent_detected", {
2014
+ ip,
2015
+ userId,
2016
+ meta: {
2017
+ endpoint: url,
2018
+ method,
2019
+ agentType: agentInfo.agentType,
2020
+ agentName: agentInfo.agentName,
2021
+ agentConfidence: agentInfo.confidence,
2022
+ mcpSessionId: agentInfo.sessionId,
2023
+ isMcp: agentInfo.isMcp,
2024
+ agentSignals: agentInfo.signals.join(","),
2025
+ status,
2026
+ userAgent: ua
2027
+ }
2028
+ });
2029
+ }
938
2030
  if (client.config.detect.rateAbuse && status === 429) {
939
2031
  client.track(EventName.RATE_LIMIT, { ip, userId, meta: { url, method, statusCode: status } });
940
2032
  }
@@ -966,7 +2058,20 @@ function createFastifyPlugin(client) {
966
2058
  const method_ = req["method"]?.toUpperCase() ?? "GET";
967
2059
  const hdrs_ = req["headers"];
968
2060
  if (client.isBlocked(ip)) {
2061
+ const reason_ = client.blockReason(ip);
969
2062
  client.reportBlockedHit(ip, { method: method_, url: url_, userAgent: hdrs_?.["user-agent"] ?? "" });
2063
+ if (reason_?.source === "community") {
2064
+ client.track("http.community_threat_blocked", {
2065
+ ip,
2066
+ meta: {
2067
+ endpoint: url_,
2068
+ method: method_,
2069
+ score: reason_.score,
2070
+ topAttack: reason_.topAttack,
2071
+ source: "anomira_network"
2072
+ }
2073
+ });
2074
+ }
970
2075
  const rep = reply;
971
2076
  if (typeof rep["code"] === "function") {
972
2077
  const chained = rep["code"](403);
@@ -1021,13 +2126,62 @@ function createFastifyPlugin(client) {
1021
2126
  const userId = client.config.getUserId(req);
1022
2127
  const status = reply["statusCode"] ?? 0;
1023
2128
  const headers = req["headers"];
1024
- const ua = headers?.["user-agent"] ?? "";
2129
+ const ua = (typeof headers?.["user-agent"] === "string" ? headers["user-agent"] : "") ?? "";
1025
2130
  const latencyMs = reply["elapsedTime"] ?? 0;
2131
+ const agentInfo = detectAgent(headers ?? {});
2132
+ const fingerprint = computeBrowserFingerprint(headers ?? {}, ua);
2133
+ const h2settings = extractHttp2Settings(req);
2134
+ const upstreamTls = extractUpstreamTls(headers ?? {});
1026
2135
  client.track(EventName.REQUEST, {
1027
2136
  ip,
1028
2137
  userId,
1029
- meta: { method, endpoint: url, status, latencyMs: Math.round(latencyMs), userAgent: ua, bytes: 0 }
2138
+ meta: {
2139
+ method,
2140
+ endpoint: url,
2141
+ status,
2142
+ latencyMs: Math.round(latencyMs),
2143
+ userAgent: ua,
2144
+ bytes: 0,
2145
+ _fp: fingerprint.score,
2146
+ _fpSig: fingerprint.signals.join(","),
2147
+ ...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
2148
+ ...h2settings ? {
2149
+ _h2Window: h2settings.initialWindowSize,
2150
+ _h2HdrTable: h2settings.headerTableSize,
2151
+ _h2Score: h2settings.score,
2152
+ _h2Signals: h2settings.signals.join(",")
2153
+ } : {},
2154
+ ...upstreamTls.ja3 ? { _ja3: upstreamTls.ja3 } : {},
2155
+ ...upstreamTls.ja4 ? { _ja4: upstreamTls.ja4 } : {},
2156
+ ...agentInfo.isAgent ? {
2157
+ agentDetected: true,
2158
+ agentType: agentInfo.agentType,
2159
+ agentName: agentInfo.agentName,
2160
+ agentConfidence: agentInfo.confidence,
2161
+ mcpSessionId: agentInfo.sessionId,
2162
+ isMcp: agentInfo.isMcp,
2163
+ agentSignals: agentInfo.signals.join(",")
2164
+ } : {}
2165
+ }
1030
2166
  });
2167
+ if (agentInfo.isAgent) {
2168
+ client.track("http.agent_detected", {
2169
+ ip,
2170
+ userId,
2171
+ meta: {
2172
+ endpoint: url,
2173
+ method,
2174
+ agentType: agentInfo.agentType,
2175
+ agentName: agentInfo.agentName,
2176
+ agentConfidence: agentInfo.confidence,
2177
+ mcpSessionId: agentInfo.sessionId,
2178
+ isMcp: agentInfo.isMcp,
2179
+ agentSignals: agentInfo.signals.join(","),
2180
+ status,
2181
+ userAgent: ua
2182
+ }
2183
+ });
2184
+ }
1031
2185
  if (client.config.detect.rateAbuse && status === 429) {
1032
2186
  client.track(EventName.RATE_LIMIT, { ip, userId, meta: { url, method, statusCode: status } });
1033
2187
  }