@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
@@ -1,12 +1,21 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { scanBoundedJson, } from "../lib/bounded-json.mjs";
3
- import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
4
- import { DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS, } from "../lib/domain-control-flight-recorder.mjs";
2
+ import { latestObservationCohort } from "../packages/evidence/latest-observations.mjs";
3
+ import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDomainControlRecords, canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
4
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
+ import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
6
+ import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
7
+ import { scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
8
+ import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
9
+ import { MAX_FLIGHT_RECORDER_VALUES, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
5
10
  import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
6
11
  import { CliUsageError } from "./errors.mjs";
7
12
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
8
13
  export { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, };
9
14
  const CLI_REVIEW_INPUT_KEY_SET = new Set(CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
15
+ export const DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS = Object.freeze({
16
+ registry_nameservers: 'nameservers', delegation_ds: 'ds', mail_exchangers: 'mx', caa_policy: 'caa',
17
+ tls_certificate: 'tlsIssuer', tls_public_key: 'tlsSpkiSha256', registrar_lock: 'registrarLock',
18
+ });
10
19
  function record(value) {
11
20
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
12
21
  }
@@ -21,116 +30,147 @@ function state(value) {
21
30
  return 'unavailable';
22
31
  }
23
32
  function text(value, maximum = 500) {
24
- if (typeof value !== 'string')
33
+ if (typeof value !== 'string' || value.length > maximum || /[\u0000-\u001f\u007f]/u.test(value))
25
34
  return null;
26
- const normalized = value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum);
35
+ const normalized = value.trim();
27
36
  return normalized || null;
28
37
  }
29
- function list(value, normalizer = (item) => text(item)) {
30
- if (!Array.isArray(value))
31
- return [];
32
- return [...new Set(value.slice(0, 128).flatMap((item) => {
33
- const normalized = normalizer(item);
34
- return normalized ? [normalized.toLowerCase()] : [];
35
- }))].sort().slice(0, 32);
36
- }
37
- function hostname(value) {
38
- return text(value, 253)?.toLowerCase().replace(/\.$/u, '') ?? null;
39
- }
40
- function mxValue(value) {
41
- const item = record(value);
42
- const exchange = hostname(item.exchange ?? item.host ?? item.value);
43
- if (!exchange)
44
- return text(value);
45
- const priority = Number.isSafeInteger(item.priority) && Number(item.priority) >= 0 ? Number(item.priority) : null;
46
- return priority === null ? exchange : `${priority} ${exchange}`;
47
- }
48
- function caaValue(value) {
49
- const item = record(value);
50
- const tag = text(item.tag, 32)?.toLowerCase();
51
- const recordValue = text(item.value, 500)?.toLowerCase();
52
- if (!tag || !recordValue)
53
- return text(value);
54
- const critical = Number.isSafeInteger(item.critical) ? Number(item.critical) : Number.isSafeInteger(item.flags) ? Number(item.flags) : 0;
55
- return `${critical} ${tag} ${recordValue}`;
56
- }
57
- function dsValue(value) {
58
- const item = record(value);
59
- const keyTag = Number.isSafeInteger(item.keyTag) ? item.keyTag : null;
60
- const algorithm = Number.isSafeInteger(item.algorithm) ? item.algorithm : null;
61
- const digestType = Number.isSafeInteger(item.digestType) ? item.digestType : null;
62
- const digest = text(item.digest, 512)?.toLowerCase();
63
- return keyTag !== null && algorithm !== null && digestType !== null && digest
64
- ? `${keyTag} ${algorithm} ${digestType} ${digest}`
65
- : text(value);
66
- }
67
- function field(id, source, sourceState, values) {
68
- return Object.freeze({ id, source, state: sourceState, values: Object.freeze([...values].sort()) });
38
+ function sourceClock(...values) {
39
+ const declared = values.filter((value) => value !== undefined && value !== null);
40
+ const times = declared.map(normalizeExplicitIsoTimestamp);
41
+ return times.length && times.every((time) => time !== null && time === times[0]) ? times[0] : null;
42
+ }
43
+ function sourceState(value) {
44
+ const result = state(value.status);
45
+ return result === 'observed' && (value.complete === false || value.truncated === true) ? 'partial' : result;
46
+ }
47
+ function dnsRecordState(dns, name) {
48
+ const query = record(record(dns.diagnostics)[name]);
49
+ if (!Object.keys(query).length)
50
+ return sourceState(dns);
51
+ const values = record(dns.records)[name];
52
+ const contradictoryAbsence = query.status === 'not_found' && Array.isArray(values) && values.length > 0;
53
+ if ((query.status === 'success' || query.status === 'not_found') && query.truncated === false && query.discarded === 0
54
+ && !contradictoryAbsence && (query.error === undefined || query.error === null))
55
+ return 'observed';
56
+ return state(query.status) === 'observed' || query.status === 'not_found' ? 'partial' : state(query.status);
57
+ }
58
+ function field(id, source, sourceState, values, observedAt) {
59
+ return Object.freeze({ id, source, state: sourceState, values: Object.freeze([...values].sort()), observedAt });
60
+ }
61
+ function recordField(id, source, sourceState, input, normalize, observedAt) {
62
+ const rows = Array.isArray(input) ? input.slice(0, MAX_DOMAIN_CONTROL_INPUT_RECORDS) : [];
63
+ const values = new Set();
64
+ let invalid = Array.isArray(input) || (input === undefined && sourceState !== 'observed') ? 0 : 1;
65
+ for (const row of rows) {
66
+ let value = '';
67
+ try {
68
+ value = normalize(row);
69
+ }
70
+ catch (error) {
71
+ if (!(error instanceof TypeError))
72
+ throw error;
73
+ }
74
+ if (value)
75
+ values.add(value);
76
+ else
77
+ invalid += 1;
78
+ }
79
+ const sorted = [...values].sort();
80
+ const omitted = Math.max(0, (Array.isArray(input) ? input.length : 0) - rows.length)
81
+ + Math.max(0, sorted.length - MAX_FLIGHT_RECORDER_VALUES);
82
+ return field(id, invalid || omitted ? `${source} (${invalid} invalid; ${omitted} omitted records)` : source, sourceState === 'observed' && (invalid || omitted) ? 'partial' : sourceState, sorted.slice(0, MAX_FLIGHT_RECORDER_VALUES), observedAt);
69
83
  }
70
84
  function diagnostic(document, key) {
71
- return state(record(document.diagnostics)[key] && record(record(document.diagnostics)[key]).status);
85
+ return sourceState(record(record(document.diagnostics)[key]));
86
+ }
87
+ function registryClock(document, key) {
88
+ const detail = record(record(document.diagnostics)[key]);
89
+ const publication = record(document[key]);
90
+ const named = key === 'rdap' ? 'fetchedAt' : 'queriedAt';
91
+ return sourceClock(detail.observedAt, detail[named], publication.observedAt, publication[named]);
72
92
  }
73
93
  function registryParsed(document) {
74
94
  const rdapState = diagnostic(document, 'rdap');
75
95
  const whoisState = diagnostic(document, 'whois');
76
96
  const rdap = record(record(document.rdap).parsed);
77
97
  if (rdapState === 'observed' && Object.keys(rdap).length)
78
- return { parsed: rdap, source: 'Registry RDAP', sourceState: rdapState };
98
+ return { parsed: rdap, source: 'Registry RDAP', sourceState: rdapState, observedAt: registryClock(document, 'rdap') };
79
99
  const whois = record(record(document.whois).parsed);
80
- return { parsed: whois, source: 'WHOIS', sourceState: whoisState };
100
+ return { parsed: whois, source: 'WHOIS', sourceState: whoisState, observedAt: registryClock(document, 'whois') };
81
101
  }
82
102
  function lockValue(parsed) {
83
- const statuses = list(parsed.statuses ?? parsed.status, (item) => text(item, 120)?.replace(/[\s_-]+/gu, '').toLowerCase() ?? null);
84
- return [statuses.some((item) => item.includes('transferprohibited')) ? 'required' : 'not_required'];
103
+ const input = parsed.statuses ?? parsed.status;
104
+ const statuses = Array.isArray(input) ? canonicalPostureRecords('registration_lock', input) : null;
105
+ return statuses ? postureTransferRestriction(statuses) : null;
85
106
  }
86
107
  function pageIdentityValue(availability) {
87
108
  const identity = record(availability.pageIdentity);
88
109
  const components = [
89
- text(identity.bodySha256 ?? availability.pageBodySha256, 64),
90
- text(identity.faviconHash ?? availability.faviconHash, 128),
91
- text(identity.title ?? availability.pageTitle, 240),
110
+ text(identity.bodySha256, 64),
111
+ text(identity.faviconHash, 128),
112
+ text(identity.title, 240),
92
113
  ].filter((item) => Boolean(item));
93
- return components.length ? [components.join(' | ').toLowerCase()] : [];
114
+ return components.length ? [components.join(' | ')] : [];
94
115
  }
95
116
  export function domainControlObservationFromSavedLookup(document) {
96
117
  const availability = record(document.availability);
97
118
  const dns = record(availability.dns);
98
119
  const dnsRecords = record(dns.records);
120
+ const dnsAt = sourceClock(dns.observedAt, dns.checkedAt);
99
121
  const delegation = record(dns.delegation);
100
122
  const delegationRecords = record(delegation.records);
101
123
  const tls = record(availability.tls);
102
- const tlsState = state(tls.status);
124
+ const tlsState = sourceState(tls);
125
+ const tlsAt = sourceClock(tls.observedAt, tls.checkedAt);
103
126
  const certificate = record(tls.certificate);
104
127
  const publicKey = record(certificate.publicKey);
105
128
  const http = record(availability.http);
106
- const httpState = state(http.status);
107
129
  const registry = registryParsed(document);
108
130
  const rdapParsed = record(record(document.rdap).parsed);
109
131
  const rdapState = diagnostic(document, 'rdap');
110
- const dnsState = state(dns.status);
132
+ const rdapAt = registryClock(document, 'rdap');
133
+ const scalar = (id, source, status, value, observedAt, maximum = 500) => recordField(id, source, status, [value], (item) => text(item, maximum) ?? '', observedAt);
134
+ const hostname = (value) => canonicalDomainControlRecords([value], 'nameservers')[0] ?? '';
135
+ const identity = record(availability.pageIdentity);
136
+ const identityValues = pageIdentityValue(availability);
111
137
  const fields = [
112
- field('registrar', registry.source, registry.sourceState, list([record(registry.parsed.registrar).name ?? registry.parsed.registrar], (item) => text(item, 300))),
113
- field('registrar_lock', registry.source, registry.sourceState, registry.sourceState === 'observed' ? lockValue(registry.parsed) : []),
114
- field('registry_dnssec', 'Registry RDAP', rdapState, list([rdapParsed.dnssec], (item) => text(item, 80))),
115
- field('registry_nameservers', 'Registry RDAP', rdapState, list(rdapParsed.nameservers, hostname)),
116
- field('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), list(record(record(document.whois).parsed).nameservers, hostname)),
117
- field('delegated_nameservers', 'DNS', dnsState, list(dnsRecords.ns, hostname)),
118
- field('delegation_ds', 'DNS delegation', state(delegation.status), list(delegationRecords.ds, dsValue)),
119
- field('mail_exchangers', 'DNS', dnsState, list(dnsRecords.mx, mxValue)),
120
- field('caa_policy', 'DNS', dnsState, list(record(dns.caaPolicy).records ?? dnsRecords.caa, caaValue)),
121
- field('tls_certificate', 'TLS', tlsState, list([certificate.fingerprintSha256 ?? tls.fingerprintSha256], (item) => text(item, 128))),
122
- field('tls_public_key', 'TLS', tlsState, list([publicKey.fingerprintSha256 ?? tls.spkiSha256], (item) => text(item, 128))),
123
- field('http_origin', 'HTTP', httpState, list([http.finalOrigin ?? availability.httpFinalOrigin], (item) => text(item, 500))),
124
- field('page_identity', 'Static page identity', state(record(availability.pageIdentity).status ?? availability.pageIdentityStatus ?? http.status), pageIdentityValue(availability)),
138
+ scalar('registrar', registry.source, registry.sourceState, record(registry.parsed.registrar).name ?? registry.parsed.registrar, registry.observedAt, 300),
139
+ scalar('registrar_lock', registry.source, registry.sourceState, lockValue(registry.parsed), registry.observedAt, 20),
140
+ scalar('registry_dnssec', 'Registry RDAP', rdapState, rdapParsed.dnssec, rdapAt, 80),
141
+ recordField('registry_nameservers', 'Registry RDAP', rdapState, rdapParsed.nameservers, hostname, rdapAt),
142
+ recordField('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), record(record(document.whois).parsed).nameservers, hostname, registryClock(document, 'whois')),
143
+ recordField('delegated_nameservers', 'DNS NS', dnsRecordState(dns, 'ns'), dnsRecords.ns, hostname, dnsAt),
144
+ recordField('delegation_ds', 'DNS delegation', Object.hasOwn(delegationRecords, 'ds') ? sourceState(delegation) : 'unsupported', delegationRecords.ds, canonicalDsRecord, sourceClock(delegation.observedAt)),
145
+ recordField('mail_exchangers', 'DNS MX', dnsRecordState(dns, 'mx'), dnsRecords.mx, canonicalMxRecord, dnsAt),
146
+ recordField('caa_policy', 'DNS CAA', dnsRecordState(dns, 'caa'), dnsRecords.caa, canonicalCaaRecord, dnsAt),
147
+ scalar('tls_certificate', 'TLS', tlsState, certificate.fingerprintSha256 ?? tls.fingerprintSha256, tlsAt, 128),
148
+ scalar('tls_public_key', 'TLS', tlsState, publicKey.fingerprintSha256 ?? tls.spkiSha256, tlsAt, 128),
149
+ scalar('http_origin', 'HTTP', sourceState(http), http.finalOrigin, sourceClock(http.observedAt, http.checkedAt)),
150
+ field('page_identity', 'Static page identity', sourceState(identity) === 'observed' && !identityValues.length ? 'partial' : sourceState(identity), identityValues, sourceClock(identity.observedAt)),
125
151
  ];
126
152
  return Object.freeze({
127
- domain: document.registrableDomain,
128
- observedAt: document.generatedAt,
129
- collectionDepth: document.mode,
130
- fields: Object.freeze(fields.filter((item) => DOMAIN_CONTROL_FLIGHT_RECORDER_FIELDS.includes(item.id))),
153
+ domain: document.registrableDomain, capturedAt: document.generatedAt, collectionDepth: document.mode,
154
+ fields: Object.freeze(fields.map((item) => item.state === 'observed' && item.observedAt !== null
155
+ && Date.parse(item.observedAt) > Date.parse(document.generatedAt) ? Object.freeze({ ...item, state: 'partial' }) : item)),
156
+ });
157
+ }
158
+ function reviewFields(document, observation) {
159
+ return observation.fields.map((candidate) => {
160
+ if (candidate.id !== 'tls_certificate')
161
+ return candidate;
162
+ const issuer = record(record(record(document.availability).tls).certificate).issuer;
163
+ const value = record(issuer);
164
+ const commonNames = Array.isArray(value.commonNames) ? value.commonNames : [];
165
+ const organisationNames = Array.isArray(value.organizationNames) ? value.organizationNames : [];
166
+ return recordField(candidate.id, candidate.source, candidate.state, [commonNames[0] ?? organisationNames[0] ?? issuer], (item) => text(item, 300) ?? '', candidate.observedAt);
131
167
  });
132
168
  }
133
169
  export function buildCliDomainControlReview(inputText, generatedAt = new Date().toISOString()) {
170
+ const reviewTime = normalizeExplicitIsoTimestamp(generatedAt);
171
+ if (!reviewTime)
172
+ throw new CliUsageError('Domain-control review time must include a valid explicit timezone.');
173
+ generatedAt = reviewTime;
134
174
  if (Buffer.byteLength(inputText, 'utf8') > MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES) {
135
175
  throw new CliUsageError(`Domain-control review input is limited to ${MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES} bytes.`);
136
176
  }
@@ -161,7 +201,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
161
201
  }
162
202
  const input = record(parsed);
163
203
  if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
164
- || input.version !== CLI_DOMAIN_CONTROL_REVIEW_VERSION
204
+ || !SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS.some((version) => version === input.version)
165
205
  || !Array.isArray(input.lookups)) {
166
206
  throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${CLI_DOMAIN_CONTROL_REVIEW_VERSION}.`);
167
207
  }
@@ -173,42 +213,49 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
173
213
  throw new CliUsageError(`Domain-control review requires from ${MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS} to ${MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS} saved Lookup documents.`);
174
214
  }
175
215
  const manifest = verifyDomainControlManifest(input.manifest);
216
+ if (input.version === PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION && manifest.version !== PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION) {
217
+ throw new CliUsageError(`Public domain-control review input version ${PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION} requires manifest version ${PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION}.`);
218
+ }
176
219
  const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
177
- const latest = new Map();
220
+ const byDomain = new Map();
178
221
  for (const lookup of lookups) {
179
- const current = latest.get(lookup.registrableDomain);
180
- if (!current || Date.parse(lookup.generatedAt) > Date.parse(current.generatedAt))
181
- latest.set(lookup.registrableDomain, lookup);
222
+ const group = byDomain.get(lookup.registrableDomain) ?? [];
223
+ group.push(lookup);
224
+ byDomain.set(lookup.registrableDomain, group);
182
225
  }
183
- const observations = [...latest.values()].map(domainControlObservationFromSavedLookup);
226
+ let ignoredHistoricalLookups = 0;
227
+ const selected = [...byDomain.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([domain, documents]) => {
228
+ const cohort = latestObservationCohort(documents, (document) => document.generatedAt);
229
+ ignoredHistoricalLookups += cohort.superseded;
230
+ const captures = cohort.latest.map((document) => ({ document, observation: domainControlObservationFromSavedLookup(document) }));
231
+ const first = captures[0];
232
+ if (!first || !cohort.observedAt || cohort.undated.length)
233
+ throw new CliUsageError('Saved Lookup observation times could not be ordered.');
234
+ const mergedFields = mergeConcurrentDomainControlFields(captures.map((capture) => capture.observation.fields));
235
+ const futureCapture = Date.parse(cohort.observedAt) > Date.parse(generatedAt);
236
+ const observation = Object.freeze({
237
+ domain,
238
+ capturedAt: cohort.observedAt,
239
+ collectionDepth: captures.every((capture) => capture.observation.collectionDepth === first.observation.collectionDepth)
240
+ ? first.observation.collectionDepth : 'unknown',
241
+ fields: futureCapture ? mergedFields.map((field) => ({ ...field, state: 'partial' })) : mergedFields,
242
+ });
243
+ const fields = mergeConcurrentDomainControlFields(captures.map((capture) => reviewFields(capture.document, capture.observation)));
244
+ return { observation, reviewFields: futureCapture || observation.collectionDepth === 'unknown'
245
+ ? fields.map((field) => ({ ...field, state: 'partial' })) : fields };
246
+ });
247
+ const observations = selected.map((item) => item.observation);
184
248
  const review = reviewDomainControlManifest({
185
249
  schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
186
250
  version: DOMAIN_CONTROL_REVIEW_VERSION,
187
251
  manifest,
188
- observations: observations.map((item) => ({
189
- domain: item.domain,
190
- fields: Object.fromEntries(item.fields.flatMap((candidate) => {
191
- const mapping = {
192
- registry_nameservers: 'nameservers',
193
- delegation_ds: 'ds',
194
- mail_exchangers: 'mx',
195
- caa_policy: 'caa',
196
- tls_certificate: 'tlsIssuer',
197
- tls_public_key: 'tlsSpkiSha256',
198
- registrar_lock: 'registrarLock',
199
- };
200
- const target = mapping[candidate.id];
252
+ observations: selected.map(({ observation, reviewFields: fields }) => ({
253
+ domain: observation.domain,
254
+ fields: Object.fromEntries(fields.flatMap((candidate) => {
255
+ const target = DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS[candidate.id];
201
256
  if (!target)
202
257
  return [];
203
- const candidateValues = candidate.id === 'tls_certificate'
204
- ? list([record(record(record(latest.get(item.domain)?.availability).tls).certificate).issuer], (value) => {
205
- const issuer = record(value);
206
- const commonNames = Array.isArray(issuer.commonNames) ? issuer.commonNames : [];
207
- const organisationNames = Array.isArray(issuer.organizationNames) ? issuer.organizationNames : [];
208
- return text(commonNames[0] ?? organisationNames[0] ?? value, 300);
209
- })
210
- : candidate.values;
211
- return [[target, { state: candidate.state, values: candidateValues, source: candidate.source, observedAt: item.observedAt }]];
258
+ return [[target, { state: candidate.state, values: candidate.values, source: candidate.source, observedAt: candidate.observedAt }]];
212
259
  })),
213
260
  })),
214
261
  }, generatedAt);
@@ -221,7 +268,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
221
268
  input: Object.freeze({
222
269
  lookupsReceived: lookups.length,
223
270
  latestDomainObservations: observations.length,
224
- ignoredHistoricalLookups: lookups.length - observations.length,
271
+ ignoredHistoricalLookups,
225
272
  }),
226
273
  limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
227
274
  });
@@ -242,6 +289,7 @@ export function formatCliDomainControlReview(document) {
242
289
  `Domains ${review.domains.length}`,
243
290
  `Lookups ${document.input.lookupsReceived}`,
244
291
  `Drift ${review.counts.drift ?? 0}`,
292
+ `Due ${review.counts.due ?? 0}`,
245
293
  `Incomplete ${(review.counts.partial ?? 0) + (review.counts.unavailable ?? 0) + (review.counts.unsupported ?? 0)}`,
246
294
  '',
247
295
  ...review.domains.map((item) => `${item.domain} ${item.state}`),
package/cli/errors.mjs CHANGED
@@ -1,10 +1,14 @@
1
+ import { stripVTControlCharacters } from 'node:util';
2
+ import { hasUnsafeCliText } from "../packages/contracts/cli-grammar.mjs";
1
3
  const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
2
- const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
4
+ const MAX_CLI_DIAGNOSTIC_BYTES = 4_096;
3
5
  const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
4
6
  class CliUsageError extends Error {
5
- constructor(message) {
7
+ reason;
8
+ constructor(message, reason = 'invalid_input') {
6
9
  super(message);
7
10
  this.name = 'CliUsageError';
11
+ this.reason = reason;
8
12
  }
9
13
  }
10
14
  function errorMessage(error) {
@@ -20,13 +24,40 @@ function boundedCliErrorMessage(error, fallback = 'Unexpected command failure')
20
24
  .trim()
21
25
  .slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
22
26
  }
23
- function hasUnsafeCliText(value) {
24
- return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
27
+ // A transient stderr preview, never a checkpoint field. Bound before collecting
28
+ // and redact request URLs and credential-shaped fields before shortening text.
29
+ function createCliDiagnosticOutput() {
30
+ const chunks = [];
31
+ let remaining = MAX_CLI_DIAGNOSTIC_BYTES;
32
+ return Object.freeze({
33
+ stream: {
34
+ isTTY: false,
35
+ columns: 80,
36
+ write(chunk) {
37
+ if (remaining > 0 && chunk.length > 0) {
38
+ const bytes = Buffer.from(chunk.slice(0, remaining), 'utf8').subarray(0, remaining);
39
+ chunks.push(bytes.toString('utf8'));
40
+ remaining -= bytes.byteLength;
41
+ }
42
+ return true;
43
+ },
44
+ },
45
+ value() {
46
+ const source = stripVTControlCharacters(chunks.join('')).replace(CLI_DEFAULT_IGNORABLE_GLOBAL_RE, '');
47
+ if (!source.trim())
48
+ return '';
49
+ const redacted = source
50
+ .replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']*/giu, '[URL omitted]')
51
+ .replace(/\b(authorization|cookie|password|passphrase|token|secret|api[-_ ]?key)\s*[:=]\s*(?:"[^"]*(?:"|$)|'[^']*(?:'|$)|[^\r\n,;]*)/giu, '$1=[omitted]')
52
+ .replace(/\b(?:bearer|basic)\s+[^\s,;]+/giu, '[credential omitted]');
53
+ return boundedCliErrorMessage(redacted);
54
+ },
55
+ });
25
56
  }
26
57
  function boundedCliInputError(error, label) {
27
58
  if (error instanceof CliUsageError)
28
59
  return error;
29
60
  const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
30
- return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`);
61
+ return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`, 'input_unavailable');
31
62
  }
32
- export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, hasUnsafeCliText, };
63
+ export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, createCliDiagnosticOutput, hasUnsafeCliText, };
@@ -0,0 +1,46 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
4
+ import { terminalOptions } from "./argument-values.mjs";
5
+ function parseInspectArchiveArguments(parsed) {
6
+ const expectedContentDigest = parsed.optionValue('--expect-content-digest');
7
+ try {
8
+ parseArchiveContentDigest(expectedContentDigest);
9
+ }
10
+ catch {
11
+ throw new CliUsageError('--expect-content-digest requires sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
12
+ }
13
+ return {
14
+ action: 'inspect-archive',
15
+ source: parsed.positionalValue('source'),
16
+ passphraseSource: parsed.optionValue('--passphrase-file'),
17
+ search: parsed.optionValue('--search'),
18
+ reveal: parsed.hasOption('--reveal'),
19
+ requireMatch: parsed.hasOption('--require-match'),
20
+ expectedContentDigest,
21
+ output: parsed.hasOption('--json') ? 'json' : 'terminal',
22
+ ...terminalOptions(parsed),
23
+ };
24
+ }
25
+ function parseSignArtifactArguments(parsed) {
26
+ return {
27
+ action: 'sign-artifact',
28
+ source: parsed.positionalValue('source'),
29
+ privateKeySource: parsed.optionValue('--private-key-file'),
30
+ };
31
+ }
32
+ function parseVerifySignatureArguments(parsed) {
33
+ return {
34
+ action: 'verify-signature',
35
+ source: parsed.positionalValue('source'),
36
+ publicKeySource: parsed.optionValue('--public-key-file'),
37
+ trustStoreSource: parsed.optionValue('--trust-store-file'),
38
+ output: parsed.hasOption('--json') ? 'json' : 'terminal',
39
+ ...terminalOptions(parsed),
40
+ };
41
+ }
42
+ export const EVIDENCE_ARGUMENT_PARSERS = Object.freeze({
43
+ 'inspect-archive': parseInspectArchiveArguments,
44
+ 'sign-artifact': parseSignArtifactArguments,
45
+ 'verify-signature': parseVerifySignatureArguments,
46
+ });
@@ -0,0 +1,69 @@
1
+ import { WORKSPACE_ARCHIVE_VERSION } from "../packages/contracts/case-portability.mjs";
2
+ import { LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION, LEGACY_WORKSPACE_ARCHIVE_SCOPE, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
3
+ export const EVIDENCE_COMMAND_DEFINITIONS = Object.freeze({
4
+ "inspect-archive": commandSeed({
5
+ reference: {
6
+ description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
7
+ example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
8
+ boundary: 'Exact values require --reveal. New content comparisons use the reported sorted-json-v2:sha256 identity with --expect-content-digest; bare sha256 hashes retain their legacy locale-sensitive meaning. Retired and future archives are rejected. The archive is read locally and is never uploaded.',
9
+ },
10
+ collection: { scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
11
+ summary: 'Inspect an archive locally',
12
+ options: ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
13
+ positionals: OPTIONAL_FILE_POSITIONAL,
14
+ constraints: Object.freeze([
15
+ constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
16
+ constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
17
+ ]),
18
+ handlerOwner: 'evidence',
19
+ networkEffect: 'offline',
20
+ common: false,
21
+ schemaIdentifiers: Object.freeze(['whoisleuth.workspace-archive-inspection']),
22
+ primaryArtefacts: Object.freeze([]),
23
+ planSupport: false,
24
+ additionalOutputFormats: Object.freeze([]),
25
+ bootstrapProfile: 'allowed',
26
+ }),
27
+ "sign-artifact": commandSeed({
28
+ reference: {
29
+ description: 'Sign one reviewed response packet or supported manifest with a local private key.',
30
+ example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
31
+ boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
32
+ },
33
+ collection: { scope: 'Reads one selected artefact and one local private key without transmitting either.' },
34
+ summary: 'Sign a reviewed artefact locally',
35
+ options: ['--private-key-file'],
36
+ positionals: OPTIONAL_FILE_POSITIONAL,
37
+ constraints: Object.freeze([
38
+ constraint({ kind: 'required', options: ['--private-key-file'] }),
39
+ ]),
40
+ handlerOwner: 'evidence',
41
+ networkEffect: 'offline',
42
+ common: false,
43
+ schemaIdentifiers: Object.freeze(['whoisleuth.signed-evidence-package']),
44
+ primaryArtefacts: Object.freeze([]),
45
+ planSupport: false,
46
+ additionalOutputFormats: Object.freeze([]),
47
+ bootstrapProfile: 'allowed',
48
+ }),
49
+ "verify-signature": commandSeed({
50
+ reference: {
51
+ description: 'Verify one signed package and report embedded-artefact assurance separately. --trust-store-file also checks an explicit local fingerprint policy and emits a signer-trust report; unknown, retired, revoked or future-reviewed entries exit 4.',
52
+ example: 'whoisleuth verify-signature packet.signed.json --trust-store-file trust.json --json',
53
+ boundary: 'A valid signature proves package consistency for the embedded key, not identity, authority or evidence accuracy. With --trust-store-file, unknown, retired, revoked or future-reviewed entries exit 4 even if --public-key-file matches. Replacement fingerprints need their own trusted entry; no signing date overrides current revocation.',
54
+ },
55
+ collection: { scope: 'Reads one selected signed package, optional local public key and explicit fingerprint trust file. No automatic trust discovery or network requests.' },
56
+ summary: 'Verify a signed evidence package',
57
+ options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
58
+ positionals: OPTIONAL_FILE_POSITIONAL,
59
+ constraints: EMPTY_CONSTRAINTS,
60
+ handlerOwner: 'evidence',
61
+ networkEffect: 'offline',
62
+ common: false,
63
+ schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification', 'whoisleuth.evidence-signer-trust-store', 'whoisleuth.evidence-signer-trust-report']),
64
+ primaryArtefacts: Object.freeze([]),
65
+ planSupport: false,
66
+ additionalOutputFormats: Object.freeze([]),
67
+ bootstrapProfile: 'allowed',
68
+ }),
69
+ });
@@ -1,15 +1,15 @@
1
1
  import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, } from "./artifact-verify.mjs";
2
2
  import { formatArchiveInspection, inspectWorkspaceArchive, } from "./archive-inspect.mjs";
3
3
  import { MAX_SIGNING_KEY_FILE_BYTES, formatEvidenceSignatureVerification, signEvidencePackage, verifyEvidencePackageSignature, } from "./evidence-signing.mjs";
4
+ import { commandHasOwner } from "./command-reference.mjs";
4
5
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
5
6
  import EXIT_CODES from "./exit-codes.mjs";
6
7
  import { formatJsonDocument } from "./formatters/json.mjs";
7
8
  import { readCliTextInput } from "./input.mjs";
9
+ import { assessEvidenceSignerTrust, formatSignerTrustReport, MAX_SIGNER_TRUST_STORE_BYTES } from "./signer-trust.mjs";
8
10
  import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
9
11
  export function isEvidenceCommand(args) {
10
- return args.action === 'inspect-archive'
11
- || args.action === 'sign-artifact'
12
- || args.action === 'verify-signature';
12
+ return commandHasOwner(args.action, 'evidence');
13
13
  }
14
14
  export function evidenceCommandFailureLabel(action) {
15
15
  if (action === 'inspect-archive')
@@ -18,12 +18,12 @@ export function evidenceCommandFailureLabel(action) {
18
18
  return 'Evidence signing';
19
19
  return 'Evidence signature verification';
20
20
  }
21
- async function readArtifact(source, label, dependencies) {
21
+ async function readArtifact(source, label, dependencies, maximumBytes = MAX_OFFLINE_ARTIFACT_BYTES) {
22
22
  try {
23
23
  return dependencies.readArtifactInput
24
24
  ? await dependencies.readArtifactInput(source)
25
25
  : await readCliTextInput(source, dependencies.stdin, {
26
- maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES,
26
+ maximumBytes,
27
27
  label,
28
28
  ...(dependencies.signal ? { signal: dependencies.signal } : {}),
29
29
  });
@@ -105,12 +105,22 @@ async function runVerifySignatureCommand(args, dependencies) {
105
105
  ? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile, dependencies)
106
106
  : null;
107
107
  const report = await verifyEvidencePackageSignature(input, publicKey);
108
+ let trustReport = null;
109
+ if (args.trustStoreSource) {
110
+ const trustRaw = await readArtifact(args.trustStoreSource, 'Signer trust store', dependencies, MAX_SIGNER_TRUST_STORE_BYTES);
111
+ try {
112
+ trustReport = assessEvidenceSignerTrust(report, trustRaw, dependencies.now ? dependencies.now() : new Date().toISOString());
113
+ }
114
+ catch (error) {
115
+ throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid signer trust store.'));
116
+ }
117
+ }
108
118
  if (!args.quiet) {
109
119
  dependencies.stdout.write(args.output === 'json'
110
- ? formatJsonDocument(report)
111
- : formatEvidenceSignatureVerification(report));
120
+ ? formatJsonDocument(trustReport ?? report)
121
+ : trustReport ? formatSignerTrustReport(trustReport) : formatEvidenceSignatureVerification(report));
112
122
  }
113
- return EXIT_CODES.SUCCESS;
123
+ return trustReport && trustReport.state !== 'trusted' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
114
124
  }
115
125
  const EVIDENCE_COMMAND_HANDLERS = Object.freeze({
116
126
  'inspect-archive': runInspectArchiveCommand,
@@ -169,7 +169,8 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
169
169
  }
170
170
  catch { /* Signature validity remains independently reportable. */ }
171
171
  let publicKeyMatched = null;
172
- if (trustedPublicKeyPem) {
172
+ const trustedKeySupplied = trustedPublicKeyPem !== undefined && trustedPublicKeyPem !== null;
173
+ if (trustedKeySupplied) {
173
174
  const trustedDer = spkiDer(importPublicKey(trustedPublicKeyPem, 'Trusted public key file'));
174
175
  publicKeyMatched = trustedDer.equals(publicDer);
175
176
  if (!publicKeyMatched)
@@ -181,7 +182,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
181
182
  state: 'signature_valid',
182
183
  signature: Object.freeze({
183
184
  state: 'valid',
184
- signerTrust: trustedPublicKeyPem ? 'trusted_key' : 'embedded_key_only',
185
+ signerTrust: trustedKeySupplied ? 'trusted_key' : 'embedded_key_only',
185
186
  signedAt,
186
187
  keyIdSha256: signature.keyIdSha256,
187
188
  publicKeyMatched,
@@ -216,11 +217,11 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
216
217
  ]),
217
218
  });
218
219
  }
219
- export function formatEvidenceSignatureVerification(report) {
220
+ export function formatEvidenceSignatureVerification(report, includeStandaloneTrust = true) {
220
221
  const lines = [
221
222
  'WHOISleuth evidence signature verification',
222
223
  `State: ${report.state}`,
223
- `Signer trust: ${report.signature.signerTrust}`,
224
+ ...(includeStandaloneTrust ? [`Signer trust: ${report.signature.signerTrust}`] : []),
224
225
  `Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'unrecognised'} v${report.artifact.version ?? 'unrecognised'}`,
225
226
  `Artifact assurance: ${report.artifact.assurance.state}`,
226
227
  `Signed: ${report.signature.signedAt}`,