@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +83 -61
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,4 +1,4 @@
1
- import { PUBLIC_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
1
+ import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
2
2
  import { caseStatusIsClosed } from "./case-record-decisions.mjs";
3
3
  const PRESERVE = Object.freeze({
4
4
  durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
@@ -71,6 +71,12 @@ function publicClosureHistory(preV13HistoryUnavailable) {
71
71
  const CASE_FIELD_RULES = Object.freeze({
72
72
  id: preservedField('id'),
73
73
  domain: preservedField('domain'),
74
+ title: fieldRule('title', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? '' : record.title ?? ''), {
75
+ audienceExclusions: {
76
+ trusted: { label: 'Case titles', order: 1, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
77
+ public: { label: 'Case titles', order: 1, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
78
+ },
79
+ }),
74
80
  status: fieldRule('status', PUBLIC_TRANSFORM, (record, profile) => (profile === 'public' && caseStatusIsClosed(record.status) && record.closures.records.length
75
81
  ? 'reviewing'
76
82
  : record.status)),
@@ -133,6 +139,12 @@ const CASE_FIELD_RULES = Object.freeze({
133
139
  branches: fieldRule('branches', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? [] : structuredClone(record.branches ?? [])), {
134
140
  audienceExclusions: { public: { label: 'Investigation branches', order: 5 } },
135
141
  }),
142
+ attachments: fieldRule('attachments', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? undefined : structuredClone(record.attachments)), {
143
+ audienceExclusions: {
144
+ trusted: { label: 'Retained file references and provenance', order: 5, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
145
+ public: { label: 'Retained file references and provenance', order: 5, sinceVersion: INCIDENT_CASE_SCHEMA_VERSION },
146
+ },
147
+ }),
136
148
  createdAt: preservedField('createdAt'),
137
149
  updatedAt: preservedField('updatedAt'),
138
150
  });
@@ -140,10 +152,13 @@ function projectCase(record, profile) {
140
152
  const projected = {};
141
153
  for (const key of Object.keys(CASE_FIELD_RULES)) {
142
154
  const rule = CASE_FIELD_RULES[key];
155
+ const value = rule.value(record, profile);
156
+ if (value === undefined)
157
+ continue;
143
158
  Object.defineProperty(projected, key, {
144
159
  configurable: true,
145
160
  enumerable: true,
146
- value: rule.value(record, profile),
161
+ value,
147
162
  writable: true,
148
163
  });
149
164
  }
@@ -167,38 +182,26 @@ const OUTSIDE_SCHEMA_EXCLUSIONS = Object.freeze({
167
182
  trusted: Object.freeze({ label: 'Raw upstream payloads and credentials', order: 4 }),
168
183
  public: Object.freeze({ label: 'Raw upstream payloads and credentials', order: 7 }),
169
184
  });
170
- function currentAudienceExclusions(audience) {
185
+ function currentAudienceExclusions(audience, caseVersion) {
171
186
  const entries = [
172
187
  ...Object.values(CASE_FIELD_RULES).flatMap((rule) => (rule.audienceExclusions[audience] ? [rule.audienceExclusions[audience]] : [])),
173
188
  OUTSIDE_SCHEMA_EXCLUSIONS[audience],
174
189
  ];
175
190
  const orderByLabel = new Map();
176
- const labelByOrder = new Map();
177
191
  for (const entry of entries) {
192
+ if (entry.sinceVersion !== undefined && caseVersion < entry.sinceVersion)
193
+ continue;
178
194
  if (!entry.label || entry.label.length > 160 || entry.label.trim() !== entry.label
179
195
  || !Number.isSafeInteger(entry.order) || entry.order < 1 || entry.order > 64) {
180
196
  throw new TypeError('Case audience exclusion metadata is malformed or exceeds its bound.');
181
197
  }
182
198
  const prior = orderByLabel.get(entry.label);
183
- if (prior !== undefined && prior !== entry.order) {
184
- throw new TypeError(`Case audience exclusion ${entry.label} has conflicting presentation order.`);
185
- }
186
- const priorLabel = labelByOrder.get(entry.order);
187
- if (priorLabel !== undefined && priorLabel !== entry.label) {
188
- throw new TypeError(`Case audience exclusions ${priorLabel} and ${entry.label} share a presentation order.`);
189
- }
190
- orderByLabel.set(entry.label, entry.order);
191
- labelByOrder.set(entry.order, entry.label);
199
+ orderByLabel.set(entry.label, Math.min(prior ?? entry.order, entry.order));
192
200
  }
193
201
  return Object.freeze([...orderByLabel]
194
- .sort((left, right) => left[1] - right[1])
202
+ .sort((left, right) => left[1] - right[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0))
195
203
  .map(([label]) => label));
196
204
  }
197
- const CURRENT_AUDIENCE_EXCLUSIONS = Object.freeze({
198
- internal: currentAudienceExclusions('internal'),
199
- trusted: currentAudienceExclusions('trusted'),
200
- public: currentAudienceExclusions('public'),
201
- });
202
205
  // Schema 12 is an immutable public compatibility contract and deliberately
203
206
  // remains independent of the current field-policy projection.
204
207
  const PUBLIC_V12_EXCLUSIONS = Object.freeze([
@@ -213,6 +216,6 @@ const PUBLIC_V12_EXCLUSIONS = Object.freeze([
213
216
  export function caseAudienceExclusions(audience, caseVersion) {
214
217
  return audience === 'public' && caseVersion === PUBLIC_CASE_SCHEMA_VERSION
215
218
  ? PUBLIC_V12_EXCLUSIONS
216
- : CURRENT_AUDIENCE_EXCLUSIONS[audience];
219
+ : currentAudienceExclusions(audience, caseVersion);
217
220
  }
218
221
  export { CASE_FIELD_RULES };
@@ -8,18 +8,19 @@
8
8
  // contains raw RDAP/WHOIS material. This module deliberately excludes all raw
9
9
  // registry/web responses, contacts, cookies, screenshots, and authentication
10
10
  // data. Reports contain only the normalized case record.
11
- import { caseEvidenceIncomparableReasons, compareCaseEvidence, latestCaseEvidence } from "./case-model.mjs";
11
+ import { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence } from "./case-model.mjs";
12
12
  import { httpSecurityHeaderLabel } from "./http-summary.mjs";
13
13
  import { analystInteroperabilityTags } from "../../lib/analyst-taxonomy.mjs";
14
14
  import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../../lib/portable-generator.mjs";
15
15
  import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
16
16
  import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
17
- import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
17
+ import { CASE_RECHECK_CONDITIONS } from "./case-recheck-model.mjs";
18
+ import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
18
19
  // ---------------------------------------------------------------------------
19
20
  // Constants
20
21
  // ---------------------------------------------------------------------------
21
22
  export { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION };
22
- const LIMITATIONS_TEXT = [
23
+ const PUBLISHED_V2_3_LIMITATIONS = Object.freeze([
23
24
  'This report contains normalised browser-local observations from WHOISleuth analyst cases.',
24
25
  'It is not a live lookup and does not contain raw WHOIS, RDAP, DNS, HTML, or responses collected during website checks.',
25
26
  'Absence of a signal (e.g. no MX record observed) does not prove nonexistence. It may not have been evaluated.',
@@ -28,6 +29,11 @@ const LIMITATIONS_TEXT = [
28
29
  'Brand Profile references record an explicit analyst-selected association only; they do not establish ownership, attribution, intent, safety, or maliciousness.',
29
30
  'Provider workflow outcomes and independently observed technical effects remain separately attributed. A provider outcome does not establish independent remediation, absence, or safety; neither evidence family establishes legal sufficiency or provider performance.',
30
31
  'Generated locally in the browser. Review the package before sharing it.',
32
+ ]);
33
+ const LIMITATIONS_TEXT = [
34
+ ...PUBLISHED_V2_3_LIMITATIONS.slice(0, 2),
35
+ 'Snapshot hostnames are excluded; supporting DNS, TLS and web observations need not concern the Case registration domain.',
36
+ ...PUBLISHED_V2_3_LIMITATIONS.slice(2),
31
37
  ].join(' ');
32
38
  function escapeMarkdownInline(text) {
33
39
  return String(text)
@@ -100,6 +106,7 @@ function pickKnownSnapshotFields(snapshot) {
100
106
  return {
101
107
  id: snapshot.id,
102
108
  fingerprint: snapshot.fingerprint,
109
+ ...(snapshot.factorOrder ? { factorOrder: snapshot.factorOrder } : {}),
103
110
  firstCapturedAt: snapshot.firstCapturedAt,
104
111
  capturedAt: snapshot.capturedAt,
105
112
  source: snapshot.source,
@@ -165,51 +172,17 @@ export function buildCaseReport(caseRecord, options = {}) {
165
172
  const now = generatedAt || new Date().toISOString();
166
173
  const generator = buildPortableGeneratorMetadata(options.applicationVersion);
167
174
  // --- Build JSON report ---
168
- const timelineEntries = [];
169
- if (Array.isArray(caseRecord.evidenceHistory) && caseRecord.evidenceHistory.length > 0) {
170
- const chronological = [...caseRecord.evidenceHistory];
171
- for (let i = 0; i < chronological.length; i++) {
172
- const snapshot = chronological[i];
173
- if (!snapshot)
174
- continue;
175
- const isBaseline = i === 0;
176
- const hasRepeatedObservation = snapshot.firstCapturedAt !== snapshot.capturedAt;
177
- let changes = null;
178
- let hasIncomparableChange = false;
179
- let incomparableReasons = [];
180
- if (!isBaseline) {
181
- const previous = chronological[i - 1];
182
- if (!previous)
183
- continue;
184
- const rawChanges = compareCaseEvidence(previous, snapshot);
185
- incomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
186
- const hostnameContextChanged = incomparableReasons.includes('observation-context');
187
- if (rawChanges.length > 0 && !hostnameContextChanged) {
188
- changes = rawChanges.map((change) => ({
189
- field: change.field,
190
- label: change.label,
191
- before: change.before,
192
- after: change.after,
193
- tone: change.tone,
194
- }));
195
- }
196
- else if (snapshot.fingerprint !== previous.fingerprint
197
- && incomparableReasons.length === 0) {
198
- incomparableReasons = ['other'];
199
- }
200
- hasIncomparableChange = incomparableReasons.length > 0;
201
- }
202
- timelineEntries.push({
203
- snapshot: pickKnownSnapshotFields(snapshot),
204
- isBaseline,
205
- hasRepeatedObservation,
206
- changes,
207
- hasIncomparableChange,
208
- incomparableReasons,
209
- });
210
- }
211
- }
212
- const latest = latestCaseEvidence({ evidenceHistory: caseRecord.evidenceHistory ?? undefined });
175
+ const timeline = caseEvidenceTimeline(caseRecord.evidenceHistory);
176
+ const timelineEntries = timeline.map((entry) => ({
177
+ snapshot: pickKnownSnapshotFields(entry.snapshot),
178
+ isBaseline: entry.isBaseline,
179
+ hasRepeatedObservation: entry.hasRepeatedObservation,
180
+ changes: entry.changes?.map(({ field, label, before, after, tone }) => ({ field, label, before, after, tone })) ?? null,
181
+ hasIncomparableChange: entry.hasIncomparableChange,
182
+ incomparableReasons: [...entry.incomparableReasons],
183
+ }));
184
+ const selection = currentCaseEvidence(caseRecord);
185
+ const latest = selection.snapshot;
213
186
  const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
214
187
  const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
215
188
  const observedEffects = caseRecord.observedEffects ?? {
@@ -231,6 +204,7 @@ export function buildCaseReport(caseRecord, options = {}) {
231
204
  application: generator,
232
205
  case: {
233
206
  id: caseRecord.id,
207
+ title: caseRecord.title ?? '',
234
208
  domain: caseRecord.domain,
235
209
  status: caseRecord.status,
236
210
  disposition: caseRecord.disposition,
@@ -290,12 +264,28 @@ export function buildCaseReport(caseRecord, options = {}) {
290
264
  })),
291
265
  },
292
266
  responseLifecycle,
293
- limitations: LIMITATIONS_TEXT,
267
+ limitations: [LIMITATIONS_TEXT, selection.limitation, ...new Set(timeline.map((entry) => entry.orderingLimitation))].filter(Boolean).join(' '),
294
268
  };
295
269
  // --- Build Markdown ---
296
270
  const md = buildMarkdown(json, options.includeAttribution !== false);
297
271
  return { json, markdown: md };
298
272
  }
273
+ /** Expected strict reader projection; historical disclosure wording is immutable. */
274
+ export function buildCaseReportVerificationProjection(caseRecord, options, schemaVersion) {
275
+ const current = buildCaseReport(caseRecord, options).json;
276
+ if (schemaVersion === CASE_REPORT_SCHEMA_VERSION)
277
+ return current;
278
+ if (schemaVersion !== PUBLISHED_V2_3_CASE_REPORT_SCHEMA_VERSION) {
279
+ throw new TypeError('No strict Case report projection is defined for this version.');
280
+ }
281
+ const { title: _title, ...publishedCase } = current.case;
282
+ return {
283
+ ...current,
284
+ case: publishedCase,
285
+ schemaVersion,
286
+ limitations: PUBLISHED_V2_3_LIMITATIONS.join(' ') + current.limitations.slice(LIMITATIONS_TEXT.length),
287
+ };
288
+ }
299
289
  // ---------------------------------------------------------------------------
300
290
  // Markdown generation
301
291
  // ---------------------------------------------------------------------------
@@ -310,6 +300,11 @@ function buildMarkdown(report, includeAttribution) {
310
300
  const domain = escapeMarkdownInline(report.case.domain || 'unknown');
311
301
  lines.push(`# Case Report: ${domain}`);
312
302
  lines.push('');
303
+ if (report.case.title) {
304
+ lines.push(`**Incident:** ${escapeMarkdownInline(report.case.title)}`);
305
+ lines.push('');
306
+ }
307
+ lines.push(`**Case ID:** ${escapeMarkdownInline(report.case.id)}`);
313
308
  // Metadata
314
309
  lines.push(`**Generated:** ${escapeMarkdownInline(report.generatedAt)}`);
315
310
  lines.push(`**Report schema:** ${escapeMarkdownInline(report.schema)} v${report.schemaVersion}`);
@@ -355,7 +350,9 @@ function buildMarkdown(report, includeAttribution) {
355
350
  lines.push(`- **Source:** ${escapeMarkdownInline(formatReportValue(a.source))}`);
356
351
  }
357
352
  else {
358
- lines.push('No evidence captured.');
353
+ lines.push(report.evidenceTimeline.length
354
+ ? 'Retained snapshots have no unique latest assessment. Review the timeline and limitations.'
355
+ : 'No evidence captured.');
359
356
  }
360
357
  lines.push('');
361
358
  // Evidence timeline
@@ -478,7 +475,7 @@ function buildMarkdown(report, includeAttribution) {
478
475
  lines.push('No evidence pins recorded.');
479
476
  for (const pin of response.evidencePins) {
480
477
  lines.push(`- **${escapeMarkdownInline(pin.label)}:** ${escapeMarkdownInline(pin.value)}`);
481
- lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt)}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
478
+ lines.push(` Source: ${escapeMarkdownInline(pin.source)}; observed ${escapeMarkdownInline(pin.observedAt ?? 'Time unavailable')}; completeness ${escapeMarkdownInline(pin.completeness)}.`);
482
479
  if (pin.limitations.length)
483
480
  lines.push(` Limitations: ${escapeMarkdownInline(pin.limitations.join('; '))}`);
484
481
  }
@@ -488,7 +485,7 @@ function buildMarkdown(report, includeAttribution) {
488
485
  if (!response.sightings.length)
489
486
  lines.push('No source-qualified sightings recorded.');
490
487
  for (const sighting of response.sightings) {
491
- lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt)})`);
488
+ lines.push(`- **${escapeMarkdownInline(sighting.state.replaceAll('_', ' '))}:** ${escapeMarkdownInline(sighting.category)} (${escapeMarkdownInline(sighting.observedAt ?? 'Observation time unavailable')})`);
492
489
  lines.push(` Source: ${escapeMarkdownInline(sighting.source)}; class ${escapeMarkdownInline(sighting.sourceClass)}; completeness ${escapeMarkdownInline(sighting.completeness)}.`);
493
490
  if (sighting.evidencePinId)
494
491
  lines.push(` Evidence pin: ${escapeMarkdownInline(sighting.evidencePinId)}`);
@@ -512,6 +509,11 @@ function buildMarkdown(report, includeAttribution) {
512
509
  lines.push(`- **${escapeMarkdownInline(assertion.state)}:** ${escapeMarkdownInline(assertion.statement)}`);
513
510
  if (assertion.rationale)
514
511
  lines.push(` ${escapeMarkdownInline(assertion.rationale)}`);
512
+ if (assertion.recheck) {
513
+ lines.push(` Recheck target: ${escapeMarkdownInline(assertion.recheck.targetHostname)}; comparison conditions: ${escapeMarkdownInline(assertion.recheck.conditions)}`);
514
+ if (assertion.recheck.baselinePinId)
515
+ lines.push(` Baseline evidence pin: ${escapeMarkdownInline(assertion.recheck.baselinePinId)}`);
516
+ }
515
517
  if (assertion.provenance) {
516
518
  lines.push(` External provenance: ${escapeMarkdownInline(assertion.provenance.format.toUpperCase())}; source ${escapeMarkdownInline(assertion.provenance.sourceName)}; file SHA-256 ${escapeMarkdownInline(assertion.provenance.sourceDigestSha256)}.`);
517
519
  if (assertion.provenance.publisher)
@@ -623,10 +625,19 @@ function buildMarkdown(report, includeAttribution) {
623
625
  : `- Latest independently observed change time: Withheld because the independent change state is ${escapeMarkdownInline(report.responseLifecycle.observedChangeState)}.`);
624
626
  if (observedEffect)
625
627
  lines.push(`- Latest independent review: ${escapeMarkdownInline(observedEffect.state.replaceAll('_', ' '))} at ${escapeMarkdownInline(observedEffect.observedAt)} from ${escapeMarkdownInline(observedEffect.source)}.`);
628
+ else if (response.observedEffects.reviews.length)
629
+ lines.push('- A single latest independent review cannot be selected from the retained observation times. Review the individual records below.');
626
630
  if (!response.observedEffects.reviews.length)
627
631
  lines.push('- No independent observed-effect review recorded.');
628
632
  for (const review of response.observedEffects.reviews) {
629
633
  lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
634
+ if (review.recheck) {
635
+ lines.push(` Question ${escapeMarkdownInline(review.recheck.questionId)}: ${escapeMarkdownInline(review.recheck.question)}`);
636
+ lines.push(` Recheck target: ${escapeMarkdownInline(review.recheck.targetHostname)}; retained comparison conditions: ${escapeMarkdownInline(review.recheck.conditions)}`);
637
+ lines.push(` Condition comparison: ${escapeMarkdownInline(CASE_RECHECK_CONDITIONS[review.recheck.conditionsMatch])}`);
638
+ if (review.recheck.baselinePinId)
639
+ lines.push(` Baseline evidence pin: ${escapeMarkdownInline(review.recheck.baselinePinId)}`);
640
+ }
630
641
  if (review.evidencePinId)
631
642
  lines.push(` Evidence pin: ${escapeMarkdownInline(review.evidencePinId)}`);
632
643
  if (review.sightingId)