@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,4 +1,5 @@
1
1
  import { MAX_WORKSPACE_INPUT_ARRAY_LENGTH, MAX_WORKSPACE_INPUT_GRAPH_DEPTH, MAX_WORKSPACE_INPUT_GRAPH_NODES, MAX_WORKSPACE_INPUT_OBJECT_KEYS, MAX_WORKSPACE_INPUT_STRING_CODE_UNITS, } from "../contracts/workspace-portability.mjs";
2
+ import { boundedJsonLimitsForBytes } from "../analysis/bounded-json.mjs";
2
3
  function inputError(label, detail) {
3
4
  return new TypeError(`${label} must be bounded ordinary JSON data; ${detail}.`);
4
5
  }
@@ -27,9 +28,7 @@ function ownDataDescriptors(value, label) {
27
28
  return [key, descriptor];
28
29
  });
29
30
  }
30
- export function ordinaryWorkspaceRecord(value, label = 'Workspace input') {
31
- if (!value || typeof value !== 'object' || Array.isArray(value))
32
- return null;
31
+ function ordinaryRecordDescriptors(value, label) {
33
32
  let prototype;
34
33
  try {
35
34
  prototype = Object.getPrototypeOf(value);
@@ -47,19 +46,33 @@ export function ordinaryWorkspaceRecord(value, label = 'Workspace input') {
47
46
  if (descriptors.some(([, descriptor]) => !descriptor.enumerable)) {
48
47
  throw inputError(label, 'non-enumerable object fields are not supported');
49
48
  }
49
+ return descriptors;
50
+ }
51
+ export function ordinaryWorkspaceRecord(value, label = 'Workspace input') {
52
+ if (!value || typeof value !== 'object' || Array.isArray(value))
53
+ return null;
54
+ ordinaryRecordDescriptors(value, label);
50
55
  return value;
51
56
  }
52
- export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
57
+ export function assertWorkspaceInputGraph(value, label = 'Workspace input', options = {}) {
58
+ const limits = options.maximumBytes === undefined ? null : boundedJsonLimitsForBytes(options.maximumBytes);
59
+ const maximumStringCodeUnits = limits?.maximumStringCodeUnits ?? MAX_WORKSPACE_INPUT_STRING_CODE_UNITS;
53
60
  const pending = [{ value, depth: 0, label }];
54
61
  const seen = new WeakSet();
55
62
  let nodes = 0;
63
+ let values = 0;
64
+ let keys = 0;
56
65
  let stringCodeUnits = 0;
57
66
  while (pending.length) {
58
67
  const current = pending.pop();
59
68
  const candidate = current.value;
69
+ values += 1;
70
+ if (limits && values > limits.maximumValues) {
71
+ throw inputError(label, 'the graph exceeds the byte-budgeted value ceiling');
72
+ }
60
73
  if (typeof candidate === 'string') {
61
74
  stringCodeUnits += candidate.length;
62
- if (stringCodeUnits > MAX_WORKSPACE_INPUT_STRING_CODE_UNITS) {
75
+ if (stringCodeUnits > maximumStringCodeUnits) {
63
76
  throw inputError(label, 'aggregate text exceeds the string ceiling');
64
77
  }
65
78
  continue;
@@ -76,7 +89,7 @@ export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
76
89
  throw inputError(label, 'the nesting depth exceeds the graph ceiling');
77
90
  }
78
91
  nodes += 1;
79
- if (nodes > MAX_WORKSPACE_INPUT_GRAPH_NODES) {
92
+ if (!limits && nodes > MAX_WORKSPACE_INPUT_GRAPH_NODES) {
80
93
  throw inputError(label, 'the graph exceeds the node ceiling');
81
94
  }
82
95
  if (seen.has(candidate))
@@ -118,8 +131,16 @@ export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
118
131
  }
119
132
  continue;
120
133
  }
121
- const record = ordinaryWorkspaceRecord(candidate, current.label);
122
- const descriptors = ownDataDescriptors(record, current.label);
134
+ const descriptors = ordinaryRecordDescriptors(candidate, current.label);
135
+ if (limits) {
136
+ keys += descriptors.length;
137
+ if (keys > limits.maximumKeys)
138
+ throw inputError(label, 'the graph exceeds the byte-budgeted key ceiling');
139
+ stringCodeUnits += descriptors.reduce((total, [key]) => total + key.length, 0);
140
+ if (stringCodeUnits > maximumStringCodeUnits) {
141
+ throw inputError(label, 'aggregate text exceeds the string ceiling');
142
+ }
143
+ }
123
144
  for (const [key, descriptor] of descriptors) {
124
145
  pending.push({
125
146
  value: descriptor.value,
@@ -1,4 +1,4 @@
1
- import { normalizeDomain } from "../cases/case-model.mjs";
1
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
2
2
  import { parse } from 'tldts';
3
3
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
4
4
  export const INVESTIGATION_GUIDE_SCHEMA = 'whoisleuth.investigation-recipe';
@@ -13,7 +13,7 @@ export const MAX_INVESTIGATION_GUIDE_TIMESTAMP_LENGTH = 64;
13
13
  export const MAX_INVESTIGATION_GUIDE_SERIALIZED_BYTES = 12_288;
14
14
  export const MAX_INVESTIGATION_GUIDE_EXPORT_BYTES = 16_384;
15
15
  const CONTROL_RE = /[\x00-\x1f\x7f]/u;
16
- const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
16
+ export const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
17
17
  const GUIDE_STATUSES = new Set(['active', 'paused']);
18
18
  const GUIDE_OUTCOMES = new Set(['pending', 'complete', 'partial', 'skipped']);
19
19
  function stage(id, workspace, label, detail, expectedEvidence, requestImpact, prerequisite, completionCriteria, instructions, requiresApproval) {
@@ -1,8 +1,27 @@
1
- import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, } from "./investigation-guide.mjs";
1
+ import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, SAFE_TEMPLATE_ID_RE, } from "./investigation-guide.mjs";
2
2
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
3
  import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
4
+ import { exact, HEX_DIGEST_RE, text } from "../evidence/artifact-structure.mjs";
5
+ import { SORTED_JSON_V2 } from "../evidence/artifact-integrity.mjs";
4
6
  import { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
5
7
  export { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
8
+ export function readTemplateLessonRevision(raw) {
9
+ if (raw === undefined)
10
+ return undefined;
11
+ assertWorkspaceInputGraph(raw, 'Template revision provenance');
12
+ const value = exact(raw, ['parentTemplateId', 'parentContentSha256', 'lessonContentSha256', 'canonicalization', 'applicability', 'rationale'], 'Template revision provenance');
13
+ if (typeof value.parentTemplateId !== 'string' || !SAFE_TEMPLATE_ID_RE.test(value.parentTemplateId)
14
+ || value.canonicalization !== SORTED_JSON_V2
15
+ || typeof value.parentContentSha256 !== 'string' || !HEX_DIGEST_RE.test(value.parentContentSha256)
16
+ || typeof value.lessonContentSha256 !== 'string' || !HEX_DIGEST_RE.test(value.lessonContentSha256))
17
+ throw new TypeError('Template revision provenance is invalid.');
18
+ const applicability = text(value.applicability, 'Template applicability', 400).trim();
19
+ const rationale = text(value.rationale, 'Template revision reason', 400).trim();
20
+ if (!applicability || !rationale)
21
+ throw new TypeError('Template applicability and revision reason must not be empty.');
22
+ return { parentTemplateId: value.parentTemplateId, parentContentSha256: value.parentContentSha256, lessonContentSha256: value.lessonContentSha256,
23
+ canonicalization: SORTED_JSON_V2, applicability, rationale };
24
+ }
6
25
  function record(value) {
7
26
  return ordinaryWorkspaceRecord(value, 'Investigation-template input');
8
27
  }
@@ -17,11 +36,13 @@ function template(raw) {
17
36
  const snapshot = normalizeInvestigationGuideTemplateSnapshot(value);
18
37
  const createdAt = timestamp(value?.createdAt);
19
38
  const updatedAt = timestamp(value?.updatedAt);
39
+ const lessonRevision = readTemplateLessonRevision(value?.lessonRevision);
20
40
  return snapshot && createdAt && updatedAt
21
- ? { ...snapshot, createdAt, updatedAt }
41
+ ? { ...snapshot, createdAt, updatedAt, ...(lessonRevision ? { lessonRevision } : {}) }
22
42
  : null;
23
43
  }
24
44
  export function normalizeInvestigationTemplate(raw) {
45
+ assertWorkspaceInputGraph(raw, 'Investigation template');
25
46
  return template(raw);
26
47
  }
27
48
  export function normalizeInvestigationTemplateStore(raw) {
@@ -33,6 +54,9 @@ export function normalizeInvestigationTemplateStore(raw) {
33
54
  throw new Error(`Investigation-template schema ${String(value.version)} is unsupported; no data was changed.`);
34
55
  }
35
56
  const source = Array.isArray(raw) ? raw : Array.isArray(value?.templates) ? value.templates : [];
57
+ if (value?.version === 2 && source.some(candidate => record(candidate)?.lessonRevision !== undefined)) {
58
+ throw new Error('Template revision provenance requires schema 3; no data was changed.');
59
+ }
36
60
  const templates = [];
37
61
  const seen = new Set();
38
62
  for (const candidate of source.slice(0, MAX_INVESTIGATION_TEMPLATES * 2)) {
@@ -85,6 +109,9 @@ export function saveInvestigationTemplate(localRaw, candidateRaw) {
85
109
  if (!candidate)
86
110
  throw new Error('The investigation template is incomplete or invalid.');
87
111
  const local = normalizeInvestigationTemplateStore(localRaw).templates;
112
+ if (!local.some((item) => item.id === candidate.id) && local.length >= MAX_INVESTIGATION_TEMPLATES) {
113
+ throw new Error(`Investigation-template storage is full (${MAX_INVESTIGATION_TEMPLATES} templates). Export and remove a template before saving another; no templates were changed.`);
114
+ }
88
115
  return normalizeInvestigationTemplateStore([
89
116
  candidate,
90
117
  ...local.filter((item) => item.id !== candidate.id),
@@ -126,11 +153,23 @@ export function mergeInvestigationTemplates(localRaw, incomingRaw) {
126
153
  const byId = new Map(local.map((item) => [item.id, item]));
127
154
  let added = 0;
128
155
  let updated = 0;
156
+ let skipped = 0;
129
157
  for (const item of incoming) {
130
- if (byId.has(item.id))
158
+ const existing = byId.get(item.id);
159
+ if (existing) {
160
+ if (item.updatedAt <= existing.updatedAt) {
161
+ skipped += 1;
162
+ continue;
163
+ }
131
164
  updated += 1;
132
- else
165
+ }
166
+ else {
167
+ if (byId.size >= MAX_INVESTIGATION_TEMPLATES) {
168
+ skipped += 1;
169
+ continue;
170
+ }
133
171
  added += 1;
172
+ }
134
173
  byId.set(item.id, item);
135
174
  }
136
175
  const templates = normalizeInvestigationTemplateStore([...byId.values()]).templates;
@@ -138,7 +177,7 @@ export function mergeInvestigationTemplates(localRaw, incomingRaw) {
138
177
  templates,
139
178
  added,
140
179
  updated,
141
- skipped: Math.max(0, incoming.length - added - updated),
142
- pruned: Math.max(0, byId.size - templates.length),
180
+ skipped,
181
+ pruned: 0,
143
182
  };
144
183
  }
@@ -2,15 +2,15 @@
2
2
  // deliberately accepts only bounded, normalized page-identity fields. Raw
3
3
  // HTML, URLs, headers, redirect inventories, and parser diagnostics never
4
4
  // cross this storage boundary.
5
- import { normalizeDomain } from "../cases/case-model.mjs";
6
- import { isInformativePerceptualHash as isInformativeFaviconHash } from "../../lib/perceptual-hash-comparison.mjs";
5
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
6
+ import { isInformativePerceptualHash as isInformativeFaviconHash } from "../analysis/perceptual-hash-comparison.mjs";
7
7
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
8
- import { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_VERSION, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
9
- export { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_VERSION, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
8
+ import { PAGE_FINGERPRINT_TOKEN_LIMITS } from "../contracts/page-fingerprints.mjs";
9
+ import { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_PARSERS, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
10
+ export { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_VERSION, PAGE_FINGERPRINT_PARSERS, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
10
11
  const SHA256_RE = /^[a-f0-9]{64}$/i;
11
12
  const SIMHASH_RE = /^[a-f0-9]{16}$/i;
12
13
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
13
- const MAX_HTML_TOKENS = 4096;
14
14
  const MAX_TEXT_TOKENS = 8192;
15
15
  const MAX_FORMS = 50;
16
16
  const MAX_FORM_CONTROLS = 500;
@@ -50,12 +50,12 @@ function shaComponent(raw, countKey, maximum) {
50
50
  truncated: value.truncated === true,
51
51
  };
52
52
  }
53
- function visibleTextComponent(raw) {
53
+ function visibleTextComponent(raw, maximum = MAX_TEXT_TOKENS) {
54
54
  const value = record(raw);
55
55
  if (!value || value.algorithm !== 'simhash64-v1' || typeof value.value !== 'string' || !SIMHASH_RE.test(value.value))
56
56
  return null;
57
- const tokenCount = count(value.tokenCount, MAX_TEXT_TOKENS);
58
- const featureCount = count(value.featureCount, MAX_TEXT_TOKENS);
57
+ const tokenCount = count(value.tokenCount, maximum);
58
+ const featureCount = count(value.featureCount, maximum);
59
59
  if (tokenCount === null || featureCount === null)
60
60
  return null;
61
61
  return {
@@ -66,20 +66,21 @@ function visibleTextComponent(raw) {
66
66
  truncated: value.truncated === true,
67
67
  };
68
68
  }
69
- function domComponent(raw) {
69
+ function domComponent(raw, fingerprintVersion) {
70
70
  const value = record(raw);
71
- if (!value || value.algorithm !== 'sha256' || value.parser !== 'static-tag-sequence-v1')
71
+ if (!value || value.algorithm !== 'sha256' || value.parser !== PAGE_FINGERPRINT_PARSERS[fingerprintVersion])
72
72
  return null;
73
73
  const digest = sha256(value.value);
74
- const nodeCount = count(value.nodeCount, MAX_HTML_TOKENS);
74
+ const maximum = PAGE_FINGERPRINT_TOKEN_LIMITS[fingerprintVersion];
75
+ const nodeCount = count(value.nodeCount, maximum);
75
76
  if (!digest || nodeCount === null)
76
77
  return null;
77
- const similarity = value.similarity == null ? null : visibleTextComponent(value.similarity);
78
+ const similarity = value.similarity == null ? null : visibleTextComponent(value.similarity, maximum);
78
79
  return {
79
80
  algorithm: 'sha256',
80
81
  value: digest,
81
82
  nodeCount,
82
- parser: 'static-tag-sequence-v1',
83
+ parser: PAGE_FINGERPRINT_PARSERS[fingerprintVersion],
83
84
  truncated: value.truncated === true,
84
85
  ...(value.similarity == null ? {} : { similarity }),
85
86
  };
@@ -175,11 +176,13 @@ export function normalizePageBaseline(raw) {
175
176
  const lookupDomain = normalizeDomain(value.lookupDomain);
176
177
  const observedAt = timestamp(value.observedAt);
177
178
  const pageIdentityVersion = count(value.pageIdentityVersion, PAGE_IDENTITY_VERSION);
178
- const fingerprintVersion = count(value.fingerprintVersion, PAGE_FINGERPRINT_VERSION);
179
- const normalizedHtml = shaComponent(value.normalizedHtml, 'tokenCount', MAX_HTML_TOKENS);
180
- const domStructure = domComponent(value.domStructure);
179
+ if (typeof value.fingerprintVersion !== 'number' || !Object.hasOwn(PAGE_FINGERPRINT_PARSERS, value.fingerprintVersion))
180
+ return null;
181
+ const fingerprintVersion = value.fingerprintVersion;
182
+ const normalizedHtml = shaComponent(value.normalizedHtml, 'tokenCount', PAGE_FINGERPRINT_TOKEN_LIMITS[fingerprintVersion]);
183
+ const domStructure = domComponent(value.domStructure, fingerprintVersion);
181
184
  if (!domain || !lookupDomain || !observedAt || pageIdentityVersion !== PAGE_IDENTITY_VERSION
182
- || fingerprintVersion !== PAGE_FINGERPRINT_VERSION || !normalizedHtml || !domStructure)
185
+ || !normalizedHtml || !domStructure)
183
186
  return null;
184
187
  const visibleText = value.visibleText == null ? null : visibleTextComponent(value.visibleText);
185
188
  const formStructure = value.formStructure == null ? null : formComponent(value.formStructure);
@@ -1,16 +1,11 @@
1
- import { normalizeDomain } from "../cases/case-model.mjs";
1
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
2
2
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
+ import { RELATIONSHIP_EVIDENCE_VERSION, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, RELATIONSHIP_PUBLIC_ADDRESS_VERSION } from "../contracts/offline-comparison.mjs";
4
+ import { RELATIONSHIP_TYPES, qualifyRelationshipSources, isNonPublicAddressRelationship, NON_PUBLIC_RELATIONSHIP_LIMITATION, FAVICON_RELATIONSHIP_DESCRIPTION } from "../comparison/relationship-provenance.mjs";
3
5
  import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
4
6
  import { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
5
7
  export { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
6
- export const RELATIONSHIP_OBSERVATION_TYPES = Object.freeze([
7
- 'nameserver_set',
8
- 'ip_address',
9
- 'certificate',
10
- 'tracking_identifier',
11
- 'favicon',
12
- 'official_asset',
13
- ]);
8
+ export const RELATIONSHIP_OBSERVATION_TYPES = RELATIONSHIP_TYPES;
14
9
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
15
10
  const SAFE_ID_RE = /^relationship-[a-z0-9]{1,16}-[a-z0-9]{1,16}$/u;
16
11
  const SHA256_RE = /^[a-f0-9]{64}$/iu;
@@ -39,9 +34,9 @@ const TYPE_METADATA = Object.freeze({
39
34
  description: 'These pages exposed the same recognised public tracking identifier in bounded static HTML.',
40
35
  }),
41
36
  favicon: Object.freeze({
42
- label: 'Similar favicon',
37
+ label: 'Connected favicon matches',
43
38
  method: 'Exact SHA-256 or perceptual dHash distance ≤ 6',
44
- description: 'These domains used an identical or perceptually similar favicon in the retained scan.',
39
+ description: FAVICON_RELATIONSHIP_DESCRIPTION,
45
40
  }),
46
41
  official_asset: Object.freeze({
47
42
  label: 'Official asset host match',
@@ -205,7 +200,7 @@ export function relationshipObservationId(raw) {
205
200
  }
206
201
  export function relationshipObservationStoreVersion(raw) {
207
202
  const value = record(raw);
208
- return positiveInteger(value?.version);
203
+ return typeof value?.version === 'number' && Number.isSafeInteger(value.version) && value.version > 0 ? value.version : null;
209
204
  }
210
205
  function observationList(raw) {
211
206
  if (Array.isArray(raw))
@@ -213,7 +208,7 @@ function observationList(raw) {
213
208
  const value = record(raw);
214
209
  return Array.isArray(value?.observations) ? value.observations : [];
215
210
  }
216
- export function normalizeRelationshipObservation(raw) {
211
+ export function normalizeRelationshipObservation(raw, sourceStoreVersion = RELATIONSHIP_OBSERVATION_SCHEMA_VERSION) {
217
212
  const value = record(raw);
218
213
  if (!value || typeof value.type !== 'string' || !TYPES.has(value.type))
219
214
  return null;
@@ -230,34 +225,50 @@ export function normalizeRelationshipObservation(raw) {
230
225
  const id = observationId(canonical);
231
226
  const observedAt = timestamp(value.observedAt);
232
227
  const retainedAt = timestamp(value.retainedAt);
233
- if (!observedAt || !retainedAt)
228
+ if (!retainedAt || (value.observedAt !== null && !observedAt))
234
229
  return null;
230
+ const sourceVersion = positiveInteger(value.sourceVersion) || 1;
231
+ if (value.sourceVersion !== undefined && !positiveInteger(value.sourceVersion))
232
+ throw new TypeError('This retained relationship uses an unsupported source-evidence version; no evidence was interpreted.');
233
+ if (sourceVersion > RELATIONSHIP_EVIDENCE_VERSION)
234
+ throw new TypeError('This retained relationship uses a newer source-evidence version; no evidence was interpreted.');
235
+ if (sourceStoreVersion === 1 && sourceVersion > 2)
236
+ throw new TypeError('Relationship schema 1 cannot contain newer source evidence; no evidence was interpreted.');
237
+ const qualified = qualifyRelationshipSources(sourceVersion >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? value.sourceEvidence : [], domains, { truncated: value.truncated === true, type });
238
+ const nonPublicAddress = isNonPublicAddressRelationship(type, normalizedValue);
235
239
  return {
236
240
  id,
237
241
  type,
238
- label: metadata.label,
242
+ label: nonPublicAddress ? 'Shared non-public DNS answer' : metadata.label,
239
243
  method: metadata.method,
240
244
  normalizedValue,
241
245
  displayValue: type === 'favicon' ? '' : normalizedValue,
242
246
  domains,
243
- description: metadata.description,
247
+ description: nonPublicAddress ? NON_PUBLIC_RELATIONSHIP_LIMITATION : metadata.description,
244
248
  classification: 'derived',
245
249
  source: 'bulk_relationship_analysis',
246
- sourceVersion: positiveInteger(value.sourceVersion) || 1,
247
- observedAt,
250
+ sourceVersion,
251
+ // Legacy scan timestamps remain available, but cannot qualify missing
252
+ // contributing-source timestamps or turn the old complete flag into proof.
253
+ observedAt: qualified.observedAt ?? (observedAt || null),
254
+ sourceEvidence: qualified.sourceEvidence,
248
255
  retainedAt,
249
- complete: value.complete === true,
250
- truncated: value.truncated === true
256
+ complete: value.complete === true && qualified.complete,
257
+ truncated: qualified.truncated
251
258
  || (Array.isArray(value.domains) && value.domains.length > MAX_RELATIONSHIP_OBSERVATION_DOMAINS),
252
- limitations: observationLimitations(value.limitations),
259
+ limitations: observationLimitations([...(nonPublicAddress ? [NON_PUBLIC_RELATIONSHIP_LIMITATION] : []), ...(Array.isArray(value.limitations) ? value.limitations : [])]),
253
260
  };
254
261
  }
255
262
  export function normalizeRelationshipObservationStore(raw) {
256
263
  assertWorkspaceInputGraph(raw, 'Relationship-observation store');
257
264
  assertWorkspaceDeclaredVersion(raw, 'Relationship-observation store');
265
+ const sourceStoreVersion = relationshipObservationStoreVersion(raw) ?? RELATIONSHIP_OBSERVATION_SCHEMA_VERSION;
266
+ if (sourceStoreVersion > RELATIONSHIP_OBSERVATION_SCHEMA_VERSION) {
267
+ throw new TypeError(`This relationship-observation store uses newer schema ${sourceStoreVersion}; no evidence was interpreted.`);
268
+ }
258
269
  const byId = new Map();
259
270
  for (const candidate of observationList(raw).slice(0, MAX_RELATIONSHIP_OBSERVATION_INPUTS)) {
260
- const observation = normalizeRelationshipObservation(candidate);
271
+ const observation = normalizeRelationshipObservation(candidate, sourceStoreVersion);
261
272
  if (!observation)
262
273
  continue;
263
274
  const existing = byId.get(observation.id);
@@ -283,31 +294,40 @@ export function createRelationshipObservation(raw, options = {}) {
283
294
  throw new Error('That relationship does not contain a supported bounded value and at least one valid domain.');
284
295
  }
285
296
  const now = new Date().toISOString();
286
- const observedAt = Object.hasOwn(options, 'observedAt') ? timestamp(options.observedAt) : now;
297
+ const sourceVersion = Object.hasOwn(options, 'sourceVersion') ? positiveInteger(options.sourceVersion) : RELATIONSHIP_EVIDENCE_VERSION;
298
+ if (!sourceVersion || sourceVersion > RELATIONSHIP_EVIDENCE_VERSION)
299
+ throw new TypeError('This relationship needs a supported source-evidence version.');
300
+ const nonPublicAddress = isNonPublicAddressRelationship(type, normalizedValue);
301
+ if (nonPublicAddress && sourceVersion >= RELATIONSHIP_PUBLIC_ADDRESS_VERSION)
302
+ throw new TypeError('Non-public DNS answers cannot be retained as new public-infrastructure relationships.');
303
+ const qualified = qualifyRelationshipSources(sourceVersion >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? raw.sourceEvidence : [], domains, { truncated: options.truncated === true, type });
304
+ // Older callers can supply their original scan timestamp. It does not
305
+ // qualify the missing source metadata, and an explicit current projection
306
+ // never substitutes that timestamp for unknown contributing-source times.
307
+ const observedAt = qualified.observedAt
308
+ ?? (!Object.hasOwn(raw, 'sourceEvidence') ? timestamp(options.observedAt) || null : null);
287
309
  const retainedAt = timestamp(options.retainedAt) || now;
288
- if (!observedAt) {
289
- throw new Error('A relationship observation needs the contributing scan observation time; an unknown time is not replaced with the retention time.');
290
- }
291
310
  const canonical = canonicalIdentity(type, normalizedValue, domains);
292
311
  const metadata = TYPE_METADATA[type];
293
312
  return {
294
313
  id: observationId(canonical),
295
314
  type,
296
- label: metadata.label,
315
+ label: nonPublicAddress ? 'Shared non-public DNS answer' : metadata.label,
297
316
  method: metadata.method,
298
317
  normalizedValue,
299
318
  displayValue: type === 'favicon' ? '' : normalizedValue,
300
319
  domains,
301
- description: metadata.description,
320
+ description: nonPublicAddress ? NON_PUBLIC_RELATIONSHIP_LIMITATION : metadata.description,
302
321
  classification: 'derived',
303
322
  source: 'bulk_relationship_analysis',
304
- sourceVersion: positiveInteger(options.sourceVersion) || 1,
323
+ sourceVersion,
305
324
  observedAt,
325
+ sourceEvidence: qualified.sourceEvidence,
306
326
  retainedAt,
307
- complete: options.complete === true,
308
- truncated: options.truncated === true
327
+ complete: options.complete !== false && qualified.complete,
328
+ truncated: qualified.truncated
309
329
  || (Array.isArray(raw.domains) && raw.domains.length > MAX_RELATIONSHIP_OBSERVATION_DOMAINS),
310
- limitations: observationLimitations(options.limitations),
330
+ limitations: observationLimitations([...(nonPublicAddress ? [NON_PUBLIC_RELATIONSHIP_LIMITATION] : []), ...(Array.isArray(options.limitations) ? options.limitations : [])]),
311
331
  };
312
332
  }
313
333
  function byteLength(value) {
@@ -359,12 +379,11 @@ export function deleteRelationshipObservation(rawStore, id) {
359
379
  export function mergeRelationshipObservations(localRaw, importedRaw) {
360
380
  assertWorkspaceInputGraph(localRaw, 'Local relationship-observation store');
361
381
  assertWorkspaceInputGraph(importedRaw, 'Imported relationship-observation document');
362
- assertWorkspacePortableVersion(importedRaw, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, 'Imported relationship-observation document');
382
+ const importedVersion = assertWorkspacePortableVersion(importedRaw, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, 'Imported relationship-observation document');
363
383
  const importedRecord = record(importedRaw);
364
384
  if (importedRecord?.schema !== undefined && importedRecord.schema !== RELATIONSHIP_OBSERVATION_SCHEMA) {
365
385
  throw new Error('This JSON section is not a WHOISleuth relationship-observation export.');
366
386
  }
367
- const importedVersion = relationshipObservationStoreVersion(importedRaw);
368
387
  if (importedVersion !== null && importedVersion > RELATIONSHIP_OBSERVATION_SCHEMA_VERSION) {
369
388
  throw new Error(`This relationship-observation section uses newer schema ${importedVersion}. Update the app before importing it.`);
370
389
  }
@@ -375,7 +394,7 @@ export function mergeRelationshipObservations(localRaw, importedRaw) {
375
394
  let updated = 0;
376
395
  let skipped = Math.max(0, importedInput.length - MAX_RELATIONSHIP_OBSERVATION_INPUTS);
377
396
  for (const candidate of importedInput.slice(0, MAX_RELATIONSHIP_OBSERVATION_INPUTS)) {
378
- const observation = normalizeRelationshipObservation(candidate);
397
+ const observation = normalizeRelationshipObservation(candidate, importedVersion ?? RELATIONSHIP_OBSERVATION_SCHEMA_VERSION);
379
398
  if (!observation) {
380
399
  skipped += 1;
381
400
  continue;
@@ -2,8 +2,8 @@
2
2
  // and downloads; this module owns schema migration, record normalization,
3
3
  // import merging, and exact serialized-byte accounting.
4
4
  import { compactWatchlistResults } from "./watchlist-history.mjs";
5
- import { normalizeOpportunityModelVersion } from "../../lib/opportunity-scoring.mjs";
6
- import { normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
5
+ import { normalizeOpportunityModelVersion } from "../analysis/opportunity-scoring.mjs";
6
+ import { normalizeRiskModelVersion } from "../analysis/risk-scoring.mjs";
7
7
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
8
8
  import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
9
9
  import { MAX_SHORTLIST_ENTRIES, MAX_SHORTLIST_FACTORS, MAX_SHORTLIST_INPUTS, MAX_SHORTLIST_STORE_BYTES, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, SUPPORTED_SHORTLIST_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
@@ -188,14 +188,23 @@ export function mergeShortlistStores(localRaw, importedRaw) {
188
188
  skipped++;
189
189
  continue;
190
190
  }
191
- if (imported.has(record.domain))
191
+ const prior = imported.get(record.domain);
192
+ if (prior)
192
193
  skipped++;
193
- imported.set(record.domain, record);
194
+ if (!prior || Date.parse(record.savedAt) > Date.parse(prior.savedAt))
195
+ imported.set(record.domain, record);
194
196
  }
195
197
  let added = 0;
196
198
  let updated = 0;
197
199
  for (const record of imported.values()) {
198
- if (byDomain.has(record.domain))
200
+ const existing = byDomain.get(record.domain);
201
+ // A backup must not roll back an observation. Equal-time conflicts retain
202
+ // the local record rather than choosing whichever file was imported last.
203
+ if (existing && Date.parse(record.savedAt) <= Date.parse(existing.savedAt)) {
204
+ skipped++;
205
+ continue;
206
+ }
207
+ if (existing)
199
208
  updated++;
200
209
  else if (byDomain.size >= MAX_SHORTLIST_ENTRIES) {
201
210
  skipped++;