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