@slicedearth/whoisleuth-cli 2.1.0 → 2.3.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.
- package/README.md +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +24 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +63 -13
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +240 -3
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +183 -53
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +115 -46
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalArtifactJsonV2 } from "../../packages/evidence/artifact-integrity.mjs";
|
|
3
|
-
import { CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
|
|
3
|
+
import { CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
|
|
4
4
|
import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../../packages/cases/case-response-model.mjs";
|
|
5
5
|
import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
|
|
6
6
|
const CASE_RESPONSE_PREFLIGHT_IDS_V6 = [
|
|
@@ -21,6 +21,9 @@ const CASE_RESPONSE_PREFLIGHT_IDS_V8 = [
|
|
|
21
21
|
'action_tracking',
|
|
22
22
|
];
|
|
23
23
|
const PUBLIC_CASE_ACTION_STATES = ['planned', 'ready_for_review', 'submitted', 'acknowledged', 'resolved', 'closed'];
|
|
24
|
+
const PRE_PLATFORM_PROFILE_IDS = RESPONSE_PACKET_PROFILE_IDS.filter((id) => id !== 'application_platform');
|
|
25
|
+
const PRE_PLATFORM_CONTACT_KINDS = RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform');
|
|
26
|
+
const PRE_PLATFORM_ACTION_TYPES = CASE_ACTION_TYPES.filter((type) => type !== 'platform_report');
|
|
24
27
|
function legacyProfileRedactions(profileId) {
|
|
25
28
|
if (profileId === 'registrar' || profileId === 'registry') {
|
|
26
29
|
return ['Remove unnecessary personal registration data', 'Remove credentials, tokens, and URL fragments'];
|
|
@@ -68,7 +71,7 @@ function validateCaseResponsePacketV6(value) {
|
|
|
68
71
|
if (root.reviewRequired !== true || root.submissionPerformed !== false)
|
|
69
72
|
fail('Case-response packet review state');
|
|
70
73
|
const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
|
|
71
|
-
const profileId = enumeration(profile.id,
|
|
74
|
+
const profileId = enumeration(profile.id, PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
|
|
72
75
|
text(profile.label, 'Case-response profile label', 200);
|
|
73
76
|
text(profile.audience, 'Case-response profile audience', 300);
|
|
74
77
|
text(profile.subject, 'Case-response profile subject', 500);
|
|
@@ -124,7 +127,7 @@ function validateCaseResponsePacketV6(value) {
|
|
|
124
127
|
const contactKeys = new Set();
|
|
125
128
|
for (const candidate of contacts) {
|
|
126
129
|
const contact = exact(candidate, ['kind', 'contact', 'source', 'limitations'], 'Case-response contact');
|
|
127
|
-
const kind = enumeration(contact.kind,
|
|
130
|
+
const kind = enumeration(contact.kind, PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
|
|
128
131
|
const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
129
132
|
const contactKey = `${kind}\u0000${contactValue.toLowerCase()}`;
|
|
130
133
|
if (contactKeys.has(contactKey))
|
|
@@ -160,7 +163,7 @@ function validateCaseResponsePacketV6(value) {
|
|
|
160
163
|
const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
|
|
161
164
|
for (const candidate of history) {
|
|
162
165
|
const action = exact(candidate, ['type', 'recipient', 'contactSource', 'state', 'reference', 'outcome', 'createdAt', 'updatedAt'], 'Case-response escalation action');
|
|
163
|
-
enumeration(action.type,
|
|
166
|
+
enumeration(action.type, PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
|
|
164
167
|
text(action.recipient, 'Case-response action recipient', 320, true);
|
|
165
168
|
text(action.contactSource, 'Case-response action source', 120, true);
|
|
166
169
|
enumeration(action.state, PUBLIC_CASE_ACTION_STATES, 'Case-response action state');
|
|
@@ -221,14 +224,15 @@ function validateCurrentActionSummary(value, label) {
|
|
|
221
224
|
}
|
|
222
225
|
return total;
|
|
223
226
|
}
|
|
224
|
-
function
|
|
227
|
+
function validateCaseResponsePacketV7ToV9(value, version) {
|
|
225
228
|
const current = version === CASE_RESPONSE_PACKET_VERSION;
|
|
229
|
+
const modern = version === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION || current;
|
|
226
230
|
const root = exact(value, [
|
|
227
231
|
'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
|
|
228
232
|
'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
|
|
229
233
|
'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
|
|
230
234
|
'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
|
|
231
|
-
...(
|
|
235
|
+
...(modern ? ['recipientRoute', 'actionBinding'] : []),
|
|
232
236
|
], `Case-response packet v${version}`);
|
|
233
237
|
if (root.schemaVersion !== version)
|
|
234
238
|
fail(`Case-response packet v${version}`);
|
|
@@ -236,14 +240,14 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
236
240
|
if (root.reviewRequired !== true || root.submissionPerformed !== false)
|
|
237
241
|
fail('Case-response packet review state');
|
|
238
242
|
const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
|
|
239
|
-
const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
|
|
243
|
+
const profileId = enumeration(profile.id, current ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
|
|
240
244
|
text(profile.label, 'Case-response profile label', 200);
|
|
241
245
|
text(profile.audience, 'Case-response profile audience', 300);
|
|
242
246
|
text(profile.subject, 'Case-response profile subject', 500);
|
|
243
247
|
for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
|
|
244
248
|
strings(profile[key], `Case-response profile ${key}`, 24, 500);
|
|
245
249
|
const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
|
|
246
|
-
const expectedRedactions =
|
|
250
|
+
const expectedRedactions = modern ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
|
|
247
251
|
if (!selectedProfile
|
|
248
252
|
|| profile.label !== selectedProfile.label
|
|
249
253
|
|| profile.audience !== selectedProfile.audience
|
|
@@ -282,8 +286,10 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
282
286
|
const contacts = array(root.contacts, 'Case-response contacts', MAX_RESPONSE_CONTACTS);
|
|
283
287
|
const contactKeys = new Set();
|
|
284
288
|
for (const candidate of contacts) {
|
|
285
|
-
const contact = exact(candidate, [
|
|
286
|
-
|
|
289
|
+
const contact = exact(candidate, [
|
|
290
|
+
'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
291
|
+
], 'Case-response contact');
|
|
292
|
+
const kind = enumeration(contact.kind, current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
|
|
287
293
|
const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
288
294
|
const key = `${kind}\u0000${contactValue.toLowerCase()}`;
|
|
289
295
|
if (contactKeys.has(key))
|
|
@@ -291,13 +297,18 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
291
297
|
contactKeys.add(key);
|
|
292
298
|
text(contact.source, 'Case-response contact source', 120);
|
|
293
299
|
iso(contact.observedAt, 'Case-response contact observedAt', true);
|
|
300
|
+
if (current)
|
|
301
|
+
iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
|
|
294
302
|
const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
|
|
295
303
|
const routeAge = contact.observedAt === null
|
|
296
304
|
? null
|
|
297
305
|
: Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
|
|
306
|
+
const reviewExpired = current && contact.reviewAfter !== null
|
|
307
|
+
&& Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
|
|
298
308
|
const expectedFreshness = routeAge === null
|
|
299
309
|
? 'unknown'
|
|
300
|
-
: routeAge < -300_000 ||
|
|
310
|
+
: routeAge < -300_000 || reviewExpired
|
|
311
|
+
|| (contact.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000)
|
|
301
312
|
? 'stale'
|
|
302
313
|
: 'current';
|
|
303
314
|
if (freshness !== expectedFreshness)
|
|
@@ -308,24 +319,29 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
308
319
|
let selectedActionId = null;
|
|
309
320
|
let lineageActionIds = [];
|
|
310
321
|
let lineageComplete = true;
|
|
311
|
-
if (
|
|
322
|
+
if (modern) {
|
|
312
323
|
if (root.recipientRoute !== null) {
|
|
313
324
|
recipientRoute = exact(root.recipientRoute, [
|
|
314
|
-
'actionId', 'kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations',
|
|
325
|
+
'actionId', 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
|
|
315
326
|
], 'Case-response recipient route');
|
|
316
327
|
text(recipientRoute.actionId, 'Case-response recipient action id', 64);
|
|
317
|
-
enumeration(recipientRoute.kind, [...RESPONSE_CONTACT_KINDS, 'manual'], 'Case-response recipient kind');
|
|
328
|
+
enumeration(recipientRoute.kind, [...(current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
|
|
318
329
|
text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
|
|
319
330
|
text(recipientRoute.source, 'Case-response recipient source', 120);
|
|
320
331
|
iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
|
|
332
|
+
if (current)
|
|
333
|
+
iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
|
|
321
334
|
const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
|
|
322
335
|
const routeAge = recipientRoute.observedAt === null
|
|
323
336
|
? null
|
|
324
337
|
: Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
|
|
338
|
+
const reviewExpired = current && recipientRoute.reviewAfter !== null
|
|
339
|
+
&& Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
|
|
325
340
|
const expectedFreshness = routeAge === null
|
|
326
341
|
? 'unknown'
|
|
327
342
|
: routeAge < -MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS
|
|
328
|
-
||
|
|
343
|
+
|| reviewExpired
|
|
344
|
+
|| (recipientRoute.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000) ? 'stale' : 'current';
|
|
329
345
|
if (freshness !== expectedFreshness)
|
|
330
346
|
fail('Case-response recipient freshness');
|
|
331
347
|
strings(recipientRoute.limitations, 'Case-response recipient limitations', MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH);
|
|
@@ -354,6 +370,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
354
370
|
|| selectedContact.contact !== recipientRoute.contact
|
|
355
371
|
|| selectedContact.source !== recipientRoute.source
|
|
356
372
|
|| selectedContact.observedAt !== recipientRoute.observedAt
|
|
373
|
+
|| (current && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
|
|
357
374
|
|| selectedContact.freshness !== recipientRoute.freshness)
|
|
358
375
|
fail('Case-response selected contact projection');
|
|
359
376
|
}
|
|
@@ -395,7 +412,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
395
412
|
...(profileId === 'internal_soc' ? [] : ['recipient_route']),
|
|
396
413
|
...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
|
|
397
414
|
]);
|
|
398
|
-
const requiredContact =
|
|
415
|
+
const requiredContact = modern
|
|
399
416
|
? recipientRoute ?? undefined
|
|
400
417
|
: selectedProfile?.requiredContactKind
|
|
401
418
|
? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
|
|
@@ -490,8 +507,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
490
507
|
|| (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
|
|
491
508
|
fail('Case-response authorisation');
|
|
492
509
|
const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
|
|
493
|
-
const preflightIds =
|
|
494
|
-
if (preflight.version !== (
|
|
510
|
+
const preflightIds = modern ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
|
|
511
|
+
if (preflight.version !== (modern ? 3 : 2))
|
|
495
512
|
fail('Case-response preflight');
|
|
496
513
|
const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
|
|
497
514
|
const actualCounts = { block: 0, caution: 0, pass: 0 };
|
|
@@ -515,8 +532,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
515
532
|
const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
|
|
516
533
|
const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
|
|
517
534
|
const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
|
|
518
|
-
if ((!
|
|
519
|
-
|| (
|
|
535
|
+
if ((!modern && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
|
|
536
|
+
|| (modern && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
|
|
520
537
|
|| escalationHistoryOmitted !== actionTotal - history.length
|
|
521
538
|
|| (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
|
|
522
539
|
fail('Case-response action-history bounds');
|
|
@@ -528,18 +545,21 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
528
545
|
'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
|
|
529
546
|
'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
|
|
530
547
|
'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
|
|
531
|
-
...(
|
|
548
|
+
...(modern ? ['routeObservedAt'] : []),
|
|
549
|
+
...(current ? ['routeReviewAfter'] : []),
|
|
532
550
|
], 'Case-response escalation action');
|
|
533
551
|
const actionId = text(action.actionId, 'Case-response action id', 64);
|
|
534
552
|
if (actionIds.has(actionId))
|
|
535
553
|
fail('Case-response action identity');
|
|
536
554
|
actionIds.add(actionId);
|
|
537
555
|
actionIdOrder.push(actionId);
|
|
538
|
-
enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
|
|
556
|
+
enumeration(action.type, current ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
|
|
539
557
|
text(action.recipient, 'Case-response action recipient', 320);
|
|
540
558
|
text(action.contactSource, 'Case-response action source', 120);
|
|
541
|
-
if (
|
|
559
|
+
if (modern)
|
|
542
560
|
iso(action.routeObservedAt, 'Case-response action route observedAt', true);
|
|
561
|
+
if (current)
|
|
562
|
+
iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
|
|
543
563
|
enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
|
|
544
564
|
optionalText(action.reference, 'Case-response action reference', 500);
|
|
545
565
|
if (action.providerOutcome !== null)
|
|
@@ -638,7 +658,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
638
658
|
iso(action.createdAt, 'Case-response action createdAt');
|
|
639
659
|
iso(action.updatedAt, 'Case-response action updatedAt');
|
|
640
660
|
}
|
|
641
|
-
if (
|
|
661
|
+
if (modern) {
|
|
642
662
|
if (actionIds.size !== lineageActionIds.length
|
|
643
663
|
|| actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
|
|
644
664
|
fail('Case-response action lineage');
|
|
@@ -670,11 +690,11 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
670
690
|
const latestProviderEvents = latestProviderTime === null
|
|
671
691
|
? []
|
|
672
692
|
: providerEvents.filter((event) => event.occurredAt === latestProviderTime);
|
|
673
|
-
const expectedLatestProvider = (
|
|
693
|
+
const expectedLatestProvider = (modern ? lineageComplete : escalationHistoryOmitted === 0)
|
|
674
694
|
&& latestProviderEvents.length === 1 && latestProviderEvents[0].applied
|
|
675
695
|
? latestProviderEvents[0]
|
|
676
696
|
: null;
|
|
677
|
-
const expectedProviderState = (
|
|
697
|
+
const expectedProviderState = (modern ? !lineageComplete : escalationHistoryOmitted > 0)
|
|
678
698
|
? 'ambiguous'
|
|
679
699
|
: providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
|
|
680
700
|
if (providerOutcomeState !== expectedProviderState
|
|
@@ -707,7 +727,9 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
707
727
|
strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
|
|
708
728
|
const reviewedInputs = {
|
|
709
729
|
contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
|
|
710
|
-
version: current
|
|
730
|
+
version: current
|
|
731
|
+
? CASE_RESPONSE_REVIEW_INPUTS_VERSION
|
|
732
|
+
: modern ? LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
|
|
711
733
|
profile: {
|
|
712
734
|
id: profile.id,
|
|
713
735
|
label: profile.label,
|
|
@@ -721,7 +743,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
|
|
|
721
743
|
case: root.case,
|
|
722
744
|
incident: root.incident,
|
|
723
745
|
contacts: root.contacts,
|
|
724
|
-
...(
|
|
746
|
+
...(modern ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
|
|
725
747
|
selectedEvidence: root.selectedEvidence,
|
|
726
748
|
contradictions: root.contradictions,
|
|
727
749
|
readiness: root.readiness,
|
|
@@ -759,7 +781,10 @@ export function validateCaseResponsePacket(value) {
|
|
|
759
781
|
if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
|
|
760
782
|
return validateCaseResponsePacketV6(value);
|
|
761
783
|
if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
|
|
762
|
-
return
|
|
784
|
+
return validateCaseResponsePacketV7ToV9(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
|
|
785
|
+
}
|
|
786
|
+
if (value.schemaVersion === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION) {
|
|
787
|
+
return validateCaseResponsePacketV7ToV9(value, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION);
|
|
763
788
|
}
|
|
764
789
|
if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
|
|
765
790
|
throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
|
|
@@ -767,5 +792,5 @@ export function validateCaseResponsePacket(value) {
|
|
|
767
792
|
if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
|
|
768
793
|
throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is newer than the supported version ${CASE_RESPONSE_PACKET_VERSION}; no data was changed.`);
|
|
769
794
|
}
|
|
770
|
-
|
|
795
|
+
validateCaseResponsePacketV7ToV9(value, CASE_RESPONSE_PACKET_VERSION);
|
|
771
796
|
}
|
|
@@ -5,7 +5,7 @@ import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../.
|
|
|
5
5
|
import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../../packages/investigation/lookup-claim-passport.mjs";
|
|
6
6
|
import { LOOKUP_CLAIM_IDS, LOOKUP_CLAIM_READINESS_VERSION, LOOKUP_CLAIM_REQUIREMENT_IDS, } from "../../packages/investigation/lookup-claim-readiness.mjs";
|
|
7
7
|
import { normalizeDomainControlManifestDocument } from "../../packages/evidence/domain-control-runtime.mjs";
|
|
8
|
-
import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION } from "../../lib/domain-change-packet.mjs";
|
|
8
|
+
import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION, } from "../../lib/domain-change-packet.mjs";
|
|
9
9
|
import { DOMAIN_CONTROL_MANIFEST_SCHEMA } from "../../packages/contracts/domain-control-manifest.mjs";
|
|
10
10
|
import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, } from "../investigation-manifest.mjs";
|
|
11
11
|
import { array, boolean, domain, digest, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, validateIntegrity, } from "./structure-primitives.mjs";
|
|
@@ -501,18 +501,36 @@ function expectedDomainChangeSummary(before, after) {
|
|
|
501
501
|
const beforeRows = rows(before);
|
|
502
502
|
const afterRows = rows(after);
|
|
503
503
|
const keys = [...new Set([...beforeRows.keys(), ...afterRows.keys()])].sort();
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
504
|
+
const collectionComplete = (review) => {
|
|
505
|
+
const row = (review.authoritativeRecordMatrix[0]);
|
|
506
|
+
const observations = row?.observations ?? [];
|
|
507
|
+
return observations.length >= 2 && observations.every((item) => item.state === 'observed');
|
|
508
|
+
};
|
|
509
|
+
const rowEvidence = (review, row) => {
|
|
510
|
+
if (!row)
|
|
511
|
+
return { state: collectionComplete(review) ? 'complete' : 'unavailable', values: [] };
|
|
512
|
+
const observations = row.observations;
|
|
513
|
+
if (observations.some((item) => item.state !== 'observed'))
|
|
514
|
+
return { state: 'partial', values: [] };
|
|
515
|
+
if (row.state === 'different')
|
|
516
|
+
return { state: 'inconsistent', values: [] };
|
|
517
|
+
if (row.state !== 'aligned')
|
|
518
|
+
return { state: 'insufficient', values: [] };
|
|
519
|
+
return { state: 'complete', values: [...new Set(observations.flatMap((item) => item.values))].sort() };
|
|
520
|
+
};
|
|
521
|
+
const changed = [];
|
|
522
|
+
for (const key of keys.slice(0, 500)) {
|
|
523
|
+
const left = rowEvidence(before, beforeRows.get(key));
|
|
524
|
+
const right = rowEvidence(after, afterRows.get(key));
|
|
513
525
|
const [owner, type] = key.split('\u0000');
|
|
514
|
-
|
|
515
|
-
|
|
526
|
+
if (left.state !== 'complete' || right.state !== 'complete') {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
else if (!sameValues(left.values, right.values)) {
|
|
530
|
+
changed.push({ owner: owner ?? '', type: type ?? '', beforeValues: left.values, afterValues: right.values });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return changed;
|
|
516
534
|
}
|
|
517
535
|
function validateDomainChangePacket(value) {
|
|
518
536
|
const root = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'reference', 'state', 'gate', 'summary', 'evidence', 'limitations', 'integrity'], 'Domain change packet');
|
package/cli/bulk-checkpoint.mjs
CHANGED
|
@@ -54,6 +54,24 @@ function normalizeCompactResult(value) {
|
|
|
54
54
|
return null;
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
|
+
function normalizeCollectionContext(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const context = recordOrNull(value);
|
|
61
|
+
if (!context || Object.keys(context).some((key) => !['dnsResolver', 'resolverServers'].includes(key)))
|
|
62
|
+
return null;
|
|
63
|
+
const resolverServers = context.resolverServers;
|
|
64
|
+
if (!Array.isArray(resolverServers) || resolverServers.length > 3
|
|
65
|
+
|| resolverServers.some((server) => typeof server !== 'string' || !server || server.length > 45))
|
|
66
|
+
return null;
|
|
67
|
+
if (context.dnsResolver === 'system_default' && resolverServers.length === 0) {
|
|
68
|
+
return { dnsResolver: 'system_default', resolverServers: [] };
|
|
69
|
+
}
|
|
70
|
+
if (context.dnsResolver === 'analyst_selected' && resolverServers.length > 0) {
|
|
71
|
+
return { dnsResolver: 'analyst_selected', resolverServers: [...resolverServers] };
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
57
75
|
function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
58
76
|
const item = recordOrNull(value);
|
|
59
77
|
const index = item?.index;
|
|
@@ -67,10 +85,16 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
|
67
85
|
: normalizedTimestamp(item.observedAt) ?? undefined;
|
|
68
86
|
if (observedAt === undefined)
|
|
69
87
|
return null;
|
|
88
|
+
const collectionContext = normalizeCollectionContext(item.collectionContext);
|
|
89
|
+
if (collectionContext === null)
|
|
90
|
+
return null;
|
|
70
91
|
if (item.ok === false) {
|
|
71
92
|
if (typeof item.error !== 'string' || !item.error || item.error.length > 300)
|
|
72
93
|
return null;
|
|
73
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
index: Number(index), query, ok: false, error: item.error, observedAt,
|
|
96
|
+
...(collectionContext ? { collectionContext } : {}),
|
|
97
|
+
};
|
|
74
98
|
}
|
|
75
99
|
const result = normalizeCompactResult(item.result);
|
|
76
100
|
if (item.ok !== true || !result)
|
|
@@ -79,7 +103,10 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
|
79
103
|
const storedClassified = recordOrNull(item.classified);
|
|
80
104
|
if (storedClassified?.type !== classified.type || storedClassified.value !== classified.value)
|
|
81
105
|
return null;
|
|
82
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
index: Number(index), query, ok: true, classified, result, observedAt,
|
|
108
|
+
...(collectionContext ? { collectionContext } : {}),
|
|
109
|
+
};
|
|
83
110
|
}
|
|
84
111
|
function parseBulkCheckpoint(text, options) {
|
|
85
112
|
if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
|
|
@@ -214,8 +241,10 @@ async function createBulkCheckpointWriter(options) {
|
|
|
214
241
|
}
|
|
215
242
|
}
|
|
216
243
|
function scheduleWrite() {
|
|
244
|
+
if (writeFailure)
|
|
245
|
+
return;
|
|
217
246
|
dirty = true;
|
|
218
|
-
if (activeWrite
|
|
247
|
+
if (activeWrite)
|
|
219
248
|
return;
|
|
220
249
|
const pending = drainWrites();
|
|
221
250
|
activeWrite = pending;
|
|
@@ -250,6 +279,8 @@ async function createBulkCheckpointWriter(options) {
|
|
|
250
279
|
},
|
|
251
280
|
async flush() {
|
|
252
281
|
while (activeWrite || dirty) {
|
|
282
|
+
if (writeFailure && !activeWrite)
|
|
283
|
+
break;
|
|
253
284
|
if (!activeWrite)
|
|
254
285
|
scheduleWrite();
|
|
255
286
|
if (activeWrite)
|
package/cli/case-pack.mjs
CHANGED
|
@@ -4,42 +4,16 @@ import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from
|
|
|
4
4
|
import { buildCaseReport } from "../packages/cases/case-report.mjs";
|
|
5
5
|
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
6
6
|
import { assertBoundedJsonStructure, scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
7
|
-
import { assertCaseBrandProfileIds, normalizeDomain, normalizeCaseStore, safeId, } from "../packages/cases/case-model.mjs";
|
|
7
|
+
import { assertCaseBrandProfileIds, CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, CASE_RECORD_FIELD_NAMES, caseAudienceExclusions, normalizeDomain, normalizeCaseStore, projectCaseForAudience, safeId, } from "../packages/cases/case-model.mjs";
|
|
8
8
|
import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
|
|
9
9
|
import { CliUsageError } from "./errors.mjs";
|
|
10
10
|
export { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, };
|
|
11
11
|
const MAX_CASE_PACK_REFERENCE_SCAN_DEPTH = 64;
|
|
12
|
-
const INTERNAL_EXCLUSIONS = Object.freeze(['Raw upstream payloads and credentials are outside the case schema.']);
|
|
13
|
-
const TRUSTED_EXCLUSIONS = Object.freeze(['Case notes', 'Recipient values', 'Manual trail targets', 'Raw upstream payloads and credentials']);
|
|
14
|
-
const PUBLIC_EXCLUSIONS = Object.freeze([
|
|
15
|
-
'Case notes',
|
|
16
|
-
'Brand Profile references',
|
|
17
|
-
'Actions and recipient values',
|
|
18
|
-
'Analyst assertions',
|
|
19
|
-
'Investigation branches',
|
|
20
|
-
'Manual trail targets',
|
|
21
|
-
'Raw upstream payloads and credentials',
|
|
22
|
-
'Independent observed-effect reviews and closure history',
|
|
23
|
-
]);
|
|
24
|
-
const PUBLIC_V12_EXCLUSIONS = Object.freeze(PUBLIC_EXCLUSIONS.slice(0, -1));
|
|
25
|
-
const SENSITIVE_CASE_PACK_FIELDS = Object.freeze([
|
|
26
|
-
'brandProfileIds', 'notes', 'actions', 'assertions', 'manualTrail', 'observedEffects',
|
|
27
|
-
'closures', 'branches', 'recipient', 'target',
|
|
28
|
-
]);
|
|
29
|
-
const PUBLIC_OBSERVED_EFFECT_LIMITATION = 'Independent observed-effect review records were excluded from this public Case pack.';
|
|
30
|
-
const PUBLIC_CLOSURE_LIMITATION = 'Deliberate closure records were excluded from this public Case pack.';
|
|
31
12
|
function record(value) {
|
|
32
13
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
33
14
|
? value
|
|
34
15
|
: null;
|
|
35
16
|
}
|
|
36
|
-
function expectedExclusions(audience, caseVersion = CASE_SCHEMA_VERSION) {
|
|
37
|
-
if (audience === 'internal')
|
|
38
|
-
return INTERNAL_EXCLUSIONS;
|
|
39
|
-
if (audience === 'trusted')
|
|
40
|
-
return TRUSTED_EXCLUSIONS;
|
|
41
|
-
return caseVersion === PUBLIC_CASE_SCHEMA_VERSION ? PUBLIC_V12_EXCLUSIONS : PUBLIC_EXCLUSIONS;
|
|
42
|
-
}
|
|
43
17
|
function stringListsMatch(value, expected) {
|
|
44
18
|
return Array.isArray(value)
|
|
45
19
|
&& value.length === expected.length
|
|
@@ -105,7 +79,7 @@ function assertSensitiveFieldPlacement(root, serializedLength) {
|
|
|
105
79
|
const entry = stack.pop();
|
|
106
80
|
if (!entry || !entry.value || typeof entry.value !== 'object')
|
|
107
81
|
continue;
|
|
108
|
-
for (const key of
|
|
82
|
+
for (const key of CASE_AUDIENCE_SENSITIVE_FIELD_NAMES) {
|
|
109
83
|
if (Object.hasOwn(entry.value, key) && !allowedSensitiveFieldPath(key, entry.path)) {
|
|
110
84
|
throw new TypeError('The CLI case pack contains audience-sensitive data outside its versioned case or report fields.');
|
|
111
85
|
}
|
|
@@ -170,42 +144,22 @@ function assertCanonicalCaseIdentities(cases, label) {
|
|
|
170
144
|
}
|
|
171
145
|
}
|
|
172
146
|
function assertCurrentCaseProjection(rawCases, normalised, label) {
|
|
173
|
-
|
|
174
|
-
|
|
147
|
+
const requiredFields = new Set(CASE_RECORD_FIELD_NAMES);
|
|
148
|
+
if (rawCases.length !== normalised.length || rawCases.some((item, index) => {
|
|
149
|
+
const raw = record(item);
|
|
150
|
+
return !raw
|
|
151
|
+
|| Object.keys(raw).length !== requiredFields.size
|
|
152
|
+
|| Object.keys(raw).some((key) => !requiredFields.has(key))
|
|
153
|
+
|| !canonicalValuesMatch(raw, normalised[index]);
|
|
154
|
+
})) {
|
|
175
155
|
throw new TypeError(`${label} contains a schema ${CASE_SCHEMA_VERSION} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
|
|
176
156
|
}
|
|
177
157
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
reviews: [],
|
|
181
|
-
omitted: 0,
|
|
182
|
-
preV13HistoryUnavailable,
|
|
183
|
-
limitations: [
|
|
184
|
-
...(preV13HistoryUnavailable
|
|
185
|
-
? ['Migrated from a pre-v13 Case; earlier independent observed-effect review history is unavailable.']
|
|
186
|
-
: []),
|
|
187
|
-
'Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.',
|
|
188
|
-
PUBLIC_OBSERVED_EFFECT_LIMITATION,
|
|
189
|
-
].sort(),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function publicClosureHistory(preV13HistoryUnavailable) {
|
|
193
|
-
return {
|
|
194
|
-
records: [],
|
|
195
|
-
omitted: 0,
|
|
196
|
-
preV13HistoryUnavailable,
|
|
197
|
-
limitations: [
|
|
198
|
-
...(preV13HistoryUnavailable
|
|
199
|
-
? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.']
|
|
200
|
-
: []),
|
|
201
|
-
'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
|
|
202
|
-
PUBLIC_CLOSURE_LIMITATION,
|
|
203
|
-
].sort(),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
function assertAudienceFields(value, audience) {
|
|
158
|
+
/** Historical packs keep their immutable wire shape, so only their retained audience guarantees are checked here. */
|
|
159
|
+
function assertLegacyAudienceFields(value, normalised, audience) {
|
|
207
160
|
if (audience === 'internal')
|
|
208
161
|
return;
|
|
162
|
+
const expected = projectCaseForAudience(normalised, audience);
|
|
209
163
|
if (Object.hasOwn(value, 'notes') && (!Array.isArray(value.notes) || value.notes.length !== 0)) {
|
|
210
164
|
throw new TypeError('The CLI case pack contains Case notes excluded by its audience.');
|
|
211
165
|
}
|
|
@@ -218,11 +172,8 @@ function assertAudienceFields(value, audience) {
|
|
|
218
172
|
for (const [field, list] of [['observedEffects', 'reviews'], ['closures', 'records']]) {
|
|
219
173
|
if (Object.hasOwn(value, field)) {
|
|
220
174
|
const history = record(value[field]);
|
|
221
|
-
const expected = field === 'observedEffects'
|
|
222
|
-
? publicObservedEffectHistory(history?.preV13HistoryUnavailable === true)
|
|
223
|
-
: publicClosureHistory(history?.preV13HistoryUnavailable === true);
|
|
224
175
|
if (!history || !Array.isArray(history[list]) || history[list].length !== 0
|
|
225
|
-
|| !canonicalValuesMatch(history, expected)) {
|
|
176
|
+
|| !canonicalValuesMatch(history, expected[field])) {
|
|
226
177
|
throw new TypeError(`The public CLI case pack contains ${field} excluded by its audience.`);
|
|
227
178
|
}
|
|
228
179
|
}
|
|
@@ -245,6 +196,20 @@ function assertAudienceFields(value, audience) {
|
|
|
245
196
|
}
|
|
246
197
|
}
|
|
247
198
|
}
|
|
199
|
+
function assertCurrentAudienceProjection(value, normalised, audience) {
|
|
200
|
+
const expected = projectCaseForAudience(normalised, audience);
|
|
201
|
+
for (const field of CASE_RECORD_FIELD_NAMES) {
|
|
202
|
+
if (!canonicalValuesMatch(value[field], expected[field])) {
|
|
203
|
+
throw new TypeError(`The CLI case pack contains ${field} excluded by its audience or transformed incorrectly.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function assertAudienceProjection(value, normalised, audience, currentSchema) {
|
|
208
|
+
if (currentSchema)
|
|
209
|
+
assertCurrentAudienceProjection(value, normalised, audience);
|
|
210
|
+
else
|
|
211
|
+
assertLegacyAudienceFields(value, normalised, audience);
|
|
212
|
+
}
|
|
248
213
|
function reportApplicationVersion(report) {
|
|
249
214
|
return record(report.application)?.version;
|
|
250
215
|
}
|
|
@@ -261,28 +226,6 @@ function assertCurrentReportProjection(report, rawCase) {
|
|
|
261
226
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report projection.');
|
|
262
227
|
}
|
|
263
228
|
}
|
|
264
|
-
function redactedCase(record, audience) {
|
|
265
|
-
if (audience === 'internal')
|
|
266
|
-
return structuredClone(record);
|
|
267
|
-
return {
|
|
268
|
-
...structuredClone(record),
|
|
269
|
-
status: audience === 'public' && record.status === 'resolved' && record.closures.records.length
|
|
270
|
-
? 'reviewing'
|
|
271
|
-
: record.status,
|
|
272
|
-
brandProfileIds: audience === 'public' ? [] : [...record.brandProfileIds],
|
|
273
|
-
notes: [],
|
|
274
|
-
actions: audience === 'public' ? [] : record.actions.map((item) => ({ ...item, recipient: '[redacted]' })),
|
|
275
|
-
manualTrail: record.manualTrail.map((item) => ({ ...item, target: null })),
|
|
276
|
-
assertions: audience === 'public' ? [] : structuredClone(record.assertions),
|
|
277
|
-
observedEffects: audience === 'public'
|
|
278
|
-
? publicObservedEffectHistory(record.observedEffects.preV13HistoryUnavailable)
|
|
279
|
-
: structuredClone(record.observedEffects),
|
|
280
|
-
closures: audience === 'public'
|
|
281
|
-
? publicClosureHistory(record.closures.preV13HistoryUnavailable)
|
|
282
|
-
: structuredClone(record.closures),
|
|
283
|
-
branches: audience === 'public' ? [] : structuredClone(record.branches ?? []),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
229
|
export function buildCliCasePack(input, options, generatedAt = new Date().toISOString()) {
|
|
287
230
|
if (!options.reviewed)
|
|
288
231
|
throw new CliUsageError('case-pack requires --reviewed after the audience-specific output has been checked.');
|
|
@@ -337,7 +280,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
337
280
|
catch (cause) {
|
|
338
281
|
throw new CliUsageError(cause instanceof Error ? cause.message : `Case-pack schema ${CASE_SCHEMA_VERSION} input is not exact.`);
|
|
339
282
|
}
|
|
340
|
-
const cases = normalised.map((item) =>
|
|
283
|
+
const cases = normalised.map((item) => projectCaseForAudience(item, options.audience));
|
|
341
284
|
const brandProfileReferencesOmitted = options.audience === 'public'
|
|
342
285
|
? normalised.reduce((count, item) => count + item.brandProfileIds.length, 0)
|
|
343
286
|
: 0;
|
|
@@ -346,7 +289,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
346
289
|
includeNotes: false,
|
|
347
290
|
generatedAt,
|
|
348
291
|
}).json);
|
|
349
|
-
const exclusions =
|
|
292
|
+
const exclusions = caseAudienceExclusions(options.audience, CASE_SCHEMA_VERSION);
|
|
350
293
|
const packet = Object.freeze({
|
|
351
294
|
schema: CLI_CASE_PACK_SCHEMA,
|
|
352
295
|
version: CLI_CASE_PACK_VERSION,
|
|
@@ -436,7 +379,7 @@ export function verifyCliCasePack(input) {
|
|
|
436
379
|
const audience = packet.audience;
|
|
437
380
|
if (!Number.isSafeInteger(redactionManifest.sourceCaseCount)
|
|
438
381
|
|| redactionManifest.sourceCaseCount !== root.cases.length
|
|
439
|
-
|| !stringListsMatch(redactionManifest.excluded,
|
|
382
|
+
|| !stringListsMatch(redactionManifest.excluded, caseAudienceExclusions(audience, root.version))) {
|
|
440
383
|
throw new TypeError('The CLI case pack has an invalid audience redaction manifest.');
|
|
441
384
|
}
|
|
442
385
|
assertCanonicalCaseIdentities(root.cases, 'The CLI case pack');
|
|
@@ -444,7 +387,8 @@ export function verifyCliCasePack(input) {
|
|
|
444
387
|
if (normalised.length !== root.cases.length) {
|
|
445
388
|
throw new TypeError('The CLI case pack contains an invalid case collection.');
|
|
446
389
|
}
|
|
447
|
-
|
|
390
|
+
const currentCaseSchema = root.version === CASE_SCHEMA_VERSION;
|
|
391
|
+
if (currentCaseSchema)
|
|
448
392
|
assertCurrentCaseProjection(root.cases, normalised, 'The CLI case pack');
|
|
449
393
|
const normalisedByDomain = new Map(normalised.map((item) => [item.domain, item]));
|
|
450
394
|
const caseReferenceLists = [];
|
|
@@ -464,7 +408,10 @@ export function verifyCliCasePack(input) {
|
|
|
464
408
|
|| !normalisedByDomain.has(rawCase.domain)) {
|
|
465
409
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
|
|
466
410
|
}
|
|
467
|
-
|
|
411
|
+
const normalisedCase = normalisedByDomain.get(rawCase.domain);
|
|
412
|
+
if (!normalisedCase)
|
|
413
|
+
throw new TypeError('The CLI case pack contains an invalid Case projection.');
|
|
414
|
+
assertAudienceProjection(rawCase, normalisedCase, audience, currentCaseSchema);
|
|
468
415
|
try {
|
|
469
416
|
if (!Object.hasOwn(rawCase, 'brandProfileIds') || !Object.hasOwn(reportCase, 'brandProfileIds'))
|
|
470
417
|
throw new Error('missing');
|
|
@@ -474,8 +421,8 @@ export function verifyCliCasePack(input) {
|
|
|
474
421
|
catch {
|
|
475
422
|
throw new TypeError('The CLI case pack contains invalid Brand Profile references.');
|
|
476
423
|
}
|
|
477
|
-
if (
|
|
478
|
-
assertCurrentReportProjection(report,
|
|
424
|
+
if (currentCaseSchema)
|
|
425
|
+
assertCurrentReportProjection(report, normalisedCase);
|
|
479
426
|
}
|
|
480
427
|
const omitted = redactionManifest.brandProfileReferencesOmitted;
|
|
481
428
|
if (!Object.hasOwn(redactionManifest, 'brandProfileReferencesOmitted')
|