@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,6 +1,6 @@
1
1
  import { domainToASCII } from 'node:url';
2
- import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
- import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, parseExternalFindingsDocument, } from "../packages/interchange/external-findings-import.mjs";
2
+ import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
4
4
  export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
5
5
  export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
6
6
  export const MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES = 4 * 1024 * 1024;
@@ -49,9 +49,9 @@ function domain(value, label) {
49
49
  function limitations(value) {
50
50
  if (value === undefined)
51
51
  return Object.freeze([]);
52
- if (!Array.isArray(value) || value.length > 8)
53
- throw new TypeError('profile.limitations must contain no more than 8 entries.');
54
- return Object.freeze([...new Set(value.map((entry, index) => requireBoundedString(entry, `profile.limitations[${index}]`, 240)))]);
52
+ if (!Array.isArray(value) || value.length > MAX_EXTERNAL_FINDING_LIMITATIONS)
53
+ throw new TypeError(`profile.limitations must contain no more than ${MAX_EXTERNAL_FINDING_LIMITATIONS} entries.`);
54
+ return Object.freeze([...new Set(value.map((entry, index) => requireBoundedString(entry, `profile.limitations[${index}]`, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH)))]);
55
55
  }
56
56
  export function mapExternalObservations(inputRaw) {
57
57
  const input = requireRecord(inputRaw, 'External observation mapping');
@@ -112,7 +112,7 @@ export function mapExternalObservations(inputRaw) {
112
112
  : null,
113
113
  structuredObservation: null,
114
114
  });
115
- }).sort((left, right) => left.domain.localeCompare(right.domain) || left.observedAt.localeCompare(right.observedAt) || left.summary.localeCompare(right.summary));
115
+ }).sort((left, right) => compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.observedAt, right.observedAt) || compareCodeUnits(left.summary, right.summary));
116
116
  const selected = [];
117
117
  const seen = new Set();
118
118
  const domainCounts = new Map();
@@ -140,11 +140,10 @@ export function mapExternalObservations(inputRaw) {
140
140
  source,
141
141
  findings: selected.map((finding) => ({
142
142
  ...finding,
143
- limitations: [
144
- ...finding.limitations,
143
+ limitations: retainExternalFindingLimitations(finding.limitations, [
145
144
  `Mapped locally with declarative profile ${profile.id} version ${profile.version}; the profile does not verify the upstream field semantics.`,
146
- ...(truncated ? ['The supplied records exceeded the browser import boundary; only the first deterministic bounded findings were retained.'] : []),
147
- ].slice(0, 8),
145
+ ...(truncated ? [`The supplied records exceeded the browser import boundary; ${selected.length} of ${seen.size} unique findings were retained in deterministic order.`] : []),
146
+ ]),
148
147
  })),
149
148
  };
150
149
  return parseExternalFindingsDocument(document);
@@ -168,6 +168,12 @@ function formatTerminalPosture(document) {
168
168
  `Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
169
169
  '',
170
170
  ];
171
+ for (const value of dmarcAuthorizations.slice(0, 10)) {
172
+ const item = terminalRecord(value);
173
+ lines.push(`Reporting ${safeTerminalValue(item.destination)} · ${safeTerminalValue(item.reportType)} · ${item.state === 'self' ? 'same organisational scope' : safeTerminalValue(item.state).replaceAll('_', ' ')}`);
174
+ if (item.error)
175
+ lines.push(` Detail ${safeTerminalValue(item.error)}`);
176
+ }
171
177
  for (const value of checks) {
172
178
  const item = terminalRecord(value);
173
179
  lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.summary)}`);
@@ -1,6 +1,8 @@
1
1
  import { registryAccessProfileLabel } from "../registry-access.mjs";
2
- import { technologyEvidenceRoles, } from "../../lib/technology-evidence-role.mjs";
2
+ import { dnsAddressFamilyEvidence } from "../../packages/evidence/dns-query-outcome.mjs";
3
+ import { appendTechnologyLines } from "./terminal-technology.mjs";
3
4
  import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
5
+ import { securityPostureReview, summarizeSecurityPostureReview } from "../../lib/website-security-posture.mjs";
4
6
  import { appendSection, boundedTerminalComponent, boundedTerminalList, boundedTerminalWithSuffix, MAX_TLS_TERMINAL_ALT_NAMES, MAX_TLS_TERMINAL_PURPOSES, safeTerminalValue, terminalCount, terminalCountSummary, terminalDisplayCount, terminalRecord, titleCase, } from "./terminal-shared.mjs";
5
7
  const MAX_LOOKUP_TERMINAL_RECORDS = 5;
6
8
  const MAX_LOOKUP_TERMINAL_NAMES = 5;
@@ -10,13 +12,6 @@ const MAX_LOOKUP_TERMINAL_LIMITATIONS = 3;
10
12
  const MAX_LOOKUP_TERMINAL_CIDRS = 5;
11
13
  const MAX_LOOKUP_TERMINAL_FINDINGS = 5;
12
14
  const MAX_LOOKUP_TERMINAL_ABUSE_ROUTES = 6;
13
- function terminalTechnologyRoleNames(findings, role) {
14
- const names = findings
15
- .filter((finding) => technologyEvidenceRoles(finding).includes(role))
16
- .slice(0, 6)
17
- .map((finding) => safeTerminalValue(terminalRecord(finding).name, 'Unnamed indicator'));
18
- return names.length ? boundedTerminalList(names, Math.max(0, findings.filter((finding) => technologyEvidenceRoles(finding).includes(role)).length - names.length)) : 'None retained';
19
- }
20
15
  function formatLookupDnsRecord(value) {
21
16
  if (typeof value === 'string' || typeof value === 'number')
22
17
  return safeTerminalValue(value);
@@ -244,6 +239,11 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
244
239
  targetLines.push(`Input host ${safeTerminalValue(document.inputHostname)}`);
245
240
  targetLines.push(`Registry query ${safeTerminalValue(document.registrableDomain)}`);
246
241
  }
242
+ if (availability.observationHostname) {
243
+ targetLines.push(`DNS/TLS/web ${safeTerminalValue(availability.observationHostname)}`);
244
+ }
245
+ if (availability.webObservationMode === 'selected_url')
246
+ targetLines.push('Website scope Explicitly selected URL; query omitted from provenance');
247
247
  if (availability.applicable) {
248
248
  targetLines.push(`Availability ${titleCase(availability.state)}`);
249
249
  targetLines.push(`Confidence ${titleCase(availability.confidence)}`);
@@ -320,6 +320,11 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
320
320
  if (Object.keys(dns).length) {
321
321
  dnsLines.push(`Evidence ${titleCase(dns.status)}`);
322
322
  dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
323
+ for (const family of ['a', 'aaaa']) {
324
+ const evidence = dnsAddressFamilyEvidence(dns, family);
325
+ if (evidence.value)
326
+ dnsLines.push(`${family.toUpperCase().padEnd(14)} ${safeTerminalValue(evidence.value)}`);
327
+ }
323
328
  const recordTypes = [
324
329
  ['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
325
330
  ['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
@@ -486,7 +491,7 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
486
491
  const technology = terminalRecord(availability.technologyProfile);
487
492
  const browserLibraries = terminalRecord(technology.browserLibraryProfile);
488
493
  const posture = terminalRecord(availability.securityPosture);
489
- const postureSummary = terminalRecord(posture.summary);
494
+ const postureReview = summarizeSecurityPostureReview(posture.findings);
490
495
  const pageIdentity = terminalRecord(availability.pageIdentity);
491
496
  const pageRole = terminalRecord(availability.pageRoleProfile);
492
497
  const clientBehavior = terminalRecord(availability.clientBehaviorProfile);
@@ -578,47 +583,20 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
578
583
  if (detail !== 'summary' && visible.length)
579
584
  websiteLines.push(`Declarations ${safeTerminalValue(visible.join('; '))}`);
580
585
  }
581
- if (technology.status || technology.source === 'derived') {
582
- const findings = Array.isArray(technology.findings) ? technology.findings : [];
583
- websiteLines.push(`Technology ${titleCase(technology.status)} · ${findings.length} indicator${findings.length === 1 ? '' : 's'}`);
584
- const visible = findings.slice(0, 6).map((finding) => {
585
- const item = terminalRecord(finding);
586
- const qualifiers = [item.category, item.confidence ? `${item.confidence} signature strength` : null].filter(Boolean).map((value) => safeTerminalValue(value));
587
- return `${safeTerminalValue(item.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
588
- });
589
- if (detail !== 'summary' && visible.length) {
590
- const omitted = findings.length - visible.length;
591
- websiteLines.push(`Indicators ${safeTerminalValue(`${visible.join('; ')}${omitted > 0 ? `; +${omitted} more` : ''}`)}`);
592
- }
593
- if (detail !== 'summary') {
594
- const nameservers = Array.isArray(availability.nameservers)
595
- ? availability.nameservers.slice(0, MAX_LOOKUP_TERMINAL_NAMES).map((value) => safeTerminalValue(value))
596
- : [];
597
- websiteLines.push(`Nameservers ${nameservers.length ? boundedTerminalList(nameservers, Math.max(0, availability.nameservers.length - nameservers.length)) : 'Unavailable'} · identity does not establish operator or web-host ownership`);
598
- websiteLines.push(`Observed edge ${terminalTechnologyRoleNames(findings, 'observed_edge')}`);
599
- websiteLines.push(`App platform ${terminalTechnologyRoleNames(findings, 'application_platform')}`);
600
- websiteLines.push(`Framework/run ${terminalTechnologyRoleNames(findings, 'framework_runtime')}`);
601
- websiteLines.push(`Embedded deps ${terminalTechnologyRoleNames(findings, 'embedded_dependency')}`);
602
- websiteLines.push('Origin host Not established from retained evidence');
603
- }
604
- if (detail !== 'summary' && (browserLibraries.profileVersion === 1 || browserLibraries.source === 'derived')) {
605
- const libraries = Array.isArray(browserLibraries.findings) ? browserLibraries.findings : [];
606
- const advisoryMatches = libraries.filter((finding) => terminalCount(terminalRecord(finding).advisoryCount) > 0).length;
607
- websiteLines.push(`JS libraries ${titleCase(browserLibraries.status)} · ${libraries.length} apparent · `
608
- + `${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
609
- }
610
- }
586
+ appendTechnologyLines(websiteLines, {
587
+ technology, libraries: browserLibraries, nameservers: availability.nameservers,
588
+ generatedAt: document.generatedAt, detail,
589
+ });
611
590
  if (posture.status || posture.source === 'derived') {
612
591
  websiteLines.push(`Posture ${titleCase(posture.status)}`);
613
- if (detail !== 'summary' && Object.keys(postureSummary).length)
614
- websiteLines.push(`Posture counts ${terminalCount(postureSummary.observed)} observed · `
615
- + `${terminalCount(postureSummary.potentialExposure)} potential exposure · `
616
- + `${terminalCount(postureSummary.observedAbsence)} observed absence · `
617
- + `${terminalCount(postureSummary.unavailable)} unavailable`);
592
+ if (detail !== 'summary' && Array.isArray(posture.findings))
593
+ websiteLines.push(`Posture checks Needs review ${postureReview.needsReview} · `
594
+ + `Other findings ${postureReview.otherFindings} · `
595
+ + `Could not assess ${postureReview.unavailable}`);
618
596
  if (detail === 'verbose' && positiveSourceStatus(posture.status)) {
619
597
  const findings = boundedFindingLabels(posture.findings, (finding) => {
620
- const state = finding.state ? ` (${titleCase(finding.state)})` : '';
621
- return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${state}`;
598
+ const review = securityPostureReview(finding);
599
+ return `${safeTerminalValue(finding.label, 'Unlabelled finding')}${review.label ? ` (${review.label})` : ''}`;
622
600
  });
623
601
  if (findings)
624
602
  websiteLines.push(`Posture labels ${findings}`);
@@ -6,7 +6,9 @@ const MAX_TLS_TERMINAL_PURPOSES = 8;
6
6
  // through soft hyphens, combining grapheme joiners, variation selectors, tag
7
7
  // characters, bidi controls, or other zero-width formatting code points.
8
8
  const TERMINAL_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/gu;
9
- function safeTerminalValue(value, fallback = '—') {
9
+ function safeTerminalValue(value, fallback = '—', maximum = MAX_TERMINAL_VALUE_LENGTH) {
10
+ if (!Number.isSafeInteger(maximum) || maximum < 1)
11
+ throw new TypeError('Terminal text requires a positive finite character bound.');
10
12
  if (value === null || value === undefined || value === '')
11
13
  return fallback;
12
14
  const normalized = String(value)
@@ -16,8 +18,8 @@ function safeTerminalValue(value, fallback = '—') {
16
18
  .trim();
17
19
  if (!normalized)
18
20
  return fallback;
19
- return normalized.length > MAX_TERMINAL_VALUE_LENGTH
20
- ? `${normalized.slice(0, MAX_TERMINAL_VALUE_LENGTH - 1)}…`
21
+ return normalized.length > maximum
22
+ ? `${normalized.slice(0, maximum - 1)}…`
21
23
  : normalized;
22
24
  }
23
25
  function boundedTerminalList(values, omitted) {
@@ -0,0 +1,114 @@
1
+ import { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_LIBRARY_FINDINGS, MAX_TECHNOLOGY_FINDINGS, } from "../../lib/lookup-child-profile-contract.mjs";
2
+ import { TECHNOLOGY_EVIDENCE_ROLE_ORDER, technologyEvidenceRoles, } from "../../lib/technology-evidence-role.mjs";
3
+ import { MAX_OBSERVATION_LIMITATIONS, readObservationTime } from "../../packages/evidence/observation.mjs";
4
+ import { boundedTerminalList, safeTerminalValue, terminalCount, terminalRecord, titleCase } from "./terminal-shared.mjs";
5
+ const ROLE_LABELS = {
6
+ observed_edge: 'Observed edge',
7
+ application_platform: 'App platform',
8
+ framework_runtime: 'Framework/run',
9
+ embedded_dependency: 'Embedded deps',
10
+ };
11
+ const SUMMARY_FINDINGS = 6;
12
+ const SUMMARY_NAMESERVERS = 5;
13
+ function observationLine(label, value, generatedAt) {
14
+ const time = readObservationTime(value, generatedAt);
15
+ if (!time.observedAt)
16
+ return `${label.padEnd(15)}Observed time unavailable`;
17
+ const age = time.ageDays === null ? 'age unavailable'
18
+ : time.ageDays === 0 ? 'less than a day old at output generation'
19
+ : `${time.ageDays} day${time.ageDays === 1 ? '' : 's'} old at output generation`;
20
+ return `${label.padEnd(15)}${time.observedAt} · ${age}`;
21
+ }
22
+ function appendLimitations(lines, value) {
23
+ if (!Array.isArray(value))
24
+ return;
25
+ for (const limitation of value.slice(0, MAX_OBSERVATION_LIMITATIONS)) {
26
+ lines.push(` Limit ${safeTerminalValue(limitation)}`);
27
+ }
28
+ }
29
+ /** Presents admitted source records; it never collects or infers a hidden origin. */
30
+ export function appendTechnologyLines(lines, input) {
31
+ const { technology, libraries, nameservers, generatedAt, detail } = input;
32
+ if (!technology.status && technology.source !== 'derived')
33
+ return;
34
+ const suppliedFindings = Array.isArray(technology.findings) ? technology.findings : [];
35
+ const findings = suppliedFindings.slice(0, MAX_TECHNOLOGY_FINDINGS).map((value) => {
36
+ const finding = terminalRecord(value);
37
+ return { finding, roles: technologyEvidenceRoles(finding) };
38
+ });
39
+ lines.push(`Technology ${titleCase(technology.status)} · ${findings.length} indicator${findings.length === 1 ? '' : 's'}`);
40
+ if (suppliedFindings.length > findings.length)
41
+ lines.push('Technology Additional findings exceed the admitted profile limit');
42
+ if (detail === 'summary')
43
+ return;
44
+ lines.push(observationLine('Tech observed', technology.observedAt, generatedAt));
45
+ if (detail === 'standard' && findings.length) {
46
+ const visible = findings.slice(0, SUMMARY_FINDINGS).map(({ finding }) => {
47
+ const qualifiers = [finding.category, finding.confidence ? `${safeTerminalValue(finding.confidence)} signature strength` : null]
48
+ .filter(Boolean).map((value) => safeTerminalValue(value));
49
+ return `${safeTerminalValue(finding.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
50
+ });
51
+ lines.push(`Indicators ${boundedTerminalList(visible, findings.length - visible.length)}`);
52
+ }
53
+ const retainedNameservers = Array.isArray(nameservers) ? nameservers : [];
54
+ const visibleNameservers = retainedNameservers.slice(0, SUMMARY_NAMESERVERS).map((value) => safeTerminalValue(value));
55
+ lines.push(`Nameservers ${visibleNameservers.length ? boundedTerminalList(visibleNameservers, retainedNameservers.length - visibleNameservers.length) : 'Unavailable'} · identity does not establish operator or web-host ownership`);
56
+ for (const role of TECHNOLOGY_EVIDENCE_ROLE_ORDER) {
57
+ const matching = findings.filter((item) => item.roles.includes(role));
58
+ const names = matching.slice(0, SUMMARY_FINDINGS).map(({ finding }) => safeTerminalValue(finding.name, 'Unnamed indicator'));
59
+ lines.push(`${ROLE_LABELS[role].padEnd(15)}${names.length ? boundedTerminalList(names, matching.length - names.length) : 'None retained'}`);
60
+ }
61
+ lines.push('Origin host Not established from retained evidence');
62
+ if (detail === 'verbose') {
63
+ for (const { finding, roles } of findings) {
64
+ lines.push(`Indicator ${safeTerminalValue(finding.name, 'Unnamed indicator')}`);
65
+ lines.push(` Role ${roles.map((role) => ROLE_LABELS[role]).join(', ') || 'Not retained'}`);
66
+ lines.push(` Strength ${safeTerminalValue(finding.confidence, 'Unknown')} signature strength`);
67
+ const suppliedEvidence = Array.isArray(finding.evidence) ? finding.evidence : [];
68
+ const evidence = suppliedEvidence.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY);
69
+ if (!evidence.length)
70
+ lines.push(' Signal No detailed signal was retained');
71
+ for (const value of evidence) {
72
+ const item = terminalRecord(value);
73
+ if (typeof item.source !== 'string' || !item.source.trim()
74
+ || typeof item.description !== 'string' || !item.description.trim()) {
75
+ lines.push(' Signal Incomplete retained signal record');
76
+ continue;
77
+ }
78
+ const role = TECHNOLOGY_EVIDENCE_ROLE_ORDER.find((candidate) => candidate === item.role);
79
+ lines.push(` Source ${safeTerminalValue(item.source, 'Not retained')} · ${role ? ROLE_LABELS[role] : 'Role not retained'}`);
80
+ lines.push(` Signal ${safeTerminalValue(item.description, 'Not retained')}`);
81
+ }
82
+ if (suppliedEvidence.length > evidence.length)
83
+ lines.push(' Limit Additional signals exceed the admitted profile limit');
84
+ }
85
+ appendLimitations(lines, technology.limitations);
86
+ }
87
+ if (libraries.profileVersion !== 1 && libraries.profileVersion !== 2 && libraries.source !== 'derived')
88
+ return;
89
+ const suppliedLibraries = Array.isArray(libraries.findings) ? libraries.findings : [];
90
+ const libraryFindings = suppliedLibraries.slice(0, MAX_LIBRARY_FINDINGS).map(terminalRecord);
91
+ const advisoryMatches = libraryFindings.filter((finding) => terminalCount(finding.advisoryCount) > 0).length;
92
+ lines.push(`JS libraries ${titleCase(libraries.status)} · ${libraryFindings.length} apparent · ${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
93
+ lines.push(observationLine('Lib observed', libraries.observedAt, generatedAt));
94
+ if (suppliedLibraries.length > libraryFindings.length)
95
+ lines.push('JS libraries Additional findings exceed the admitted profile limit');
96
+ if (detail !== 'verbose')
97
+ return;
98
+ const catalogue = terminalRecord(libraries.catalog);
99
+ lines.push(`Catalogue ${safeTerminalValue(catalogue.name, 'Component catalogue')} · ${safeTerminalValue(catalogue.version, 'Version unavailable')}`);
100
+ for (const library of libraryFindings) {
101
+ lines.push(`Library ${safeTerminalValue(library.name, 'Unnamed library')} · ${safeTerminalValue(library.apparentVersion, 'Version unavailable')}`);
102
+ const methods = Array.isArray(library.detectionMethods) ? library.detectionMethods.slice(0, 4).map((value) => safeTerminalValue(value)) : [];
103
+ lines.push(` Detected by ${methods.join(', ') || 'Not retained'}`);
104
+ lines.push(` Advisories ${terminalCount(library.advisoryCount)} catalogue matches`);
105
+ if (library.highestSeverity)
106
+ lines.push(` Severity ${safeTerminalValue(library.highestSeverity)}`);
107
+ for (const [field, label] of [['advisoryIdentifiers', 'Identifier'], ['knownExploitedIdentifiers', 'Known exploited']]) {
108
+ const values = Array.isArray(library[field]) ? library[field] : [];
109
+ for (const value of values.slice(0, 16))
110
+ lines.push(` ${label.padEnd(15)} ${safeTerminalValue(value)}`);
111
+ }
112
+ }
113
+ appendLimitations(lines, libraries.limitations);
114
+ }
@@ -0,0 +1,39 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { terminalOptions, jsonOutput, parseTwoFileComparisonArguments, uniqueSources, } from "./argument-values.mjs";
4
+ function parseDiffArguments(parsed) {
5
+ const compared = parseTwoFileComparisonArguments(parsed, 'diff');
6
+ const leftSessionId = parsed.optionValue('--left-session');
7
+ const rightSessionId = parsed.optionValue('--right-session');
8
+ for (const [option, value] of [
9
+ ['--left-session', leftSessionId],
10
+ ['--right-session', rightSessionId],
11
+ ]) {
12
+ if (value !== null && !/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
13
+ throw new CliUsageError(`${option} requires one bounded saved-session ID.`);
14
+ }
15
+ }
16
+ return { action: 'diff', ...compared, leftSessionId, rightSessionId };
17
+ }
18
+ export const HISTORY_ARGUMENT_PARSERS = Object.freeze({
19
+ diff: parseDiffArguments,
20
+ reconcile: (parsed) => ({
21
+ action: 'reconcile',
22
+ sources: uniqueSources(parsed, 'reconcile'),
23
+ output: jsonOutput(parsed),
24
+ ...terminalOptions(parsed),
25
+ }),
26
+ timeline: (parsed) => ({
27
+ action: 'timeline',
28
+ sources: uniqueSources(parsed, 'timeline'),
29
+ output: jsonOutput(parsed),
30
+ ...terminalOptions(parsed),
31
+ }),
32
+ export: (parsed) => ({
33
+ action: 'export',
34
+ source: parsed.positionalValue('source'),
35
+ format: parsed.hasOption('--markdown') ? 'markdown' : parsed.hasOption('--html') ? 'html' : 'json',
36
+ compact: parsed.hasOption('--compact'),
37
+ includeAttribution: !parsed.hasOption('--no-attribution'),
38
+ }),
39
+ });
@@ -0,0 +1,88 @@
1
+ import { LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION } from "../packages/contracts/lookup-evidence.mjs";
2
+ import { PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS, positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
3
+ export const HISTORY_COMMAND_DEFINITIONS = Object.freeze({
4
+ diff: commandSeed({
5
+ reference: {
6
+ description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
7
+ example: 'whoisleuth diff earlier.json later.json --json',
8
+ boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. Saved Lookups can describe the same or different domains; same-domain comparisons preserve observation times and collection uncertainty. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
9
+ },
10
+ collection: { scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
11
+ summary: 'Compare two compatible retained artefacts',
12
+ options: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
13
+ positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
14
+ constraints: EMPTY_CONSTRAINTS,
15
+ handlerOwner: 'history',
16
+ networkEffect: 'offline',
17
+ common: true,
18
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-diff']),
19
+ primaryArtefacts: Object.freeze(['Retained-evidence comparison']),
20
+ planSupport: false,
21
+ additionalOutputFormats: Object.freeze([]),
22
+ bootstrapProfile: 'allowed',
23
+ }),
24
+ reconcile: commandSeed({
25
+ reference: {
26
+ description: 'Reconcile bounded values across independently labelled observations of one domain.',
27
+ example: 'whoisleuth reconcile office.json mobile.json external.json --json',
28
+ boundary: 'The command is offline, accepts 2 to 5 saved observations for one domain, and never treats labels as proof of network independence or majority agreement as truth.',
29
+ },
30
+ collection: { scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
31
+ summary: 'Reconcile independently labelled observations',
32
+ options: ['--json', '--quiet', '--no-color'],
33
+ positionals: Object.freeze([positional('sources', 'file', 2, 5)]),
34
+ constraints: EMPTY_CONSTRAINTS,
35
+ handlerOwner: 'history',
36
+ networkEffect: 'offline',
37
+ common: false,
38
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-reconciliation']),
39
+ primaryArtefacts: Object.freeze([]),
40
+ planSupport: false,
41
+ additionalOutputFormats: Object.freeze([]),
42
+ bootstrapProfile: 'allowed',
43
+ }),
44
+ timeline: commandSeed({
45
+ reference: {
46
+ description: 'Build an ordered same-domain history from saved Lookup observations.',
47
+ example: 'whoisleuth timeline first.json second.json latest.json --json',
48
+ boundary: 'The command is offline, accepts 2 to 20 bounded inputs for one domain, retains no filenames or raw registry payloads, and does not treat changed collection conditions as a domain change.',
49
+ },
50
+ collection: { scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
51
+ summary: 'Build same-domain history from saved lookups',
52
+ options: ['--json', '--quiet', '--no-color'],
53
+ positionals: Object.freeze([positional('sources', 'file', 2, 20)]),
54
+ constraints: EMPTY_CONSTRAINTS,
55
+ handlerOwner: 'history',
56
+ networkEffect: 'offline',
57
+ common: false,
58
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-timeline']),
59
+ primaryArtefacts: Object.freeze(['Bounded retained-observation timeline']),
60
+ planSupport: false,
61
+ additionalOutputFormats: Object.freeze([]),
62
+ bootstrapProfile: 'allowed',
63
+ }),
64
+ export: commandSeed({
65
+ reference: {
66
+ description: 'Convert one saved lookup into a versioned evidence report.',
67
+ example: 'whoisleuth export lookup.json --markdown',
68
+ boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schemas ${PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
69
+ },
70
+ collection: { scope: 'Reads one saved Lookup and writes one bounded report.' },
71
+ summary: 'Convert a lookup to an evidence report',
72
+ options: ['--markdown', '--html', '--compact', '--no-attribution'],
73
+ positionals: OPTIONAL_FILE_POSITIONAL,
74
+ constraints: Object.freeze([
75
+ constraint({ kind: 'mutually_exclusive', options: ['--markdown', '--html'] }),
76
+ constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
77
+ constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
78
+ ]),
79
+ handlerOwner: 'history',
80
+ networkEffect: 'offline',
81
+ common: true,
82
+ schemaIdentifiers: Object.freeze(['whoisleuth.lookup-evidence']),
83
+ primaryArtefacts: Object.freeze(['Portable evidence report']),
84
+ planSupport: false,
85
+ additionalOutputFormats: Object.freeze(['JSON']),
86
+ bootstrapProfile: 'allowed',
87
+ }),
88
+ });
@@ -0,0 +1,33 @@
1
+ import { buildManagedIndicatorRevision, managedIndicatorState, parseManagedIndicatorJson, readManagedIndicatorSet } from "../packages/interchange/managed-indicator-set.mjs";
2
+ import { exportManagedIndicators } from "../packages/interchange/managed-indicator-export.mjs";
3
+ import { MAX_MANAGED_INDICATOR_PLAN_BYTES, MAX_MANAGED_INDICATOR_SET_BYTES } from "../packages/contracts/analyst-interchange.mjs";
4
+ import { formatJsonDocument } from "./formatters/json.mjs";
5
+ import { safeTerminalValue } from "./formatters/terminal.mjs";
6
+ import EXIT_CODES from "./exit-codes.mjs";
7
+ export async function runIndicatorSetCommand(args, dependencies, context) {
8
+ context.setFailureLabel('Indicator revision');
9
+ const raw = dependencies.readArtifactInput ? await dependencies.readArtifactInput(args.source)
10
+ : await context.readInput(args.source, args.operation === 'revise' ? MAX_MANAGED_INDICATOR_PLAN_BYTES : MAX_MANAGED_INDICATOR_SET_BYTES, 'Indicator input');
11
+ const input = parseManagedIndicatorJson(raw, args.operation === 'revise');
12
+ const now = context.now();
13
+ const revision = args.operation === 'revise' ? await buildManagedIndicatorRevision(input, now) : null;
14
+ const manifest = revision?.manifest ?? await readManagedIndicatorSet(input);
15
+ dependencies.signal?.throwIfAborted();
16
+ if (args.quiet)
17
+ return EXIT_CODES.SUCCESS;
18
+ if (args.operation === 'stix' || args.operation === 'misp')
19
+ context.writeStdout((await exportManagedIndicators(manifest, args.operation)).content);
20
+ else if (args.output === 'json')
21
+ context.writeStdout(formatJsonDocument(manifest));
22
+ else {
23
+ const lines = [`${safeTerminalValue(manifest.name)} · revision ${manifest.revision}`, `Set ${manifest.id}`, `Content digest ${manifest.integrity.digestSha256}`,
24
+ 'Content verified; authorship and earlier revisions are not authenticated.'];
25
+ if (revision)
26
+ lines.push(`Changes: ${revision.changes.length} · unchanged: ${revision.unchanged} · excluded candidates: ${revision.exclusions.length}`);
27
+ for (const entry of manifest.entries)
28
+ lines.push(`${entry.domain} · ${managedIndicatorState(entry, now)} · ${entry.id}`, ` Review expires ${entry.expiresAt} · source observed ${entry.observation.observedAt ?? 'time unavailable'}`, ` ${safeTerminalValue(entry.reviewBasis)}`);
29
+ lines.push('Save the manifest for the next revision. Nothing was submitted or applied.');
30
+ context.writeStdout(context.terminal(`${lines.join('\n')}\n`, args.color));
31
+ }
32
+ return EXIT_CODES.SUCCESS;
33
+ }
@@ -1,46 +1,6 @@
1
- const SUPPORT_INLINE_COMMANDS = Object.freeze([
2
- 'completion',
3
- 'commands',
4
- 'manual',
5
- 'doctor',
6
- 'registry-support',
7
- 'registry-doctor',
8
- 'registry-cohort',
9
- 'registry-scaffold',
10
- 'risk-calibrate',
11
- 'lookalike-calibrate',
12
- ]);
13
- const REVIEW_INLINE_COMMANDS = Object.freeze([
14
- 'verify-artifact',
15
- 'interchange-report',
16
- 'source-report',
17
- 'compare',
18
- 'page-compare',
19
- 'mail-review',
20
- 'mail-headers',
21
- 'review-evidence',
22
- 'brief',
23
- 'case-pack',
24
- ]);
25
- const ASSURANCE_INLINE_COMMANDS = Object.freeze([
26
- 'manifest',
27
- 'map-observations',
28
- 'oam-export',
29
- 'domain-control',
30
- 'assurance',
31
- 'change-packet',
32
- 'sharing-review',
33
- 'ct-intake',
34
- ]);
35
- const WORKFLOW_INLINE_COMMANDS = Object.freeze([
36
- 'monitor-once',
37
- 'workflow-plan',
38
- 'workflow-run',
39
- ]);
40
- const HISTORY_INLINE_COMMANDS = Object.freeze([
41
- 'diff',
42
- 'reconcile',
43
- 'timeline',
44
- 'export',
45
- ]);
46
- export { ASSURANCE_INLINE_COMMANDS, HISTORY_INLINE_COMMANDS, REVIEW_INLINE_COMMANDS, SUPPORT_INLINE_COMMANDS, WORKFLOW_INLINE_COMMANDS, };
1
+ import { inlineCommandsFor } from "./command-reference.mjs";
2
+ export const SUPPORT_INLINE_COMMANDS = inlineCommandsFor('support');
3
+ export const REVIEW_INLINE_COMMANDS = inlineCommandsFor('review');
4
+ export const ASSURANCE_INLINE_COMMANDS = inlineCommandsFor('assurance');
5
+ export const WORKFLOW_INLINE_COMMANDS = inlineCommandsFor('workflow');
6
+ export const HISTORY_INLINE_COMMANDS = inlineCommandsFor('history');
@@ -1,35 +1,8 @@
1
- import { CLI_COMMANDS, commandDefinition } from "./command-reference.mjs";
2
- import { ASSURANCE_INLINE_COMMANDS, HISTORY_INLINE_COMMANDS, REVIEW_INLINE_COMMANDS, SUPPORT_INLINE_COMMANDS, WORKFLOW_INLINE_COMMANDS, } from "./inline-command-families.mjs";
3
- const FAMILY_COMMANDS = Object.freeze([
4
- Object.freeze({ family: 'support', commands: SUPPORT_INLINE_COMMANDS }),
5
- Object.freeze({ family: 'review', commands: REVIEW_INLINE_COMMANDS }),
6
- Object.freeze({ family: 'assurance', commands: ASSURANCE_INLINE_COMMANDS }),
7
- Object.freeze({ family: 'workflow', commands: WORKFLOW_INLINE_COMMANDS }),
8
- Object.freeze({ family: 'history', commands: HISTORY_INLINE_COMMANDS }),
9
- ]);
10
- function buildInlineCommandOwnership() {
11
- const ownership = new Map();
12
- for (const { family, commands } of FAMILY_COMMANDS) {
13
- for (const command of commands) {
14
- if (ownership.has(command))
15
- throw new Error(`Inline CLI command ${command} has more than one command-family owner.`);
16
- ownership.set(command, family);
17
- }
18
- }
19
- return ownership;
20
- }
21
- const INLINE_COMMAND_OWNERSHIP = buildInlineCommandOwnership();
22
- const INLINE_CLI_COMMANDS = Object.freeze(CLI_COMMANDS.filter((command) => commandDefinition(command).execution.handlerOwner === 'inline'));
23
- function validateInlineCommandOwnership() {
24
- const missing = INLINE_CLI_COMMANDS.filter((command) => !INLINE_COMMAND_OWNERSHIP.has(command));
25
- const unexpected = [...INLINE_COMMAND_OWNERSHIP.keys()].filter((command) => !INLINE_CLI_COMMANDS.includes(command));
26
- if (missing.length || unexpected.length) {
27
- throw new Error(`Inline CLI command-family ownership is inconsistent (missing: ${missing.join(', ') || 'none'}; unexpected: ${unexpected.join(', ') || 'none'}).`);
28
- }
29
- }
30
- validateInlineCommandOwnership();
1
+ import { CLI_COMMANDS, INLINE_COMMAND_FAMILIES, inlineCommandFamily, inlineCommandsFor, } from "./command-reference.mjs";
2
+ const FAMILY_COMMANDS = Object.freeze(INLINE_COMMAND_FAMILIES.map(family => Object.freeze({ family, commands: inlineCommandsFor(family) })));
3
+ const INLINE_CLI_COMMANDS = Object.freeze(CLI_COMMANDS.filter((command) => inlineCommandFamily(command) !== null));
31
4
  async function runInlineCommand(args, dependencies, context) {
32
- const family = INLINE_COMMAND_OWNERSHIP.get(args.action);
5
+ const family = inlineCommandFamily(args.action);
33
6
  if (family === 'support') {
34
7
  const { runSupportCommand } = await import("./support-command-runner.mjs");
35
8
  return runSupportCommand(args, dependencies, context);
package/cli/input.mjs CHANGED
@@ -64,7 +64,7 @@ export async function readCliTextInput(source, stdin, options) {
64
64
  if (error instanceof CliUsageError)
65
65
  throw error;
66
66
  const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
67
- throw new CliUsageError(message);
67
+ throw new CliUsageError(message, error instanceof TypeError ? 'invalid_input' : 'input_unavailable');
68
68
  }
69
69
  }
70
70
  if (!stdin || stdin.isTTY)
@@ -142,7 +142,7 @@ export async function readCliHeaderInput(source, stdin, options) {
142
142
  if (error instanceof CliUsageError || options.signal?.aborted)
143
143
  throw error;
144
144
  const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
145
- throw new CliUsageError(message);
145
+ throw new CliUsageError(message, error instanceof TypeError ? 'invalid_input' : 'input_unavailable');
146
146
  }
147
147
  finally {
148
148
  await handle?.close();