@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

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 (418) hide show
  1. package/NOTICE +8 -2
  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/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -4,7 +4,7 @@
4
4
  // Risk. Published contact and policy values are normalized into a small,
5
5
  // bounded record; the response body itself is discarded after parsing.
6
6
  import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
7
- import { createObservation } from "../packages/evidence/observation.mjs";
7
+ import { createObservation, normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
8
8
  const SECURITY_TXT_VERSION = 1;
9
9
  const SECURITY_TXT_PATH = '/.well-known/security.txt';
10
10
  const SECURITY_TXT_TIMEOUT_MS = 6_000;
@@ -18,7 +18,6 @@ const CONTROL_CHARACTER_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
18
18
  const URI_CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/u;
19
19
  const FIELD_RE = /^([A-Za-z][A-Za-z-]*):[ \t]*(.*)$/u;
20
20
  const LANGUAGE_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/iu;
21
- const RFC3339_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
22
21
  const CONTACT_PROTOCOLS = new Set(['https:', 'mailto:', 'tel:']);
23
22
  const HTTPS_PROTOCOLS = new Set(['https:']);
24
23
  const ENCRYPTION_PROTOCOLS = new Set(['https:', 'dns:', 'openpgp4fpr:']);
@@ -222,8 +221,9 @@ function parseSecurityTxt(text, options = {}) {
222
221
  malformedCount += languages.length - valid.length;
223
222
  }
224
223
  else if (field === 'expires') {
225
- if (RFC3339_RE.test(value) && Number.isFinite(Date.parse(value)))
226
- expires.push(new Date(value).toISOString());
224
+ const expiry = normalizeExplicitIsoTimestamp(value);
225
+ if (expiry)
226
+ expires.push(expiry);
227
227
  else
228
228
  malformedCount += 1;
229
229
  }
@@ -235,15 +235,18 @@ function parseSecurityTxt(text, options = {}) {
235
235
  const boundedPolicies = uniqueBounded(policies);
236
236
  const boundedEncryption = uniqueBounded(encryption);
237
237
  const boundedCanonical = uniqueBounded(canonical);
238
+ const boundedCanonicalComparison = uniqueBounded(canonicalComparison);
239
+ const canonicalComparisonTruncated = valuesTruncated || boundedCanonicalComparison.truncated;
238
240
  const boundedLanguages = uniqueBounded(preferredLanguages);
239
- valuesTruncated ||= [boundedContacts, boundedPolicies, boundedEncryption, boundedCanonical, boundedLanguages]
241
+ valuesTruncated ||= [boundedContacts, boundedPolicies, boundedEncryption, boundedCanonical, boundedCanonicalComparison, boundedLanguages]
240
242
  .some((entry) => entry.truncated);
241
243
  const expiresAt = expires.length === 1 ? expires[0] : null;
242
244
  const finalUrl = options.finalUrl ? normalizedHttpsUrl(options.finalUrl) : null;
243
245
  const finalComparisonUrl = options.finalUrl ? normalizedCanonicalComparisonUrl(options.finalUrl) : null;
244
246
  const canonicalMatches = boundedCanonical.values.length && finalUrl && finalComparisonUrl
245
- ? canonicalComparison.slice(0, MAX_SECURITY_TXT_VALUES)
246
- .some((value) => value === finalComparisonUrl)
247
+ ? boundedCanonicalComparison.values.includes(finalComparisonUrl)
248
+ ? true
249
+ : canonicalComparisonTruncated ? null : false
247
250
  : null;
248
251
  const stale = Boolean(expiresAt && Date.parse(expiresAt) <= (options.now ?? Date.now()));
249
252
  const requiredMalformed = boundedContacts.values.length === 0 || expires.length !== 1;
@@ -1,54 +1,2 @@
1
- // Dependency-free semantic-version validation shared by release tooling,
2
- // runtime identity, and the frontend build. Callers provide a bounded label so
3
- // errors retain their local context without reimplementing SemVer parsing.
4
- export const MAX_SEMANTIC_VERSION_LENGTH = 128;
5
- function fail(label, detail) {
6
- throw new TypeError(`${label} ${detail}`);
7
- }
8
- function validateIdentifierList(value, label, kind, forbidNumericLeadingZero) {
9
- const identifiers = value.split('.');
10
- if (identifiers.some((identifier) => identifier.length === 0)) {
11
- fail(label, `${kind} contains an empty identifier.`);
12
- }
13
- for (const identifier of identifiers) {
14
- if (!/^[0-9A-Za-z-]+$/u.test(identifier)) {
15
- fail(label, `${kind} contains an invalid identifier.`);
16
- }
17
- if (forbidNumericLeadingZero && /^[0-9]+$/u.test(identifier) && identifier.length > 1 && identifier.startsWith('0')) {
18
- fail(label, `${kind} numeric identifiers must not contain leading zeroes.`);
19
- }
20
- }
21
- }
22
- export function normalizeBoundedSemanticVersion(value, label = 'Semantic') {
23
- if (typeof value !== 'string'
24
- || value.length === 0
25
- || value.length > MAX_SEMANTIC_VERSION_LENGTH
26
- || value.trim() !== value) {
27
- fail(label, 'version must be a bounded semantic-version string.');
28
- }
29
- const buildParts = value.split('+');
30
- if (buildParts.length > 2)
31
- fail(label, 'version contains more than one build-metadata separator.');
32
- const [precedence, buildMetadata] = buildParts;
33
- if (!precedence)
34
- fail(label, 'version must include numeric precedence.');
35
- const prereleaseSeparator = precedence.indexOf('-');
36
- const core = prereleaseSeparator === -1 ? precedence : precedence.slice(0, prereleaseSeparator);
37
- const prerelease = prereleaseSeparator === -1 ? undefined : precedence.slice(prereleaseSeparator + 1);
38
- const coreParts = core.split('.');
39
- if (coreParts.length !== 3 || coreParts.some((part) => !/^(?:0|[1-9][0-9]*)$/u.test(part))) {
40
- fail(label, 'version must contain major, minor, and patch numbers without leading zeroes.');
41
- }
42
- if (prerelease !== undefined)
43
- validateIdentifierList(prerelease, label, 'prerelease', true);
44
- if (buildMetadata !== undefined)
45
- validateIdentifierList(buildMetadata, label, 'build metadata', false);
46
- return value;
47
- }
48
- export function normalizeBoundedStableSemanticVersion(value, label = 'Stable semantic') {
49
- const version = normalizeBoundedSemanticVersion(value, label);
50
- if (version.includes('-') || version.includes('+')) {
51
- fail(label, 'version must not include prerelease or build identifiers.');
52
- }
53
- return version;
54
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/semantic-version.mjs";
@@ -376,6 +376,8 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
376
376
  throw new ServiceBindingDnsError('DNS response question does not match the query');
377
377
  }
378
378
  const records = [];
379
+ const aliases = new Map();
380
+ const answerOwners = new Set();
379
381
  let truncated = false;
380
382
  for (let index = 0; index < recordCount; index += 1) {
381
383
  const ownerResult = decodeDnsName(message, offset);
@@ -388,9 +390,18 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
388
390
  offset += 10;
389
391
  const rdataEnd = offset + rdataLength;
390
392
  assertRange(message, offset, rdataLength);
393
+ if (index < answerCount && rrClass === DNS_CLASS_IN && rrType === 5) {
394
+ const alias = decodeDnsName(message, offset);
395
+ if (alias.nextOffset !== rdataEnd || alias.name === '.'
396
+ || (aliases.has(ownerResult.name) && aliases.get(ownerResult.name) !== alias.name)) {
397
+ throw new ServiceBindingDnsError('DNS answer contains an invalid or ambiguous alias');
398
+ }
399
+ aliases.set(ownerResult.name, alias.name);
400
+ }
391
401
  if (index < answerCount
392
402
  && rrType === recordTypeCode(expected.type)
393
403
  && rrClass === DNS_CLASS_IN) {
404
+ answerOwners.add(ownerResult.name);
394
405
  const parsed = parseServiceBindingRecord(message, ownerResult.name, expected.type, ttl, offset, rdataEnd);
395
406
  truncated = parsed.truncated || truncated;
396
407
  if (records.length < MAX_SERVICE_BINDING_RECORDS)
@@ -402,6 +413,22 @@ function parseServiceBindingDnsResponse(messageValue, expected) {
402
413
  }
403
414
  if (offset !== message.length)
404
415
  throw new ServiceBindingDnsError('DNS response has trailing bytes');
416
+ const allowedOwners = new Set();
417
+ let owner = question.name;
418
+ while (true) {
419
+ if (allowedOwners.has(owner))
420
+ throw new ServiceBindingDnsError('DNS answer contains a cyclic alias chain');
421
+ allowedOwners.add(owner);
422
+ const next = aliases.get(owner);
423
+ if (!next)
424
+ break;
425
+ if (answerOwners.has(owner))
426
+ throw new ServiceBindingDnsError('DNS alias owner also contains service-binding records');
427
+ owner = next;
428
+ }
429
+ if ([...answerOwners].some(value => !allowedOwners.has(value))) {
430
+ throw new ServiceBindingDnsError('DNS service-binding answer owner is not bound to the query');
431
+ }
405
432
  const unique = new Map();
406
433
  for (const record of records)
407
434
  unique.set(JSON.stringify(record), record);
@@ -442,6 +469,7 @@ function parseResolverEndpoint(value) {
442
469
  }
443
470
  function defaultTcpExchange(query, resolver, options) {
444
471
  return new Promise((resolve, reject) => {
472
+ options.signal?.throwIfAborted();
445
473
  const socket = createConnection({
446
474
  host: resolver.address,
447
475
  port: resolver.port,
@@ -457,12 +485,19 @@ function defaultTcpExchange(query, resolver, options) {
457
485
  settled = true;
458
486
  if (hardDeadline)
459
487
  clearTimeout(hardDeadline);
488
+ options.signal?.removeEventListener('abort', abort);
460
489
  socket.destroy();
461
490
  if (error)
462
491
  reject(error);
463
492
  else
464
493
  resolve(response);
465
494
  };
495
+ const abort = () => finish(options.signal?.reason ?? new Error('DNS TCP query cancelled'));
496
+ options.signal?.addEventListener('abort', abort, { once: true });
497
+ if (options.signal?.aborted) {
498
+ abort();
499
+ return;
500
+ }
466
501
  hardDeadline = setTimeout(() => finish(new ServiceBindingDnsError('DNS TCP query timed out', 'ETIMEOUT')), options.timeoutMs);
467
502
  hardDeadline.unref();
468
503
  socket.setTimeout(options.timeoutMs, () => finish(new ServiceBindingDnsError('DNS TCP query timed out', 'ETIMEOUT')));
@@ -25,6 +25,7 @@ const MAX_SMTP_RESPONSE_BYTES = 16 * 1024;
25
25
  const MAX_SMTP_REPLY_LINES = 64;
26
26
  const MAX_SMTP_LINE_BYTES = 1_000;
27
27
  const MAX_SMTP_CAPABILITIES = 32;
28
+ const SMTP_CAPABILITY_OMISSION = 'The retained EHLO capability inventory is incomplete; additional names were omitted. STARTTLS detection used the full bounded reply.';
28
29
  const MAX_SMTP_CERTIFICATE_BYTES = 256 * 1024;
29
30
  const SMTP_ENDPOINT_TIMEOUT_MS = 8_000;
30
31
  const MAIL_TRANSPORT_TOTAL_TIMEOUT_MS = 30_000;
@@ -138,19 +139,27 @@ function normalizeSmtpReply(linesValue) {
138
139
  const sha256 = crypto.createHash('sha256').update(`${lines.join('\r\n')}\r\n`, 'ascii').digest('hex');
139
140
  return Object.freeze({ code, lines: Object.freeze(lines), bytes, sha256 });
140
141
  }
141
- function smtpCapabilities(reply) {
142
- if (!reply || reply.code !== 250)
143
- return Object.freeze([]);
142
+ function smtpCapabilityInventory(reply) {
143
+ // The reply has already passed line and byte admission. The display cap must
144
+ // not decide which protocol extensions the endpoint advertised.
145
+ const seen = new Set();
144
146
  const output = [];
145
- for (const line of reply.lines.slice(1)) {
147
+ for (const line of reply?.code === 250 ? reply.lines.slice(1) : []) {
146
148
  const capability = line.slice(4).trim().split(/\s+/u)[0]?.toUpperCase() || '';
147
- if (!/^[A-Z0-9][A-Z0-9-]{0,31}$/u.test(capability) || output.includes(capability))
149
+ if (!/^[A-Z0-9][A-Z0-9-]{0,31}$/u.test(capability) || seen.has(capability))
148
150
  continue;
149
- if (output.length >= MAX_SMTP_CAPABILITIES)
150
- break;
151
- output.push(capability);
151
+ seen.add(capability);
152
+ if (output.length < MAX_SMTP_CAPABILITIES)
153
+ output.push(capability);
152
154
  }
153
- return Object.freeze(output.sort());
155
+ return Object.freeze({
156
+ capabilities: Object.freeze(output.sort()),
157
+ starttlsAdvertised: seen.has('STARTTLS'),
158
+ truncated: seen.size > output.length,
159
+ });
160
+ }
161
+ function smtpCapabilities(reply) {
162
+ return smtpCapabilityInventory(reply).capabilities;
154
163
  }
155
164
  async function runSmtpConversation(hostname, connection) {
156
165
  try {
@@ -162,8 +171,7 @@ async function runSmtpConversation(hostname, connection) {
162
171
  connection.assertIdle();
163
172
  await connection.write(EHLO_COMMAND);
164
173
  const ehlo = await connection.readReply();
165
- const capabilities = smtpCapabilities(ehlo);
166
- const starttlsAdvertised = capabilities.includes('STARTTLS');
174
+ const { capabilities, starttlsAdvertised } = smtpCapabilityInventory(ehlo);
167
175
  if (!starttlsAdvertised) {
168
176
  const diagnostics = connection.diagnostics();
169
177
  return Object.freeze({ connectedAddress: connection.remoteAddress, greeting, ehlo, capabilities, starttlsAdvertised, starttlsReply: null, starttlsState: 'not_advertised', tls: null, ...diagnostics });
@@ -197,12 +205,17 @@ class SocketReplyReader {
197
205
  #endedError = null;
198
206
  bytesRead = 0;
199
207
  lineCount = 0;
208
+ #onData = (chunk) => this.#receive(chunk);
209
+ #onError = (error) => this.#fail(error);
210
+ #onEnd = () => this.#fail(new Error('SMTP server closed the connection before completing a reply.'));
211
+ #onTimeout = () => this.#fail(new Error('SMTP transport timed out.'));
200
212
  constructor(socket) {
201
213
  this.#socket = socket;
202
- socket.on('data', (chunk) => this.#receive(Buffer.from(chunk)));
203
- socket.once('error', (error) => this.#fail(error));
204
- socket.once('end', () => this.#fail(new Error('SMTP server closed the connection before completing a reply.')));
205
- socket.once('timeout', () => this.#fail(new Error('SMTP transport timed out.')));
214
+ socket.on('data', this.#onData);
215
+ socket.once('error', this.#onError);
216
+ socket.once('end', this.#onEnd);
217
+ socket.once('close', this.#onEnd);
218
+ socket.once('timeout', this.#onTimeout);
206
219
  }
207
220
  #receive(chunk) {
208
221
  if (this.#endedError)
@@ -273,10 +286,14 @@ class SocketReplyReader {
273
286
  }
274
287
  release() {
275
288
  this.assertIdle();
276
- this.#socket.removeAllListeners('data');
277
- this.#socket.removeAllListeners('error');
278
- this.#socket.removeAllListeners('end');
279
- this.#socket.removeAllListeners('timeout');
289
+ this.detach();
290
+ }
291
+ detach() {
292
+ this.#socket.removeListener('data', this.#onData);
293
+ this.#socket.removeListener('error', this.#onError);
294
+ this.#socket.removeListener('end', this.#onEnd);
295
+ this.#socket.removeListener('close', this.#onEnd);
296
+ this.#socket.removeListener('timeout', this.#onTimeout);
280
297
  }
281
298
  }
282
299
  function smtpStartTlsOptions(socket, hostname) {
@@ -289,10 +306,18 @@ function smtpStartTlsOptions(socket, hostname) {
289
306
  checkServerIdentity: () => undefined,
290
307
  };
291
308
  }
292
- async function defaultSmtpProbe(options) {
293
- const socket = net.createConnection({ host: options.address, port: SMTP_PORT, family: options.family });
309
+ async function defaultSmtpProbe(options, transport = {
310
+ connect: options => net.createConnection(options),
311
+ startTls: options => tls.connect(options),
312
+ }) {
313
+ options.signal.throwIfAborted();
314
+ const socket = transport.connect({ host: options.address, port: SMTP_PORT, family: options.family });
315
+ let activeSocket = socket;
316
+ let reader = null;
294
317
  socket.setTimeout(options.timeoutMs);
295
- const aborted = () => socket.destroy(new Error('SMTP endpoint review timed out.'));
318
+ // Closing the active phase rejects its pending read/handshake. Do not emit
319
+ // an unhandled error on the raw socket after ownership has moved to TLS.
320
+ const aborted = () => { activeSocket.destroy(); };
296
321
  options.signal.addEventListener('abort', aborted, { once: true });
297
322
  try {
298
323
  await new Promise((resolve, reject) => {
@@ -300,38 +325,48 @@ async function defaultSmtpProbe(options) {
300
325
  socket.removeListener('connect', connected);
301
326
  socket.removeListener('error', failed);
302
327
  socket.removeListener('timeout', timedOut);
328
+ socket.removeListener('close', closed);
303
329
  };
304
330
  const connected = () => { cleanup(); resolve(); };
305
331
  const failed = (error) => { cleanup(); socket.destroy(); reject(error); };
306
332
  const timedOut = () => { cleanup(); socket.destroy(); reject(new Error('SMTP connection timed out.')); };
333
+ const closed = () => failed(new Error('SMTP connection closed before connecting.'));
307
334
  socket.once('connect', connected);
308
335
  socket.once('error', failed);
309
336
  socket.once('timeout', timedOut);
337
+ socket.once('close', closed);
310
338
  if (options.signal.aborted)
311
339
  aborted();
312
340
  });
313
- const reader = new SocketReplyReader(socket);
341
+ const replyReader = new SocketReplyReader(socket);
342
+ reader = replyReader;
314
343
  const connection = {
315
344
  remoteAddress: socket.remoteAddress || null,
316
- readReply: () => reader.readReply(),
317
- assertIdle: () => reader.assertIdle(),
345
+ readReply: () => replyReader.readReply(),
346
+ assertIdle: () => replyReader.assertIdle(),
318
347
  write: (command) => new Promise((resolve, reject) => socket.write(command, (error) => error ? reject(error) : resolve())),
319
348
  startTls: async (hostname) => {
320
- reader.release();
321
- const tlsSocket = tls.connect(smtpStartTlsOptions(socket, hostname));
349
+ replyReader.release();
350
+ const tlsSocket = transport.startTls(smtpStartTlsOptions(socket, hostname));
351
+ activeSocket = tlsSocket;
322
352
  tlsSocket.setTimeout(options.timeoutMs);
323
353
  await new Promise((resolve, reject) => {
324
354
  const cleanup = () => {
325
355
  tlsSocket.removeListener('secureConnect', secured);
326
356
  tlsSocket.removeListener('error', failed);
327
357
  tlsSocket.removeListener('timeout', timedOut);
358
+ tlsSocket.removeListener('close', closed);
328
359
  };
329
360
  const secured = () => { cleanup(); resolve(); };
330
361
  const failed = (error) => { cleanup(); tlsSocket.destroy(); reject(error); };
331
362
  const timedOut = () => { cleanup(); tlsSocket.destroy(); reject(new Error('SMTP STARTTLS handshake timed out.')); };
363
+ const closed = () => failed(new Error('SMTP STARTTLS connection closed before completing the handshake.'));
332
364
  tlsSocket.once('secureConnect', secured);
333
365
  tlsSocket.once('error', failed);
334
366
  tlsSocket.once('timeout', timedOut);
367
+ tlsSocket.once('close', closed);
368
+ if (options.signal.aborted)
369
+ aborted();
335
370
  });
336
371
  const cipher = tlsSocket.getCipher();
337
372
  return Object.freeze({
@@ -343,13 +378,15 @@ async function defaultSmtpProbe(options) {
343
378
  remoteAddress: tlsSocket.remoteAddress || null,
344
379
  });
345
380
  },
346
- diagnostics: () => ({ bytesRead: reader.bytesRead, lineCount: reader.lineCount }),
347
- destroy: () => socket.destroy(),
381
+ diagnostics: () => ({ bytesRead: replyReader.bytesRead, lineCount: replyReader.lineCount }),
382
+ destroy: () => { activeSocket.destroy(); socket.destroy(); },
348
383
  };
349
384
  return await runSmtpConversation(options.hostname, connection);
350
385
  }
351
386
  finally {
352
387
  options.signal.removeEventListener('abort', aborted);
388
+ reader?.detach();
389
+ activeSocket.destroy();
353
390
  socket.destroy();
354
391
  }
355
392
  }
@@ -486,11 +523,12 @@ function addressAuthentication(selected) {
486
523
  })
487
524
  : Object.freeze({ state: 'unavailable', detail: 'No address candidate was retained for cryptographic authentication review.' });
488
525
  }
489
- function endpointLimitations(addressState) {
526
+ function endpointLimitations(addressState, capabilitiesTruncated = false) {
490
527
  const addressLimitation = addressState === 'connected'
491
528
  ? 'The endpoint observation is one point-in-time connection to one revalidated and pinned public address. Other addresses or later observations may differ.'
492
529
  : 'The endpoint did not produce confirmed connection evidence. Its address field retains only the highest completed public selection or revalidation stage, if any.';
493
530
  return Object.freeze([
531
+ ...(capabilitiesTruncated ? [SMTP_CAPABILITY_OMISSION] : []),
494
532
  addressLimitation,
495
533
  'Public-address validation, fresh revalidation, and connection pinning do not cryptographically authenticate the selected A or AAAA RRset or any CNAME chain.',
496
534
  'Only the SMTP greeting, EHLO capability names, and optional STARTTLS negotiation were observed. No authentication, recipient, mailbox, catch-all, relay, or message command was attempted.',
@@ -587,10 +625,12 @@ async function collectEndpoint(options) {
587
625
  const tlsaComplete = !['indeterminate', 'timed_out', 'unavailable'].includes(tlsa.state)
588
626
  && (tlsa.state !== 'validated' || !tlsa.dane || !['partial', 'unavailable', 'invalid'].includes(tlsa.dane.state));
589
627
  const certificateComplete = smtp.starttlsState !== 'negotiated' || certificate.observation.state === 'observed';
628
+ const capabilitiesTruncated = smtpCapabilityInventory(smtp.ehlo).truncated;
590
629
  const complete = smtpState === 'observed'
591
630
  && dnssec.report.completeness === 'complete'
592
631
  && tlsaComplete
593
- && certificateComplete;
632
+ && certificateComplete
633
+ && !capabilitiesTruncated;
594
634
  return Object.freeze({
595
635
  host: options.host,
596
636
  observedAt: options.observedAt,
@@ -604,7 +644,7 @@ async function collectEndpoint(options) {
604
644
  dnssec: dnssec.report,
605
645
  tlsa,
606
646
  failure: null,
607
- limitations: endpointLimitations(addressState),
647
+ limitations: endpointLimitations(addressState, capabilitiesTruncated),
608
648
  });
609
649
  }
610
650
  catch (error) {
@@ -627,7 +667,7 @@ async function collectEndpoint(options) {
627
667
  dnssec: dnssec?.report ?? null,
628
668
  tlsa: unavailableTlsa('unavailable', 'TLSA and DANE comparison was unavailable because endpoint collection did not complete.'),
629
669
  failure: Object.freeze({ stage, detail }),
630
- limitations: endpointLimitations(addressState),
670
+ limitations: endpointLimitations(addressState, smtpCapabilityInventory(connectedSmtp?.ehlo ?? null).truncated),
631
671
  });
632
672
  }
633
673
  }
@@ -748,6 +788,7 @@ async function collectMailTransportReview(inputValue, options, dependencies = {}
748
788
  retries: 0,
749
789
  }),
750
790
  limitations: Object.freeze([
791
+ ...(endpoints.some((endpoint) => endpoint.limitations.includes(SMTP_CAPABILITY_OMISSION)) ? [SMTP_CAPABILITY_OMISSION] : []),
751
792
  'This command performs active network collection only for the selected MX hosts in this explicitly authorised run. It is not invoked by Lookup, Bulk, monitoring, or automatic recipes.',
752
793
  'DNSSEC, TLSA or DANE, PKIX, certificate identity, STARTTLS, SMTP transport, MTA-STS context, and TLS-RPT context retain separate states and provenance. One family never supplies or upgrades another family.',
753
794
  ACTIVE_DANE_TA_LIMITATION,
@@ -784,4 +825,4 @@ function formatMailTransportReview(review) {
784
825
  lines.push(` - ${limitation}`);
785
826
  return `${lines.join('\n')}\n`;
786
827
  }
787
- export { EHLO_COMMAND, MAIL_TRANSPORT_INPUT_SCHEMA, MAIL_TRANSPORT_INPUT_VERSION, MAIL_TRANSPORT_REVIEW_SCHEMA, MAIL_TRANSPORT_REVIEW_VERSION, MAIL_TRANSPORT_TOTAL_TIMEOUT_MS, MAX_MAIL_TRANSPORT_CONCURRENCY, MAX_MAIL_TRANSPORT_ADDRESS_CANDIDATES, MAX_MAIL_TRANSPORT_DNS_ALIASES, MAX_MAIL_TRANSPORT_INPUT_BYTES, MAX_MAIL_TRANSPORT_TARGETS, MAX_SMTP_CAPABILITIES, MAX_SMTP_LINE_BYTES, MAX_SMTP_REPLY_LINES, MAX_SMTP_RESPONSE_BYTES, SMTP_ENDPOINT_TIMEOUT_MS, STARTTLS_COMMAND, collectMailTransportReview, certificateObservation, formatMailTransportReview, normalizeSmtpReply, parseMailTransportInput, resolveSelectedHost, runSmtpConversation, smtpStartTlsOptions, smtpCapabilities, };
828
+ export { EHLO_COMMAND, MAIL_TRANSPORT_INPUT_SCHEMA, MAIL_TRANSPORT_INPUT_VERSION, MAIL_TRANSPORT_REVIEW_SCHEMA, MAIL_TRANSPORT_REVIEW_VERSION, MAIL_TRANSPORT_TOTAL_TIMEOUT_MS, MAX_MAIL_TRANSPORT_CONCURRENCY, MAX_MAIL_TRANSPORT_ADDRESS_CANDIDATES, MAX_MAIL_TRANSPORT_DNS_ALIASES, MAX_MAIL_TRANSPORT_INPUT_BYTES, MAX_MAIL_TRANSPORT_TARGETS, MAX_SMTP_CAPABILITIES, MAX_SMTP_LINE_BYTES, MAX_SMTP_REPLY_LINES, MAX_SMTP_RESPONSE_BYTES, SMTP_ENDPOINT_TIMEOUT_MS, STARTTLS_COMMAND, collectMailTransportReview, certificateObservation, formatMailTransportReview, normalizeSmtpReply, parseMailTransportInput, resolveSelectedHost, runSmtpConversation, smtpStartTlsOptions, smtpCapabilities, SocketReplyReader, defaultSmtpProbe, };