@slicedearth/whoisleuth-cli 1.32.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 (189) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
  3. package/NOTICE +76 -0
  4. package/README.md +84 -0
  5. package/TRADEMARKS.md +41 -0
  6. package/bin/whoisleuth.mjs +35 -0
  7. package/cli/archive-inspect.mjs +198 -0
  8. package/cli/arguments.mjs +1022 -0
  9. package/cli/artifact-verify.mjs +210 -0
  10. package/cli/bulk-checkpoint.mjs +218 -0
  11. package/cli/bulk-command-runner.mjs +109 -0
  12. package/cli/bulk-output.mjs +112 -0
  13. package/cli/bulk.mjs +133 -0
  14. package/cli/compare.mjs +238 -0
  15. package/cli/completion.mjs +175 -0
  16. package/cli/csv.mjs +1 -0
  17. package/cli/discover.mjs +50 -0
  18. package/cli/discovery-command-runner.mjs +36 -0
  19. package/cli/discovery-observation-snapshot.mjs +315 -0
  20. package/cli/discovery-scan-command-runner.mjs +141 -0
  21. package/cli/discovery-scan.mjs +325 -0
  22. package/cli/discovery-snapshot.mjs +133 -0
  23. package/cli/discovery-workflow.mjs +73 -0
  24. package/cli/doctor.mjs +137 -0
  25. package/cli/errors.mjs +21 -0
  26. package/cli/evidence-command-arguments.mjs +145 -0
  27. package/cli/evidence-command-runner.mjs +106 -0
  28. package/cli/evidence-signing.mjs +215 -0
  29. package/cli/exit-codes.mjs +9 -0
  30. package/cli/export-evidence.mjs +28 -0
  31. package/cli/formatters/evidence-report.mjs +3 -0
  32. package/cli/formatters/html.mjs +69 -0
  33. package/cli/formatters/json.mjs +135 -0
  34. package/cli/formatters/markdown.mjs +3 -0
  35. package/cli/formatters/terminal.mjs +658 -0
  36. package/cli/http.mjs +27 -0
  37. package/cli/lookup-command-runner.mjs +87 -0
  38. package/cli/lookup-diff.mjs +171 -0
  39. package/cli/mail-review.mjs +204 -0
  40. package/cli/manual.mjs +34 -0
  41. package/cli/network-command-runner.mjs +88 -0
  42. package/cli/output-file.mjs +97 -0
  43. package/cli/page-compare.mjs +123 -0
  44. package/cli/posture.mjs +20 -0
  45. package/cli/progress-events.mjs +53 -0
  46. package/cli/progress.mjs +56 -0
  47. package/cli/registry-access.mjs +3 -0
  48. package/cli/registry-support.mjs +120 -0
  49. package/cli/risk-calibration.mjs +294 -0
  50. package/cli/runner-types.mjs +1 -0
  51. package/cli/runner.mjs +726 -0
  52. package/cli/saved-lookup.mjs +87 -0
  53. package/cli/source-reliability.mjs +727 -0
  54. package/cli/strict-exit.mjs +55 -0
  55. package/cli/terminal-presentation.mjs +147 -0
  56. package/docs/cli.md +827 -0
  57. package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
  58. package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
  59. package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
  60. package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
  61. package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
  62. package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
  63. package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
  64. package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
  65. package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
  66. package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
  67. package/frontend/src/lib/analysis/bulk-sort.js +49 -0
  68. package/frontend/src/lib/analysis/campaign-model.js +265 -0
  69. package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
  70. package/frontend/src/lib/analysis/case-migration-model.js +248 -0
  71. package/frontend/src/lib/analysis/case-model.js +5 -0
  72. package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
  73. package/frontend/src/lib/analysis/case-record-core.js +232 -0
  74. package/frontend/src/lib/analysis/case-record-model.js +7 -0
  75. package/frontend/src/lib/analysis/case-record-operations.js +224 -0
  76. package/frontend/src/lib/analysis/case-response-model.js +640 -0
  77. package/frontend/src/lib/analysis/case-response-packet.js +573 -0
  78. package/frontend/src/lib/analysis/case-storage-model.js +93 -0
  79. package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
  80. package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
  81. package/frontend/src/lib/analysis/http-summary.js +165 -0
  82. package/frontend/src/lib/analysis/investigation-guide.js +500 -0
  83. package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
  84. package/frontend/src/lib/analysis/page-baseline.js +272 -0
  85. package/frontend/src/lib/analysis/page-similarity.js +179 -0
  86. package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
  87. package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
  88. package/frontend/src/lib/analysis/scoring.js +135 -0
  89. package/frontend/src/lib/analysis/shortlist-model.js +209 -0
  90. package/frontend/src/lib/analysis/utils.js +265 -0
  91. package/frontend/src/lib/analysis/watchlist-history.js +643 -0
  92. package/frontend/src/lib/analysis/watchlist-store.js +127 -0
  93. package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
  94. package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
  95. package/frontend/src/lib/analysis/workspace-archive.js +472 -0
  96. package/lib/abort.mjs +26 -0
  97. package/lib/abusech-auth.mjs +19 -0
  98. package/lib/availability.mjs +716 -0
  99. package/lib/bounded-contract-normalizers.mjs +61 -0
  100. package/lib/browser-library-profile.mjs +310 -0
  101. package/lib/bulk-comparison-evidence.mjs +83 -0
  102. package/lib/classify.mjs +93 -0
  103. package/lib/client-behavior-profile.mjs +83 -0
  104. package/lib/credential-surface-profile.mjs +66 -0
  105. package/lib/csv.mjs +23 -0
  106. package/lib/ct-query.mjs +27 -0
  107. package/lib/ct-search.mjs +393 -0
  108. package/lib/distributed-operation-budget.mjs +461 -0
  109. package/lib/dns-delegation-health.mjs +344 -0
  110. package/lib/dns-intelligence.mjs +739 -0
  111. package/lib/dns-mx.mjs +24 -0
  112. package/lib/dns-resolver-selection.mjs +33 -0
  113. package/lib/domain-posture-analysis.mjs +254 -0
  114. package/lib/domain-posture-parsers.mjs +358 -0
  115. package/lib/domain-posture.mjs +745 -0
  116. package/lib/error-detail.mjs +10 -0
  117. package/lib/evidence-export.mjs +431 -0
  118. package/lib/evidence-report-markdown.mjs +71 -0
  119. package/lib/evidence-report.mjs +391 -0
  120. package/lib/external-intelligence-risk.mjs +108 -0
  121. package/lib/favicon.mjs +146 -0
  122. package/lib/feature-policy.mjs +70 -0
  123. package/lib/generated/retire-browser-catalog.mjs +8724 -0
  124. package/lib/generated/unicode-confusables-17.mjs +93 -0
  125. package/lib/hostname.mjs +38 -0
  126. package/lib/html-signals.mjs +680 -0
  127. package/lib/http-intelligence.mjs +232 -0
  128. package/lib/idn-confusable-policy.mjs +86 -0
  129. package/lib/idn-confusables.mjs +465 -0
  130. package/lib/lookup-cache.mjs +130 -0
  131. package/lib/lookup-progress.mjs +291 -0
  132. package/lib/lookup-source-progress.mjs +109 -0
  133. package/lib/lookup-threat-provider-inventory.mjs +13 -0
  134. package/lib/lookup.mjs +592 -0
  135. package/lib/observation.mjs +114 -0
  136. package/lib/observed-network-context.mjs +355 -0
  137. package/lib/operation-budget.mjs +489 -0
  138. package/lib/page-fingerprints.mjs +481 -0
  139. package/lib/page-role-profile.mjs +117 -0
  140. package/lib/perceptual-hash.mjs +451 -0
  141. package/lib/rdap-attempts.mjs +34 -0
  142. package/lib/rdap-bootstrap.mjs +173 -0
  143. package/lib/rdap-capabilities.mjs +292 -0
  144. package/lib/rdap-client.mjs +125 -0
  145. package/lib/rdap-normalization.mjs +691 -0
  146. package/lib/rdap-registrar.mjs +196 -0
  147. package/lib/rdap-transport.mjs +39 -0
  148. package/lib/rdap-types.mjs +1 -0
  149. package/lib/rdap-validation.mjs +94 -0
  150. package/lib/rdap.mjs +16 -0
  151. package/lib/registry-access.mjs +32 -0
  152. package/lib/registry-capabilities.mjs +117 -0
  153. package/lib/registry-capability-catalogue.mjs +1206 -0
  154. package/lib/registry-comparison.mjs +318 -0
  155. package/lib/registry-dates.mjs +190 -0
  156. package/lib/registry-insights.mjs +325 -0
  157. package/lib/response-policy.mjs +256 -0
  158. package/lib/risk-scoring.mjs +164 -0
  159. package/lib/safe-fetch.mjs +446 -0
  160. package/lib/security-txt.mjs +359 -0
  161. package/lib/service-binding-dns.mjs +608 -0
  162. package/lib/sslbl-certificates.generated.mjs +143 -0
  163. package/lib/sslbl-intelligence.mjs +241 -0
  164. package/lib/static-html-analysis.mjs +340 -0
  165. package/lib/structured-data-identity.mjs +323 -0
  166. package/lib/threat-intelligence-contract.mjs +5 -0
  167. package/lib/threat-intelligence-definition-registry.mjs +34 -0
  168. package/lib/threat-intelligence-fixtures.mjs +82 -0
  169. package/lib/threat-intelligence-manifests.mjs +512 -0
  170. package/lib/threat-intelligence-runtime.mjs +662 -0
  171. package/lib/threat-intelligence-targets.mjs +168 -0
  172. package/lib/threat-intelligence-types.mjs +3 -0
  173. package/lib/threatfox-intelligence.mjs +307 -0
  174. package/lib/tls-intelligence.mjs +814 -0
  175. package/lib/typosquat-generator.mjs +884 -0
  176. package/lib/urlhaus-intelligence.mjs +361 -0
  177. package/lib/urlscan-intelligence.mjs +349 -0
  178. package/lib/website-security-posture.mjs +325 -0
  179. package/lib/website-technology.mjs +437 -0
  180. package/lib/whois-authority.mjs +98 -0
  181. package/lib/whois-chain.mjs +261 -0
  182. package/lib/whois-contacts.mjs +163 -0
  183. package/lib/whois-contracts.mjs +3 -0
  184. package/lib/whois-normalization.mjs +27 -0
  185. package/lib/whois-transport.mjs +100 -0
  186. package/lib/whois-values.mjs +92 -0
  187. package/lib/whois.mjs +1187 -0
  188. package/package.json +55 -0
  189. package/third-party-notices.txt +5571 -0
@@ -0,0 +1,224 @@
1
+ // Pure, framework-neutral analyst-case records, evidence histories, bounded
2
+ // record normalization, and analyst updates.
3
+ import { appendCaseAction, appendCaseAssertion, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseSighting, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.js";
4
+ import { CASE_DISPOSITIONS, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CASE_SOURCES, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_SOURCE, DEFAULT_STATUS, EVIDENCE_SOURCES, MAX_CASES, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTES_PER_CASE, MAX_NOTE_LENGTH, MAX_TAGS_PER_CASE, MAX_TAG_LENGTH, } from "./case-record-contracts.js";
5
+ import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, deterministicId, isoOrNow, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.js";
6
+ import { normalizeEvidenceHistory, } from "./case-evidence-model.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Case normalization
9
+ // ---------------------------------------------------------------------------
10
+ // A case's own source maps onto snapshot provenance for newly captured
11
+ // evidence. A hand-opened ('manual') case has no scan provenance.
12
+ function inferCaptureSource(caseSource) {
13
+ return typeof caseSource === 'string' && EVIDENCE_SOURCE_SET.has(caseSource) && caseSource !== 'import'
14
+ ? caseSource
15
+ : DEFAULT_EVIDENCE_SOURCE;
16
+ }
17
+ // Builds a case's bounded current-schema evidence history. Uses a lenient
18
+ // local fallback timestamp so recoverable current data always loads.
19
+ function normalizeCaseEvidence(record, createdAt, updatedAt, now) {
20
+ const localFallback = updatedAt || createdAt || now;
21
+ if (Array.isArray(record.evidenceHistory)) {
22
+ return normalizeEvidenceHistory(record.evidenceHistory, { source: DEFAULT_EVIDENCE_SOURCE, fallback: localFallback });
23
+ }
24
+ return [];
25
+ }
26
+ /**
27
+ * Validates a single case field-by-field for LOCAL recovery: missing scalars
28
+ * and timestamps are defaulted so our own stored data always loads. Returns
29
+ * null when the record has no usable domain. An `existing` record preserves
30
+ * stable identity/timestamps across an update. Import validation is separate
31
+ * (see mergeCases) so a defaulted value can never win over local data.
32
+ * @param {unknown} raw
33
+ * @param {CaseRecord} [existing]
34
+ * @param {string} [nowIso]
35
+ * @returns {CaseRecord | null}
36
+ */
37
+ export function normalizeCase(raw, existing, nowIso) {
38
+ const now = nowIso || new Date().toISOString();
39
+ const record = objectRecord(raw);
40
+ const domain = normalizeDomain(existing ? existing.domain : record.domain);
41
+ if (!domain)
42
+ return null;
43
+ const createdAt = existing ? existing.createdAt : isoOrNow(record.createdAt, now);
44
+ const updatedAt = isoOrNow(record.updatedAt, now);
45
+ const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt);
46
+ const pinIds = new Set(evidencePins.map((item) => item.id));
47
+ return {
48
+ id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
49
+ domain,
50
+ status: normalizeStatus(record.status),
51
+ disposition: normalizeDisposition(record.disposition),
52
+ tags: normalizeTags(record.tags),
53
+ notes: normalizeNotes(record.notes, now),
54
+ source: normalizeSource(record.source),
55
+ evidenceHistory: normalizeCaseEvidence(record, createdAt, updatedAt, now),
56
+ evidencePins,
57
+ decisions: normalizeCaseDecisions(record.decisions, updatedAt, pinIds),
58
+ actions: normalizeCaseActions(record.actions, updatedAt),
59
+ assertions: normalizeCaseAssertions(record.assertions, updatedAt, pinIds),
60
+ manualTrail: normalizeCaseManualTrail(record.manualTrail, updatedAt),
61
+ sightings: normalizeCaseSightings(record.sightings, updatedAt, pinIds),
62
+ createdAt,
63
+ updatedAt,
64
+ };
65
+ }
66
+ /**
67
+ * @param {{ domain: unknown, status?: unknown, disposition?: unknown, source?: unknown, tags?: unknown, evidence?: unknown, note?: unknown }} input
68
+ * @param {string} [nowIso]
69
+ * @returns {CaseRecord}
70
+ */
71
+ export function createCase(input, nowIso) {
72
+ const now = nowIso || new Date().toISOString();
73
+ const domain = normalizeDomain(input.domain);
74
+ if (!domain)
75
+ throw new Error('A valid domain is required to open a case.');
76
+ const noteBody = normalizeNoteBody(input.note);
77
+ const source = normalizeSource(input.source);
78
+ const evidencePins = input.evidencePins !== undefined
79
+ ? appendCaseEvidencePins([], input.evidencePins, now)
80
+ : input.evidencePin !== undefined
81
+ ? appendCaseEvidencePin([], input.evidencePin, now)
82
+ : [];
83
+ const pinIds = new Set(evidencePins.map((item) => item.id));
84
+ return {
85
+ id: makeId(),
86
+ domain,
87
+ status: normalizeStatus(input.status),
88
+ disposition: normalizeDisposition(input.disposition),
89
+ tags: normalizeTags(input.tags),
90
+ notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
91
+ source,
92
+ evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
93
+ source: inferCaptureSource(source),
94
+ fallback: now,
95
+ }),
96
+ evidencePins,
97
+ decisions: input.decision !== undefined
98
+ ? appendCaseDecision([], input.decision, now)
99
+ : [],
100
+ actions: input.action !== undefined
101
+ ? appendCaseAction([], input.action, now)
102
+ : [],
103
+ assertions: input.assertion !== undefined
104
+ ? appendCaseAssertion([], input.assertion, now)
105
+ : [],
106
+ manualTrail: input.trailEvent !== undefined
107
+ ? appendCaseManualTrailEvent([], input.trailEvent, now)
108
+ : [],
109
+ sightings: input.sighting !== undefined
110
+ ? appendCaseSighting([], input.sighting, now, pinIds)
111
+ : [],
112
+ createdAt: now,
113
+ updatedAt: now,
114
+ };
115
+ }
116
+ /**
117
+ * Opens the existing case for a domain, or creates one. Returns a new array
118
+ * (callers persist it) plus the resolved record and whether it was created.
119
+ * @param {CaseRecord[]} cases
120
+ * @param {{ domain: unknown, status?: unknown, disposition?: unknown, source?: unknown, tags?: unknown, evidence?: unknown, note?: unknown }} input
121
+ * @param {string} [nowIso]
122
+ * @returns {{ cases: CaseRecord[], record: CaseRecord, created: boolean }}
123
+ */
124
+ export function openOrCreateCase(cases, input, nowIso) {
125
+ const domain = normalizeDomain(input.domain);
126
+ if (!domain)
127
+ throw new Error('A valid domain is required to open a case.');
128
+ const existing = cases.find((item) => item.domain === domain);
129
+ if (existing)
130
+ return { cases, record: existing, created: false };
131
+ if (cases.length >= MAX_CASES)
132
+ throw new Error(`Cases are limited to ${MAX_CASES}. Delete or export some first.`);
133
+ const record = createCase({ ...input, domain }, nowIso);
134
+ return { cases: [record, ...cases], record, created: true };
135
+ }
136
+ /**
137
+ * Applies a partial update to one case by id, bumping updatedAt. A `note` in
138
+ * the patch is appended (respecting the per-case note bound); a `tags` array
139
+ * replaces the tag set; an `evidence` value appends a new snapshot to the
140
+ * history (deduplicated, so a materially identical re-capture only advances the
141
+ * latest observation time). Returns a new array and the updated record.
142
+ * @param {CaseRecord[]} cases
143
+ * @param {string} id
144
+ * @param {{ status?: unknown, disposition?: unknown, tags?: unknown, source?: unknown, evidence?: unknown, note?: unknown }} patch
145
+ * @param {string} [nowIso]
146
+ * @returns {{ cases: CaseRecord[], record: CaseRecord }}
147
+ */
148
+ export function updateCase(cases, id, patch, nowIso) {
149
+ const now = nowIso || new Date().toISOString();
150
+ const index = cases.findIndex((item) => item.id === id);
151
+ if (index < 0)
152
+ throw new Error('That case no longer exists.');
153
+ const current = cases[index];
154
+ if (!current)
155
+ throw new Error('That case no longer exists.');
156
+ let notes = current.notes;
157
+ if (patch.note !== undefined) {
158
+ const body = normalizeNoteBody(patch.note);
159
+ if (!body)
160
+ throw new Error('A note cannot be empty.');
161
+ if (notes.length >= MAX_NOTES_PER_CASE) {
162
+ throw new Error(`Each case is limited to ${MAX_NOTES_PER_CASE} notes.`);
163
+ }
164
+ notes = [...notes, { id: makeId(), body, createdAt: now }];
165
+ }
166
+ const source = patch.source !== undefined ? normalizeSource(patch.source) : current.source;
167
+ let evidenceHistory = current.evidenceHistory;
168
+ if (patch.evidence !== undefined) {
169
+ evidenceHistory = normalizeEvidenceHistory([...current.evidenceHistory, ...(patch.evidence ? [patch.evidence] : [])], { source: inferCaptureSource(source), fallback: now });
170
+ }
171
+ let evidencePins = current.evidencePins;
172
+ if (patch.evidencePins !== undefined) {
173
+ evidencePins = appendCaseEvidencePins(evidencePins, patch.evidencePins, now);
174
+ }
175
+ if (patch.evidencePin !== undefined) {
176
+ evidencePins = appendCaseEvidencePin(evidencePins, patch.evidencePin, now);
177
+ }
178
+ const pinIds = new Set(evidencePins.map((item) => item.id));
179
+ let decisions = current.decisions;
180
+ if (patch.decision !== undefined) {
181
+ decisions = appendCaseDecision(current.decisions, patch.decision, now, pinIds);
182
+ }
183
+ let actions = current.actions;
184
+ if (patch.action !== undefined) {
185
+ actions = appendCaseAction(current.actions, patch.action, now);
186
+ }
187
+ if (patch.actionUpdate !== undefined) {
188
+ actions = updateCaseAction(actions, patch.actionUpdate, now);
189
+ }
190
+ let assertions = current.assertions;
191
+ if (patch.assertion !== undefined) {
192
+ assertions = appendCaseAssertion(current.assertions, patch.assertion, now, pinIds);
193
+ }
194
+ if (patch.assertionUpdate !== undefined) {
195
+ assertions = updateCaseAssertion(assertions, patch.assertionUpdate, now, pinIds);
196
+ }
197
+ let manualTrail = current.manualTrail;
198
+ if (patch.trailEvent !== undefined) {
199
+ manualTrail = appendCaseManualTrailEvent(current.manualTrail, patch.trailEvent, now);
200
+ }
201
+ let sightings = current.sightings;
202
+ if (patch.sighting !== undefined) {
203
+ sightings = appendCaseSighting(current.sightings, patch.sighting, now, pinIds);
204
+ }
205
+ const record = {
206
+ ...current,
207
+ status: patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
208
+ disposition: patch.disposition !== undefined ? normalizeDisposition(patch.disposition) : current.disposition,
209
+ tags: patch.tags !== undefined ? normalizeTags(patch.tags) : current.tags,
210
+ source,
211
+ evidenceHistory,
212
+ evidencePins,
213
+ decisions,
214
+ actions,
215
+ assertions,
216
+ manualTrail,
217
+ sightings,
218
+ notes,
219
+ updatedAt: now,
220
+ };
221
+ const next = [...cases];
222
+ next[index] = record;
223
+ return { cases: next, record };
224
+ }