blackveil-dns 2.6.2 → 2.9.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/stdio.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { z, ZodError } from 'zod';
3
- import { checkSubdomailing as checkSubdomailing$1, checkSVCBHTTPS, checkDANEHTTPS, checkDANE, checkHTTPSecurity, checkTLSRPT, checkBIMI, checkCAA, checkNS, checkMTASTS, checkSSL, checkDNSSEC, checkDKIM, checkDMARC, checkSPF, checkSubdomainTakeover as checkSubdomainTakeover$1, createFinding as createFinding$1, buildCheckResult as buildCheckResult$1, parseDmarcTags, checkMX } from '@blackveil/dns-checks';
3
+ import { checkSubdomailing as checkSubdomailing$1, checkSVCBHTTPS, checkDANEHTTPS, checkDANE, checkHTTPSecurity, checkTLSRPT, checkBIMI, checkCAA, checkNS, checkMTASTS, checkSSL, checkDNSSEC, checkDKIM, checkDMARC, checkSPF, checkSubdomainTakeover as checkSubdomainTakeover$1, createFinding as createFinding$1, buildCheckResult as buildCheckResult$1, checkMX, parseDmarcTags } from '@blackveil/dns-checks';
4
4
  import { PROFILE_WEIGHTS, createFinding, buildCheckResult, detectDomainContext, getProfileWeights, computeScanScore, IMPORTANCE_WEIGHTS, scoreToGrade } from '@blackveil/dns-checks/scoring';
5
+ import 'drizzle-orm';
6
+ import 'drizzle-orm/durable-sqlite';
5
7
 
6
8
  var __defProp = Object.defineProperty;
7
9
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -75,7 +77,7 @@ var init_log = __esm({
75
77
  });
76
78
 
77
79
  // src/lib/config.ts
78
- var BLOCKED_SUFFIXES, BLOCKED_HOSTS, BLOCKED_IP_PATTERNS, BLOCKED_DNS_REBINDING, MAX_DOMAIN_LENGTH, MAX_LABEL_LENGTH, LABEL_REGEX, HTTPS_TIMEOUT_MS, DNS_TIMEOUT_MS, DNS_RETRIES, DOH_EDGE_CACHE_TTL, INFLIGHT_CLEANUP_MS, DNS_RETRY_BASE_DELAY_MS, DNS_CONFIRM_WITH_SECONDARY_ON_EMPTY, GLOBAL_DAILY_TOOL_LIMIT, TIER_DAILY_LIMITS, TIER_TOOL_DAILY_LIMITS, FREE_TOOL_DAILY_LIMITS, TIER_CONCURRENT_LIMITS;
80
+ var BLOCKED_SUFFIXES, BLOCKED_HOSTS, BLOCKED_IP_PATTERNS, BLOCKED_DNS_REBINDING, MAX_DOMAIN_LENGTH, MAX_LABEL_LENGTH, LABEL_REGEX, HTTPS_TIMEOUT_MS, DNS_TIMEOUT_MS, DNS_RETRIES, DOH_EDGE_CACHE_TTL, INFLIGHT_CLEANUP_MS, DNS_RETRY_BASE_DELAY_MS, DNS_CONFIRM_WITH_SECONDARY_ON_EMPTY, GLOBAL_DAILY_TOOL_LIMIT, TIER_DAILY_LIMITS, TIER_TOOL_DAILY_LIMITS, FREE_TOOL_DAILY_LIMITS, TIER_CONCURRENT_LIMITS, IP_LOCK_TTL_MS, IP_LOCK_RETRY_MS;
79
81
  var init_config = __esm({
80
82
  "src/lib/config.ts"() {
81
83
  BLOCKED_SUFFIXES = [
@@ -195,7 +197,14 @@ var init_config = __esm({
195
197
  resolve_spf_chain: 100,
196
198
  discover_subdomains: 50,
197
199
  map_compliance: 75,
198
- simulate_attack_paths: 75
200
+ simulate_attack_paths: 75,
201
+ check_dbl: 30,
202
+ check_rbl: 20,
203
+ cymru_asn: 30,
204
+ rdap_lookup: 20,
205
+ check_nsec_walkability: 30,
206
+ check_dnssec_chain: 30,
207
+ check_fast_flux: 10
199
208
  };
200
209
  TIER_CONCURRENT_LIMITS = {
201
210
  free: 3,
@@ -205,6 +214,8 @@ var init_config = __esm({
205
214
  partner: 50,
206
215
  owner: Infinity
207
216
  };
217
+ IP_LOCK_TTL_MS = 500;
218
+ IP_LOCK_RETRY_MS = 200;
208
219
  }
209
220
  });
210
221
 
@@ -213,6 +224,7 @@ var cache_exports = {};
213
224
  __export(cache_exports, {
214
225
  IN_MEMORY_CACHE: () => IN_MEMORY_CACHE,
215
226
  TTLCache: () => TTLCache,
227
+ cacheDelete: () => cacheDelete,
216
228
  cacheGet: () => cacheGet,
217
229
  cacheSet: () => cacheSet,
218
230
  cacheSetDeferred: () => cacheSetDeferred,
@@ -230,6 +242,15 @@ async function cacheGet(key, kv) {
230
242
  }
231
243
  return IN_MEMORY_CACHE.get(key);
232
244
  }
245
+ async function cacheDelete(key, kv) {
246
+ IN_MEMORY_CACHE.delete(key);
247
+ if (kv) {
248
+ try {
249
+ await kv.delete(key);
250
+ } catch {
251
+ }
252
+ }
253
+ }
233
254
  function cacheSetDeferred(key, value, ctx, kv, ttlSeconds) {
234
255
  ctx.waitUntil(cacheSet(key, value, kv, ttlSeconds));
235
256
  }
@@ -424,6 +445,7 @@ var init_dns_types = __esm({
424
445
  DNSKEY: 48,
425
446
  DS: 43,
426
447
  RRSIG: 46,
448
+ NSEC3PARAM: 51,
427
449
  PTR: 12,
428
450
  SRV: 33,
429
451
  HTTPS: 65
@@ -485,7 +507,7 @@ function hasTypedAnswers(response, type) {
485
507
  function retryDelay(attempt) {
486
508
  return new Promise((r) => setTimeout(r, DNS_RETRY_BASE_DELAY_MS * (attempt + 1) + Math.random() * 50));
487
509
  }
488
- async function fetchDohResponse(url, timeoutMs, opts) {
510
+ async function fetchDohOutcome(url, timeoutMs, opts) {
489
511
  try {
490
512
  const headers = { Accept: "application/dns-json" };
491
513
  if (opts?.token) headers["X-BV-Token"] = opts.token;
@@ -496,11 +518,25 @@ async function fetchDohResponse(url, timeoutMs, opts) {
496
518
  ...opts?.useEdgeCache ? { cf: { cacheTtl: DOH_EDGE_CACHE_TTL, cacheEverything: true } } : {}
497
519
  });
498
520
  const response = opts?.semaphore ? await opts.semaphore.run(doFetch) : await doFetch();
499
- if (!response.ok) return null;
521
+ if (!response.ok) {
522
+ logError("DNS fetch non-2xx", {
523
+ severity: "warn",
524
+ category: "dns-transport",
525
+ details: { url: url.replace(/name=[^&]+/, "name=<domain>"), status: response.status }
526
+ });
527
+ return { kind: "error", reason: "http" };
528
+ }
500
529
  const data = await response.json();
501
530
  const parsed = DohResponseSchema.safeParse(data);
502
- if (!parsed.success) return null;
503
- return parsed.data;
531
+ if (!parsed.success) {
532
+ logError("DNS parse failure", {
533
+ severity: "warn",
534
+ category: "dns-transport",
535
+ details: { url: url.replace(/name=[^&]+/, "name=<domain>") }
536
+ });
537
+ return { kind: "error", reason: "parse" };
538
+ }
539
+ return { kind: "ok", response: parsed.data };
504
540
  } catch (err) {
505
541
  const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
506
542
  logError(isTimeout ? "DNS fetch timeout" : "DNS fetch failed", {
@@ -508,7 +544,7 @@ async function fetchDohResponse(url, timeoutMs, opts) {
508
544
  category: "dns-transport",
509
545
  details: { url: url.replace(/name=[^&]+/, "name=<domain>"), errorType: isTimeout ? "timeout" : "network" }
510
546
  });
511
- return null;
547
+ return { kind: "error", reason: isTimeout ? "timeout" : "network" };
512
548
  }
513
549
  }
514
550
  async function queryDns(domain, type, dnssecCheck = false, opts) {
@@ -570,43 +606,38 @@ async function queryDnsUncached(domain, type, dnssecCheck = false, opts) {
570
606
  }
571
607
  const data = validated.data;
572
608
  if (confirmWithSecondaryOnEmpty && !opts?.skipSecondaryConfirmation && !hasTypedAnswers(data, type)) {
573
- const secondaryResult = await confirmWithSecondaryResolvers(domain, type, dnssecCheck, timeoutMs, sem, opts);
574
- if (secondaryResult) return secondaryResult;
609
+ const secondaryOpts = opts?.secondaryDoh ? { secondaryDoh: { url: opts.secondaryDoh.endpoint, token: opts.secondaryDoh.token } } : void 0;
610
+ const secondaryResult = await confirmWithSecondaryResolvers(domain, type, dnssecCheck, timeoutMs, sem, secondaryOpts);
611
+ if ("kind" in secondaryResult && secondaryResult.kind === "unconfirmed") {
612
+ return data;
613
+ }
614
+ const confirmedResponse = secondaryResult;
615
+ return confirmedResponse;
575
616
  }
576
617
  return data;
577
618
  }
578
619
  throw new DnsQueryError("DNS query failed after retries", domain, type);
579
620
  }
580
621
  async function confirmWithSecondaryResolvers(domain, type, dnssecCheck, timeoutMs, sem, opts) {
581
- const hasBvDns = !!opts?.secondaryDoh?.endpoint;
622
+ const bvDnsUrl = opts?.secondaryDoh ? buildDohUrl(opts.secondaryDoh.url, domain, type, dnssecCheck) : null;
582
623
  const googleUrl = buildDohUrl(GOOGLE_DOH_ENDPOINT, domain, type, dnssecCheck);
583
- if (hasBvDns) {
584
- const bvDnsUrl = buildDohUrl(opts.secondaryDoh.endpoint, domain, type, dnssecCheck);
585
- const candidates = [
586
- fetchDohResponse(bvDnsUrl, timeoutMs, { token: opts.secondaryDoh.token, semaphore: sem }),
587
- fetchDohResponse(googleUrl, timeoutMs, { useEdgeCache: true, semaphore: sem })
588
- ];
589
- const results = await Promise.allSettled(candidates);
590
- for (const r of results) {
591
- if (r.status === "fulfilled" && r.value && hasTypedAnswers(r.value, type)) {
592
- return r.value;
593
- }
594
- }
595
- const allFailed = results.every((r) => r.status === "rejected" || !r.value);
596
- if (allFailed) {
597
- logError("All secondary DNS resolvers failed", {
598
- severity: "warn",
599
- category: "dns-transport",
600
- details: { domain, type }
601
- });
602
- }
603
- return null;
624
+ const candidates = [
625
+ bvDnsUrl ? fetchDohOutcome(bvDnsUrl, timeoutMs, { token: opts.secondaryDoh.token, semaphore: sem }) : Promise.resolve({ kind: "error", reason: "network" }),
626
+ fetchDohOutcome(googleUrl, timeoutMs, { useEdgeCache: true, semaphore: sem })
627
+ ];
628
+ const results = await Promise.allSettled(candidates);
629
+ for (const r of results) {
630
+ if (r.status === "fulfilled" && r.value.kind === "ok" && hasTypedAnswers(r.value.response, type)) return r.value.response;
604
631
  }
605
- const google = await fetchDohResponse(googleUrl, timeoutMs, { useEdgeCache: true, semaphore: sem });
606
- if (google && hasTypedAnswers(google, type)) {
607
- return google;
632
+ for (const r of results) {
633
+ if (r.status === "fulfilled" && r.value.kind === "ok") return r.value.response;
608
634
  }
609
- return null;
635
+ logError("All secondary resolvers failed", {
636
+ severity: "warn",
637
+ category: "dns-transport",
638
+ details: { type }
639
+ });
640
+ return { kind: "unconfirmed" };
610
641
  }
611
642
  var DOH_ENDPOINT, GOOGLE_DOH_ENDPOINT, DnsQueryError;
612
643
  var init_dns_transport = __esm({
@@ -701,6 +732,21 @@ var init_dns2 = __esm({
701
732
  }
702
733
  });
703
734
 
735
+ // src/lib/dns-query-adapter.ts
736
+ function makeQueryDNS(dnsOptions) {
737
+ return async (domain, type) => {
738
+ if (type === "TXT") {
739
+ return queryTxtRecords(domain, dnsOptions);
740
+ }
741
+ return queryDnsRecords(domain, type, dnsOptions);
742
+ };
743
+ }
744
+ var init_dns_query_adapter = __esm({
745
+ "src/lib/dns-query-adapter.ts"() {
746
+ init_dns2();
747
+ }
748
+ });
749
+
704
750
  // src/lib/provider-signature-source.ts
705
751
  function normalizeSha256(value) {
706
752
  return value.trim().toLowerCase().replace(/^sha256:/, "");
@@ -932,19 +978,11 @@ var check_mx_exports = {};
932
978
  __export(check_mx_exports, {
933
979
  checkMx: () => checkMx
934
980
  });
935
- function makeQueryDNS15(dnsOptions) {
936
- return async (domain, type) => {
937
- if (type === "TXT") {
938
- return queryTxtRecords(domain, dnsOptions);
939
- }
940
- return queryDnsRecords(domain, type, dnsOptions);
941
- };
942
- }
943
981
  async function checkMx(domain, options, dnsOptions) {
944
982
  const timeout = dnsOptions?.timeoutMs ?? 5e3;
945
983
  const baseResult = await checkMX(
946
984
  domain,
947
- makeQueryDNS15(dnsOptions),
985
+ makeQueryDNS(dnsOptions),
948
986
  { timeout }
949
987
  );
950
988
  const hasCritical = baseResult.findings.some((f) => f.severity === "critical");
@@ -953,6 +991,7 @@ async function checkMx(domain, options, dnsOptions) {
953
991
  return baseResult;
954
992
  }
955
993
  const findings = [...baseResult.findings];
994
+ let providerDetectionFailed = false;
956
995
  try {
957
996
  const mxAnswers = await queryDnsRecords(domain, "MX", dnsOptions);
958
997
  const mxTargets = mxAnswers.map((answer) => {
@@ -982,6 +1021,7 @@ async function checkMx(domain, options, dnsOptions) {
982
1021
  );
983
1022
  }
984
1023
  if (providerSignatures.degraded) {
1024
+ providerDetectionFailed = true;
985
1025
  findings.push(
986
1026
  createFinding$1(
987
1027
  "mx",
@@ -1000,6 +1040,7 @@ async function checkMx(domain, options, dnsOptions) {
1000
1040
  }
1001
1041
  }
1002
1042
  } catch (err) {
1043
+ providerDetectionFailed = true;
1003
1044
  logEvent({
1004
1045
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1005
1046
  severity: "warn",
@@ -1010,11 +1051,16 @@ async function checkMx(domain, options, dnsOptions) {
1010
1051
  details: { phase: "provider_detection" }
1011
1052
  });
1012
1053
  }
1013
- return { ...baseResult, findings };
1054
+ return {
1055
+ ...baseResult,
1056
+ findings,
1057
+ ...providerDetectionFailed ? { metadata: { providerDetectionFailed: true } } : {}
1058
+ };
1014
1059
  }
1015
1060
  var init_check_mx = __esm({
1016
1061
  "src/tools/check-mx.ts"() {
1017
1062
  init_dns2();
1063
+ init_dns_query_adapter();
1018
1064
  init_provider_signatures();
1019
1065
  init_log();
1020
1066
  }
@@ -1215,8 +1261,8 @@ function checkToolDailyRateLimitInMemory(principalId, toolName, limit) {
1215
1261
  const key = `${principalId}:${toolName.trim().toLowerCase()}`;
1216
1262
  const entry = getOrCreateToolDailyEntry(key);
1217
1263
  entry.timestamps = pruneTimestamps(entry.timestamps, DAY_MS, now);
1218
- const count = entry.timestamps.length;
1219
- if (count >= limit) {
1264
+ const count2 = entry.timestamps.length;
1265
+ if (count2 >= limit) {
1220
1266
  const oldestInWindow = entry.timestamps[0];
1221
1267
  const retryAfterMs = oldestInWindow + DAY_MS - now;
1222
1268
  return {
@@ -1360,6 +1406,7 @@ var CircuitBreaker = class {
1360
1406
 
1361
1407
  // src/lib/rate-limiter.ts
1362
1408
  init_log();
1409
+ init_config();
1363
1410
  var MINUTE_LIMIT = 50;
1364
1411
  var HOUR_LIMIT = 300;
1365
1412
  var CONTROL_PLANE_MINUTE_LIMIT = 60;
@@ -1368,6 +1415,7 @@ var MINUTE_MS2 = 6e4;
1368
1415
  var HOUR_MS2 = 36e5;
1369
1416
  var DAY_MS2 = 864e5;
1370
1417
  var KV_IP_LOCK_TAILS = /* @__PURE__ */ new Map();
1418
+ var KV_IP_LOCK_MAX = 5e3;
1371
1419
  var quotaCoordinatorBreaker = new CircuitBreaker({
1372
1420
  name: "QuotaCoordinator",
1373
1421
  failureThreshold: 3,
@@ -1428,12 +1476,15 @@ async function checkScopedRateLimitKV(ip, scope, minuteLimit, hourLimit, kv) {
1428
1476
  });
1429
1477
  }
1430
1478
  async function checkRateLimitKV(ip, kv) {
1431
- return checkScopedRateLimitKV(ip, "tools", MINUTE_LIMIT, HOUR_LIMIT, kv);
1479
+ return checkScopedRateLimitKVWithAdvisory(ip, "tools", MINUTE_LIMIT, HOUR_LIMIT, kv);
1432
1480
  }
1433
1481
  async function checkControlPlaneRateLimitKV(ip, kv) {
1434
1482
  return checkScopedRateLimitKV(ip, "control", CONTROL_PLANE_MINUTE_LIMIT, CONTROL_PLANE_HOUR_LIMIT, kv);
1435
1483
  }
1436
1484
  async function withIpKvLock(ip, work) {
1485
+ if (KV_IP_LOCK_TAILS.size >= KV_IP_LOCK_MAX && !KV_IP_LOCK_TAILS.has(ip)) {
1486
+ return work();
1487
+ }
1437
1488
  const prevTail = KV_IP_LOCK_TAILS.get(ip) ?? Promise.resolve();
1438
1489
  let release;
1439
1490
  const current = new Promise((resolve) => {
@@ -1451,6 +1502,33 @@ async function withIpKvLock(ip, work) {
1451
1502
  }
1452
1503
  }
1453
1504
  }
1505
+ async function withIpKvAdvisoryLock(ip, kv, work) {
1506
+ const contended = KV_IP_LOCK_TAILS.has(ip);
1507
+ if (!contended) return work();
1508
+ const advisoryKey = `lk:ip:${ip}`;
1509
+ try {
1510
+ const held = await kv.get(advisoryKey);
1511
+ if (held) await new Promise((r) => setTimeout(r, IP_LOCK_RETRY_MS));
1512
+ await kv.put(advisoryKey, String(Date.now()), { expirationTtl: Math.max(1, Math.ceil(IP_LOCK_TTL_MS / 1e3)) });
1513
+ } catch {
1514
+ }
1515
+ try {
1516
+ return await work();
1517
+ } finally {
1518
+ try {
1519
+ await kv.delete(advisoryKey);
1520
+ } catch {
1521
+ }
1522
+ }
1523
+ }
1524
+ async function checkScopedRateLimitKVWithAdvisory(ip, scope, minuteLimit, hourLimit, kv) {
1525
+ try {
1526
+ return await withIpKvAdvisoryLock(ip, kv, () => checkScopedRateLimitKV(ip, scope, minuteLimit, hourLimit, kv));
1527
+ } catch {
1528
+ logError("[rate-limiter] KV error, falling back to in-memory");
1529
+ return checkScopedRateLimitInMemory(ip, scope, minuteLimit, hourLimit);
1530
+ }
1531
+ }
1454
1532
  async function checkToolDailyRateLimitKV(principalId, toolName, limit, kv) {
1455
1533
  return withIpKvLock(`tool:${principalId}:${toolName}`, async () => {
1456
1534
  const now = Date.now();
@@ -1682,15 +1760,15 @@ function checkSessionCreateRateLimitInMemory(ip) {
1682
1760
  remaining: SESSION_CREATE_LIMIT_PER_MINUTE - recent.length
1683
1761
  };
1684
1762
  }
1685
- function evictOldestSessionCreateIps(count) {
1686
- if (count <= 0) return;
1763
+ function evictOldestSessionCreateIps(count2) {
1764
+ if (count2 <= 0) return;
1687
1765
  const entries = [];
1688
1766
  for (const [key, timestamps] of SESSION_CREATE_BY_IP.entries()) {
1689
1767
  const latest = timestamps.length > 0 ? timestamps[timestamps.length - 1] : 0;
1690
1768
  entries.push([key, latest]);
1691
1769
  }
1692
1770
  entries.sort((a, b) => a[1] - b[1]);
1693
- for (let i = 0; i < count && i < entries.length; i++) {
1771
+ for (let i = 0; i < count2 && i < entries.length; i++) {
1694
1772
  SESSION_CREATE_BY_IP.delete(entries[i][0]);
1695
1773
  }
1696
1774
  }
@@ -1828,7 +1906,7 @@ function generateSessionId() {
1828
1906
  crypto.getRandomValues(bytes);
1829
1907
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1830
1908
  }
1831
- async function createSession(kv) {
1909
+ async function createSession(kv, analytics) {
1832
1910
  const id = generateSessionId();
1833
1911
  const now = Date.now();
1834
1912
  const record = { createdAt: now, lastAccessedAt: now };
@@ -1838,6 +1916,7 @@ async function createSession(kv) {
1838
1916
  await createSessionKVRecord(id, kv, record);
1839
1917
  } catch {
1840
1918
  logError("[session] KV create failed, in-memory fallback active", { category: "session" });
1919
+ analytics?.emitDegradationEvent({ degradationType: "kv_fallback", component: "session" });
1841
1920
  }
1842
1921
  }
1843
1922
  return id;
@@ -2372,6 +2451,11 @@ var GenerateRolloutPlanArgs = z.object({
2372
2451
  timeline: TimelineSchema.optional().describe("Rollout speed: aggressive, standard, conservative (default: standard)"),
2373
2452
  format: FormatSchema.optional().describe("Output verbosity. Auto-detected if omitted.")
2374
2453
  }).passthrough();
2454
+ var CheckFastFluxArgs = z.object({
2455
+ domain: DomainSchema.describe("Domain to check (e.g., example.com)"),
2456
+ rounds: z.number().int().min(3).max(5).optional().describe("Number of query rounds (3-5, default 3)."),
2457
+ format: FormatSchema.optional().describe("Output verbosity. Auto-detected if omitted.")
2458
+ }).passthrough();
2375
2459
  var TOOL_SCHEMA_MAP = {
2376
2460
  check_mx: BaseDomainArgs,
2377
2461
  check_spf: BaseDomainArgs,
@@ -2416,7 +2500,14 @@ var TOOL_SCHEMA_MAP = {
2416
2500
  resolve_spf_chain: BaseDomainArgs,
2417
2501
  discover_subdomains: BaseDomainArgs,
2418
2502
  map_compliance: BaseDomainArgs,
2419
- simulate_attack_paths: BaseDomainArgs
2503
+ simulate_attack_paths: BaseDomainArgs,
2504
+ check_dbl: BaseDomainArgs,
2505
+ check_rbl: BaseDomainArgs,
2506
+ cymru_asn: BaseDomainArgs,
2507
+ rdap_lookup: BaseDomainArgs,
2508
+ check_nsec_walkability: BaseDomainArgs,
2509
+ check_dnssec_chain: BaseDomainArgs,
2510
+ check_fast_flux: CheckFastFluxArgs
2420
2511
  };
2421
2512
 
2422
2513
  // src/handlers/tool-args.ts
@@ -2519,15 +2610,7 @@ function extractExplainFindingArgs(args) {
2519
2610
  init_cache();
2520
2611
 
2521
2612
  // src/tools/check-spf.ts
2522
- init_dns2();
2523
- function makeQueryDNS(dnsOptions) {
2524
- return async (domain, type) => {
2525
- if (type === "TXT") {
2526
- return queryTxtRecords(domain, dnsOptions);
2527
- }
2528
- return queryDnsRecords(domain, type, dnsOptions);
2529
- };
2530
- }
2613
+ init_dns_query_adapter();
2531
2614
  async function checkSpf(domain, dnsOptions) {
2532
2615
  return checkSPF(
2533
2616
  domain,
@@ -2537,25 +2620,17 @@ async function checkSpf(domain, dnsOptions) {
2537
2620
  }
2538
2621
 
2539
2622
  // src/tools/check-dmarc.ts
2540
- init_dns2();
2541
- function makeQueryDNS2(dnsOptions) {
2542
- return async (domain, type) => {
2543
- if (type === "TXT") {
2544
- return queryTxtRecords(domain, dnsOptions);
2545
- }
2546
- return queryDnsRecords(domain, type, dnsOptions);
2547
- };
2548
- }
2623
+ init_dns_query_adapter();
2549
2624
  async function checkDmarc(domain, dnsOptions) {
2550
2625
  return checkDMARC(
2551
2626
  domain,
2552
- makeQueryDNS2(dnsOptions),
2627
+ makeQueryDNS(dnsOptions),
2553
2628
  { timeout: dnsOptions?.timeoutMs ?? 5e3 }
2554
2629
  );
2555
2630
  }
2556
2631
 
2557
2632
  // src/tools/check-dkim.ts
2558
- init_dns2();
2633
+ init_dns_query_adapter();
2559
2634
  var HIGH_CONFIDENCE_DKIM_PROVIDERS = /* @__PURE__ */ new Set([
2560
2635
  "amazon ses",
2561
2636
  "sendgrid",
@@ -2565,18 +2640,10 @@ var HIGH_CONFIDENCE_DKIM_PROVIDERS = /* @__PURE__ */ new Set([
2565
2640
  "microsoft 365"
2566
2641
  ]);
2567
2642
  var MEDIUM_CONFIDENCE_DKIM_PROVIDERS = /* @__PURE__ */ new Set(["proofpoint", "mimecast"]);
2568
- function makeQueryDNS3(dnsOptions) {
2569
- return async (domain, type) => {
2570
- if (type === "TXT") {
2571
- return queryTxtRecords(domain, dnsOptions);
2572
- }
2573
- return queryDnsRecords(domain, type, dnsOptions);
2574
- };
2575
- }
2576
2643
  async function checkDkim(domain, selector, dnsOptions) {
2577
2644
  return checkDKIM(
2578
2645
  domain,
2579
- makeQueryDNS3(dnsOptions),
2646
+ makeQueryDNS(dnsOptions),
2580
2647
  { timeout: dnsOptions?.timeoutMs ?? 5e3, selector }
2581
2648
  );
2582
2649
  }
@@ -2629,6 +2696,10 @@ function applyProviderDkimContext(dkimResult, provider) {
2629
2696
 
2630
2697
  // src/tools/check-dnssec.ts
2631
2698
  init_dns2();
2699
+ init_dns_query_adapter();
2700
+
2701
+ // src/lib/adaptive-weights.ts
2702
+ var MATURITY_THRESHOLD = 200;
2632
2703
  var SCORING_NOTE_DELTA_THRESHOLD = 3;
2633
2704
  var CRITICAL_MAIL_CATEGORIES = /* @__PURE__ */ new Set(["dmarc", "spf", "dkim", "ssl"]);
2634
2705
  var CRITICAL_MAIL_PROFILES = /* @__PURE__ */ new Set(["mail_enabled", "enterprise_mail"]);
@@ -2699,19 +2770,61 @@ function formatNote(category, delta, provider) {
2699
2770
  function capitalizeWords(s) {
2700
2771
  return s.replace(/\b\w/g, (c) => c.toUpperCase());
2701
2772
  }
2702
- function makeQueryDNS4(dnsOptions) {
2703
- return async (domain, type) => {
2704
- if (type === "TXT") {
2705
- return queryTxtRecords(domain, dnsOptions);
2706
- }
2707
- return queryDnsRecords(domain, type, dnsOptions);
2708
- };
2773
+ var GOOGLE_DOH_ENDPOINT2 = "https://dns.google/resolve";
2774
+ var AD_CONFIRM_TIMEOUT_MS = 3e3;
2775
+ async function confirmAdWithGoogle(domain, timeoutMs = AD_CONFIRM_TIMEOUT_MS) {
2776
+ try {
2777
+ const url = `${GOOGLE_DOH_ENDPOINT2}?name=${encodeURIComponent(domain)}&type=A&cd=0`;
2778
+ const resp = await fetch(url, {
2779
+ method: "GET",
2780
+ redirect: "manual",
2781
+ headers: { Accept: "application/dns-json" },
2782
+ signal: AbortSignal.timeout(timeoutMs)
2783
+ });
2784
+ if (!resp.ok) return false;
2785
+ const data = await resp.json();
2786
+ return data.AD === true;
2787
+ } catch {
2788
+ return false;
2789
+ }
2790
+ }
2791
+ async function augmentWithSource(domain, baseResult, dnsOptions) {
2792
+ const [dnskeyResult, dsResult] = await Promise.allSettled([
2793
+ queryDnsRecords(domain, "DNSKEY", dnsOptions),
2794
+ queryDnsRecords(domain, "DS", dnsOptions)
2795
+ ]);
2796
+ const hasDnskey = dnskeyResult.status === "fulfilled" && dnskeyResult.value.length > 0;
2797
+ const hasDs = dsResult.status === "fulfilled" && dsResult.value.length > 0;
2798
+ const dnssecSource = hasDnskey && hasDs ? "domain_configured" : "tld_inherited";
2799
+ if (dnssecSource === "tld_inherited") {
2800
+ const inheritedFinding = createFinding(
2801
+ "dnssec",
2802
+ "DNSSEC inherited from TLD",
2803
+ "info",
2804
+ `DNSSEC validation passes but ${domain} does not have its own DNSKEY or DS records. DNSSEC protection is inherited from the TLD registry, not configured by the domain owner.`,
2805
+ { dnssecSource: "tld_inherited" }
2806
+ );
2807
+ return buildCheckResult("dnssec", [...baseResult.findings, inheritedFinding]);
2808
+ }
2809
+ if (baseResult.findings.length > 0) {
2810
+ const [first, ...rest] = baseResult.findings;
2811
+ const tagged = { ...first, metadata: { ...first.metadata ?? {}, dnssecSource: "domain_configured" } };
2812
+ return buildCheckResult("dnssec", [tagged, ...rest]);
2813
+ }
2814
+ const configuredFinding = createFinding(
2815
+ "dnssec",
2816
+ "DNSSEC configured by domain owner",
2817
+ "info",
2818
+ `${domain} has DNSKEY and DS records \u2014 DNSSEC is explicitly configured by the domain owner.`,
2819
+ { dnssecSource: "domain_configured" }
2820
+ );
2821
+ return buildCheckResult("dnssec", [configuredFinding]);
2709
2822
  }
2710
2823
  async function checkDnssec2(domain, dnsOptions) {
2711
2824
  try {
2712
2825
  const baseResult = await checkDNSSEC(
2713
2826
  domain,
2714
- makeQueryDNS4(dnsOptions),
2827
+ makeQueryDNS(dnsOptions),
2715
2828
  {
2716
2829
  timeout: dnsOptions?.timeoutMs ?? 5e3,
2717
2830
  rawQueryDNS: async (d, type, dnssecFlag) => {
@@ -2720,51 +2833,49 @@ async function checkDnssec2(domain, dnsOptions) {
2720
2833
  }
2721
2834
  }
2722
2835
  );
2723
- const dnssecAbsent = baseResult.findings.some((f) => f.title === "DNSSEC not enabled") || baseResult.findings.some((f) => f.title === "DNSSEC check failed") || baseResult.findings.some((f) => f.title === "DNSSEC chain of trust incomplete") || baseResult.findings.some((f) => f.title === "DNSSEC validation failing");
2836
+ const isDnsTransportError = baseResult.findings.some((f) => f.title === "DNSSEC check failed");
2837
+ if (isDnsTransportError) {
2838
+ return { ...baseResult, checkStatus: "error" };
2839
+ }
2840
+ const dnssecAbsent = baseResult.findings.some((f) => f.title === "DNSSEC not enabled") || baseResult.findings.some((f) => f.title === "DNSSEC check failed") || baseResult.findings.some((f) => f.title === "DNSSEC chain of trust incomplete");
2724
2841
  if (dnssecAbsent) {
2725
2842
  return baseResult;
2726
2843
  }
2727
- const [dnskeyResult, dsResult] = await Promise.allSettled([
2728
- queryDnsRecords(domain, "DNSKEY", dnsOptions),
2729
- queryDnsRecords(domain, "DS", dnsOptions)
2730
- ]);
2731
- const hasDnskey = dnskeyResult.status === "fulfilled" && dnskeyResult.value.length > 0;
2732
- const hasDs = dsResult.status === "fulfilled" && dsResult.value.length > 0;
2733
- const dnssecSource = hasDnskey && hasDs ? "domain_configured" : "tld_inherited";
2734
- if (dnssecSource === "tld_inherited") {
2735
- const inheritedFinding = createFinding(
2736
- "dnssec",
2737
- "DNSSEC inherited from TLD",
2738
- "info",
2739
- `DNSSEC validation passes but ${domain} does not have its own DNSKEY or DS records. DNSSEC protection is inherited from the TLD registry, not configured by the domain owner.`,
2740
- { dnssecSource: "tld_inherited" }
2741
- );
2742
- return buildCheckResult("dnssec", [...baseResult.findings, inheritedFinding]);
2743
- }
2744
- if (baseResult.findings.length > 0) {
2745
- const [first, ...rest] = baseResult.findings;
2746
- const tagged = { ...first, metadata: { ...first.metadata ?? {}, dnssecSource: "domain_configured" } };
2747
- return buildCheckResult("dnssec", [tagged, ...rest]);
2844
+ const validationFailing = baseResult.findings.some((f) => f.title === "DNSSEC validation failing");
2845
+ if (validationFailing) {
2846
+ const googleConfirmsAd = await confirmAdWithGoogle(domain, dnsOptions?.timeoutMs ?? AD_CONFIRM_TIMEOUT_MS);
2847
+ if (googleConfirmsAd) {
2848
+ const correctedResult = await checkDNSSEC(
2849
+ domain,
2850
+ makeQueryDNS(dnsOptions),
2851
+ {
2852
+ timeout: dnsOptions?.timeoutMs ?? 5e3,
2853
+ rawQueryDNS: async (d, type, dnssecFlag) => {
2854
+ const resp = await queryDns(d, type, dnssecFlag ?? false, dnsOptions);
2855
+ return { AD: true, Answer: resp.Answer };
2856
+ }
2857
+ }
2858
+ );
2859
+ return augmentWithSource(domain, correctedResult, dnsOptions);
2860
+ }
2861
+ return baseResult;
2748
2862
  }
2749
- const configuredFinding = createFinding(
2750
- "dnssec",
2751
- "DNSSEC configured by domain owner",
2752
- "info",
2753
- `${domain} has DNSKEY and DS records \u2014 DNSSEC is explicitly configured by the domain owner.`,
2754
- { dnssecSource: "domain_configured" }
2755
- );
2756
- return buildCheckResult("dnssec", [configuredFinding]);
2863
+ return augmentWithSource(domain, baseResult, dnsOptions);
2757
2864
  } catch (err) {
2758
2865
  if (err instanceof DnsQueryError) {
2759
- return buildCheckResult("dnssec", [
2760
- createFinding(
2761
- "dnssec",
2762
- "DNSSEC check could not complete",
2763
- "info",
2764
- `Unable to verify DNSSEC for ${domain} \u2014 DNS query failed: ${err.message}`,
2765
- { checkStatus: "error" }
2766
- )
2767
- ]);
2866
+ const message = err.message;
2867
+ return {
2868
+ ...buildCheckResult("dnssec", [
2869
+ createFinding(
2870
+ "dnssec",
2871
+ "DNSSEC check could not complete",
2872
+ "info",
2873
+ `DNS query failed (${message}). DNSSEC posture unknown.`,
2874
+ { dnsError: message, checkStatus: "error" }
2875
+ )
2876
+ ]),
2877
+ checkStatus: "error"
2878
+ };
2768
2879
  }
2769
2880
  throw err;
2770
2881
  }
@@ -2777,38 +2888,23 @@ async function checkSsl(domain) {
2777
2888
  }
2778
2889
 
2779
2890
  // src/tools/check-mta-sts.ts
2780
- init_dns2();
2891
+ init_dns_query_adapter();
2781
2892
  init_config();
2782
- function makeQueryDNS5(dnsOptions) {
2783
- return async (domain, type) => {
2784
- if (type === "TXT") {
2785
- return queryTxtRecords(domain, dnsOptions);
2786
- }
2787
- return queryDnsRecords(domain, type, dnsOptions);
2788
- };
2789
- }
2790
2893
  async function checkMtaSts(domain, dnsOptions) {
2791
2894
  return checkMTASTS(
2792
2895
  domain,
2793
- makeQueryDNS5(dnsOptions),
2896
+ makeQueryDNS(dnsOptions),
2794
2897
  { timeout: dnsOptions?.timeoutMs ?? HTTPS_TIMEOUT_MS, fetchFn: fetch }
2795
2898
  );
2796
2899
  }
2797
2900
 
2798
2901
  // src/tools/check-ns.ts
2799
2902
  init_dns2();
2800
- function makeQueryDNS6(dnsOptions) {
2801
- return async (domain, type) => {
2802
- if (type === "TXT") {
2803
- return queryTxtRecords(domain, dnsOptions);
2804
- }
2805
- return queryDnsRecords(domain, type, dnsOptions);
2806
- };
2807
- }
2903
+ init_dns_query_adapter();
2808
2904
  async function checkNs(domain, dnsOptions) {
2809
2905
  return checkNS(
2810
2906
  domain,
2811
- makeQueryDNS6(dnsOptions),
2907
+ makeQueryDNS(dnsOptions),
2812
2908
  {
2813
2909
  timeout: dnsOptions?.timeoutMs ?? 5e3,
2814
2910
  rawQueryDNS: async (d, type, dnssecFlag) => {
@@ -2820,55 +2916,31 @@ async function checkNs(domain, dnsOptions) {
2820
2916
  }
2821
2917
 
2822
2918
  // src/tools/check-caa.ts
2823
- init_dns2();
2824
- function makeQueryDNS7(dnsOptions) {
2825
- return async (domain, type) => {
2826
- if (type === "TXT") {
2827
- return queryTxtRecords(domain, dnsOptions);
2828
- }
2829
- return queryDnsRecords(domain, type, dnsOptions);
2830
- };
2831
- }
2919
+ init_dns_query_adapter();
2832
2920
  async function checkCaa(domain, dnsOptions) {
2833
2921
  return checkCAA(
2834
2922
  domain,
2835
- makeQueryDNS7(dnsOptions),
2923
+ makeQueryDNS(dnsOptions),
2836
2924
  { timeout: dnsOptions?.timeoutMs ?? 5e3 }
2837
2925
  );
2838
2926
  }
2839
2927
 
2840
2928
  // src/tools/check-bimi.ts
2841
- init_dns2();
2842
- function makeQueryDNS8(dnsOptions) {
2843
- return async (domain, type) => {
2844
- if (type === "TXT") {
2845
- return queryTxtRecords(domain, dnsOptions);
2846
- }
2847
- return queryDnsRecords(domain, type, dnsOptions);
2848
- };
2849
- }
2929
+ init_dns_query_adapter();
2850
2930
  async function checkBimi(domain, dnsOptions) {
2851
2931
  return checkBIMI(
2852
2932
  domain,
2853
- makeQueryDNS8(dnsOptions),
2933
+ makeQueryDNS(dnsOptions),
2854
2934
  { timeout: dnsOptions?.timeoutMs ?? 5e3, fetchFn: fetch }
2855
2935
  );
2856
2936
  }
2857
2937
 
2858
2938
  // src/tools/check-tlsrpt.ts
2859
- init_dns2();
2860
- function makeQueryDNS9(dnsOptions) {
2861
- return async (domain, type) => {
2862
- if (type === "TXT") {
2863
- return queryTxtRecords(domain, dnsOptions);
2864
- }
2865
- return queryDnsRecords(domain, type, dnsOptions);
2866
- };
2867
- }
2939
+ init_dns_query_adapter();
2868
2940
  async function checkTlsrpt(domain, dnsOptions) {
2869
2941
  return checkTLSRPT(
2870
2942
  domain,
2871
- makeQueryDNS9(dnsOptions),
2943
+ makeQueryDNS(dnsOptions),
2872
2944
  { timeout: dnsOptions?.timeoutMs ?? 5e3 }
2873
2945
  );
2874
2946
  }
@@ -3945,9 +4017,9 @@ async function checkTxtHygiene(domain, dnsOptions) {
3945
4017
  }
3946
4018
  }
3947
4019
  const duplicates = [];
3948
- for (const [, { service, count }] of prefixCounts) {
3949
- if (count >= 2) {
3950
- duplicates.push({ service, count });
4020
+ for (const [, { service, count: count2 }] of prefixCounts) {
4021
+ if (count2 >= 2) {
4022
+ duplicates.push({ service, count: count2 });
3951
4023
  }
3952
4024
  }
3953
4025
  if (duplicates.length > 0) {
@@ -4042,6 +4114,38 @@ async function checkTxtHygiene(domain, dnsOptions) {
4042
4114
  return buildCheckResult("txt_hygiene", findings);
4043
4115
  }
4044
4116
  init_config();
4117
+ var SCANNER_USER_AGENT = "Mozilla/5.0 (compatible; BlackVeilDNSScanner/1.0; +https://blackveilsecurity.com)";
4118
+ var WAF_CHALLENGE_FINGERPRINTS = [
4119
+ {
4120
+ name: "cloudflare",
4121
+ // cf-ray header is conclusive on its own; body title is a belt-and-suspenders signal
4122
+ matchHeaders: (h) => !!(h.get("cf-ray") && (h.get("server") ?? "").toLowerCase().includes("cloudflare")),
4123
+ matchBody: (body) => /just a moment/i.test(body)
4124
+ },
4125
+ {
4126
+ name: "akamai",
4127
+ matchHeaders: (h) => (h.get("server") ?? "").toLowerCase().includes("akamaighost")
4128
+ }
4129
+ ];
4130
+ function detectWafChallenge(headers, body) {
4131
+ for (const fp of WAF_CHALLENGE_FINGERPRINTS) {
4132
+ if (!fp.matchHeaders(headers)) continue;
4133
+ if (fp.matchBody && body !== void 0 && !fp.matchBody(body)) continue;
4134
+ return fp.name;
4135
+ }
4136
+ return null;
4137
+ }
4138
+ var MERGE_HEADERS = [
4139
+ "content-security-policy",
4140
+ "x-frame-options",
4141
+ "x-content-type-options",
4142
+ "permissions-policy",
4143
+ "referrer-policy",
4144
+ "cross-origin-resource-policy",
4145
+ "cross-origin-opener-policy",
4146
+ "cross-origin-embedder-policy"
4147
+ ];
4148
+ var MAX_REDIRECT_HOPS = 3;
4045
4149
  function detectCdnProvider(headers) {
4046
4150
  if (headers.get("cf-ray") || headers.get("server")?.toLowerCase().includes("cloudflare")) {
4047
4151
  return "Cloudflare";
@@ -4061,9 +4165,107 @@ function detectCdnProvider(headers) {
4061
4165
  }
4062
4166
  return null;
4063
4167
  }
4168
+ async function fetchWithRedirects(url, timeoutMs) {
4169
+ let response = await fetch(url, {
4170
+ method: "HEAD",
4171
+ redirect: "manual",
4172
+ headers: { "User-Agent": SCANNER_USER_AGENT },
4173
+ signal: AbortSignal.timeout(timeoutMs)
4174
+ });
4175
+ for (let hop = 0; hop < MAX_REDIRECT_HOPS; hop++) {
4176
+ const status = response.status;
4177
+ const isRedirect = status >= 300 && status < 400 || response.type === "opaqueredirect" || status === 0 && response.headers.get("location");
4178
+ if (!isRedirect) break;
4179
+ const location = response.headers.get("location");
4180
+ if (!location) break;
4181
+ let nextUrl;
4182
+ try {
4183
+ nextUrl = new URL(location, response.url || void 0).href;
4184
+ } catch {
4185
+ break;
4186
+ }
4187
+ if (!nextUrl.startsWith("https://")) break;
4188
+ try {
4189
+ response = await fetch(nextUrl, {
4190
+ method: "HEAD",
4191
+ redirect: "manual",
4192
+ headers: { "User-Agent": SCANNER_USER_AGENT },
4193
+ signal: AbortSignal.timeout(timeoutMs)
4194
+ });
4195
+ } catch {
4196
+ break;
4197
+ }
4198
+ }
4199
+ return response;
4200
+ }
4201
+ function mergeSecurityHeaders(a, b) {
4202
+ const merged = new Headers();
4203
+ a.forEach((value, key) => merged.set(key, value));
4204
+ for (const header of MERGE_HEADERS) {
4205
+ if (!merged.has(header) && b.has(header)) {
4206
+ merged.set(header, b.get(header));
4207
+ }
4208
+ }
4209
+ b.forEach((value, key) => {
4210
+ if (!merged.has(key)) merged.set(key, value);
4211
+ });
4212
+ return merged;
4213
+ }
4214
+ async function dualFetchHeaders(domain, timeoutMs) {
4215
+ const url = `https://${domain}`;
4216
+ const results = await Promise.allSettled([fetchWithRedirects(url, timeoutMs), fetchWithRedirects(url, timeoutMs)]);
4217
+ const responses = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
4218
+ if (responses.length === 0) return null;
4219
+ const usable = responses.filter((r) => r.ok || r.status >= 300 && r.status < 400);
4220
+ if (usable.length === 0) return null;
4221
+ if (usable.length === 1) {
4222
+ return { headers: usable[0].headers, ok: usable[0].ok, status: usable[0].status };
4223
+ }
4224
+ const merged = mergeSecurityHeaders(usable[0].headers, usable[1].headers);
4225
+ const primary = usable[0].ok ? usable[0] : usable[1].ok ? usable[1] : usable[0];
4226
+ return { headers: merged, ok: primary.ok, status: primary.status };
4227
+ }
4228
+ async function fetchBodyForWafDetection(url, timeoutMs) {
4229
+ try {
4230
+ const response = await fetch(url, {
4231
+ method: "GET",
4232
+ redirect: "manual",
4233
+ headers: { "User-Agent": SCANNER_USER_AGENT },
4234
+ signal: AbortSignal.timeout(timeoutMs)
4235
+ });
4236
+ return await response.text();
4237
+ } catch {
4238
+ return "";
4239
+ }
4240
+ }
4064
4241
  async function checkHttpSecurity(domain) {
4242
+ const dualResult = await dualFetchHeaders(domain, HTTPS_TIMEOUT_MS);
4243
+ if (dualResult) {
4244
+ const headersForWaf = dualResult.headers;
4245
+ const needsBody = WAF_CHALLENGE_FINGERPRINTS.some((fp) => fp.matchHeaders(headersForWaf) && fp.matchBody);
4246
+ const body = needsBody ? await fetchBodyForWafDetection(`https://${domain}`, HTTPS_TIMEOUT_MS) : void 0;
4247
+ const wafName = detectWafChallenge(headersForWaf, body);
4248
+ if (wafName) {
4249
+ const finding = createFinding(
4250
+ "http_security",
4251
+ `${wafName.charAt(0).toUpperCase() + wafName.slice(1)} WAF challenge intercepted`,
4252
+ "info",
4253
+ `The fetched response appears to be a WAF/CDN challenge page, not the real site. Header analysis is inconclusive.`,
4254
+ { wafChallenge: wafName, inconclusive: true }
4255
+ );
4256
+ const base2 = buildCheckResult("http_security", [finding]);
4257
+ return { ...base2, score: 0, passed: false, checkStatus: "error" };
4258
+ }
4259
+ }
4065
4260
  let capturedHeaders = null;
4066
4261
  const capturingFetch = async (input, init) => {
4262
+ if (dualResult) {
4263
+ capturedHeaders = dualResult.headers;
4264
+ return new Response(null, {
4265
+ status: dualResult.status,
4266
+ headers: dualResult.headers
4267
+ });
4268
+ }
4067
4269
  const response = await fetch(input, init);
4068
4270
  capturedHeaders = response.headers;
4069
4271
  return response;
@@ -4086,18 +4288,11 @@ async function checkHttpSecurity(domain) {
4086
4288
 
4087
4289
  // src/tools/check-dane.ts
4088
4290
  init_dns2();
4089
- function makeQueryDNS10(dnsOptions) {
4090
- return async (domain, type) => {
4091
- if (type === "TXT") {
4092
- return queryTxtRecords(domain, dnsOptions);
4093
- }
4094
- return queryDnsRecords(domain, type, dnsOptions);
4095
- };
4096
- }
4291
+ init_dns_query_adapter();
4097
4292
  async function checkDane(domain, dnsOptions) {
4098
4293
  return checkDANE(
4099
4294
  domain,
4100
- makeQueryDNS10(dnsOptions),
4295
+ makeQueryDNS(dnsOptions),
4101
4296
  {
4102
4297
  timeout: dnsOptions?.timeoutMs ?? 5e3,
4103
4298
  rawQueryDNS: async (d, type, dnssecFlag) => {
@@ -4110,18 +4305,11 @@ async function checkDane(domain, dnsOptions) {
4110
4305
 
4111
4306
  // src/tools/check-dane-https.ts
4112
4307
  init_dns2();
4113
- function makeQueryDNS11(dnsOptions) {
4114
- return async (domain, type) => {
4115
- if (type === "TXT") {
4116
- return queryTxtRecords(domain, dnsOptions);
4117
- }
4118
- return queryDnsRecords(domain, type, dnsOptions);
4119
- };
4120
- }
4308
+ init_dns_query_adapter();
4121
4309
  async function checkDaneHttps(domain, dnsOptions) {
4122
4310
  return checkDANEHTTPS(
4123
4311
  domain,
4124
- makeQueryDNS11(dnsOptions),
4312
+ makeQueryDNS(dnsOptions),
4125
4313
  {
4126
4314
  timeout: dnsOptions?.timeoutMs ?? 5e3,
4127
4315
  rawQueryDNS: async (d, type, dnssecFlag) => {
@@ -4133,19 +4321,11 @@ async function checkDaneHttps(domain, dnsOptions) {
4133
4321
  }
4134
4322
 
4135
4323
  // src/tools/check-svcb-https.ts
4136
- init_dns2();
4137
- function makeQueryDNS12(dnsOptions) {
4138
- return async (domain, type) => {
4139
- if (type === "TXT") {
4140
- return queryTxtRecords(domain, dnsOptions);
4141
- }
4142
- return queryDnsRecords(domain, type, dnsOptions);
4143
- };
4144
- }
4324
+ init_dns_query_adapter();
4145
4325
  async function checkSvcbHttps(domain, dnsOptions) {
4146
4326
  return checkSVCBHTTPS(
4147
4327
  domain,
4148
- makeQueryDNS12(dnsOptions),
4328
+ makeQueryDNS(dnsOptions),
4149
4329
  { timeout: dnsOptions?.timeoutMs ?? 5e3 }
4150
4330
  );
4151
4331
  }
@@ -4771,19 +4951,11 @@ async function checkZoneHygiene(domain, dnsOptions) {
4771
4951
  }
4772
4952
 
4773
4953
  // src/tools/check-subdomailing.ts
4774
- init_dns2();
4775
- function makeQueryDNS13(dnsOptions) {
4776
- return async (domain, type) => {
4777
- if (type === "TXT") {
4778
- return queryTxtRecords(domain, dnsOptions);
4779
- }
4780
- return queryDnsRecords(domain, type, dnsOptions);
4781
- };
4782
- }
4954
+ init_dns_query_adapter();
4783
4955
  async function checkSubdomailing(domain, dnsOptions) {
4784
4956
  return checkSubdomailing$1(
4785
4957
  domain,
4786
- makeQueryDNS13(dnsOptions),
4958
+ makeQueryDNS(dnsOptions),
4787
4959
  { timeout: dnsOptions?.timeoutMs ?? 5e3 }
4788
4960
  );
4789
4961
  }
@@ -4881,20 +5053,12 @@ function applyInteractionPenalties(score, config) {
4881
5053
  init_cache();
4882
5054
 
4883
5055
  // src/tools/check-subdomain-takeover.ts
4884
- init_dns2();
5056
+ init_dns_query_adapter();
4885
5057
  init_config();
4886
- function makeQueryDNS14(dnsOptions) {
4887
- return async (domain, type) => {
4888
- if (type === "TXT") {
4889
- return queryTxtRecords(domain, dnsOptions);
4890
- }
4891
- return queryDnsRecords(domain, type, dnsOptions);
4892
- };
4893
- }
4894
5058
  async function checkSubdomainTakeover(domain, dnsOptions) {
4895
5059
  return checkSubdomainTakeover$1(
4896
5060
  domain,
4897
- makeQueryDNS14(dnsOptions),
5061
+ makeQueryDNS(dnsOptions),
4898
5062
  { timeout: dnsOptions?.timeoutMs ?? HTTPS_TIMEOUT_MS, fetchFn: fetch }
4899
5063
  );
4900
5064
  }
@@ -4964,6 +5128,11 @@ function upsertCheckResult(results, updated) {
4964
5128
  return results.map((result) => result.category === updated.category ? updated : result);
4965
5129
  }
4966
5130
  async function addOutboundProviderInference(results, runtimeOptions) {
5131
+ const mxResult = results.find((result) => result.category === "mx");
5132
+ const providerDetectionFailed = Boolean(
5133
+ mxResult?.metadata?.providerDetectionFailed
5134
+ );
5135
+ if (providerDetectionFailed) return results;
4967
5136
  const spfResult = results.find((result) => result.category === "spf");
4968
5137
  const dkimResult = results.find((result) => result.category === "dkim");
4969
5138
  const signalDomains = extractSpfSignalDomains(spfResult);
@@ -5103,6 +5272,34 @@ function adjustForNoSendDomain(results) {
5103
5272
  // src/tools/scan-domain.ts
5104
5273
  init_log();
5105
5274
 
5275
+ // src/lib/profile-accumulator.ts
5276
+ init_log();
5277
+ var ADAPTIVE_WEIGHT_KV_TTL_SECONDS = 60;
5278
+ function awKey(profile, provider) {
5279
+ return `aw:${profile}:${provider}`;
5280
+ }
5281
+ async function publishAdaptiveWeightSummary(profile, provider, weights, kv) {
5282
+ try {
5283
+ await kv.put(awKey(profile, provider), JSON.stringify(weights), {
5284
+ expirationTtl: ADAPTIVE_WEIGHT_KV_TTL_SECONDS
5285
+ });
5286
+ } catch {
5287
+ logError("[profile-accumulator] KV publish failed", {
5288
+ severity: "warn",
5289
+ category: "profile-accumulator"
5290
+ });
5291
+ }
5292
+ }
5293
+ async function getAdaptiveWeights(profile, provider, kv) {
5294
+ try {
5295
+ const raw = await kv.get(awKey(profile, provider));
5296
+ if (!raw) return null;
5297
+ return JSON.parse(raw);
5298
+ } catch {
5299
+ return null;
5300
+ }
5301
+ }
5302
+
5106
5303
  // src/tools/scan/maturity-staging.ts
5107
5304
  function capMaturityStage(maturity, score) {
5108
5305
  if (score < 50 && maturity.stage > 2) {
@@ -5635,6 +5832,59 @@ var EXPLANATIONS = {
5635
5832
  explanation: "An HTTPS/SVCB record is configured, advertising modern connection capabilities for this domain.",
5636
5833
  recommendation: "No action required. Consider adding ECH for enhanced privacy.",
5637
5834
  references: ["https://datatracker.ietf.org/doc/html/rfc9460"]
5835
+ },
5836
+ // --- Intelligence tools ---
5837
+ DBL_LISTED: {
5838
+ title: "Domain Listed on Blocklist",
5839
+ severity: "high",
5840
+ explanation: "The domain appears on one or more DNS-based Domain Block Lists (DBLs), indicating it has been flagged for spam, phishing, or malware distribution.",
5841
+ impact: "Listed domains may have email deliverability issues and are often blocked by recipient mail servers.",
5842
+ adverseConsequences: "Legitimate email from this domain may be silently dropped or quarantined.",
5843
+ recommendation: "Investigate the listing reason. For Spamhaus DBL, check https://check.spamhaus.org/. Request delisting after resolving the underlying issue.",
5844
+ references: ["https://www.spamhaus.org/dbl/", "https://uribl.com/", "https://www.surbl.org/"]
5845
+ },
5846
+ RBL_LISTED: {
5847
+ title: "IP Listed on Real-time Blocklist",
5848
+ severity: "high",
5849
+ explanation: "One or more mail server IPs are listed on DNS-based Real-time Blocklists, indicating the IP has been flagged for sending spam or malicious traffic.",
5850
+ impact: "Email from listed IPs is likely to be rejected or quarantined by recipient servers.",
5851
+ adverseConsequences: "Significant email deliverability degradation. May require IP change or delisting process.",
5852
+ recommendation: "Check Spamhaus at https://check.spamhaus.org/. For shared hosting, contact your provider. For dedicated IPs, resolve the abuse issue and request delisting.",
5853
+ references: ["https://www.spamhaus.org/zen/", "https://www.spamcop.net/"]
5854
+ },
5855
+ ASN_HIGH_RISK: {
5856
+ title: "High-Risk ASN Detected",
5857
+ severity: "medium",
5858
+ explanation: "The domain resolves to IP addresses in an Autonomous System commonly associated with abuse infrastructure (bulletproof hosting, botnets).",
5859
+ recommendation: "Verify the hosting choice is intentional. High-risk ASNs are not inherently malicious but are statistically over-represented in abuse reports.",
5860
+ references: ["https://www.team-cymru.com/ip-asn-mapping"]
5861
+ },
5862
+ FAST_FLUX_DETECTED: {
5863
+ title: "Fast-Flux Behavior Detected",
5864
+ severity: "high",
5865
+ explanation: "The domain shows rapidly rotating IP addresses with very low TTLs, a technique commonly used by botnets and phishing operations to evade takedown.",
5866
+ impact: "Fast-flux domains are a strong indicator of malicious infrastructure.",
5867
+ adverseConsequences: "Associating with fast-flux infrastructure damages domain reputation and may trigger automated blocking.",
5868
+ recommendation: "Investigate immediately. If this is a CDN or legitimate load balancer, TTLs are typically higher and rotation patterns are predictable.",
5869
+ references: ["https://en.wikipedia.org/wiki/Fast_flux"]
5870
+ },
5871
+ DNSSEC_CHAIN_BROKEN: {
5872
+ title: "DNSSEC Chain of Trust Broken",
5873
+ severity: "high",
5874
+ explanation: "The DNSSEC chain of trust has a gap \u2014 a DS record exists at the parent zone but the corresponding DNSKEY is missing or mismatched at the child zone.",
5875
+ impact: "DNSSEC-validating resolvers will return SERVFAIL for this domain, causing complete resolution failure for security-conscious clients.",
5876
+ adverseConsequences: "Domain may be unreachable for users behind DNSSEC-validating resolvers.",
5877
+ recommendation: "Ensure the DS record at the parent matches a DNSKEY at the child zone. Use `delv +vtrace` for detailed chain debugging.",
5878
+ references: ["https://datatracker.ietf.org/doc/html/rfc4033"]
5879
+ },
5880
+ NSEC_WALKABLE: {
5881
+ title: "Zone Walkable (No NSEC3)",
5882
+ severity: "high",
5883
+ explanation: "The zone does not appear to use NSEC3, meaning it likely uses plain NSEC records which allow full zone enumeration (zone walking).",
5884
+ impact: "Attackers can discover all hostnames in the zone without brute-forcing, exposing internal infrastructure.",
5885
+ adverseConsequences: "Complete zone enumeration reveals the attack surface \u2014 internal hosts, staging environments, and service names.",
5886
+ recommendation: "Deploy NSEC3 with at least algorithm 1 (SHA-1) and consider using salt. RFC 9276 recommends 0 iterations with no salt as the minimum.",
5887
+ references: ["https://datatracker.ietf.org/doc/html/rfc5155", "https://datatracker.ietf.org/doc/html/rfc9276"]
5638
5888
  }
5639
5889
  };
5640
5890
  var DEFAULT_EXPLANATION = {
@@ -5711,6 +5961,26 @@ var CATEGORY_FALLBACK_IMPACT = {
5711
5961
  SVCB_HTTPS: {
5712
5962
  impact: "Modern transport capabilities (ALPN, ECH) cannot be advertised via DNS, reducing connection efficiency and privacy.",
5713
5963
  adverseConsequences: "Clients require additional round-trips to negotiate protocols, and ECH-based privacy is unavailable."
5964
+ },
5965
+ RBL: {
5966
+ impact: "Mail server IPs are listed on one or more DNS blocklists, likely degrading email deliverability.",
5967
+ adverseConsequences: "Outbound email may be silently dropped or quarantined by recipient servers."
5968
+ },
5969
+ DBL: {
5970
+ impact: "Domain is flagged on DNS-based domain blocklists, indicating prior spam or abuse association.",
5971
+ adverseConsequences: "Domain reputation is damaged. Email and web traffic may be blocked by security tools."
5972
+ },
5973
+ FAST_FLUX: {
5974
+ impact: "DNS resolution shows fast-flux patterns \u2014 rapidly rotating IPs with low TTLs.",
5975
+ adverseConsequences: "Strong indicator of botnet or phishing infrastructure. Domain reputation will be severely impacted."
5976
+ },
5977
+ DNSSEC_CHAIN: {
5978
+ impact: "DNSSEC chain structure has gaps or uses weak algorithms, reducing trust in DNS responses.",
5979
+ adverseConsequences: "DNSSEC-validating resolvers may fail to resolve the domain or accept spoofed responses."
5980
+ },
5981
+ NSEC_WALKABILITY: {
5982
+ impact: "Zone denial-of-existence parameters allow enumeration of zone contents.",
5983
+ adverseConsequences: "Attackers can map the full attack surface by walking the zone without brute-forcing."
5714
5984
  }
5715
5985
  };
5716
5986
  var SEVERITY_FALLBACK_IMPACT = {
@@ -6079,11 +6349,86 @@ function formatScanReport(result, format = "full") {
6079
6349
  var CACHE_PREFIX = "cache:";
6080
6350
  var PER_CHECK_TIMEOUT_MS = 8e3;
6081
6351
  var SCAN_TIMEOUT_MS = 12e3;
6352
+ var RETRY_BUDGET_MS = 3e3;
6353
+ var MAX_RETRIES_PER_SCAN = 3;
6354
+ var RETRY_TIMEOUT_MS = 2500;
6082
6355
  var adaptiveWeightCache = /* @__PURE__ */ new Map();
6083
6356
  var ADAPTIVE_CACHE_TTL_MS = 6e4;
6084
6357
  var ADAPTIVE_CACHE_MAX_ENTRIES = 100;
6085
6358
  var ADAPTIVE_FETCH_TIMEOUT_MS = 200;
6359
+ function shouldRetry(result) {
6360
+ return result.checkStatus === "error" && result.score === 0;
6361
+ }
6362
+ async function runCheckRetry(category, domain, scanDns, runtimeOptions) {
6363
+ const retryDns = { ...scanDns, queryCache: /* @__PURE__ */ new Map() };
6364
+ const timeoutPromise = new Promise(
6365
+ (_, reject) => setTimeout(() => reject(new Error("Retry timed out")), RETRY_TIMEOUT_MS)
6366
+ );
6367
+ let checkPromise;
6368
+ switch (category) {
6369
+ case "spf":
6370
+ checkPromise = checkSpf(domain, retryDns);
6371
+ break;
6372
+ case "dmarc":
6373
+ checkPromise = checkDmarc(domain, retryDns);
6374
+ break;
6375
+ case "dkim":
6376
+ checkPromise = checkDkim(domain, void 0, retryDns);
6377
+ break;
6378
+ case "dnssec":
6379
+ checkPromise = checkDnssec2(domain, retryDns);
6380
+ break;
6381
+ case "ssl":
6382
+ checkPromise = checkSsl(domain);
6383
+ break;
6384
+ case "mta_sts":
6385
+ checkPromise = checkMtaSts(domain, retryDns);
6386
+ break;
6387
+ case "ns":
6388
+ checkPromise = checkNs(domain, retryDns);
6389
+ break;
6390
+ case "caa":
6391
+ checkPromise = checkCaa(domain, retryDns);
6392
+ break;
6393
+ case "bimi":
6394
+ checkPromise = checkBimi(domain, retryDns);
6395
+ break;
6396
+ case "tlsrpt":
6397
+ checkPromise = checkTlsrpt(domain, retryDns);
6398
+ break;
6399
+ case "subdomain_takeover":
6400
+ checkPromise = checkSubdomainTakeover(domain, retryDns);
6401
+ break;
6402
+ case "http_security":
6403
+ checkPromise = checkHttpSecurity(domain);
6404
+ break;
6405
+ case "dane":
6406
+ checkPromise = checkDane(domain, retryDns);
6407
+ break;
6408
+ case "dane_https":
6409
+ checkPromise = checkDaneHttps(domain, retryDns);
6410
+ break;
6411
+ case "svcb_https":
6412
+ checkPromise = checkSvcbHttps(domain, retryDns);
6413
+ break;
6414
+ case "subdomailing":
6415
+ checkPromise = checkSubdomailing(domain, retryDns);
6416
+ break;
6417
+ case "mx":
6418
+ checkPromise = checkMx(domain, {
6419
+ providerSignaturesUrl: runtimeOptions?.providerSignaturesUrl,
6420
+ providerSignaturesAllowedHosts: runtimeOptions?.providerSignaturesAllowedHosts,
6421
+ providerSignaturesSha256: runtimeOptions?.providerSignaturesSha256
6422
+ }, retryDns);
6423
+ break;
6424
+ default:
6425
+ return { ...buildCheckResult(category, []), score: 0, passed: false, checkStatus: "error" };
6426
+ }
6427
+ return Promise.race([checkPromise, timeoutPromise]);
6428
+ }
6086
6429
  async function scanDomain(domain, kv, runtimeOptions) {
6430
+ crypto.randomUUID();
6431
+ const scanStartTime = Date.now();
6087
6432
  const explicitProfile = runtimeOptions?.profile;
6088
6433
  const isExplicit = explicitProfile && explicitProfile !== "auto";
6089
6434
  const cacheKey = isExplicit ? `${CACHE_PREFIX}${domain}:profile:${explicitProfile}` : `${CACHE_PREFIX}${domain}`;
@@ -6173,6 +6518,21 @@ async function scanDomain(domain, kv, runtimeOptions) {
6173
6518
  degradedStatuses.set(r.category, r.checkStatus);
6174
6519
  }
6175
6520
  }
6521
+ if (!timedOut && Date.now() - scanStartTime < SCAN_TIMEOUT_MS - RETRY_BUDGET_MS) {
6522
+ const retryable = checkResults.map((r, idx) => ({ r, idx })).filter(({ r }) => shouldRetry(r)).slice(0, MAX_RETRIES_PER_SCAN);
6523
+ if (retryable.length > 0) {
6524
+ const retrySettled = await Promise.allSettled(
6525
+ retryable.map(({ r }) => runCheckRetry(r.category, domain, scanDns, runtimeOptions))
6526
+ );
6527
+ for (let i = 0; i < retryable.length; i++) {
6528
+ const s = retrySettled[i];
6529
+ if (s.status === "fulfilled" && s.value.checkStatus !== "error" && s.value.score > 0) {
6530
+ checkResults[retryable[i].idx] = s.value;
6531
+ degradedStatuses.delete(retryable[i].r.category);
6532
+ }
6533
+ }
6534
+ }
6535
+ }
6176
6536
  if (timedOut) {
6177
6537
  const completedCategories = new Set(checkResults.map((r) => r.category));
6178
6538
  for (const category of ALL_CHECK_CATEGORIES) {
@@ -6186,7 +6546,7 @@ async function scanDomain(domain, kv, runtimeOptions) {
6186
6546
  )
6187
6547
  ];
6188
6548
  const result2 = buildCheckResult(category, findings);
6189
- checkResults.push({ ...result2, score: 0, checkStatus: "timeout" });
6549
+ checkResults.push({ ...result2, score: 0, passed: false, checkStatus: "timeout" });
6190
6550
  degradedStatuses.set(category, "timeout");
6191
6551
  }
6192
6552
  }
@@ -6197,7 +6557,7 @@ async function scanDomain(domain, kv, runtimeOptions) {
6197
6557
  if (degradedStatuses.size > 0) {
6198
6558
  checkResults = checkResults.map((r) => {
6199
6559
  const status = degradedStatuses.get(r.category);
6200
- return status ? { ...r, score: 0, checkStatus: status } : r;
6560
+ return status ? { ...r, score: 0, passed: false, checkStatus: status } : r;
6201
6561
  });
6202
6562
  }
6203
6563
  let domainContext = detectDomainContext(checkResults);
@@ -6217,12 +6577,31 @@ async function scanDomain(domain, kv, runtimeOptions) {
6217
6577
  }
6218
6578
  const scoringContext = isExplicit ? domainContext : void 0;
6219
6579
  let adaptiveResponse = null;
6220
- if (runtimeOptions?.profileAccumulator) {
6580
+ const adaptiveProvider = domainContext.detectedProvider ?? "";
6581
+ if (kv && adaptiveProvider) {
6582
+ const kvWeights = await getAdaptiveWeights(domainContext.profile, adaptiveProvider, kv);
6583
+ if (kvWeights) {
6584
+ adaptiveResponse = {
6585
+ profile: domainContext.profile,
6586
+ provider: adaptiveProvider,
6587
+ sampleCount: MATURITY_THRESHOLD,
6588
+ blendFactor: 1,
6589
+ weights: kvWeights,
6590
+ boundHits: []
6591
+ };
6592
+ }
6593
+ }
6594
+ if (!adaptiveResponse && runtimeOptions?.profileAccumulator) {
6221
6595
  adaptiveResponse = await fetchAdaptiveWeights(
6222
6596
  runtimeOptions.profileAccumulator,
6223
6597
  domainContext.profile,
6224
6598
  domainContext.detectedProvider
6225
6599
  );
6600
+ if (adaptiveResponse && adaptiveProvider && kv && runtimeOptions.waitUntil) {
6601
+ runtimeOptions.waitUntil(
6602
+ publishAdaptiveWeightSummary(domainContext.profile, adaptiveProvider, adaptiveResponse.weights, kv)
6603
+ );
6604
+ }
6226
6605
  }
6227
6606
  if (adaptiveResponse?.boundHits.length) {
6228
6607
  domainContext.signals.push(`adaptive bound hits: ${adaptiveResponse.boundHits.join(", ")}`);
@@ -9680,164 +10059,1254 @@ All evaluated attack vectors are blocked by the current security configuration.`
9680
10059
  return lines.join("\n").trimEnd();
9681
10060
  }
9682
10061
 
9683
- // src/handlers/tool-execution.ts
9684
- init_log();
9685
- function logToolSuccess(options) {
9686
- options.analytics?.emitToolEvent({
9687
- toolName: options.toolName,
9688
- status: options.status,
9689
- durationMs: options.durationMs,
9690
- domain: options.domain,
9691
- isError: false,
9692
- score: options.score,
9693
- cacheStatus: options.cacheStatus,
9694
- country: options.country,
9695
- clientType: options.clientType,
9696
- authTier: options.authTier,
9697
- keyHash: options.keyHash
9698
- });
9699
- logEvent({
9700
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9701
- tool: options.toolName,
9702
- domain: options.domain,
9703
- result: options.logResult,
9704
- details: options.logDetails,
9705
- durationMs: options.durationMs,
9706
- severity: options.severity ?? (options.status === "pass" ? "info" : "warn")
9707
- });
9708
- }
9709
- function logToolFailure(options) {
9710
- options.analytics?.emitToolEvent({
9711
- toolName: options.toolName,
9712
- status: "error",
9713
- durationMs: options.durationMs,
9714
- domain: options.domain,
9715
- isError: true,
9716
- score: options.score,
9717
- cacheStatus: options.cacheStatus,
9718
- country: options.country,
9719
- clientType: options.clientType,
9720
- authTier: options.authTier,
9721
- keyHash: options.keyHash
9722
- });
9723
- logError(options.error instanceof Error ? options.error : String(options.error), {
9724
- tool: options.toolName,
9725
- domain: options.domain,
9726
- details: options.args,
9727
- severity: options.severity ?? "error"
9728
- });
9729
- }
9730
-
9731
- // src/handlers/tool-formatters.ts
9732
- function mcpError(message) {
9733
- return { type: "text", text: `Error: ${message}` };
9734
- }
9735
- function mcpText(text) {
9736
- return { type: "text", text };
9737
- }
9738
- function buildToolContent(text, structuredData, format) {
9739
- const content = [mcpText(text)];
9740
- if (format === "full") {
9741
- content.push(mcpText(`<!-- STRUCTURED_RESULT
9742
- ${JSON.stringify(structuredData)}
9743
- STRUCTURED_RESULT -->`));
10062
+ // src/tools/check-dbl.ts
10063
+ init_dns2();
10064
+ var CATEGORY = "dbl";
10065
+ var SPAMHAUS_CODES = {
10066
+ "127.0.1.2": "Spam domain",
10067
+ "127.0.1.4": "Phishing domain",
10068
+ "127.0.1.5": "Malware domain",
10069
+ "127.0.1.6": "Botnet C&C domain",
10070
+ "127.0.1.102": "Abused legit spam domain",
10071
+ "127.0.1.103": "Abused legit spammed redirector",
10072
+ "127.0.1.104": "Abused legit phishing domain",
10073
+ "127.0.1.105": "Abused legit malware domain",
10074
+ "127.0.1.106": "Abused legit botnet C&C domain"
10075
+ };
10076
+ function decodeSpamhaus(ip) {
10077
+ if (/^127\.255\.255\./.test(ip)) return null;
10078
+ const label = SPAMHAUS_CODES[ip];
10079
+ if (label) {
10080
+ return { label, detail: `Spamhaus DBL return code ${ip}: ${label}` };
9744
10081
  }
9745
- return content;
10082
+ if (/^127\.0\.1\./.test(ip)) {
10083
+ return { label: "Listed (unknown code)", detail: `Spamhaus DBL return code ${ip}: unknown listing type` };
10084
+ }
10085
+ return null;
9746
10086
  }
9747
- function formatCheckResult(result, format = "full") {
9748
- const lines = [];
9749
- lines.push(`## ${result.category.toUpperCase()} Check`);
9750
- lines.push(`**Status:** ${result.passed ? "\u2705 Passed" : "\u274C Failed"}`);
9751
- lines.push(`**Score:** ${result.score}/100`);
9752
- lines.push("");
9753
- if (result.findings.length > 0) {
9754
- lines.push("### Findings");
9755
- for (const finding of result.findings) {
9756
- if (format === "compact") {
9757
- const isHighPriority = finding.severity === "critical" || finding.severity === "high";
9758
- const detailLimit = isHighPriority ? 4e3 : 300;
9759
- lines.push(`- [${finding.severity.toUpperCase()}] ${sanitizeOutputText(finding.title, 120)} \u2014 ${sanitizeOutputText(finding.detail, detailLimit)}`);
10087
+ var URIBL_FLAGS = [
10088
+ { mask: 2, label: "Black" },
10089
+ { mask: 4, label: "Grey" },
10090
+ { mask: 8, label: "Red" }
10091
+ ];
10092
+ function decodeUribl(ip) {
10093
+ const octet = parseInt(ip.split(".")[3], 10);
10094
+ if (!Number.isFinite(octet) || octet === 0) return null;
10095
+ if ((octet & 1) !== 0 && (octet & -2) === 0) return null;
10096
+ const matched = URIBL_FLAGS.filter((f) => (octet & f.mask) !== 0).map((f) => f.label);
10097
+ if (matched.length === 0) return null;
10098
+ const labels = matched.join(", ");
10099
+ return { label: labels, detail: `URIBL flags: ${labels} (return code ${ip})` };
10100
+ }
10101
+ var SURBL_FLAGS = [
10102
+ { mask: 2, label: "SC (SpamCop)" },
10103
+ { mask: 4, label: "WS (sa-blacklist)" },
10104
+ { mask: 8, label: "PH (Phishing)" },
10105
+ { mask: 16, label: "MW (Malware)" },
10106
+ { mask: 32, label: "AB (AbuseButler)" },
10107
+ { mask: 64, label: "JP" },
10108
+ { mask: 128, label: "CR (Cracked)" }
10109
+ ];
10110
+ function decodeSurbl(ip) {
10111
+ const octet = parseInt(ip.split(".")[3], 10);
10112
+ if (!Number.isFinite(octet) || octet === 0) return null;
10113
+ const matched = SURBL_FLAGS.filter((f) => (octet & f.mask) !== 0).map((f) => f.label);
10114
+ if (matched.length === 0) return null;
10115
+ const labels = matched.join(", ");
10116
+ return { label: labels, detail: `SURBL flags: ${labels} (return code ${ip})` };
10117
+ }
10118
+ var DBL_ZONES = [
10119
+ { name: "Spamhaus DBL", zone: "dbl.spamhaus.org", decode: decodeSpamhaus, severity: "high" },
10120
+ { name: "URIBL", zone: "multi.uribl.com", decode: decodeUribl, severity: "medium" },
10121
+ { name: "SURBL", zone: "multi.surbl.org", decode: decodeSurbl, severity: "medium" }
10122
+ ];
10123
+ async function checkDbl(domain, dnsOptions) {
10124
+ const findings = [];
10125
+ const results = await Promise.allSettled(
10126
+ DBL_ZONES.map(async (zone) => {
10127
+ const queryName = `${domain}.${zone.zone}`;
10128
+ const answers = await queryDnsRecords(queryName, "A", dnsOptions);
10129
+ return { zone, answers };
10130
+ })
10131
+ );
10132
+ let listedCount = 0;
10133
+ let checkedCount = 0;
10134
+ for (const result of results) {
10135
+ if (result.status === "rejected") {
10136
+ const zoneIndex = results.indexOf(result);
10137
+ const zone2 = DBL_ZONES[zoneIndex];
10138
+ findings.push(
10139
+ createFinding(
10140
+ CATEGORY,
10141
+ `${zone2.name} lookup error`,
10142
+ "low",
10143
+ `DNS query error for ${domain} on ${zone2.name} (${zone2.zone}). Partial results may be available from other blocklists.`,
10144
+ { zone: zone2.zone, error: true }
10145
+ )
10146
+ );
10147
+ continue;
10148
+ }
10149
+ checkedCount++;
10150
+ const { zone, answers } = result.value;
10151
+ if (answers.length === 0) {
10152
+ continue;
10153
+ }
10154
+ const ip = answers[0];
10155
+ if (zone.zone === "dbl.spamhaus.org" && /^127\.255\.255\./.test(ip)) {
10156
+ findings.push(
10157
+ createFinding(
10158
+ CATEGORY,
10159
+ `${zone.name} query rate-limited`,
10160
+ "low",
10161
+ `Spamhaus DBL returned ${ip}, indicating a query quota or rate limit. This is not a listing. Results from this zone are unavailable.`,
10162
+ { zone: zone.zone, returnCode: ip, quotaError: true }
10163
+ )
10164
+ );
10165
+ continue;
10166
+ }
10167
+ if (zone.zone === "multi.uribl.com") {
10168
+ const uriblOctet = parseInt(ip.split(".")[3], 10);
10169
+ if (uriblOctet === 1) {
10170
+ findings.push(
10171
+ createFinding(
10172
+ CATEGORY,
10173
+ `${zone.name} query rate-limited`,
10174
+ "info",
10175
+ `URIBL returned ${ip}, indicating the querier is rate-limited or blocked. This is not a listing. Results from this zone are unavailable.`,
10176
+ { zone: zone.zone, returnCode: ip, quotaError: true }
10177
+ )
10178
+ );
9760
10179
  continue;
9761
10180
  }
9762
- const icon = finding.severity === "info" ? "\u2139\uFE0F" : finding.severity === "low" ? "\u26A0\uFE0F" : finding.severity === "medium" ? "\u{1F536}" : finding.severity === "high" ? "\u{1F534}" : "\u{1F6A8}";
9763
- lines.push(`- ${icon} **[${finding.severity.toUpperCase()}]** ${sanitizeOutputText(finding.title, 120)}`);
9764
- lines.push(` ${sanitizeOutputText(finding.detail)}`);
9765
- const verificationStatus = finding.category === "subdomain_takeover" && finding.metadata?.verificationStatus ? String(finding.metadata.verificationStatus) : void 0;
9766
- if (verificationStatus) {
9767
- lines.push(` Takeover Verification: ${sanitizeOutputText(verificationStatus, 80)}`);
9768
- }
9769
- const confidence = finding.metadata?.confidence ? String(finding.metadata.confidence) : void 0;
9770
- if (confidence) {
9771
- lines.push(` Confidence: ${sanitizeOutputText(confidence, 80)}`);
9772
- }
9773
- if (finding.severity !== "info") {
9774
- const narrative = resolveImpactNarrative({
9775
- category: finding.category,
9776
- severity: finding.severity,
9777
- title: finding.title,
9778
- detail: finding.detail
9779
- });
9780
- if (narrative.impact) {
9781
- lines.push(` Potential Impact: ${narrative.impact}`);
9782
- }
9783
- if (narrative.adverseConsequences) {
9784
- lines.push(` Adverse Consequences: ${narrative.adverseConsequences}`);
9785
- }
9786
- }
10181
+ }
10182
+ const decoded = zone.decode(ip);
10183
+ if (decoded) {
10184
+ listedCount++;
10185
+ findings.push(
10186
+ createFinding(
10187
+ CATEGORY,
10188
+ `Listed on ${zone.name}`,
10189
+ zone.severity,
10190
+ `${domain} is listed on ${zone.name}: ${decoded.detail}`,
10191
+ { zone: zone.zone, returnCode: ip, labels: decoded.label }
10192
+ )
10193
+ );
9787
10194
  }
9788
10195
  }
9789
- return lines.join("\n");
10196
+ if (listedCount === 0 && findings.length === 0) {
10197
+ findings.push(
10198
+ createFinding(
10199
+ CATEGORY,
10200
+ "Domain not listed on any blocklist",
10201
+ "info",
10202
+ `${domain} is not listed on any of the ${checkedCount} checked DNS-based domain blocklists (Spamhaus DBL, URIBL, SURBL).`,
10203
+ { zonesChecked: checkedCount }
10204
+ )
10205
+ );
10206
+ } else if (listedCount === 0 && findings.every((f) => f.severity === "low")) {
10207
+ findings.push(
10208
+ createFinding(
10209
+ CATEGORY,
10210
+ "Domain not listed on any blocklist",
10211
+ "info",
10212
+ `${domain} was not found on any of the successfully queried blocklists.`,
10213
+ { zonesChecked: checkedCount }
10214
+ )
10215
+ );
10216
+ }
10217
+ return buildCheckResult(CATEGORY, findings);
9790
10218
  }
9791
- var KNOWN_ACRONYMS = /* @__PURE__ */ new Set(["mx", "spf", "dmarc", "dkim", "dnssec", "ssl", "mta", "sts", "ns", "caa", "bimi", "tlsrpt", "http", "https", "dane", "svcb", "srv", "txt", "doh", "rpm"]);
9792
- function toolNameToTitle(name) {
9793
- return name.split("_").map((word) => KNOWN_ACRONYMS.has(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
10219
+
10220
+ // src/tools/check-rbl.ts
10221
+ init_dns2();
10222
+
10223
+ // src/lib/ip-utils.ts
10224
+ function reverseIPv4(ip) {
10225
+ return ip.split(".").reverse().join(".");
9794
10226
  }
9795
- function toInputSchema(schema) {
9796
- const jsonSchema = z.toJSONSchema(schema);
9797
- delete jsonSchema.$schema;
9798
- if (jsonSchema.additionalProperties !== void 0 && typeof jsonSchema.additionalProperties === "object" && jsonSchema.additionalProperties !== null && Object.keys(jsonSchema.additionalProperties).length === 0) {
9799
- delete jsonSchema.additionalProperties;
10227
+ function isPrivateIP(ip) {
10228
+ if (ip.includes(":")) {
10229
+ if (ip === "::1") return true;
10230
+ const lower = ip.toLowerCase();
10231
+ if (lower.startsWith("fe80:")) return true;
10232
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
10233
+ return false;
9800
10234
  }
9801
- return jsonSchema;
10235
+ const parts = ip.split(".").map(Number);
10236
+ if (parts.length !== 4) return false;
10237
+ const [a, b] = parts;
10238
+ if (a === 10) return true;
10239
+ if (a === 172 && b >= 16 && b <= 31) return true;
10240
+ if (a === 192 && b === 168) return true;
10241
+ if (a === 127) return true;
10242
+ if (a === 169 && b === 254) return true;
10243
+ return false;
9802
10244
  }
9803
- var TOOL_DEFS = {
9804
- check_mx: {
9805
- description: "Validate MX records and email provider detection.",
9806
- schema: BaseDomainArgs,
9807
- group: "email_auth",
10245
+
10246
+ // src/tools/check-rbl.ts
10247
+ var RBL_ZONES = [
10248
+ { name: "Spamhaus ZEN", zone: "zen.spamhaus.org" },
10249
+ { name: "SpamCop", zone: "bl.spamcop.net" },
10250
+ { name: "UCEProtect L1", zone: "dnsbl-1.uceprotect.net" },
10251
+ { name: "UCEProtect L2", zone: "dnsbl-2.uceprotect.net" },
10252
+ { name: "Mailspike", zone: "bl.mailspike.net" },
10253
+ { name: "Barracuda", zone: "b.barracudacentral.org" },
10254
+ { name: "PSBL", zone: "psbl.surriel.com" },
10255
+ { name: "SORBS", zone: "dnsbl.sorbs.net" }
10256
+ ];
10257
+ var CATEGORY2 = "rbl";
10258
+ var MAX_MX_IPS = 4;
10259
+ var SPAMHAUS_CODES2 = {
10260
+ "0.2": "SBL \u2014 direct spam source",
10261
+ "0.3": "SBL CSS \u2014 spam support service",
10262
+ "0.4": "XBL CBL \u2014 exploited host",
10263
+ "0.5": "XBL \u2014 exploited host (NJABL)",
10264
+ "0.9": "SBL DROP \u2014 hijacked netblock",
10265
+ "0.10": "PBL ISP \u2014 end-user IP",
10266
+ "0.11": "PBL ISP \u2014 end-user IP"
10267
+ };
10268
+ function decodeSpamhausCode(ip) {
10269
+ if (ip.startsWith("127.255.255.")) return null;
10270
+ const lastTwo = ip.split(".").slice(2).join(".");
10271
+ return SPAMHAUS_CODES2[lastTwo] ?? `Listed (${ip})`;
10272
+ }
10273
+ function isMailspikePositive(ip) {
10274
+ const last = parseInt(ip.split(".").pop() ?? "0", 10);
10275
+ return last >= 10 && last <= 14;
10276
+ }
10277
+ async function checkRbl(domain, dnsOptions) {
10278
+ const findings = [];
10279
+ let ips = [];
10280
+ let usedFallback = false;
10281
+ try {
10282
+ const mxRecords = await queryMxRecords(domain, dnsOptions);
10283
+ if (mxRecords.length > 0) {
10284
+ const mxHosts = mxRecords.map((r) => r.exchange).filter(Boolean);
10285
+ const ipResults = await Promise.allSettled(
10286
+ mxHosts.slice(0, MAX_MX_IPS).map((host) => queryDnsRecords(host, "A", dnsOptions))
10287
+ );
10288
+ for (const r of ipResults) {
10289
+ if (r.status === "fulfilled") ips.push(...r.value);
10290
+ }
10291
+ }
10292
+ } catch {
10293
+ }
10294
+ if (ips.length === 0) {
10295
+ try {
10296
+ ips = await queryDnsRecords(domain, "A", dnsOptions);
10297
+ if (ips.length > 0) usedFallback = true;
10298
+ } catch {
10299
+ }
10300
+ }
10301
+ if (ips.length === 0) {
10302
+ findings.push(
10303
+ createFinding(CATEGORY2, "No IP addresses found", "info", `Could not resolve any IP addresses for ${domain} (no MX or A records).`, {
10304
+ domain
10305
+ })
10306
+ );
10307
+ return buildCheckResult(CATEGORY2, findings);
10308
+ }
10309
+ if (usedFallback) {
10310
+ findings.push(
10311
+ createFinding(CATEGORY2, "No MX records \u2014 using A record fallback", "info", `${domain} has no MX records. Using A record IP(s) for RBL checks.`, {
10312
+ domain,
10313
+ fallback: true
10314
+ })
10315
+ );
10316
+ }
10317
+ ips = [...new Set(ips)].slice(0, MAX_MX_IPS);
10318
+ let totalListings = 0;
10319
+ for (const ip of ips) {
10320
+ if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) continue;
10321
+ if (isPrivateIP(ip)) {
10322
+ findings.push(
10323
+ createFinding(CATEGORY2, `Private IP detected: ${ip}`, "info", `MX resolves to private IP ${ip} \u2014 RBL checks skipped for this address.`, {
10324
+ ip,
10325
+ private: true
10326
+ })
10327
+ );
10328
+ continue;
10329
+ }
10330
+ const reversed = reverseIPv4(ip);
10331
+ let ipListingCount = 0;
10332
+ let hasSpamhausListing = false;
10333
+ const ipListingIndices = [];
10334
+ const rblResults = await Promise.allSettled(
10335
+ RBL_ZONES.map(async (rbl) => {
10336
+ const queryName = `${reversed}.${rbl.zone}`;
10337
+ try {
10338
+ const answers = await queryDnsRecords(queryName, "A", dnsOptions);
10339
+ if (answers.length === 0) return { rbl, listed: false };
10340
+ const returnIp = answers[0];
10341
+ if (rbl.zone === "zen.spamhaus.org" && returnIp.startsWith("127.255.255.")) {
10342
+ return { rbl, listed: false, quota: true };
10343
+ }
10344
+ if (rbl.zone === "bl.mailspike.net" && isMailspikePositive(returnIp)) {
10345
+ return { rbl, listed: false, positiveRep: true };
10346
+ }
10347
+ return { rbl, listed: true, returnIp };
10348
+ } catch {
10349
+ return { rbl, listed: false, error: true };
10350
+ }
10351
+ })
10352
+ );
10353
+ for (const settled of rblResults) {
10354
+ if (settled.status === "rejected") continue;
10355
+ const result = settled.value;
10356
+ if (result.quota) {
10357
+ findings.push(
10358
+ createFinding(CATEGORY2, `${result.rbl.name} quota exceeded`, "info", `Spamhaus returned a quota/rate-limit response for ${ip}. Use a DQS key for reliable results.`, {
10359
+ ip,
10360
+ zone: result.rbl.zone,
10361
+ quota: true
10362
+ })
10363
+ );
10364
+ continue;
10365
+ }
10366
+ if (result.positiveRep) {
10367
+ findings.push(
10368
+ createFinding(CATEGORY2, `Positive Mailspike reputation for ${ip}`, "info", `${ip} has positive reputation on Mailspike.`, {
10369
+ ip,
10370
+ zone: result.rbl.zone,
10371
+ positiveReputation: true
10372
+ })
10373
+ );
10374
+ continue;
10375
+ }
10376
+ if (result.error || !result.listed) continue;
10377
+ ipListingCount++;
10378
+ totalListings++;
10379
+ if (result.rbl.zone === "zen.spamhaus.org") hasSpamhausListing = true;
10380
+ const severity = result.rbl.zone === "zen.spamhaus.org" ? "high" : "low";
10381
+ const detail = result.rbl.zone === "zen.spamhaus.org" ? `${ip} is listed on ${result.rbl.name}: ${decodeSpamhausCode(result.returnIp) ?? result.returnIp}.` : `${ip} is listed on ${result.rbl.name}.`;
10382
+ ipListingIndices.push(findings.length);
10383
+ findings.push(
10384
+ createFinding(CATEGORY2, `Listed on ${result.rbl.name}`, severity, detail, {
10385
+ ip,
10386
+ zone: result.rbl.zone,
10387
+ returnCode: result.returnIp
10388
+ })
10389
+ );
10390
+ }
10391
+ if (!hasSpamhausListing && ipListingCount >= 2) {
10392
+ const firstLowIdx = ipListingIndices.find((idx) => findings[idx]?.severity === "low");
10393
+ if (firstLowIdx !== void 0) {
10394
+ const f = findings[firstLowIdx];
10395
+ findings[firstLowIdx] = createFinding(
10396
+ CATEGORY2,
10397
+ f.title,
10398
+ "medium",
10399
+ f.detail + ` (elevated: listed on ${ipListingCount} RBLs)`,
10400
+ f.metadata
10401
+ );
10402
+ }
10403
+ }
10404
+ }
10405
+ if (totalListings === 0 && !findings.some((f) => f.title.includes("Listed"))) {
10406
+ findings.push(
10407
+ createFinding(CATEGORY2, "IP reputation clean \u2014 not listed on any RBL", "info", `All checked IPs for ${domain} are clean on ${RBL_ZONES.length} RBLs.`, {
10408
+ ips,
10409
+ zones: RBL_ZONES.map((z8) => z8.zone)
10410
+ })
10411
+ );
10412
+ }
10413
+ return buildCheckResult(CATEGORY2, findings);
10414
+ }
10415
+
10416
+ // src/tools/check-cymru-asn.ts
10417
+ init_dns2();
10418
+ var CATEGORY3 = "asn";
10419
+ var HIGH_RISK_ASNS = /* @__PURE__ */ new Set([
10420
+ 9009,
10421
+ // M247
10422
+ 53667,
10423
+ // Frantech/BuyVM
10424
+ 36352,
10425
+ // ColoCrossing
10426
+ 20473,
10427
+ // Vultr
10428
+ 14061,
10429
+ // DigitalOcean
10430
+ 63949
10431
+ // Linode/Akamai
10432
+ ]);
10433
+ function parseOriginTxt(txt) {
10434
+ const parts = txt.split("|").map((p) => p.trim());
10435
+ if (parts.length < 5) return null;
10436
+ const asn = parseInt(parts[0], 10);
10437
+ if (!Number.isFinite(asn) || asn <= 0) return null;
10438
+ return {
10439
+ asn,
10440
+ prefix: parts[1],
10441
+ cc: parts[2],
10442
+ registry: parts[3],
10443
+ allocated: parts[4]
10444
+ };
10445
+ }
10446
+ function parseOrgTxt(txt) {
10447
+ const parts = txt.split("|").map((p) => p.trim());
10448
+ if (parts.length < 5) return null;
10449
+ return parts[4] || null;
10450
+ }
10451
+ async function checkCymruAsn(domain, dnsOptions) {
10452
+ const findings = [];
10453
+ let ips = [];
10454
+ try {
10455
+ ips = await queryDnsRecords(domain, "A", dnsOptions);
10456
+ } catch {
10457
+ }
10458
+ if (ips.length === 0) {
10459
+ findings.push(
10460
+ createFinding(CATEGORY3, "No A records found", "info", `Could not resolve any A records for ${domain}. ASN lookup requires IPv4 addresses.`, {
10461
+ domain
10462
+ })
10463
+ );
10464
+ return buildCheckResult(CATEGORY3, findings);
10465
+ }
10466
+ ips = [...new Set(ips)];
10467
+ const seenAsns = /* @__PURE__ */ new Set();
10468
+ for (const ip of ips) {
10469
+ if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) continue;
10470
+ const reversed = reverseIPv4(ip);
10471
+ const originName = `${reversed}.origin.asn.cymru.com`;
10472
+ let originTxts = [];
10473
+ try {
10474
+ originTxts = await queryTxtRecords(originName, dnsOptions);
10475
+ } catch {
10476
+ }
10477
+ if (originTxts.length === 0) {
10478
+ findings.push(
10479
+ createFinding(CATEGORY3, `No ASN data for ${ip}`, "info", `No ASN data returned from Team Cymru for ${ip}.`, {
10480
+ ip
10481
+ })
10482
+ );
10483
+ continue;
10484
+ }
10485
+ const origin = parseOriginTxt(originTxts[0]);
10486
+ if (!origin) {
10487
+ findings.push(
10488
+ createFinding(CATEGORY3, `Unparseable ASN response for ${ip}`, "info", `Team Cymru returned an unparseable response for ${ip}.`, {
10489
+ ip,
10490
+ raw: originTxts[0]
10491
+ })
10492
+ );
10493
+ continue;
10494
+ }
10495
+ let orgName = null;
10496
+ if (!seenAsns.has(origin.asn)) {
10497
+ try {
10498
+ const orgTxts = await queryTxtRecords(`AS${origin.asn}.asn.cymru.com`, dnsOptions);
10499
+ if (orgTxts.length > 0) {
10500
+ orgName = parseOrgTxt(orgTxts[0]);
10501
+ }
10502
+ } catch {
10503
+ }
10504
+ }
10505
+ seenAsns.add(origin.asn);
10506
+ const isHighRisk = HIGH_RISK_ASNS.has(origin.asn);
10507
+ const orgLabel = orgName ? ` (${orgName})` : "";
10508
+ if (isHighRisk) {
10509
+ findings.push(
10510
+ createFinding(
10511
+ CATEGORY3,
10512
+ `High-risk ASN ${origin.asn} detected`,
10513
+ "medium",
10514
+ `${ip} is announced by high-risk ASN ${origin.asn}${orgLabel} in prefix ${origin.prefix} (${origin.cc}, ${origin.registry}).`,
10515
+ { ip, asn: origin.asn, prefix: origin.prefix, cc: origin.cc, registry: origin.registry, orgName, highRisk: true }
10516
+ )
10517
+ );
10518
+ }
10519
+ findings.push(
10520
+ createFinding(
10521
+ CATEGORY3,
10522
+ `ASN ${origin.asn} for ${ip}`,
10523
+ "info",
10524
+ `${ip} \u2192 AS${origin.asn}${orgLabel}, prefix ${origin.prefix}, country ${origin.cc}, registry ${origin.registry}, allocated ${origin.allocated}.`,
10525
+ { ip, asn: origin.asn, prefix: origin.prefix, cc: origin.cc, registry: origin.registry, allocated: origin.allocated, orgName }
10526
+ )
10527
+ );
10528
+ }
10529
+ return buildCheckResult(CATEGORY3, findings);
10530
+ }
10531
+
10532
+ // src/tools/check-rdap-lookup.ts
10533
+ var CATEGORY4 = "rdap";
10534
+ var IANA_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json";
10535
+ var FALLBACK_RDAP_SERVERS = {
10536
+ com: "https://rdap.verisign.com/com/v1/",
10537
+ net: "https://rdap.verisign.com/net/v1/",
10538
+ org: "https://rdap.org/",
10539
+ info: "https://rdap.afilias.net/rdap/info/",
10540
+ io: "https://rdap.nic.io/"
10541
+ };
10542
+ var bootstrapCache = null;
10543
+ var RDAP_TIMEOUT_MS = 1e4;
10544
+ async function fetchBootstrap() {
10545
+ if (bootstrapCache) return bootstrapCache;
10546
+ try {
10547
+ const resp = await fetch(IANA_BOOTSTRAP_URL, {
10548
+ redirect: "manual",
10549
+ signal: AbortSignal.timeout(RDAP_TIMEOUT_MS),
10550
+ headers: { Accept: "application/json" }
10551
+ });
10552
+ if (!resp.ok) return {};
10553
+ const data = await resp.json();
10554
+ const map2 = {};
10555
+ if (Array.isArray(data.services)) {
10556
+ for (const [tlds, urls] of data.services) {
10557
+ if (!Array.isArray(tlds) || !Array.isArray(urls) || urls.length === 0) continue;
10558
+ const serverUrl = urls[0];
10559
+ for (const tld of tlds) {
10560
+ if (typeof tld === "string" && typeof serverUrl === "string") {
10561
+ map2[tld.toLowerCase()] = serverUrl;
10562
+ }
10563
+ }
10564
+ }
10565
+ }
10566
+ bootstrapCache = map2;
10567
+ return map2;
10568
+ } catch {
10569
+ return {};
10570
+ }
10571
+ }
10572
+ async function resolveRdapServer(tld) {
10573
+ const normalizedTld = tld.toLowerCase();
10574
+ const bootstrap = await fetchBootstrap();
10575
+ if (bootstrap[normalizedTld]) return bootstrap[normalizedTld];
10576
+ return FALLBACK_RDAP_SERVERS[normalizedTld] ?? null;
10577
+ }
10578
+ function extractVcardName(entity) {
10579
+ if (!entity.vcardArray || entity.vcardArray[0] !== "vcard") return null;
10580
+ const properties = entity.vcardArray[1];
10581
+ if (!Array.isArray(properties)) return null;
10582
+ for (const prop of properties) {
10583
+ if (Array.isArray(prop) && prop[0] === "fn" && typeof prop[3] === "string") {
10584
+ return prop[3];
10585
+ }
10586
+ }
10587
+ return null;
10588
+ }
10589
+ function extractVcardCountry(entity) {
10590
+ if (!entity.vcardArray || entity.vcardArray[0] !== "vcard") return null;
10591
+ const properties = entity.vcardArray[1];
10592
+ if (!Array.isArray(properties)) return null;
10593
+ for (const prop of properties) {
10594
+ if (Array.isArray(prop) && prop[0] === "adr") {
10595
+ const value = prop[3];
10596
+ if (Array.isArray(value) && value.length > 0) {
10597
+ const country = value[value.length - 1];
10598
+ return typeof country === "string" && country.length > 0 ? country : null;
10599
+ }
10600
+ }
10601
+ }
10602
+ return null;
10603
+ }
10604
+ function findEntityByRole(entities, role) {
10605
+ if (!Array.isArray(entities)) return null;
10606
+ for (const entity of entities) {
10607
+ if (Array.isArray(entity.roles) && entity.roles.includes(role)) {
10608
+ return entity;
10609
+ }
10610
+ if (Array.isArray(entity.entities)) {
10611
+ const nested = findEntityByRole(entity.entities, role);
10612
+ if (nested) return nested;
10613
+ }
10614
+ }
10615
+ return null;
10616
+ }
10617
+ function findEvent(events, action) {
10618
+ if (!Array.isArray(events)) return null;
10619
+ return events.find((e) => e.eventAction === action) ?? null;
10620
+ }
10621
+ async function checkRdapLookup(domain) {
10622
+ const findings = [];
10623
+ const labels = domain.split(".");
10624
+ const tld = labels[labels.length - 1];
10625
+ const rdapServerUrl = await resolveRdapServer(tld);
10626
+ if (!rdapServerUrl) {
10627
+ findings.push(
10628
+ createFinding(CATEGORY4, "No RDAP server found", "info", `No RDAP server found for TLD ".${tld}". RDAP data unavailable for this domain.`, {
10629
+ domain,
10630
+ tld
10631
+ })
10632
+ );
10633
+ return buildCheckResult(CATEGORY4, findings);
10634
+ }
10635
+ let rdapData;
10636
+ try {
10637
+ const baseUrl = rdapServerUrl.endsWith("/") ? rdapServerUrl : `${rdapServerUrl}/`;
10638
+ const rdapUrl = `${baseUrl}domain/${domain}`;
10639
+ const resp = await fetch(rdapUrl, {
10640
+ redirect: "manual",
10641
+ signal: AbortSignal.timeout(RDAP_TIMEOUT_MS),
10642
+ headers: { Accept: "application/rdap+json, application/json" }
10643
+ });
10644
+ if (!resp.ok) {
10645
+ findings.push(
10646
+ createFinding(CATEGORY4, "RDAP lookup failed", "info", `RDAP server returned HTTP ${resp.status} for ${domain}. Registration data unavailable.`, {
10647
+ domain,
10648
+ httpStatus: resp.status
10649
+ })
10650
+ );
10651
+ return buildCheckResult(CATEGORY4, findings);
10652
+ }
10653
+ rdapData = await resp.json();
10654
+ } catch (err) {
10655
+ const message = err instanceof Error ? err.message : "Unknown error";
10656
+ findings.push(
10657
+ createFinding(CATEGORY4, "RDAP lookup failed", "info", `RDAP lookup failed for ${domain}: ${message}`, {
10658
+ domain,
10659
+ error: message
10660
+ })
10661
+ );
10662
+ return buildCheckResult(CATEGORY4, findings);
10663
+ }
10664
+ const registrarEntity = findEntityByRole(rdapData.entities, "registrar");
10665
+ const registrarName = registrarEntity ? extractVcardName(registrarEntity) : null;
10666
+ const registrantEntity = findEntityByRole(rdapData.entities, "registrant");
10667
+ const registrantName = registrantEntity ? extractVcardName(registrantEntity) : null;
10668
+ const registrantCountry = registrantEntity ? extractVcardCountry(registrantEntity) : null;
10669
+ const registrationEvent = findEvent(rdapData.events, "registration");
10670
+ const expirationEvent = findEvent(rdapData.events, "expiration");
10671
+ const lastChangedEvent = findEvent(rdapData.events, "last changed");
10672
+ let domainAgeDays = null;
10673
+ if (registrationEvent?.eventDate) {
10674
+ const creationTime = new Date(registrationEvent.eventDate).getTime();
10675
+ if (Number.isFinite(creationTime)) {
10676
+ domainAgeDays = Math.floor((Date.now() - creationTime) / (1e3 * 60 * 60 * 24));
10677
+ }
10678
+ }
10679
+ let daysUntilExpiration = null;
10680
+ if (expirationEvent?.eventDate) {
10681
+ const expirationTime = new Date(expirationEvent.eventDate).getTime();
10682
+ if (Number.isFinite(expirationTime)) {
10683
+ daysUntilExpiration = Math.floor((expirationTime - Date.now()) / (1e3 * 60 * 60 * 24));
10684
+ }
10685
+ }
10686
+ const eppStatus = Array.isArray(rdapData.status) ? rdapData.status : [];
10687
+ const metadata = {
10688
+ domain,
10689
+ registrar: registrarName,
10690
+ registrant: registrantName,
10691
+ registrantCountry,
10692
+ creationDate: registrationEvent?.eventDate ?? null,
10693
+ expirationDate: expirationEvent?.eventDate ?? null,
10694
+ lastChanged: lastChangedEvent?.eventDate ?? null,
10695
+ eppStatus,
10696
+ rdapServer: rdapServerUrl
10697
+ };
10698
+ if (domainAgeDays !== null) {
10699
+ metadata.domainAgeDays = domainAgeDays;
10700
+ }
10701
+ if (daysUntilExpiration !== null) {
10702
+ metadata.daysUntilExpiration = daysUntilExpiration;
10703
+ }
10704
+ if (domainAgeDays !== null && domainAgeDays < 30) {
10705
+ findings.push(
10706
+ createFinding(
10707
+ CATEGORY4,
10708
+ "Newly registered domain",
10709
+ "medium",
10710
+ `${domain} was registered ${domainAgeDays} day${domainAgeDays !== 1 ? "s" : ""} ago (${registrationEvent.eventDate}). Newly registered domains are commonly used for phishing and spam.`,
10711
+ metadata
10712
+ )
10713
+ );
10714
+ }
10715
+ if (daysUntilExpiration !== null && daysUntilExpiration <= 30 && daysUntilExpiration >= 0) {
10716
+ findings.push(
10717
+ createFinding(
10718
+ CATEGORY4,
10719
+ "Domain expiring soon",
10720
+ "low",
10721
+ `${domain} expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? "s" : ""} (${expirationEvent.eventDate}). Expired domains can be re-registered by attackers.`,
10722
+ metadata
10723
+ )
10724
+ );
10725
+ }
10726
+ const parts = [];
10727
+ if (registrarName) parts.push(`Registrar: ${registrarName}`);
10728
+ if (registrantName) parts.push(`Registrant: ${registrantName}`);
10729
+ if (registrantCountry) parts.push(`Country: ${registrantCountry}`);
10730
+ if (registrationEvent?.eventDate) parts.push(`Created: ${registrationEvent.eventDate.split("T")[0]}`);
10731
+ if (expirationEvent?.eventDate) parts.push(`Expires: ${expirationEvent.eventDate.split("T")[0]}`);
10732
+ if (lastChangedEvent?.eventDate) parts.push(`Updated: ${lastChangedEvent.eventDate.split("T")[0]}`);
10733
+ if (eppStatus.length > 0) parts.push(`Status: ${eppStatus.join(", ")}`);
10734
+ if (domainAgeDays !== null) parts.push(`Age: ${domainAgeDays} days`);
10735
+ findings.push(
10736
+ createFinding(
10737
+ CATEGORY4,
10738
+ "Registration details",
10739
+ "info",
10740
+ parts.length > 0 ? parts.join(". ") + "." : `RDAP data retrieved for ${domain} but no structured fields found.`,
10741
+ metadata
10742
+ )
10743
+ );
10744
+ return buildCheckResult(CATEGORY4, findings);
10745
+ }
10746
+
10747
+ // src/tools/check-nsec-walkability.ts
10748
+ init_dns2();
10749
+ var CATEGORY5 = "nsec_walkability";
10750
+ var NSEC3_HASH_ALGORITHMS = {
10751
+ 1: "SHA-1"
10752
+ };
10753
+ function parseNsec3Param(data) {
10754
+ const parts = data.trim().split(/\s+/);
10755
+ if (parts.length < 4) return null;
10756
+ const algorithm = parseInt(parts[0], 10);
10757
+ const flags = parseInt(parts[1], 10);
10758
+ const iterations = parseInt(parts[2], 10);
10759
+ const salt = parts[3];
10760
+ if (!Number.isFinite(algorithm) || !Number.isFinite(flags) || !Number.isFinite(iterations)) {
10761
+ return null;
10762
+ }
10763
+ return {
10764
+ algorithm,
10765
+ algorithmName: NSEC3_HASH_ALGORITHMS[algorithm] ?? `Unknown (${algorithm})`,
10766
+ flags,
10767
+ iterations,
10768
+ salt
10769
+ };
10770
+ }
10771
+ async function checkNsecWalkability(domain, dnsOptions) {
10772
+ const findings = [];
10773
+ let nsec3Records = [];
10774
+ try {
10775
+ nsec3Records = await queryDnsRecords(domain, "NSEC3PARAM", dnsOptions);
10776
+ } catch {
10777
+ findings.push(
10778
+ createFinding(
10779
+ CATEGORY5,
10780
+ "NSEC3PARAM query failed",
10781
+ "info",
10782
+ `DNS query for NSEC3PARAM records at ${domain} failed. Unable to assess zone walkability. Note: this analysis cannot probe for actual NSEC/NSEC3 denial records via DoH and analyzes configuration parameters only.`,
10783
+ { domain }
10784
+ )
10785
+ );
10786
+ return buildCheckResult(CATEGORY5, findings);
10787
+ }
10788
+ if (nsec3Records.length === 0) {
10789
+ findings.push(
10790
+ createFinding(
10791
+ CATEGORY5,
10792
+ "No NSEC3PARAM record found",
10793
+ "high",
10794
+ `No NSEC3PARAM record was found for ${domain}. The zone likely uses plain NSEC, which makes it fully walkable \u2014 an attacker can enumerate all zone contents by following NSEC chain links. Limitation: DoH cannot probe for actual NSEC/NSEC3 denial-of-existence records, so this assessment is based on the absence of NSEC3PARAM configuration only.`,
10795
+ { domain, walkable: true }
10796
+ )
10797
+ );
10798
+ return buildCheckResult(CATEGORY5, findings);
10799
+ }
10800
+ const params = parseNsec3Param(nsec3Records[0]);
10801
+ if (!params) {
10802
+ findings.push(
10803
+ createFinding(
10804
+ CATEGORY5,
10805
+ "Unparseable NSEC3PARAM",
10806
+ "info",
10807
+ `NSEC3PARAM record for ${domain} could not be parsed: ${nsec3Records[0]}`,
10808
+ { domain, raw: nsec3Records[0] }
10809
+ )
10810
+ );
10811
+ return buildCheckResult(CATEGORY5, findings);
10812
+ }
10813
+ const hasSalt = params.salt !== "-";
10814
+ const hasIterations = params.iterations > 0;
10815
+ if (params.flags & 1) {
10816
+ findings.push(
10817
+ createFinding(
10818
+ CATEGORY5,
10819
+ "NSEC3 opt-out enabled",
10820
+ "low",
10821
+ `NSEC3PARAM for ${domain} has the opt-out flag set (flags=${params.flags}). Opt-out allows unsigned delegations to be omitted from the NSEC3 chain, which may leave some subdomains without denial-of-existence protection.`,
10822
+ { domain, flags: params.flags, optOut: true }
10823
+ )
10824
+ );
10825
+ }
10826
+ if (!hasIterations && !hasSalt) {
10827
+ findings.push(
10828
+ createFinding(
10829
+ CATEGORY5,
10830
+ "NSEC3 with minimal parameters",
10831
+ "medium",
10832
+ `NSEC3PARAM for ${domain} uses 0 iterations and no salt (RFC 9276 recommended defaults). While NSEC3 prevents trivial zone walking, the low enumeration cost means offline dictionary attacks against the hashed names are feasible. Algorithm: ${params.algorithmName}. Note: this tool analyzes NSEC3PARAM configuration only and cannot probe for actual NSEC3 denial records via DoH.`,
10833
+ {
10834
+ domain,
10835
+ algorithm: params.algorithmName,
10836
+ algorithmId: params.algorithm,
10837
+ iterations: params.iterations,
10838
+ salt: params.salt,
10839
+ hasSalt: false
10840
+ }
10841
+ )
10842
+ );
10843
+ } else {
10844
+ findings.push(
10845
+ createFinding(
10846
+ CATEGORY5,
10847
+ "NSEC3 parameters configured",
10848
+ "info",
10849
+ `NSEC3PARAM for ${domain}: algorithm ${params.algorithmName}, ${params.iterations} iteration${params.iterations !== 1 ? "s" : ""}, salt ${hasSalt ? params.salt : "none"}. Zone uses NSEC3 hashed denial-of-existence, which mitigates trivial zone walking. Note: this tool analyzes configuration parameters only.`,
10850
+ {
10851
+ domain,
10852
+ algorithm: params.algorithmName,
10853
+ algorithmId: params.algorithm,
10854
+ iterations: params.iterations,
10855
+ salt: params.salt,
10856
+ hasSalt
10857
+ }
10858
+ )
10859
+ );
10860
+ }
10861
+ return buildCheckResult(CATEGORY5, findings);
10862
+ }
10863
+
10864
+ // src/tools/check-dnssec-chain.ts
10865
+ init_dns2();
10866
+ var CATEGORY6 = "dnssec_chain";
10867
+ var DNSSEC_ALGORITHMS = {
10868
+ 1: "RSA-MD5",
10869
+ 3: "DSA-SHA1",
10870
+ 5: "RSA-SHA1",
10871
+ 6: "DSA-NSEC3-SHA1",
10872
+ 7: "RSA-SHA1-NSEC3",
10873
+ 8: "RSA-SHA256",
10874
+ 10: "RSA-SHA512",
10875
+ 12: "ECC-GOST",
10876
+ 13: "ECDSAP256SHA256",
10877
+ 14: "ECDSAP384SHA384",
10878
+ 15: "Ed25519",
10879
+ 16: "Ed448"
10880
+ };
10881
+ var WEAK_ALGORITHMS = /* @__PURE__ */ new Set([1, 3, 5, 6, 7]);
10882
+ var DIGEST_TYPES = { 1: "SHA-1", 2: "SHA-256", 4: "SHA-384" };
10883
+ function parseDsRecord2(data) {
10884
+ const parts = data.trim().split(/\s+/);
10885
+ if (parts.length < 4) return null;
10886
+ const keyTag = parseInt(parts[0], 10);
10887
+ const algorithm = parseInt(parts[1], 10);
10888
+ const digestType = parseInt(parts[2], 10);
10889
+ const digest = parts.slice(3).join("");
10890
+ if (!Number.isFinite(keyTag) || !Number.isFinite(algorithm) || !Number.isFinite(digestType)) return null;
10891
+ return { keyTag, algorithm, digestType, digest };
10892
+ }
10893
+ function parseDnskeyRecord(data) {
10894
+ const parts = data.trim().split(/\s+/);
10895
+ if (parts.length < 4) return null;
10896
+ const flags = parseInt(parts[0], 10);
10897
+ const protocol = parseInt(parts[1], 10);
10898
+ const algorithm = parseInt(parts[2], 10);
10899
+ const pubkey = parts.slice(3).join("");
10900
+ if (!Number.isFinite(flags) || !Number.isFinite(protocol) || !Number.isFinite(algorithm)) return null;
10901
+ return { flags, protocol, algorithm, pubkey, isKsk: flags === 257 };
10902
+ }
10903
+ function determineLinkage(dsRecords, dnskeyRecords) {
10904
+ if (dsRecords.length === 0) return "no_ds";
10905
+ if (dnskeyRecords.length === 0) return "no_dnskey";
10906
+ const dsAlgs = new Set(dsRecords.map((ds) => ds.algorithm));
10907
+ const keyAlgs = new Set(dnskeyRecords.map((k) => k.algorithm));
10908
+ for (const alg of dsAlgs) {
10909
+ if (keyAlgs.has(alg)) return "linked";
10910
+ }
10911
+ return "broken";
10912
+ }
10913
+ function buildZoneHierarchy(domain) {
10914
+ const labels = domain.split(".");
10915
+ const zones = ["."];
10916
+ for (let i = labels.length - 1; i >= 0; i--) {
10917
+ zones.push(labels.slice(i).join("."));
10918
+ }
10919
+ return zones;
10920
+ }
10921
+ async function checkDnssecChain(domain, dnsOptions) {
10922
+ const findings = [];
10923
+ const zones = buildZoneHierarchy(domain);
10924
+ const zoneResults = [];
10925
+ let chainBroken = false;
10926
+ const weakAlgsFound = [];
10927
+ for (const zone of zones) {
10928
+ let dsRecords = [];
10929
+ if (zone !== ".") {
10930
+ try {
10931
+ const rawDs = await queryDnsRecords(zone, "DS", dnsOptions);
10932
+ dsRecords = rawDs.map(parseDsRecord2).filter((r) => r !== null);
10933
+ } catch {
10934
+ }
10935
+ }
10936
+ let dnskeyRecords = [];
10937
+ try {
10938
+ const rawDnskey = await queryDnsRecords(zone, "DNSKEY", dnsOptions);
10939
+ dnskeyRecords = rawDnskey.map(parseDnskeyRecord).filter((r) => r !== null);
10940
+ } catch {
10941
+ }
10942
+ const linkage = zone === "." ? dnskeyRecords.length > 0 ? "linked" : "no_dnskey" : determineLinkage(dsRecords, dnskeyRecords);
10943
+ const allAlgs = /* @__PURE__ */ new Set();
10944
+ for (const ds of dsRecords) allAlgs.add(ds.algorithm);
10945
+ for (const key of dnskeyRecords) allAlgs.add(key.algorithm);
10946
+ const algorithms = [...allAlgs].map((a) => DNSSEC_ALGORITHMS[a] ?? `Unknown(${a})`);
10947
+ const weakAlgorithms = [...allAlgs].filter((a) => WEAK_ALGORITHMS.has(a)).map((a) => DNSSEC_ALGORITHMS[a] ?? `Unknown(${a})`);
10948
+ weakAlgsFound.push(...weakAlgorithms);
10949
+ zoneResults.push({
10950
+ zone,
10951
+ dsRecords,
10952
+ dnskeyRecords,
10953
+ linkage,
10954
+ algorithms,
10955
+ weakAlgorithms
10956
+ });
10957
+ if (linkage === "no_dnskey" || linkage === "broken") {
10958
+ chainBroken = true;
10959
+ }
10960
+ if (zone !== "." && dsRecords.length === 0 && dnskeyRecords.length === 0) {
10961
+ break;
10962
+ }
10963
+ }
10964
+ let adFlag = false;
10965
+ try {
10966
+ const adResp = await queryDns(domain, "A", true, dnsOptions);
10967
+ adFlag = adResp.AD === true;
10968
+ } catch {
10969
+ }
10970
+ const lastZone = zoneResults[zoneResults.length - 1];
10971
+ const reachedTarget = lastZone?.zone === domain;
10972
+ const targetSigned = reachedTarget && (lastZone.dsRecords.length > 0 || lastZone.dnskeyRecords.length > 0);
10973
+ const chainComplete = reachedTarget && !chainBroken && targetSigned;
10974
+ if (chainBroken) {
10975
+ const brokenZones = zoneResults.filter((z8) => z8.linkage === "no_dnskey" || z8.linkage === "broken");
10976
+ for (const bz of brokenZones) {
10977
+ const reason = bz.linkage === "no_dnskey" ? "DS record exists but no DNSKEY found" : "DS and DNSKEY algorithm mismatch";
10978
+ findings.push(
10979
+ createFinding(
10980
+ CATEGORY6,
10981
+ `Broken DNSSEC chain at ${bz.zone}`,
10982
+ "high",
10983
+ `DNSSEC chain is broken at ${bz.zone}: ${reason}. Resolvers that validate DNSSEC will return SERVFAIL for this zone.`,
10984
+ { zone: bz.zone, linkage: bz.linkage }
10985
+ )
10986
+ );
10987
+ }
10988
+ }
10989
+ if (weakAlgsFound.length > 0) {
10990
+ const uniqueWeak = [...new Set(weakAlgsFound)];
10991
+ findings.push(
10992
+ createFinding(
10993
+ CATEGORY6,
10994
+ "Weak DNSSEC algorithm in chain",
10995
+ "medium",
10996
+ `DNSSEC chain uses deprecated/weak algorithm(s): ${uniqueWeak.join(", ")}. These are considered cryptographically weak and should be migrated to RSA-SHA256 (algorithm 8) or ECDSA (algorithm 13/14).`,
10997
+ { weakAlgorithms: uniqueWeak }
10998
+ )
10999
+ );
11000
+ }
11001
+ const zonesSummary = zoneResults.map((z8) => ({
11002
+ zone: z8.zone,
11003
+ dsCount: z8.dsRecords.length,
11004
+ dnskeyCount: z8.dnskeyRecords.length,
11005
+ kskCount: z8.dnskeyRecords.filter((k) => k.isKsk).length,
11006
+ zskCount: z8.dnskeyRecords.filter((k) => !k.isKsk).length,
11007
+ linkage: z8.linkage,
11008
+ algorithms: z8.algorithms,
11009
+ dsDigestTypes: [...new Set(z8.dsRecords.map((ds) => DIGEST_TYPES[ds.digestType] ?? `Unknown(${ds.digestType})`))]
11010
+ }));
11011
+ const stoppedEarly = !reachedTarget;
11012
+ let summaryStatus;
11013
+ if (stoppedEarly) {
11014
+ summaryStatus = `stopped at ${lastZone?.zone ?? "."} \u2014 zone has no DS and no DNSKEY (not signed)`;
11015
+ } else if (!targetSigned) {
11016
+ summaryStatus = `${domain} has no DS and no DNSKEY \u2014 domain is not signed`;
11017
+ } else if (chainComplete) {
11018
+ summaryStatus = "complete chain from root to target";
11019
+ } else {
11020
+ summaryStatus = "chain broken";
11021
+ }
11022
+ const summaryDetail = `DNSSEC chain walk for ${domain}: ${summaryStatus}. Zones walked: ${zoneResults.map((z8) => z8.zone).join(" \u2192 ")}. AD flag: ${adFlag}. Limitation: no cryptographic RRSIG verification; reports structure and linkage only.`;
11023
+ findings.push(
11024
+ createFinding(CATEGORY6, "DNSSEC chain summary", "info", summaryDetail, {
11025
+ chainComplete,
11026
+ adFlag,
11027
+ zonesWalked: zoneResults.length,
11028
+ zones: zonesSummary
11029
+ })
11030
+ );
11031
+ return buildCheckResult(CATEGORY6, findings);
11032
+ }
11033
+
11034
+ // src/tools/check-fast-flux.ts
11035
+ init_dns2();
11036
+ var CATEGORY7 = "fast_flux";
11037
+ var TYPE_A = 1;
11038
+ var TYPE_AAAA = 28;
11039
+ async function checkFastFlux(domain, rounds, dnsOptions, delayMs = 2e3) {
11040
+ const effectiveRounds = Math.max(3, Math.min(5, rounds ?? 3));
11041
+ const roundResults = [];
11042
+ for (let i = 0; i < effectiveRounds; i++) {
11043
+ if (i > 0 && delayMs > 0) {
11044
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
11045
+ }
11046
+ try {
11047
+ const [aResult, aaaaResult] = await Promise.allSettled([
11048
+ queryDns(domain, "A", false, dnsOptions),
11049
+ queryDns(domain, "AAAA", false, dnsOptions)
11050
+ ]);
11051
+ const answers = [];
11052
+ if (aResult.status === "fulfilled" && aResult.value.Answer) {
11053
+ answers.push(...aResult.value.Answer.filter((a) => a.type === TYPE_A));
11054
+ }
11055
+ if (aaaaResult.status === "fulfilled" && aaaaResult.value.Answer) {
11056
+ answers.push(...aaaaResult.value.Answer.filter((a) => a.type === TYPE_AAAA));
11057
+ }
11058
+ const ips = answers.map((a) => a.data).sort();
11059
+ const minTtl = answers.length > 0 ? Math.min(...answers.map((a) => a.TTL)) : Infinity;
11060
+ roundResults.push({ ips, minTtl });
11061
+ } catch {
11062
+ roundResults.push({ ips: [], minTtl: Infinity });
11063
+ }
11064
+ }
11065
+ const successfulRounds = roundResults.filter((r) => r.ips.length > 0);
11066
+ if (successfulRounds.length === 0) {
11067
+ const findings2 = [
11068
+ createFinding(
11069
+ CATEGORY7,
11070
+ "DNS queries failed",
11071
+ "medium",
11072
+ `All ${effectiveRounds} query rounds failed for ${domain}. Unable to assess fast-flux behavior.`
11073
+ )
11074
+ ];
11075
+ return buildCheckResult(CATEGORY7, findings2);
11076
+ }
11077
+ const allUniqueIps = /* @__PURE__ */ new Set();
11078
+ for (const round of roundResults) {
11079
+ for (const ip of round.ips) {
11080
+ allUniqueIps.add(ip);
11081
+ }
11082
+ }
11083
+ let ipSetChanges = 0;
11084
+ for (let i = 1; i < roundResults.length; i++) {
11085
+ const prev = roundResults[i - 1].ips.join(",");
11086
+ const curr = roundResults[i].ips.join(",");
11087
+ if (prev !== curr && roundResults[i - 1].ips.length > 0 && roundResults[i].ips.length > 0) {
11088
+ ipSetChanges++;
11089
+ }
11090
+ }
11091
+ const overallMinTtl = Math.min(...successfulRounds.map((r) => r.minTtl));
11092
+ const fluxDetected = overallMinTtl < 300 && ipSetChanges > 0;
11093
+ const findings = [];
11094
+ if (fluxDetected) {
11095
+ findings.push(
11096
+ createFinding(
11097
+ CATEGORY7,
11098
+ "Fast-flux behavior detected",
11099
+ "high",
11100
+ `${domain} shows fast-flux indicators: ${allUniqueIps.size} unique IPs observed across ${effectiveRounds} rounds with ${ipSetChanges} IP set change(s) and minimum TTL of ${overallMinTtl}s. Rotating IPs with low TTLs are characteristic of fast-flux networks used to evade takedowns.`,
11101
+ {
11102
+ domain,
11103
+ flux_detected: true,
11104
+ unique_ips: allUniqueIps.size,
11105
+ ip_set_changes: ipSetChanges,
11106
+ min_ttl: overallMinTtl,
11107
+ rounds: effectiveRounds
11108
+ }
11109
+ )
11110
+ );
11111
+ } else {
11112
+ findings.push(
11113
+ createFinding(
11114
+ CATEGORY7,
11115
+ "Stable resolution \u2014 no fast-flux indicators",
11116
+ "info",
11117
+ `${domain} resolved consistently across ${effectiveRounds} rounds: ${allUniqueIps.size} unique IP(s), ${ipSetChanges} IP set change(s), minimum TTL ${overallMinTtl === Infinity ? "N/A" : `${overallMinTtl}s`}. No fast-flux behavior detected.`,
11118
+ {
11119
+ domain,
11120
+ flux_detected: false,
11121
+ unique_ips: allUniqueIps.size,
11122
+ ip_set_changes: ipSetChanges,
11123
+ min_ttl: overallMinTtl === Infinity ? null : overallMinTtl,
11124
+ rounds: effectiveRounds
11125
+ }
11126
+ )
11127
+ );
11128
+ }
11129
+ findings.push(
11130
+ createFinding(
11131
+ CATEGORY7,
11132
+ "Detection limitations",
11133
+ "info",
11134
+ "DoH resolver caching may mask IP rotation. This check has lower fidelity than direct UDP probing against authoritative nameservers."
11135
+ )
11136
+ );
11137
+ return buildCheckResult(CATEGORY7, findings);
11138
+ }
11139
+
11140
+ // src/handlers/tool-execution.ts
11141
+ init_log();
11142
+ function buildLogContext(toolName, startTime, domain, runtimeOptions) {
11143
+ return {
11144
+ toolName,
11145
+ durationMs: Date.now() - startTime,
11146
+ domain,
11147
+ analytics: runtimeOptions?.analytics,
11148
+ country: runtimeOptions?.country,
11149
+ clientType: runtimeOptions?.clientType,
11150
+ authTier: runtimeOptions?.authTier,
11151
+ keyHash: runtimeOptions?.keyHash
11152
+ };
11153
+ }
11154
+ function logToolSuccess(options) {
11155
+ options.analytics?.emitToolEvent({
11156
+ toolName: options.toolName,
11157
+ status: options.status,
11158
+ durationMs: options.durationMs,
11159
+ domain: options.domain,
11160
+ isError: false,
11161
+ score: options.score,
11162
+ cacheStatus: options.cacheStatus,
11163
+ country: options.country,
11164
+ clientType: options.clientType,
11165
+ authTier: options.authTier,
11166
+ keyHash: options.keyHash
11167
+ });
11168
+ logEvent({
11169
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11170
+ tool: options.toolName,
11171
+ domain: options.domain,
11172
+ result: options.logResult,
11173
+ details: options.logDetails,
11174
+ durationMs: options.durationMs,
11175
+ severity: options.severity ?? (options.status === "pass" ? "info" : "warn")
11176
+ });
11177
+ }
11178
+ function logToolFailure(options) {
11179
+ options.analytics?.emitToolEvent({
11180
+ toolName: options.toolName,
11181
+ status: "error",
11182
+ durationMs: options.durationMs,
11183
+ domain: options.domain,
11184
+ isError: true,
11185
+ score: options.score,
11186
+ cacheStatus: options.cacheStatus,
11187
+ country: options.country,
11188
+ clientType: options.clientType,
11189
+ authTier: options.authTier,
11190
+ keyHash: options.keyHash
11191
+ });
11192
+ logError(options.error instanceof Error ? options.error : String(options.error), {
11193
+ tool: options.toolName,
11194
+ domain: options.domain,
11195
+ details: options.args,
11196
+ severity: options.severity ?? "error"
11197
+ });
11198
+ }
11199
+
11200
+ // src/handlers/tool-formatters.ts
11201
+ function mcpError(message) {
11202
+ return { type: "text", text: `Error: ${message}` };
11203
+ }
11204
+ function mcpText(text) {
11205
+ return { type: "text", text };
11206
+ }
11207
+ function buildToolContent(text, structuredData, format) {
11208
+ const content = [mcpText(text)];
11209
+ if (format === "full") {
11210
+ content.push(mcpText(`<!-- STRUCTURED_RESULT
11211
+ ${JSON.stringify(structuredData)}
11212
+ STRUCTURED_RESULT -->`));
11213
+ }
11214
+ return content;
11215
+ }
11216
+ function formatCheckResult(result, format = "full") {
11217
+ const lines = [];
11218
+ lines.push(`## ${result.category.toUpperCase()} Check`);
11219
+ lines.push(`**Status:** ${result.passed ? "\u2705 Passed" : "\u274C Failed"}`);
11220
+ lines.push(`**Score:** ${result.score}/100`);
11221
+ lines.push("");
11222
+ if (result.findings.length > 0) {
11223
+ lines.push("### Findings");
11224
+ for (const finding of result.findings) {
11225
+ if (format === "compact") {
11226
+ const isHighPriority = finding.severity === "critical" || finding.severity === "high";
11227
+ const detailLimit = isHighPriority ? 4e3 : 300;
11228
+ lines.push(`- [${finding.severity.toUpperCase()}] ${sanitizeOutputText(finding.title, 120)} \u2014 ${sanitizeOutputText(finding.detail, detailLimit)}`);
11229
+ continue;
11230
+ }
11231
+ const icon = finding.severity === "info" ? "\u2139\uFE0F" : finding.severity === "low" ? "\u26A0\uFE0F" : finding.severity === "medium" ? "\u{1F536}" : finding.severity === "high" ? "\u{1F534}" : "\u{1F6A8}";
11232
+ lines.push(`- ${icon} **[${finding.severity.toUpperCase()}]** ${sanitizeOutputText(finding.title, 120)}`);
11233
+ lines.push(` ${sanitizeOutputText(finding.detail)}`);
11234
+ const verificationStatus = finding.category === "subdomain_takeover" && finding.metadata?.verificationStatus ? String(finding.metadata.verificationStatus) : void 0;
11235
+ if (verificationStatus) {
11236
+ lines.push(` Takeover Verification: ${sanitizeOutputText(verificationStatus, 80)}`);
11237
+ }
11238
+ const confidence = finding.metadata?.confidence ? String(finding.metadata.confidence) : void 0;
11239
+ if (confidence) {
11240
+ lines.push(` Confidence: ${sanitizeOutputText(confidence, 80)}`);
11241
+ }
11242
+ if (finding.severity !== "info") {
11243
+ const narrative = resolveImpactNarrative({
11244
+ category: finding.category,
11245
+ severity: finding.severity,
11246
+ title: finding.title,
11247
+ detail: finding.detail
11248
+ });
11249
+ if (narrative.impact) {
11250
+ lines.push(` Potential Impact: ${narrative.impact}`);
11251
+ }
11252
+ if (narrative.adverseConsequences) {
11253
+ lines.push(` Adverse Consequences: ${narrative.adverseConsequences}`);
11254
+ }
11255
+ }
11256
+ }
11257
+ }
11258
+ return lines.join("\n");
11259
+ }
11260
+ var KNOWN_ACRONYMS = /* @__PURE__ */ new Set(["mx", "spf", "dmarc", "dkim", "dnssec", "ssl", "mta", "sts", "ns", "caa", "bimi", "tlsrpt", "http", "https", "dane", "svcb", "srv", "txt", "doh", "rpm", "dbl", "rdap", "nsec"]);
11261
+ function toolNameToTitle(name) {
11262
+ return name.split("_").map((word) => KNOWN_ACRONYMS.has(word) ? word.toUpperCase() : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
11263
+ }
11264
+ function toInputSchema(schema) {
11265
+ const jsonSchema = z.toJSONSchema(schema);
11266
+ delete jsonSchema.$schema;
11267
+ if (jsonSchema.additionalProperties !== void 0 && typeof jsonSchema.additionalProperties === "object" && jsonSchema.additionalProperties !== null && Object.keys(jsonSchema.additionalProperties).length === 0) {
11268
+ delete jsonSchema.additionalProperties;
11269
+ }
11270
+ return jsonSchema;
11271
+ }
11272
+ var TOOL_DEFS = {
11273
+ check_mx: {
11274
+ description: "Look up MX records for a domain. Shows mail servers, email provider detection, and validates configuration.",
11275
+ schema: BaseDomainArgs,
11276
+ group: "email_auth",
9808
11277
  tier: "protective",
9809
11278
  scanIncluded: true
9810
11279
  },
9811
11280
  check_spf: {
9812
- description: "Validate SPF syntax, policy, and trust surface.",
11281
+ description: "Look up and validate SPF record for a domain. Shows authorized senders, syntax issues, and trust surface.",
9813
11282
  schema: BaseDomainArgs,
9814
11283
  group: "email_auth",
9815
11284
  tier: "core",
9816
11285
  scanIncluded: true
9817
11286
  },
9818
11287
  check_dmarc: {
9819
- description: "Validate DMARC policy, alignment, and reporting.",
11288
+ description: "Look up and validate DMARC record for a domain. Shows policy enforcement, alignment mode, and reporting config.",
9820
11289
  schema: BaseDomainArgs,
9821
11290
  group: "email_auth",
9822
11291
  tier: "core",
9823
11292
  scanIncluded: true
9824
11293
  },
9825
11294
  check_dkim: {
9826
- description: "Probe DKIM selectors and validate key strength.",
11295
+ description: "Look up DKIM records for a domain. Probes common selectors and validates key strength and algorithm.",
9827
11296
  schema: CheckDkimArgs,
9828
11297
  group: "email_auth",
9829
11298
  tier: "core",
9830
11299
  scanIncluded: true
9831
11300
  },
9832
11301
  check_dnssec: {
9833
- description: "Verify DNSSEC validation and DNSKEY/DS records.",
11302
+ description: "Check DNSSEC status for a domain. Verifies DNSKEY/DS records and validation chain.",
9834
11303
  schema: BaseDomainArgs,
9835
11304
  group: "infrastructure",
9836
11305
  tier: "core",
9837
11306
  scanIncluded: true
9838
11307
  },
9839
11308
  check_ssl: {
9840
- description: "Verify SSL/TLS certificate and HTTPS config.",
11309
+ description: "Check SSL/TLS certificate for a domain. Shows issuer, expiry, protocol versions, and HTTPS configuration.",
9841
11310
  schema: BaseDomainArgs,
9842
11311
  group: "infrastructure",
9843
11312
  tier: "core",
@@ -9851,14 +11320,14 @@ var TOOL_DEFS = {
9851
11320
  scanIncluded: true
9852
11321
  },
9853
11322
  check_ns: {
9854
- description: "Analyze NS delegation and provider diversity.",
11323
+ description: "Look up NS (nameserver) records for a domain. Shows DNS provider, delegation, and redundancy.",
9855
11324
  schema: BaseDomainArgs,
9856
11325
  group: "infrastructure",
9857
11326
  tier: "protective",
9858
11327
  scanIncluded: true
9859
11328
  },
9860
11329
  check_caa: {
9861
- description: "Check authorized Certificate Authorities via CAA.",
11330
+ description: "Look up CAA records for a domain. Shows which Certificate Authorities are authorized to issue certificates.",
9862
11331
  schema: BaseDomainArgs,
9863
11332
  group: "infrastructure",
9864
11333
  tier: "protective",
@@ -9921,7 +11390,7 @@ var TOOL_DEFS = {
9921
11390
  scanIncluded: true
9922
11391
  },
9923
11392
  scan_domain: {
9924
- description: "Full DNS and email security audit. Score, grade, maturity, findings. Start here.",
11393
+ description: "Look up any domain to get a full DNS and email security audit. Use this whenever a user mentions a domain name, asks to check/scan/lookup/analyze a domain, or wants to know about a domain's security posture. Returns score, grade, maturity stage, and prioritized findings. Start here for any domain-related question.",
9925
11394
  schema: ScanDomainArgs,
9926
11395
  group: "meta",
9927
11396
  scanIncluded: false
@@ -10064,13 +11533,13 @@ var TOOL_DEFS = {
10064
11533
  scanIncluded: false
10065
11534
  },
10066
11535
  resolve_spf_chain: {
10067
- description: "Recursively resolve the full SPF include chain. Shows lookup count, tree depth, and flags issues like circular includes or exceeding the 10-lookup limit.",
11536
+ description: "Trace the full SPF include chain for a domain. Recursively resolves all includes, shows lookup count, tree depth, and flags circular includes or exceeding the 10-lookup limit.",
10068
11537
  schema: BaseDomainArgs,
10069
11538
  group: "intelligence",
10070
11539
  scanIncluded: false
10071
11540
  },
10072
11541
  discover_subdomains: {
10073
- description: "Discover subdomains via Certificate Transparency logs. Reveals shadow IT, forgotten services, and unauthorized certificate issuance.",
11542
+ description: "Find subdomains of a domain using Certificate Transparency logs. Reveals shadow IT, forgotten services, and unauthorized certificate issuance.",
10074
11543
  schema: BaseDomainArgs,
10075
11544
  group: "intelligence",
10076
11545
  scanIncluded: false
@@ -10086,6 +11555,48 @@ var TOOL_DEFS = {
10086
11555
  schema: BaseDomainArgs,
10087
11556
  group: "intelligence",
10088
11557
  scanIncluded: false
11558
+ },
11559
+ check_dbl: {
11560
+ description: "Check domain reputation against DNS-based Domain Block Lists (Spamhaus DBL, URIBL, SURBL). Returns listing status with decoded return codes.",
11561
+ schema: BaseDomainArgs,
11562
+ group: "intelligence",
11563
+ scanIncluded: false
11564
+ },
11565
+ check_rbl: {
11566
+ description: "Check MX server IP reputation against 8 DNS-based Real-time Blocklists (Spamhaus ZEN, SpamCop, UCEProtect, Mailspike, Barracuda, PSBL, SORBS). Resolves MX hosts to IPs first.",
11567
+ schema: BaseDomainArgs,
11568
+ group: "intelligence",
11569
+ scanIncluded: false
11570
+ },
11571
+ cymru_asn: {
11572
+ description: "Map domain IPs to Autonomous System Numbers via Team Cymru DNS. Returns ASN, prefix, country, registry, and organization for each IP. Flags high-risk hosting ASNs.",
11573
+ schema: BaseDomainArgs,
11574
+ group: "intelligence",
11575
+ scanIncluded: false
11576
+ },
11577
+ rdap_lookup: {
11578
+ description: "Fetch domain registration data via RDAP (modern WHOIS replacement). Returns registrar, creation/expiration dates, EPP status, registrant info, and domain age.",
11579
+ schema: BaseDomainArgs,
11580
+ group: "intelligence",
11581
+ scanIncluded: false
11582
+ },
11583
+ check_nsec_walkability: {
11584
+ description: "Assess zone walkability risk by analyzing NSEC3PARAM configuration. Detects plain NSEC zones, weak NSEC3 parameters, and opt-out flags.",
11585
+ schema: BaseDomainArgs,
11586
+ group: "intelligence",
11587
+ scanIncluded: false
11588
+ },
11589
+ check_dnssec_chain: {
11590
+ description: "Walk the DNSSEC chain of trust from root to target domain. Reports DS/DNSKEY records, algorithm usage, and linkage status at each zone level.",
11591
+ schema: BaseDomainArgs,
11592
+ group: "intelligence",
11593
+ scanIncluded: false
11594
+ },
11595
+ check_fast_flux: {
11596
+ description: "Detect fast-flux DNS behavior by performing multiple rounds of A/AAAA queries with delays. Compares IP answer sets and TTLs across rounds to identify rotating infrastructure.",
11597
+ schema: CheckFastFluxArgs,
11598
+ group: "intelligence",
11599
+ scanIncluded: false
10089
11600
  }
10090
11601
  };
10091
11602
  var TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
@@ -10148,9 +11659,16 @@ var TOOL_REGISTRY = {
10148
11659
  check_mx_reputation: { cacheKey: () => "mx_reputation", execute: (d, _args, ro) => checkMxReputation(d, buildDnsOptions(ro)), cacheTtlSeconds: 3600 },
10149
11660
  check_srv: { cacheKey: () => "srv", execute: (d, _args, ro) => checkSrv(d, buildDnsOptions(ro)) },
10150
11661
  check_zone_hygiene: { cacheKey: () => "zone_hygiene", execute: (d, _args, ro) => checkZoneHygiene(d, buildDnsOptions(ro)) },
10151
- check_subdomailing: { cacheKey: () => "subdomailing", execute: (d, _args, ro) => checkSubdomailing(d, buildDnsOptions(ro)) }
11662
+ check_subdomailing: { cacheKey: () => "subdomailing", execute: (d, _args, ro) => checkSubdomailing(d, buildDnsOptions(ro)) },
11663
+ check_dbl: { cacheKey: () => "dbl", execute: (d, _args, ro) => checkDbl(d, buildDnsOptions(ro)), cacheTtlSeconds: 3600 },
11664
+ check_rbl: { cacheKey: () => "rbl", execute: (d, _args, ro) => checkRbl(d, buildDnsOptions(ro)), cacheTtlSeconds: 3600 },
11665
+ cymru_asn: { cacheKey: () => "asn", execute: (d, _args, ro) => checkCymruAsn(d, buildDnsOptions(ro)), cacheTtlSeconds: 3600 },
11666
+ rdap_lookup: { cacheKey: () => "rdap", execute: (d) => checkRdapLookup(d), cacheTtlSeconds: 3600 },
11667
+ check_nsec_walkability: { cacheKey: () => "nsec_walkability", execute: (d, _args, ro) => checkNsecWalkability(d, buildDnsOptions(ro)), cacheTtlSeconds: 3600 },
11668
+ check_dnssec_chain: { cacheKey: () => "dnssec_chain", execute: (d, _args, ro) => checkDnssecChain(d, buildDnsOptions(ro)) },
11669
+ check_fast_flux: { cacheKey: () => "fast_flux", execute: (d, args, ro) => checkFastFlux(d, args.rounds ?? 3, buildDnsOptions(ro)) }
10152
11670
  };
10153
- var INTERACTIVE_CLIENTS = /* @__PURE__ */ new Set(["claude_code", "cursor", "vscode", "claude_desktop", "windsurf"]);
11671
+ var INTERACTIVE_CLIENTS = /* @__PURE__ */ new Set(["claude_mobile", "claude_code", "cursor", "vscode", "claude_desktop", "windsurf"]);
10154
11672
  function isInteractiveClient(clientType) {
10155
11673
  return INTERACTIVE_CLIENTS.has(clientType ?? "");
10156
11674
  }
@@ -10162,19 +11680,13 @@ function resolveFormat(args, clientType) {
10162
11680
  function buildToolErrorResult(message) {
10163
11681
  return { content: [mcpError(message)], isError: true };
10164
11682
  }
10165
- function handleExplainFindingValidationError(args, durationMs, runtimeOptions) {
11683
+ function handleExplainFindingValidationError(args, startTime, runtimeOptions) {
10166
11684
  const error2 = new Error("Missing required parameters: checkType and status");
10167
11685
  logToolFailure({
10168
- toolName: "explain_finding",
10169
- durationMs,
10170
- analytics: runtimeOptions?.analytics,
11686
+ ...buildLogContext("explain_finding", startTime, void 0, runtimeOptions),
10171
11687
  error: error2,
10172
11688
  args,
10173
- severity: "warn",
10174
- country: runtimeOptions?.country,
10175
- clientType: runtimeOptions?.clientType,
10176
- authTier: runtimeOptions?.authTier,
10177
- keyHash: runtimeOptions?.keyHash
11689
+ severity: "warn"
10178
11690
  });
10179
11691
  return buildToolErrorResult(error2.message);
10180
11692
  }
@@ -10183,6 +11695,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10183
11695
  const name = normalizeToolName(params.name);
10184
11696
  const args = params.arguments ?? {};
10185
11697
  const startTime = Date.now();
11698
+ const ctx = () => buildLogContext(name, startTime, domain, runtimeOptions);
10186
11699
  let domain;
10187
11700
  let logResult = "unknown";
10188
11701
  let logDetails;
@@ -10201,27 +11714,17 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10201
11714
  const checkName = registeredTool.cacheKey(validatedArgs);
10202
11715
  const cacheKey = `cache:${validDomain}:check:${checkName}`;
10203
11716
  const { data: result, cacheStatus } = await runWithCacheTracked(cacheKey, () => registeredTool.execute(validDomain, validatedArgs, runtimeOptions), scanCacheKV, registeredTool.cacheTtlSeconds);
10204
- if (result.partial && scanCacheKV) {
10205
- try {
10206
- await scanCacheKV.delete(cacheKey);
10207
- } catch {
10208
- }
11717
+ if (result.partial) {
11718
+ await cacheDelete(cacheKey, scanCacheKV);
10209
11719
  }
10210
11720
  runtimeOptions?.resultCapture?.(result);
10211
11721
  logResult = result.passed ? "pass" : "fail";
10212
11722
  logDetails = result;
10213
11723
  logToolSuccess({
10214
- toolName: name,
10215
- durationMs: Date.now() - startTime,
10216
- domain,
10217
- analytics: runtimeOptions?.analytics,
11724
+ ...ctx(),
10218
11725
  status: result.passed ? "pass" : "fail",
10219
11726
  logResult,
10220
11727
  logDetails,
10221
- country: runtimeOptions?.country,
10222
- clientType: runtimeOptions?.clientType,
10223
- authTier: runtimeOptions?.authTier,
10224
- keyHash: runtimeOptions?.keyHash,
10225
11728
  cacheStatus
10226
11729
  });
10227
11730
  return { content: buildToolContent(formatCheckResult(result, effectiveFormat), result, effectiveFormat) };
@@ -10234,20 +11737,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10234
11737
  const result = await scanDomain(validDomain, scanCacheKV, scanOptions);
10235
11738
  logResult = result.score.grade;
10236
11739
  logDetails = result;
10237
- logToolSuccess({
10238
- toolName: name,
10239
- durationMs: Date.now() - startTime,
10240
- domain,
10241
- analytics: runtimeOptions?.analytics,
10242
- status: result.score.overall >= 50 ? "pass" : "fail",
10243
- logResult,
10244
- logDetails,
10245
- severity: "info",
10246
- country: runtimeOptions?.country,
10247
- clientType: runtimeOptions?.clientType,
10248
- authTier: runtimeOptions?.authTier,
10249
- keyHash: runtimeOptions?.keyHash
10250
- });
11740
+ logToolSuccess({ ...ctx(), status: result.score.overall >= 50 ? "pass" : "fail", logResult, logDetails, severity: "info" });
10251
11741
  const structured = buildStructuredScanResult(result);
10252
11742
  return { content: buildToolContent(formatScanReport(result, effectiveFormat), structured, effectiveFormat) };
10253
11743
  }
@@ -10268,19 +11758,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10268
11758
  }
10269
11759
  });
10270
11760
  const batchText = formatBatchScan(batchResults, effectiveFormat);
10271
- logToolSuccess({
10272
- toolName: "batch_scan",
10273
- durationMs: Date.now() - startTime,
10274
- analytics: runtimeOptions?.analytics,
10275
- status: "pass",
10276
- logResult: `${batchResults.filter((r) => !r.error).length}/${batchResults.length} domains`,
10277
- logDetails: { totalDomains: batchResults.length },
10278
- severity: "info",
10279
- country: runtimeOptions?.country,
10280
- clientType: runtimeOptions?.clientType,
10281
- authTier: runtimeOptions?.authTier,
10282
- keyHash: runtimeOptions?.keyHash
10283
- });
11761
+ logToolSuccess({ ...ctx(), status: "pass", logResult: `${batchResults.filter((r) => !r.error).length}/${batchResults.length} domains`, logDetails: { totalDomains: batchResults.length }, severity: "info" });
10284
11762
  return { content: buildToolContent(batchText, batchResults, effectiveFormat) };
10285
11763
  }
10286
11764
  case "compare_domains": {
@@ -10298,19 +11776,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10298
11776
  }
10299
11777
  });
10300
11778
  const compareText = formatDomainComparison(compareResults, effectiveFormat);
10301
- logToolSuccess({
10302
- toolName: "compare_domains",
10303
- durationMs: Date.now() - startTime,
10304
- analytics: runtimeOptions?.analytics,
10305
- status: "pass",
10306
- logResult: `${Object.keys(compareResults.scores).length}/${compareResults.domains.length} domains compared`,
10307
- logDetails: { totalDomains: compareResults.domains.length, winner: compareResults.winner },
10308
- severity: "info",
10309
- country: runtimeOptions?.country,
10310
- clientType: runtimeOptions?.clientType,
10311
- authTier: runtimeOptions?.authTier,
10312
- keyHash: runtimeOptions?.keyHash
10313
- });
11779
+ logToolSuccess({ ...ctx(), status: "pass", logResult: `${Object.keys(compareResults.scores).length}/${compareResults.domains.length} domains compared`, logDetails: { totalDomains: compareResults.domains.length, winner: compareResults.winner }, severity: "info" });
10314
11780
  return { content: buildToolContent(compareText, compareResults, effectiveFormat) };
10315
11781
  }
10316
11782
  case "compare_baseline": {
@@ -10319,135 +11785,45 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10319
11785
  const result = compareBaseline(scan, baseline);
10320
11786
  logResult = result.passed ? "pass" : "fail";
10321
11787
  logDetails = result;
10322
- logToolSuccess({
10323
- toolName: name,
10324
- durationMs: Date.now() - startTime,
10325
- domain,
10326
- analytics: runtimeOptions?.analytics,
10327
- status: result.passed ? "pass" : "fail",
10328
- logResult,
10329
- logDetails,
10330
- severity: "info",
10331
- country: runtimeOptions?.country,
10332
- clientType: runtimeOptions?.clientType,
10333
- authTier: runtimeOptions?.authTier,
10334
- keyHash: runtimeOptions?.keyHash
10335
- });
11788
+ logToolSuccess({ ...ctx(), status: result.passed ? "pass" : "fail", logResult, logDetails, severity: "info" });
10336
11789
  return { content: buildToolContent(formatBaselineResult(result, effectiveFormat), result, effectiveFormat) };
10337
11790
  }
10338
11791
  case "generate_fix_plan": {
10339
11792
  const plan = await generateFixPlan(validDomain, scanCacheKV, runtimeOptions);
10340
11793
  logResult = plan.grade;
10341
11794
  logDetails = plan;
10342
- logToolSuccess({
10343
- toolName: name,
10344
- durationMs: Date.now() - startTime,
10345
- domain,
10346
- analytics: runtimeOptions?.analytics,
10347
- status: "pass",
10348
- logResult,
10349
- logDetails,
10350
- severity: "info",
10351
- country: runtimeOptions?.country,
10352
- clientType: runtimeOptions?.clientType,
10353
- authTier: runtimeOptions?.authTier,
10354
- keyHash: runtimeOptions?.keyHash
10355
- });
11795
+ logToolSuccess({ ...ctx(), status: "pass", logResult, logDetails, severity: "info" });
10356
11796
  return { content: buildToolContent(formatFixPlan(plan, effectiveFormat), plan, effectiveFormat) };
10357
11797
  }
10358
11798
  case "generate_spf_record": {
10359
11799
  const includeProviders = extractIncludeProviders(validatedArgs);
10360
11800
  const record = await generateSpfRecord(validDomain, includeProviders, buildDnsOptions(runtimeOptions));
10361
- logToolSuccess({
10362
- toolName: name,
10363
- durationMs: Date.now() - startTime,
10364
- domain,
10365
- analytics: runtimeOptions?.analytics,
10366
- status: "pass",
10367
- logResult: "generated",
10368
- logDetails: record,
10369
- severity: "info",
10370
- country: runtimeOptions?.country,
10371
- clientType: runtimeOptions?.clientType,
10372
- authTier: runtimeOptions?.authTier,
10373
- keyHash: runtimeOptions?.keyHash
10374
- });
11801
+ logToolSuccess({ ...ctx(), status: "pass", logResult: "generated", logDetails: record, severity: "info" });
10375
11802
  return { content: buildToolContent(formatGeneratedRecord(record, effectiveFormat), record, effectiveFormat) };
10376
11803
  }
10377
11804
  case "generate_dmarc_record": {
10378
11805
  const policy = typeof validatedArgs.policy === "string" ? validatedArgs.policy : void 0;
10379
11806
  const ruaEmail = typeof validatedArgs.rua_email === "string" ? validatedArgs.rua_email : void 0;
10380
11807
  const record = await generateDmarcRecord(validDomain, policy, ruaEmail, buildDnsOptions(runtimeOptions));
10381
- logToolSuccess({
10382
- toolName: name,
10383
- durationMs: Date.now() - startTime,
10384
- domain,
10385
- analytics: runtimeOptions?.analytics,
10386
- status: "pass",
10387
- logResult: "generated",
10388
- logDetails: record,
10389
- severity: "info",
10390
- country: runtimeOptions?.country,
10391
- clientType: runtimeOptions?.clientType,
10392
- authTier: runtimeOptions?.authTier,
10393
- keyHash: runtimeOptions?.keyHash
10394
- });
11808
+ logToolSuccess({ ...ctx(), status: "pass", logResult: "generated", logDetails: record, severity: "info" });
10395
11809
  return { content: buildToolContent(formatGeneratedRecord(record, effectiveFormat), record, effectiveFormat) };
10396
11810
  }
10397
11811
  case "generate_dkim_config": {
10398
11812
  const provider = typeof validatedArgs.provider === "string" ? validatedArgs.provider : void 0;
10399
11813
  const record = await generateDkimConfig(validDomain, provider);
10400
- logToolSuccess({
10401
- toolName: name,
10402
- durationMs: Date.now() - startTime,
10403
- domain,
10404
- analytics: runtimeOptions?.analytics,
10405
- status: "pass",
10406
- logResult: "generated",
10407
- logDetails: record,
10408
- severity: "info",
10409
- country: runtimeOptions?.country,
10410
- clientType: runtimeOptions?.clientType,
10411
- authTier: runtimeOptions?.authTier,
10412
- keyHash: runtimeOptions?.keyHash
10413
- });
11814
+ logToolSuccess({ ...ctx(), status: "pass", logResult: "generated", logDetails: record, severity: "info" });
10414
11815
  return { content: buildToolContent(formatGeneratedRecord(record, effectiveFormat), record, effectiveFormat) };
10415
11816
  }
10416
11817
  case "generate_mta_sts_policy": {
10417
11818
  const mxHosts = extractMxHosts(validatedArgs);
10418
11819
  const record = await generateMtaStsPolicy(validDomain, mxHosts, buildDnsOptions(runtimeOptions));
10419
- logToolSuccess({
10420
- toolName: name,
10421
- durationMs: Date.now() - startTime,
10422
- domain,
10423
- analytics: runtimeOptions?.analytics,
10424
- status: "pass",
10425
- logResult: "generated",
10426
- logDetails: record,
10427
- severity: "info",
10428
- country: runtimeOptions?.country,
10429
- clientType: runtimeOptions?.clientType,
10430
- authTier: runtimeOptions?.authTier,
10431
- keyHash: runtimeOptions?.keyHash
10432
- });
11820
+ logToolSuccess({ ...ctx(), status: "pass", logResult: "generated", logDetails: record, severity: "info" });
10433
11821
  return { content: buildToolContent(formatGeneratedRecord(record, effectiveFormat), record, effectiveFormat) };
10434
11822
  }
10435
11823
  case "get_benchmark": {
10436
11824
  const profile = typeof validatedArgs.profile === "string" ? validatedArgs.profile : "mail_enabled";
10437
11825
  const result = await getBenchmark(runtimeOptions?.profileAccumulator, profile);
10438
- logToolSuccess({
10439
- toolName: name,
10440
- durationMs: Date.now() - startTime,
10441
- analytics: runtimeOptions?.analytics,
10442
- status: "pass",
10443
- logResult: result.status,
10444
- logDetails: result,
10445
- severity: "info",
10446
- country: runtimeOptions?.country,
10447
- clientType: runtimeOptions?.clientType,
10448
- authTier: runtimeOptions?.authTier,
10449
- keyHash: runtimeOptions?.keyHash
10450
- });
11826
+ logToolSuccess({ ...ctx(), status: "pass", logResult: result.status, logDetails: result, severity: "info" });
10451
11827
  return { content: buildToolContent(formatBenchmark(result, effectiveFormat), result, effectiveFormat) };
10452
11828
  }
10453
11829
  case "get_provider_insights": {
@@ -10457,39 +11833,14 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10457
11833
  }
10458
11834
  const profile = typeof validatedArgs.profile === "string" ? validatedArgs.profile : "mail_enabled";
10459
11835
  const result = await getProviderInsights(runtimeOptions?.profileAccumulator, provider, profile);
10460
- logToolSuccess({
10461
- toolName: name,
10462
- durationMs: Date.now() - startTime,
10463
- analytics: runtimeOptions?.analytics,
10464
- status: "pass",
10465
- logResult: result.status,
10466
- logDetails: result,
10467
- severity: "info",
10468
- country: runtimeOptions?.country,
10469
- clientType: runtimeOptions?.clientType,
10470
- authTier: runtimeOptions?.authTier,
10471
- keyHash: runtimeOptions?.keyHash
10472
- });
11836
+ logToolSuccess({ ...ctx(), status: "pass", logResult: result.status, logDetails: result, severity: "info" });
10473
11837
  return { content: buildToolContent(formatProviderInsights(result, effectiveFormat), result, effectiveFormat) };
10474
11838
  }
10475
11839
  case "assess_spoofability": {
10476
11840
  const result = await assessSpoofability(validDomain, buildDnsOptions(runtimeOptions));
10477
11841
  logResult = result.riskLevel;
10478
11842
  logDetails = result;
10479
- logToolSuccess({
10480
- toolName: name,
10481
- durationMs: Date.now() - startTime,
10482
- domain,
10483
- analytics: runtimeOptions?.analytics,
10484
- status: result.spoofabilityScore <= 30 ? "pass" : "fail",
10485
- logResult,
10486
- logDetails,
10487
- severity: "info",
10488
- country: runtimeOptions?.country,
10489
- clientType: runtimeOptions?.clientType,
10490
- authTier: runtimeOptions?.authTier,
10491
- keyHash: runtimeOptions?.keyHash
10492
- });
11843
+ logToolSuccess({ ...ctx(), status: result.spoofabilityScore <= 30 ? "pass" : "fail", logResult, logDetails, severity: "info" });
10493
11844
  return { content: buildToolContent(formatSpoofability(result, effectiveFormat), result, effectiveFormat) };
10494
11845
  }
10495
11846
  case "check_resolver_consistency": {
@@ -10498,20 +11849,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10498
11849
  runtimeOptions?.resultCapture?.(result);
10499
11850
  logResult = result.passed ? "pass" : "fail";
10500
11851
  logDetails = result;
10501
- logToolSuccess({
10502
- toolName: name,
10503
- durationMs: Date.now() - startTime,
10504
- domain,
10505
- analytics: runtimeOptions?.analytics,
10506
- status: result.passed ? "pass" : "fail",
10507
- logResult,
10508
- logDetails,
10509
- severity: "info",
10510
- country: runtimeOptions?.country,
10511
- clientType: runtimeOptions?.clientType,
10512
- authTier: runtimeOptions?.authTier,
10513
- keyHash: runtimeOptions?.keyHash
10514
- });
11852
+ logToolSuccess({ ...ctx(), status: result.passed ? "pass" : "fail", logResult, logDetails, severity: "info" });
10515
11853
  return { content: buildToolContent(formatResolverConsistency(result, effectiveFormat), result, effectiveFormat) };
10516
11854
  }
10517
11855
  case "explain_finding": {
@@ -10519,23 +11857,11 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10519
11857
  try {
10520
11858
  explainArgs = extractExplainFindingArgs(validatedArgs);
10521
11859
  } catch {
10522
- return handleExplainFindingValidationError(args, Date.now() - startTime, runtimeOptions);
11860
+ return handleExplainFindingValidationError(args, startTime, runtimeOptions);
10523
11861
  }
10524
11862
  const { checkType, status, details } = explainArgs;
10525
11863
  const result = explainFinding(checkType, status, details);
10526
- logToolSuccess({
10527
- toolName: name,
10528
- durationMs: Date.now() - startTime,
10529
- analytics: runtimeOptions?.analytics,
10530
- status: "pass",
10531
- logResult: status,
10532
- logDetails: { checkType, details },
10533
- severity: "info",
10534
- country: runtimeOptions?.country,
10535
- clientType: runtimeOptions?.clientType,
10536
- authTier: runtimeOptions?.authTier,
10537
- keyHash: runtimeOptions?.keyHash
10538
- });
11864
+ logToolSuccess({ ...ctx(), status: "pass", logResult: status, logDetails: { checkType, details }, severity: "info" });
10539
11865
  return { content: buildToolContent(formatExplanation(result, effectiveFormat), result, effectiveFormat) };
10540
11866
  }
10541
11867
  case "validate_fix": {
@@ -10544,40 +11870,14 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10544
11870
  const result = await validateFix(validDomain, check, expected, buildDnsOptions(runtimeOptions));
10545
11871
  logResult = result.verdict;
10546
11872
  logDetails = result;
10547
- logToolSuccess({
10548
- toolName: name,
10549
- durationMs: Date.now() - startTime,
10550
- domain,
10551
- analytics: runtimeOptions?.analytics,
10552
- status: result.verdict === "fixed" ? "pass" : "fail",
10553
- logResult,
10554
- logDetails,
10555
- severity: "info",
10556
- country: runtimeOptions?.country,
10557
- clientType: runtimeOptions?.clientType,
10558
- authTier: runtimeOptions?.authTier,
10559
- keyHash: runtimeOptions?.keyHash
10560
- });
11873
+ logToolSuccess({ ...ctx(), status: result.verdict === "fixed" ? "pass" : "fail", logResult, logDetails, severity: "info" });
10561
11874
  return { content: buildToolContent(formatValidateFix(result, effectiveFormat), result, effectiveFormat) };
10562
11875
  }
10563
11876
  case "map_supply_chain": {
10564
11877
  const result = await mapSupplyChain(validDomain, buildDnsOptions(runtimeOptions));
10565
11878
  logResult = `${result.summary.totalProviders} providers`;
10566
11879
  logDetails = result;
10567
- logToolSuccess({
10568
- toolName: name,
10569
- durationMs: Date.now() - startTime,
10570
- domain,
10571
- analytics: runtimeOptions?.analytics,
10572
- status: "pass",
10573
- logResult,
10574
- logDetails,
10575
- severity: "info",
10576
- country: runtimeOptions?.country,
10577
- clientType: runtimeOptions?.clientType,
10578
- authTier: runtimeOptions?.authTier,
10579
- keyHash: runtimeOptions?.keyHash
10580
- });
11880
+ logToolSuccess({ ...ctx(), status: "pass", logResult, logDetails, severity: "info" });
10581
11881
  return { content: buildToolContent(formatSupplyChain(result, effectiveFormat), result, effectiveFormat) };
10582
11882
  }
10583
11883
  case "generate_rollout_plan": {
@@ -10586,20 +11886,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10586
11886
  const result = await generateRolloutPlan(validDomain, targetPolicy, timeline, buildDnsOptions(runtimeOptions));
10587
11887
  logResult = result.atTarget ? "at_target" : `${result.phases.length} phases`;
10588
11888
  logDetails = result;
10589
- logToolSuccess({
10590
- toolName: name,
10591
- durationMs: Date.now() - startTime,
10592
- domain,
10593
- analytics: runtimeOptions?.analytics,
10594
- status: "pass",
10595
- logResult,
10596
- logDetails,
10597
- severity: "info",
10598
- country: runtimeOptions?.country,
10599
- clientType: runtimeOptions?.clientType,
10600
- authTier: runtimeOptions?.authTier,
10601
- keyHash: runtimeOptions?.keyHash
10602
- });
11889
+ logToolSuccess({ ...ctx(), status: "pass", logResult, logDetails, severity: "info" });
10603
11890
  return { content: buildToolContent(formatRolloutPlan(result, effectiveFormat), result, effectiveFormat) };
10604
11891
  }
10605
11892
  case "analyze_drift": {
@@ -10627,77 +11914,40 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10627
11914
  const drift = computeDrift(validDomain, baselineScore, scanResult.score);
10628
11915
  logResult = drift.classification;
10629
11916
  logDetails = drift;
10630
- logToolSuccess({
10631
- toolName: name,
10632
- durationMs: Date.now() - startTime,
10633
- domain,
10634
- analytics: runtimeOptions?.analytics,
10635
- status: "pass",
10636
- logResult,
10637
- logDetails,
10638
- severity: "info",
10639
- country: runtimeOptions?.country,
10640
- clientType: runtimeOptions?.clientType,
10641
- authTier: runtimeOptions?.authTier,
10642
- keyHash: runtimeOptions?.keyHash
10643
- });
11917
+ logToolSuccess({ ...ctx(), status: "pass", logResult, logDetails, severity: "info" });
10644
11918
  return { content: buildToolContent(formatDriftReport(drift, effectiveFormat), drift, effectiveFormat) };
10645
11919
  }
10646
11920
  case "resolve_spf_chain": {
10647
11921
  const result = await resolveSpfChain(validDomain, buildDnsOptions(runtimeOptions));
10648
11922
  logResult = result.overLimit ? "over_limit" : "ok";
10649
11923
  logDetails = { totalLookups: result.totalLookups, maxDepth: result.maxDepth, issues: result.issues.length };
10650
- logToolSuccess({
10651
- toolName: name,
10652
- durationMs: Date.now() - startTime,
10653
- domain,
10654
- analytics: runtimeOptions?.analytics,
10655
- status: result.overLimit ? "fail" : "pass",
10656
- logResult,
10657
- logDetails,
10658
- severity: "info",
10659
- country: runtimeOptions?.country,
10660
- clientType: runtimeOptions?.clientType,
10661
- authTier: runtimeOptions?.authTier,
10662
- keyHash: runtimeOptions?.keyHash
10663
- });
11924
+ logToolSuccess({ ...ctx(), status: result.overLimit ? "fail" : "pass", logResult, logDetails, severity: "info" });
10664
11925
  return { content: buildToolContent(formatSpfChain(result, effectiveFormat), result, effectiveFormat) };
10665
11926
  }
10666
11927
  case "discover_subdomains": {
10667
11928
  const result = await discoverSubdomains(validDomain, runtimeOptions?.certstream);
10668
11929
  logResult = `${result.totalSubdomains} subdomains`;
10669
11930
  logDetails = { totalSubdomains: result.totalSubdomains, issues: result.issues.length };
10670
- logToolSuccess({ toolName: name, durationMs: Date.now() - startTime, domain, analytics: runtimeOptions?.analytics, status: "pass", logResult, logDetails, severity: "info", country: runtimeOptions?.country, clientType: runtimeOptions?.clientType, authTier: runtimeOptions?.authTier });
11931
+ logToolSuccess({ ...ctx(), status: "pass", logResult, logDetails, severity: "info" });
10671
11932
  return { content: buildToolContent(formatSubdomainDiscovery(result, effectiveFormat), result, effectiveFormat) };
10672
11933
  }
10673
11934
  case "map_compliance": {
10674
11935
  const result = await mapCompliance(validDomain, scanCacheKV, runtimeOptions);
10675
11936
  logResult = "mapped";
10676
11937
  logDetails = result;
10677
- logToolSuccess({ toolName: name, durationMs: Date.now() - startTime, domain, analytics: runtimeOptions?.analytics, status: "pass", logResult, logDetails, severity: "info", country: runtimeOptions?.country, clientType: runtimeOptions?.clientType, authTier: runtimeOptions?.authTier });
11938
+ logToolSuccess({ ...ctx(), status: "pass", logResult, logDetails, severity: "info" });
10678
11939
  return { content: buildToolContent(formatCompliance(result, effectiveFormat), result, effectiveFormat) };
10679
11940
  }
10680
11941
  case "simulate_attack_paths": {
10681
11942
  const result = await simulateAttackPaths(validDomain, buildDnsOptions(runtimeOptions));
10682
11943
  logResult = `${result.totalPaths} paths, risk: ${result.overallRisk}`;
10683
11944
  logDetails = { totalPaths: result.totalPaths, overallRisk: result.overallRisk };
10684
- logToolSuccess({ toolName: name, durationMs: Date.now() - startTime, domain, analytics: runtimeOptions?.analytics, status: result.overallRisk === "low" ? "pass" : "fail", logResult, logDetails, severity: "info", country: runtimeOptions?.country, clientType: runtimeOptions?.clientType, authTier: runtimeOptions?.authTier });
11945
+ logToolSuccess({ ...ctx(), status: result.overallRisk === "low" ? "pass" : "fail", logResult, logDetails, severity: "info" });
10685
11946
  return { content: buildToolContent(formatAttackPaths(result, effectiveFormat), result, effectiveFormat) };
10686
11947
  }
10687
11948
  default:
10688
- logToolFailure({
10689
- toolName: name,
10690
- durationMs: Date.now() - startTime,
10691
- domain,
10692
- analytics: runtimeOptions?.analytics,
10693
- error: `Unknown tool: ${name}`,
10694
- args,
10695
- country: runtimeOptions?.country,
10696
- clientType: runtimeOptions?.clientType,
10697
- authTier: runtimeOptions?.authTier,
10698
- keyHash: runtimeOptions?.keyHash
10699
- });
10700
- return buildToolErrorResult(`Unknown tool: ${name}. Call tools/list to see all 44 available tools.`);
11949
+ logToolFailure({ ...ctx(), error: `Unknown tool: ${name}`, args });
11950
+ return buildToolErrorResult(`Unknown tool: ${name}. Call tools/list to see all 51 available tools.`);
10701
11951
  }
10702
11952
  };
10703
11953
  return await Promise.race([
@@ -10706,36 +11956,14 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
10706
11956
  ]);
10707
11957
  } catch (err) {
10708
11958
  if (err instanceof Error && err.message === "__tool_timeout__") {
10709
- logToolFailure({
10710
- toolName: name,
10711
- durationMs: Date.now() - startTime,
10712
- domain,
10713
- analytics: runtimeOptions?.analytics,
10714
- error: "Tool call timed out",
10715
- args,
10716
- country: runtimeOptions?.country,
10717
- clientType: runtimeOptions?.clientType,
10718
- authTier: runtimeOptions?.authTier,
10719
- keyHash: runtimeOptions?.keyHash
10720
- });
11959
+ logToolFailure({ ...ctx(), error: "Tool call timed out", args });
10721
11960
  return {
10722
11961
  content: [mcpError(`${name} timed out after ${TOOL_CALL_TIMEOUT_MS / 1e3}s. Try a simpler check or retry \u2014 cached partial results make retries faster.`)],
10723
11962
  isError: true
10724
11963
  };
10725
11964
  }
10726
11965
  const message = sanitizeErrorMessage(err, `An unexpected error occurred while running ${name}. Retry the request \u2014 transient DNS failures are common.`);
10727
- logToolFailure({
10728
- toolName: name,
10729
- durationMs: Date.now() - startTime,
10730
- domain,
10731
- analytics: runtimeOptions?.analytics,
10732
- error: err,
10733
- args,
10734
- country: runtimeOptions?.country,
10735
- clientType: runtimeOptions?.clientType,
10736
- authTier: runtimeOptions?.authTier,
10737
- keyHash: runtimeOptions?.keyHash
10738
- });
11966
+ logToolFailure({ ...ctx(), error: err, args });
10739
11967
  return buildToolErrorResult(message);
10740
11968
  }
10741
11969
  }
@@ -11239,7 +12467,7 @@ async function dispatchMcpMethod(options) {
11239
12467
  };
11240
12468
  }
11241
12469
  }
11242
- const sessionId = createSessionOnInitialize ? await createSession(options.sessionStore) : options.existingSessionId;
12470
+ const sessionId = createSessionOnInitialize ? await createSession(options.sessionStore, options.analytics) : options.existingSessionId;
11243
12471
  if (createSessionOnInitialize && sessionId) {
11244
12472
  auditSessionCreated(options.ip, sessionId);
11245
12473
  options.analytics?.emitSessionEvent({
@@ -11852,7 +13080,7 @@ async function executeMcpRequest(options) {
11852
13080
  }
11853
13081
 
11854
13082
  // src/lib/server-version.ts
11855
- var SERVER_VERSION = "2.6.2";
13083
+ var SERVER_VERSION = "2.9.2";
11856
13084
 
11857
13085
  // src/stdio.ts
11858
13086
  function buildNotInitializedError(id) {