@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0

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.
Files changed (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +83 -61
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -7,6 +7,10 @@ import { nonEmptyErrorMessage } from "./error-detail.mjs";
7
7
  import { safeFetch, readTextCapped } from "./safe-fetch.mjs";
8
8
  import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
9
9
  import { classifyMxRecords } from "./dns-mx.mjs";
10
+ import { collectDnsInheritanceChecks } from "./dns-inheritance-review.mjs";
11
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
12
+ import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECK_RECORDS, POSTURE_CHECK_SOURCES } from "../packages/evidence/domain-posture-context.mjs";
13
+ import { canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
10
14
  import { parseSpfRecords, parseDmarcRecords, parseMtaStsDnsRecords, parseMtaStsPolicy, parseTlsRptRecords, parseBimiRecords, parseDkimRecords, } from "./domain-posture-parsers.mjs";
11
15
  import { buildExternalDependencies, expandSpfPolicy, validateDmarcExternalReporting, } from "./domain-posture-analysis.mjs";
12
16
  const DNS_TIMEOUT_MS = 6000;
@@ -88,14 +92,23 @@ function withTimeout(promise, label, timeoutMs = DNS_TIMEOUT_MS, timers = {
88
92
  });
89
93
  }
90
94
  async function resolveDns(label, factory, timeoutMs = DNS_TIMEOUT_MS, timers) {
95
+ const completed = (result) => {
96
+ let observedAt = null;
97
+ try {
98
+ const at = timers?.now?.();
99
+ observedAt = at instanceof Date && Number.isFinite(at.getTime()) ? at.toISOString() : null;
100
+ }
101
+ catch { /* Unknown source time remains explicit. */ }
102
+ return { ...result, observedAt };
103
+ };
91
104
  try {
92
- return { records: await withTimeout(factory(), label, timeoutMs, timers), error: null };
105
+ return completed({ records: await withTimeout(factory(), label, timeoutMs, timers), error: null });
93
106
  }
94
107
  catch (err) {
95
108
  const error = errorRecord(err);
96
109
  if (typeof error.code === 'string' && MISSING_DNS_CODES.has(error.code))
97
- return { records: [], error: null };
98
- return { records: [], error: nonEmptyErrorMessage(err, String(err)) };
110
+ return completed({ records: [], error: null });
111
+ return completed({ records: [], error: nonEmptyErrorMessage(err, String(err)) });
99
112
  }
100
113
  }
101
114
  function check(id, label, status, summary, { detail = '', records = [], remediation = '' } = {}) {
@@ -191,6 +204,11 @@ function dmarcCheck(query, authorizations = []) {
191
204
  ];
192
205
  const external = authorizations.filter((authorization) => authorization.state !== 'self');
193
206
  const unresolvedExternal = external.filter((authorization) => authorization.state !== 'authorized');
207
+ const intendedDestinations = parsed.aggregateDestinations.length + parsed.failureDestinations.length;
208
+ const omittedDestinations = Math.max(0, intendedDestinations - authorizations.length);
209
+ if (omittedDestinations) {
210
+ details.push(`Reporting authorisation coverage: ${authorizations.length} of ${intendedDestinations} destinations reviewed; ${omittedDestinations} not checked within the bounded collection. Omitted destinations are not assumed authorised.`);
211
+ }
194
212
  if (external.length > 0) {
195
213
  details.push(`${external.length} external reporting destination${external.length === 1 ? '' : 's'} checked; `
196
214
  + `${unresolvedExternal.length} could not be authorized.`);
@@ -213,11 +231,13 @@ function dmarcCheck(query, authorizations = []) {
213
231
  remediation: 'Set sp and np to quarantine or reject unless weaker subdomain treatment is intentional.',
214
232
  });
215
233
  }
216
- if (unresolvedExternal.length > 0) {
217
- return check('dmarc', 'DMARC', 'warning', `Enforced at p=${parsed.policy}; external reporting authorization is incomplete`, {
234
+ if (unresolvedExternal.length > 0 || omittedDestinations > 0) {
235
+ return check('dmarc', 'DMARC', 'warning', `Enforced at p=${parsed.policy}; reporting authorisation is incomplete`, {
218
236
  detail: `${details.join(' ')} ${unresolvedExternal.map((authorization) => `${authorization.destination}: ${authorization.state}.`).join(' ')}`,
219
237
  records: parsed.records,
220
- remediation: 'Publish the required external reporting authorisation record or remove the unavailable destination.',
238
+ remediation: omittedDestinations > 0
239
+ ? 'Review the unchecked destinations and confirm any required external reporting authorisation before relying on delivery.'
240
+ : 'Publish the required external reporting authorisation record or remove the unavailable destination.',
221
241
  });
222
242
  }
223
243
  if (!parsed.aggregateReporting) {
@@ -332,7 +352,11 @@ function matchesMtaPattern(host, pattern) {
332
352
  const normalizedPattern = String(pattern || '').toLowerCase().replace(/\.+$/, '');
333
353
  if (normalizedPattern.startsWith('*.')) {
334
354
  const suffix = normalizedPattern.slice(1);
335
- return normalizedHost.endsWith(suffix) && normalizedHost.length > suffix.length;
355
+ if (!normalizedHost.endsWith(suffix))
356
+ return false;
357
+ const label = normalizedHost.slice(0, -suffix.length);
358
+ // MTA-STS permits a wildcard for exactly one leftmost DNS label.
359
+ return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label);
336
360
  }
337
361
  return normalizedHost === normalizedPattern;
338
362
  }
@@ -565,10 +589,11 @@ function defensiveMailProfileCheck(profile, input) {
565
589
  function registrationLockCheck(registry) {
566
590
  if (registry.error)
567
591
  return queryFailureCheck('registration_lock', 'Registration controls', registry.error);
568
- const statuses = registry.statuses.map((status) => status.toLowerCase().replace(/[^a-z]/gu, ''));
592
+ const statuses = canonicalPostureRecords('registration_lock', registry.statuses) ?? [];
569
593
  if (statuses.length === 0) {
570
594
  return check('registration_lock', 'Registration controls', 'info', 'Registry lock state is unavailable', {
571
595
  detail: 'No normalised EPP status was returned. This does not describe registrar account security.',
596
+ records: registry.statuses,
572
597
  });
573
598
  }
574
599
  const transferLocks = statuses.filter((status) => ['clienttransferprohibited', 'servertransferprohibited'].includes(status));
@@ -635,11 +660,36 @@ function buildPostureReport(domain, input) {
635
660
  ...(input.registry && input.nameservers ? [nameserverCheck(input.nameservers, input.registry)] : []),
636
661
  ];
637
662
  const summary = { pass: 0, warning: 0, danger: 0, info: 0 };
638
- for (const item of checks)
663
+ for (const item of checks) {
639
664
  summary[item.status] += 1;
665
+ const source = POSTURE_CHECK_SOURCES[item.id];
666
+ const query = item.id === 'nameservers' ? input.nameservers : item.id === 'mx' ? input.mx : item.id === 'caa' ? input.caa : undefined;
667
+ const registry = item.id === 'registration_lock' ? input.registry : undefined;
668
+ if (!source || (!query && !registry))
669
+ continue;
670
+ const omittedRecords = Math.max(0, item.records.length - MAX_POSTURE_CHECK_RECORDS);
671
+ const records = item.records.slice(0, MAX_POSTURE_CHECK_RECORDS);
672
+ const error = query?.error ?? registry?.error;
673
+ const retained = canonicalPostureRecords(item.id, records);
674
+ const rawRecords = query?.records ?? registry?.statuses ?? [];
675
+ const raw = item.id === 'caa'
676
+ ? canonicalPostureRecords(item.id, rawRecords.map((record) => record && typeof record === 'object' ? caaDisplay(record) : record))
677
+ : canonicalPostureRecords(item.id, rawRecords);
678
+ const complete = !error && omittedRecords === 0 && retained !== null && raw !== null
679
+ && retained.length === raw.length && retained.every((value, index) => value === raw[index])
680
+ && (!registry || (registry.statusesComplete === true && records.length > 0));
681
+ item.sourceContext = {
682
+ version: DOMAIN_POSTURE_COMPARISON_VERSION, source,
683
+ observedAt: normalizeExplicitIsoTimestamp(query?.observedAt ?? registry?.observedAt),
684
+ state: error ? 'unavailable' : complete ? 'complete' : 'partial',
685
+ omittedRecords: complete ? 0 : omittedRecords || null,
686
+ };
687
+ item.records = records;
688
+ }
640
689
  return { domain, summary, checks };
641
690
  }
642
- async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
691
+ async function fetchMtaStsPolicy(domain, fetcher = safeFetch, signal) {
692
+ signal?.throwIfAborted();
643
693
  const controller = new AbortController();
644
694
  const timeout = setTimeout(() => controller.abort(), POLICY_TIMEOUT_MS);
645
695
  try {
@@ -648,7 +698,7 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
648
698
  // following HTTP redirects for policy discovery. A zero-hop safe fetch
649
699
  // also keeps the returned body bound to the required policy host/path.
650
700
  const res = await fetcher(url, {
651
- signal: controller.signal,
701
+ signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
652
702
  headers: whoisleuthRequestHeaders({ Accept: 'text/plain' }),
653
703
  }, 0);
654
704
  if (res.status !== 200) {
@@ -659,11 +709,13 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
659
709
  return { text: '', contentType: res.headers.get('content-type'), error: `Policy endpoint returned HTTP ${res.status}.` };
660
710
  }
661
711
  const body = await readTextCapped(res, MAX_POLICY_BYTES);
712
+ signal?.throwIfAborted();
662
713
  if (body.truncated)
663
714
  return { text: '', contentType: res.headers.get('content-type'), error: `Policy exceeds ${MAX_POLICY_BYTES} bytes.` };
664
715
  return { text: body.text, contentType: res.headers.get('content-type'), error: null };
665
716
  }
666
717
  catch (err) {
718
+ signal?.throwIfAborted();
667
719
  return {
668
720
  text: '',
669
721
  contentType: null,
@@ -676,17 +728,8 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
676
728
  clearTimeout(timeout);
677
729
  }
678
730
  }
679
- async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelectors = [], mailProtectionProfile = 'standard', } = {}, dependencies = {
680
- resolveTxt: (name) => dns.resolveTxt(name),
681
- resolveMx: (name) => dns.resolveMx(name),
682
- resolveNs: (name) => dns.resolveNs(name),
683
- resolveCaa: (name) => dns.resolveCaa(name),
684
- fetchRdapRecord,
685
- fetchMtaStsPolicy: (name) => fetchMtaStsPolicy(name),
686
- now: () => new Date(),
687
- setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
688
- clearTimer: (handle) => clearTimeout(handle),
689
- }) {
731
+ async function collectDomainPosture(domain, { dkimSelectors = [], retiredDkimSelectors = [], mailProtectionProfile = 'standard', includeInheritedDns, signal, }, dependencies) {
732
+ signal?.throwIfAborted();
690
733
  const normalizedDomain = normalizeAuditDomain(domain);
691
734
  if (!normalizedDomain)
692
735
  throw new Error('Invalid domain name for posture audit.');
@@ -717,10 +760,11 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
717
760
  ...await resolveDns(`TXT ${selector}._domainkey.${domain}`, () => dependencies.resolveTxt(`${selector}._domainkey.${domain}`), DNS_TIMEOUT_MS, dependencies),
718
761
  })),
719
762
  ]),
720
- dependencies.fetchRdapRecord('domain', domain).catch((err) => ({
763
+ dependencies.fetchRdapRecord('domain', domain, signal ? { signal } : {}).catch((err) => ({
721
764
  error: nonEmptyErrorMessage(err, String(err)),
722
765
  })),
723
766
  ]);
767
+ signal?.throwIfAborted();
724
768
  const parsedMtaDns = mtaStsDns.error ? null : parseMtaStsDnsRecords(mtaStsDns.records);
725
769
  const enrichmentStartedAt = dependencies.now();
726
770
  if (!(enrichmentStartedAt instanceof Date) || !Number.isFinite(enrichmentStartedAt.getTime())) {
@@ -728,6 +772,7 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
728
772
  }
729
773
  const enrichmentDeadline = enrichmentStartedAt.getTime() + POSTURE_ENRICHMENT_DEADLINE_MS;
730
774
  const resolveEnrichmentTxt = (name) => {
775
+ signal?.throwIfAborted();
731
776
  const observedAt = dependencies.now();
732
777
  if (!(observedAt instanceof Date) || !Number.isFinite(observedAt.getTime())) {
733
778
  return Promise.resolve({ records: [], error: 'The bounded posture-enrichment clock was unavailable.' });
@@ -737,11 +782,21 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
737
782
  return Promise.resolve({ records: [], error: 'The bounded posture-enrichment deadline was reached.' });
738
783
  return resolveDns(`TXT ${name}`, () => dependencies.resolveTxt(name), Math.min(DNS_TIMEOUT_MS, remaining), dependencies);
739
784
  };
740
- const [mtaStsPolicy, spfExpansion, dmarcAuthorizations] = await Promise.all([
741
- parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain) : Promise.resolve(null),
785
+ const enrichment = [
786
+ parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain, signal) : Promise.resolve(null),
742
787
  expandSpfPolicy(domain, spf, resolveEnrichmentTxt),
743
788
  validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt),
744
- ]);
789
+ includeInheritedDns === true
790
+ ? (dependencies.collectDnsInheritanceChecks
791
+ ? dependencies.collectDnsInheritanceChecks(domain, dmarc, nameservers, signal ? { signal } : {})
792
+ : Promise.reject(new TypeError('The explicit DNS review collector is unavailable.')))
793
+ : Promise.resolve([]),
794
+ ];
795
+ // A rejected policy request must not release the operation's capacity while
796
+ // other started enrichment collectors are still settling.
797
+ const [mtaStsPolicy, spfExpansion, dmarcAuthorizations, inheritanceChecks] = await Promise.all(enrichment)
798
+ .finally(() => Promise.allSettled(enrichment));
799
+ signal?.throwIfAborted();
745
800
  const dnssec = !rdap
746
801
  ? { value: null, error: 'RDAP did not return a domain record.' }
747
802
  : 'error' in rdap
@@ -758,6 +813,8 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
758
813
  dsRecordCount: parsedDomain.dsData.length,
759
814
  dsDataTruncated: parsedDomain.dsDataTruncated,
760
815
  error: null,
816
+ observedAt: normalizeExplicitIsoTimestamp(rdap && !('error' in rdap) ? rdap.fetchedAt : null),
817
+ statusesComplete: parsedDomain.statusesTruncated === false && parsedDomain.serverTruncated === false,
761
818
  }
762
819
  : {
763
820
  statuses: [],
@@ -794,6 +851,10 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
794
851
  if (!(checkedAt instanceof Date) || !Number.isFinite(checkedAt.getTime())) {
795
852
  throw new TypeError('Domain-posture completion time must be valid.');
796
853
  }
854
+ for (const item of inheritanceChecks) {
855
+ report.checks.push(item);
856
+ report.summary[item.status] += 1;
857
+ }
797
858
  return {
798
859
  ...report,
799
860
  checkedAt: checkedAt.toISOString(),
@@ -805,4 +866,30 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
805
866
  externalDependencies,
806
867
  };
807
868
  }
869
+ async function checkDomainPosture(domain, options = {}, dependencies) {
870
+ options.signal?.throwIfAborted();
871
+ // Cancelling one audit must never cancel another audit's resolver.
872
+ const resolver = dependencies ? null : new dns.Resolver();
873
+ const cancel = () => resolver?.cancel();
874
+ options.signal?.addEventListener('abort', cancel, { once: true });
875
+ try {
876
+ options.signal?.throwIfAborted();
877
+ return await collectDomainPosture(domain, options, dependencies ?? {
878
+ resolveTxt: name => resolver.resolveTxt(name),
879
+ resolveMx: name => resolver.resolveMx(name),
880
+ resolveNs: name => resolver.resolveNs(name),
881
+ resolveCaa: name => resolver.resolveCaa(name),
882
+ fetchRdapRecord,
883
+ fetchMtaStsPolicy: (name, signal) => fetchMtaStsPolicy(name, safeFetch, signal),
884
+ now: () => new Date(),
885
+ setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
886
+ clearTimer: handle => clearTimeout(handle),
887
+ collectDnsInheritanceChecks,
888
+ });
889
+ }
890
+ finally {
891
+ options.signal?.removeEventListener('abort', cancel);
892
+ cancel();
893
+ }
894
+ }
808
895
  export { normalizeAuditDomain, normalizeDkimSelectors, normalizeMailProtectionProfile, matchesMtaPattern, fetchMtaStsPolicy, buildPostureReport, checkDomainPosture, };