@slicedearth/whoisleuth-cli 2.6.0 → 2.7.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/DISCLOSURE +8 -5
- package/NOTICE +3 -2
- package/README.md +60 -67
- package/cli/artifact-validation/case-response.mjs +6 -857
- package/cli/artifact-validation/lookup-evidence.mjs +15 -2
- package/cli/artifact-verify.mjs +32 -1
- package/cli/case-command.mjs +17 -17
- package/cli/case-pack.mjs +7 -1
- package/cli/ci-report.mjs +17 -5
- package/cli/command-definition.mjs +5 -5
- package/cli/doctor.mjs +37 -3
- package/cli/formatters/html.mjs +3 -0
- package/cli/formatters/json.mjs +8 -1
- package/cli/formatters/terminal-lookup.mjs +24 -1
- package/cli/history-command-definitions.mjs +1 -1
- package/cli/investigation-plan.mjs +15 -0
- package/cli/lookup-brief.mjs +11 -5
- package/cli/lookup-command-runner.mjs +20 -13
- package/cli/review-command-definitions.mjs +11 -11
- package/cli/saved-lookup.mjs +22 -7
- package/cli/source-reliability.mjs +12 -10
- package/cli/support-arguments.mjs +1 -1
- package/cli/support-command-definitions.mjs +5 -4
- package/cli/support-command-runner.mjs +6 -4
- package/cli/workflow-command-definitions.mjs +2 -1
- package/docs/cli-reference.md +49 -11
- package/docs/cli.md +109 -32
- package/docs/contextual-reviews.md +10 -0
- package/docs/infrastructure-observations.md +9 -0
- package/lib/dmarc-discovery.mjs +4 -2
- package/lib/dns-inheritance-review.mjs +14 -1
- package/lib/evidence-export.mjs +10 -4
- package/lib/evidence-report.mjs +2 -1
- package/lib/generated/cisa-kev-catalog.mjs +17 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +178 -5
- package/lib/idn-confusables.mjs +5 -1
- package/lib/lookup-collection-receipt.mjs +52 -0
- package/lib/lookup-diagnostics.mjs +2 -12
- package/lib/lookup-progress.mjs +29 -31
- package/lib/lookup-response-contract.mjs +9 -0
- package/lib/lookup-response.mjs +15 -14
- package/lib/lookup-source-progress.mjs +4 -2
- package/lib/lookup.mjs +33 -2
- package/lib/rdap-capabilities.mjs +43 -9
- package/lib/registry-insights.mjs +17 -6
- package/lib/sslbl-certificates.generated.mjs +143 -142
- package/lib/whois-authority.mjs +56 -1
- package/lib/whois-chain.mjs +2 -1
- package/lib/whois-contracts.mjs +2 -0
- package/lib/whois-normalization.mjs +4 -4
- package/package.json +6 -6
- package/packages/analysis/bounded-json.mjs +12 -2
- package/packages/cases/case-attachment-model.mjs +36 -1
- package/packages/cases/case-checkpoint-receipts.mjs +68 -0
- package/packages/cases/case-evidence-links.mjs +28 -0
- package/packages/cases/case-evidence-model.mjs +39 -6
- package/packages/cases/case-migration-model.mjs +9 -1
- package/packages/cases/case-object-coverage.mjs +106 -20
- package/packages/cases/case-record-operations.mjs +13 -0
- package/packages/cases/case-record-projection.mjs +28 -2
- package/packages/cases/case-report.mjs +5 -4
- package/packages/cases/case-response-packet-validation.mjs +880 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +12 -1
- package/packages/cases/case-response-packet.mjs +2 -15
- package/packages/cases/case-response-review-inputs.mjs +18 -1
- package/packages/cases/case-type-presentation.mjs +10 -0
- package/packages/cases/case-workflow-metadata.mjs +0 -8
- package/packages/comparison/capture-context.mjs +3 -2
- package/packages/contracts/analyst-review-state-contract.mjs +1 -1
- package/packages/contracts/case-portability.mjs +59 -28
- package/packages/contracts/cli-doctor.mjs +4 -0
- package/packages/contracts/cli-lookup.mjs +57 -46
- package/packages/contracts/lookup-evidence.mjs +12 -2
- package/packages/contracts/registry-insights.mjs +6 -0
- package/packages/evidence/dmarc-policy-context.mjs +80 -0
- package/packages/evidence/lookup-collection-receipt.mjs +140 -0
- package/packages/evidence/lookup-collection-review.mjs +74 -0
- package/packages/interchange/external-findings-import.mjs +8 -4
- package/packages/investigation/bulk-mail-exposure.mjs +2 -2
- package/packages/investigation/domain-history-review.mjs +7 -1
- package/packages/workspace/brand-keyword-campaign.mjs +11 -1
- package/packages/workspace/bulk-session-model.mjs +8 -0
- package/third-party-notices.txt +29 -29
|
@@ -1,33 +1,119 @@
|
|
|
1
|
-
import { retainedCaseResponseObjects, sameCaseResponseObject } from "./case-response-object.mjs";
|
|
2
|
-
|
|
1
|
+
import { retainedCaseResponseObjects, sameCaseResponseObject, } from "./case-response-object.mjs";
|
|
2
|
+
import { correctionDelivery } from "./case-packet-correction.mjs";
|
|
3
|
+
export function caseIncidentObjects(record) {
|
|
3
4
|
const objects = new Map();
|
|
4
5
|
const explicit = retainedCaseResponseObjects(record);
|
|
5
6
|
for (const target of record.workflowMetadata?.incidentTargets ?? []) {
|
|
6
|
-
const matching = explicit.filter(object => object.incidentTargetId === target.id && object.identifier === target.url);
|
|
7
|
-
for (const object of matching.length
|
|
7
|
+
const matching = explicit.filter((object) => object.incidentTargetId === target.id && object.identifier === target.url);
|
|
8
|
+
for (const object of matching.length
|
|
9
|
+
? matching
|
|
10
|
+
: [{ kind: 'other', identifier: target.url, incidentTargetId: target.id }])
|
|
8
11
|
objects.set(JSON.stringify(object), { target, object, targetRetained: true });
|
|
9
12
|
}
|
|
10
13
|
for (const object of explicit)
|
|
11
14
|
if (!objects.has(JSON.stringify(object)))
|
|
12
15
|
objects.set(JSON.stringify(object), {
|
|
13
|
-
object,
|
|
14
|
-
|
|
16
|
+
object,
|
|
17
|
+
targetRetained: false,
|
|
18
|
+
target: {
|
|
19
|
+
id: object.incidentTargetId ?? `${object.kind}-${object.identifier}`,
|
|
20
|
+
url: object.identifier,
|
|
21
|
+
state: 'open',
|
|
22
|
+
createdAt: record.createdAt ?? '',
|
|
23
|
+
updatedAt: record.updatedAt ?? '',
|
|
24
|
+
},
|
|
15
25
|
});
|
|
16
|
-
return [...objects.values()]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
return [...objects.values()];
|
|
27
|
+
}
|
|
28
|
+
export function buildCaseIncidentCoverage(record) {
|
|
29
|
+
return caseIncidentObjects(record).map(({ target, object, targetRetained }) => {
|
|
30
|
+
const contexts = (record.actions ?? [])
|
|
31
|
+
.map((action) => ({
|
|
32
|
+
action,
|
|
33
|
+
currentlyBound: Boolean(action.responseObjects?.some((bound) => sameCaseResponseObject(bound, object))),
|
|
34
|
+
events: action.history.filter((event) => event.responseObjects?.some((bound) => sameCaseResponseObject(bound, object))),
|
|
35
|
+
}))
|
|
36
|
+
.filter((context) => context.currentlyBound || context.events.length);
|
|
37
|
+
const actions = contexts
|
|
38
|
+
.filter((context) => context.currentlyBound)
|
|
39
|
+
.map((context) => context.action);
|
|
40
|
+
const responseContexts = contexts.map(({ action, currentlyBound, events }) => {
|
|
41
|
+
let correction = null;
|
|
42
|
+
if (currentlyBound && action.correction) {
|
|
43
|
+
let linkAvailable = false;
|
|
44
|
+
try {
|
|
45
|
+
linkAvailable = Boolean(correctionDelivery(record.actions ?? [], action));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* Historical linkage can be unavailable; the draft is not re-authorised here. */
|
|
49
|
+
}
|
|
50
|
+
correction = {
|
|
51
|
+
purpose: action.correction.purpose,
|
|
52
|
+
originActionId: action.originActionId,
|
|
53
|
+
deliveryEventId: action.correction.deliveryEventId,
|
|
54
|
+
packetDigestSha256: action.correction.packetDigestSha256,
|
|
55
|
+
packetVersion: action.correction.packetVersion,
|
|
56
|
+
linkAvailable,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
actionId: action.id,
|
|
61
|
+
type: action.type,
|
|
62
|
+
recipient: action.recipient,
|
|
63
|
+
contactSource: action.contactSource,
|
|
64
|
+
routeObservedAt: action.routeObservedAt,
|
|
65
|
+
routeReviewAfter: action.routeReviewAfter,
|
|
66
|
+
contactLimitations: action.contactLimitations,
|
|
67
|
+
metadataUpdatedAt: action.metadataUpdatedAt,
|
|
68
|
+
currentState: action.state,
|
|
69
|
+
currentlyBound,
|
|
70
|
+
historyOmitted: action.historyOmitted,
|
|
71
|
+
historyLimitations: action.historyLimitations,
|
|
72
|
+
reviewEvents: events.filter((event) => event.sourceClass === 'analyst' &&
|
|
73
|
+
(event.nextState === 'reviewed' || event.nextState === 'authorised')),
|
|
74
|
+
// A repeated submitted-state update alone does not establish another delivery.
|
|
75
|
+
deliveryEvents: events.filter((event) => event.sourceClass === 'analyst' &&
|
|
76
|
+
event.nextState === 'submitted' &&
|
|
77
|
+
(event.previousState === 'authorised' || Boolean(event.packetReceipt))),
|
|
78
|
+
correction,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
const responseEvents = contexts.flatMap(({ action, events }) => events
|
|
82
|
+
.filter((event) => event.providerOutcome || event.objectOutcome || event.nextState === 'acknowledged')
|
|
83
|
+
.map((event) => ({
|
|
84
|
+
actionId: action.id,
|
|
85
|
+
eventId: event.id,
|
|
86
|
+
state: event.nextState,
|
|
87
|
+
outcome: event.objectOutcome ?? event.providerOutcome,
|
|
88
|
+
occurredAt: event.occurredAt,
|
|
89
|
+
sourceClass: event.sourceClass,
|
|
90
|
+
applied: event.applied,
|
|
91
|
+
provenance: event.provenance,
|
|
92
|
+
reference: event.reference,
|
|
93
|
+
})));
|
|
94
|
+
const reviews = (record.observedEffects?.reviews ?? []).filter((review) => sameCaseResponseObject(review.responseObject, object));
|
|
95
|
+
const latestAt = Math.max(...reviews.map((review) => Date.parse(review.observedAt)));
|
|
96
|
+
const latest = reviews.filter((review) => Date.parse(review.observedAt) === latestAt);
|
|
97
|
+
return {
|
|
98
|
+
target,
|
|
99
|
+
targetRetained,
|
|
100
|
+
responseObject: object,
|
|
101
|
+
hostname: object.kind === 'domain' || object.kind === 'hostname'
|
|
102
|
+
? object.identifier
|
|
103
|
+
: new URL(object.identifier).hostname,
|
|
23
104
|
actionCoverage: actions.length ? 'bound' : 'unknown',
|
|
24
|
-
actionIds: actions.map(action => action.id),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
105
|
+
actionIds: actions.map((action) => action.id),
|
|
106
|
+
responseContexts,
|
|
107
|
+
providerEvents: responseEvents.filter((event) => event.applied),
|
|
108
|
+
unappliedProviderEvents: responseEvents.filter((event) => !event.applied),
|
|
109
|
+
observationCoverage: !reviews.length
|
|
110
|
+
? 'unknown'
|
|
111
|
+
: new Set(latest.map((review) => `${review.state}\u0000${review.objectOutcome ?? ''}`))
|
|
112
|
+
.size > 1
|
|
113
|
+
? 'ambiguous'
|
|
114
|
+
: 'available',
|
|
30
115
|
reviews,
|
|
31
|
-
closures: (record.closures?.records ?? []).filter(closure => sameCaseResponseObject(closure.responseObject, object)),
|
|
116
|
+
closures: (record.closures?.records ?? []).filter((closure) => sameCaseResponseObject(closure.responseObject, object)),
|
|
117
|
+
};
|
|
32
118
|
});
|
|
33
119
|
}
|
|
@@ -10,6 +10,7 @@ import { appendCaseAction, appendCaseAssertion, appendCaseClosure, appendCaseDec
|
|
|
10
10
|
import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
|
|
11
11
|
import { MAX_CASE_OBJECTIVE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
|
|
12
12
|
import { caseRecordVersionInput } from "./case-record-version-input.mjs";
|
|
13
|
+
import { readCaseCheckpointReceipts, mergeCaseCheckpointReceipts } from "./case-checkpoint-receipts.mjs";
|
|
13
14
|
import { readCaseAttachments } from "./case-attachment-model.mjs";
|
|
14
15
|
import { readCaseEvidenceLinks, appendCaseEvidenceLink, withdrawCaseEvidenceLink } from "./case-evidence-links.mjs";
|
|
15
16
|
import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, assertCaseObjectObservationOutcome, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
|
|
@@ -66,6 +67,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
66
67
|
const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
|
|
67
68
|
const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
|
|
68
69
|
const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
|
|
70
|
+
const checkpointCollectionReceipts = readCaseCheckpointReceipts(record.checkpointCollectionReceipts, evidencePins, sourceVersion);
|
|
69
71
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
70
72
|
const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
|
|
71
73
|
for (const action of actions)
|
|
@@ -98,6 +100,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
|
|
|
98
100
|
source: normalizeSource(record.source),
|
|
99
101
|
evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
|
|
100
102
|
evidencePins,
|
|
103
|
+
...(checkpointCollectionReceipts === undefined ? {} : { checkpointCollectionReceipts }),
|
|
101
104
|
decisions: normalizeCaseDecisions(record.decisions, updatedAt, pinIds, timestampOptions),
|
|
102
105
|
actions,
|
|
103
106
|
assertions,
|
|
@@ -140,6 +143,7 @@ export function createCase(input, nowIso) {
|
|
|
140
143
|
const actions = input.action !== undefined
|
|
141
144
|
? appendCaseAction([], input.action, now)
|
|
142
145
|
: [];
|
|
146
|
+
const checkpointCollectionReceipts = readCaseCheckpointReceipts(input.checkpointCollectionReceipts, evidencePins);
|
|
143
147
|
const assertions = input.assertion !== undefined
|
|
144
148
|
? appendCaseAssertion([], input.assertion, now)
|
|
145
149
|
: [];
|
|
@@ -186,6 +190,7 @@ export function createCase(input, nowIso) {
|
|
|
186
190
|
caseDomain: domain,
|
|
187
191
|
}),
|
|
188
192
|
evidencePins,
|
|
193
|
+
...(checkpointCollectionReceipts === undefined ? {} : { checkpointCollectionReceipts }),
|
|
189
194
|
decisions: input.decision !== undefined
|
|
190
195
|
? appendCaseDecision([], input.decision, now)
|
|
191
196
|
: [],
|
|
@@ -318,6 +323,11 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
318
323
|
if (patch.evidencePin !== undefined) {
|
|
319
324
|
evidencePins = appendCaseEvidencePin(evidencePins, patch.evidencePin, now);
|
|
320
325
|
}
|
|
326
|
+
const previousPinIds = new Set(current.evidencePins.map(pin => pin.id));
|
|
327
|
+
const checkpointCollectionReceipts = mergeCaseCheckpointReceipts(current, {
|
|
328
|
+
evidencePins: evidencePins.filter(pin => !previousPinIds.has(pin.id)),
|
|
329
|
+
checkpointCollectionReceipts: patch.checkpointCollectionReceipts,
|
|
330
|
+
}, evidencePins);
|
|
321
331
|
const pinIds = new Set(evidencePins.map((item) => item.id));
|
|
322
332
|
const correctionInput = patch.action !== undefined ? objectRecord(patch.action) : patch.actionUpdate !== undefined
|
|
323
333
|
? { ...current.actions.find(action => action.id === objectRecord(patch.actionUpdate).id), ...objectRecord(patch.actionUpdate) } : null;
|
|
@@ -416,6 +426,7 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
416
426
|
source,
|
|
417
427
|
evidenceHistory,
|
|
418
428
|
evidencePins,
|
|
429
|
+
...(checkpointCollectionReceipts === undefined ? {} : { checkpointCollectionReceipts }),
|
|
419
430
|
...((patch.evidenceLink !== undefined || patch.evidenceLinkWithdrawal !== undefined) ? {
|
|
420
431
|
evidenceLinks: patch.evidenceLink !== undefined
|
|
421
432
|
? appendCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLink, evidencePins, now)
|
|
@@ -433,6 +444,8 @@ export function updateCase(cases, id, patch, nowIso) {
|
|
|
433
444
|
updatedAt: now,
|
|
434
445
|
};
|
|
435
446
|
const next = [...cases];
|
|
447
|
+
if (checkpointCollectionReceipts === undefined)
|
|
448
|
+
delete record.checkpointCollectionReceipts;
|
|
436
449
|
next[index] = record;
|
|
437
450
|
return { cases: next, record };
|
|
438
451
|
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
|
|
1
|
+
import { COLLECTION_RECEIPT_CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION, MAX_CASE_IMPORT_BYTES } from "../contracts/case-portability.mjs";
|
|
2
2
|
import { caseStatusIsClosed } from "./case-record-decisions.mjs";
|
|
3
|
+
import { caseEvidenceWithoutCollectionReceipts } from "./case-evidence-model.mjs";
|
|
4
|
+
import { assertBoundedJsonStructure, boundedJsonLimitsForBytes, scanBoundedJson } from "../analysis/bounded-json.mjs";
|
|
5
|
+
/** Check the whole shared envelope, including otherwise unrecognised nesting.
|
|
6
|
+
* Writer projection alone cannot enforce the audience of an imported pack. */
|
|
7
|
+
export function assertCaseCollectionReceiptAudience(root, audience) {
|
|
8
|
+
if (audience === 'internal')
|
|
9
|
+
return;
|
|
10
|
+
const limits = boundedJsonLimitsForBytes(MAX_CASE_IMPORT_BYTES);
|
|
11
|
+
assertBoundedJsonStructure(root, 'Shared Case pack', limits);
|
|
12
|
+
scanBoundedJson(JSON.stringify(root), limits, entry => {
|
|
13
|
+
if (entry.kind === 'key' && (entry.value === 'collectionReceipt' || entry.value === 'checkpointCollectionReceipts')) {
|
|
14
|
+
throw new TypeError('Shared Case packs cannot contain private collection receipts.');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
3
18
|
const PRESERVE = Object.freeze({
|
|
4
19
|
durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
|
|
5
20
|
});
|
|
@@ -101,13 +116,24 @@ const CASE_FIELD_RULES = Object.freeze({
|
|
|
101
116
|
},
|
|
102
117
|
}),
|
|
103
118
|
source: preservedField('source'),
|
|
104
|
-
evidenceHistory: fieldRule('evidenceHistory',
|
|
119
|
+
evidenceHistory: fieldRule('evidenceHistory', SHARED_REDACT, (record, profile) => (profile === 'trusted' || profile === 'public'
|
|
120
|
+
? caseEvidenceWithoutCollectionReceipts(record) : structuredClone(record.evidenceHistory)), {
|
|
121
|
+
nestedSensitiveFields: ['collectionReceipt'],
|
|
122
|
+
audienceExclusions: {
|
|
123
|
+
trusted: { label: 'Private collection receipts', order: 4, sinceVersion: COLLECTION_RECEIPT_CASE_SCHEMA_VERSION },
|
|
124
|
+
public: { label: 'Private collection receipts', order: 4, sinceVersion: COLLECTION_RECEIPT_CASE_SCHEMA_VERSION },
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
105
127
|
evidencePins: fieldRule('evidencePins', PUBLIC_REDACT, (record, profile) => structuredClone(record.evidencePins).map(pin => {
|
|
106
128
|
if (profile !== 'public')
|
|
107
129
|
return pin;
|
|
108
130
|
const { responseObject: _scope, infrastructureObservation: _infrastructure, ...publicPin } = pin;
|
|
109
131
|
return publicPin;
|
|
110
132
|
}), { nestedSensitiveFields: ['responseObject', 'infrastructureObservation'], audienceExclusions: { public: { label: 'Exact-object evidence scope and infrastructure observations', order: 4, sinceVersion: 18 } } }),
|
|
133
|
+
checkpointCollectionReceipts: fieldRule('checkpointCollectionReceipts', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public' ? undefined : structuredClone(record.checkpointCollectionReceipts)), { audienceExclusions: {
|
|
134
|
+
trusted: { label: 'Private collection receipts', order: 4, sinceVersion: COLLECTION_RECEIPT_CASE_SCHEMA_VERSION },
|
|
135
|
+
public: { label: 'Private collection receipts', order: 4, sinceVersion: COLLECTION_RECEIPT_CASE_SCHEMA_VERSION },
|
|
136
|
+
} }),
|
|
111
137
|
evidenceLinks: fieldRule('evidenceLinks', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? undefined : structuredClone(record.evidenceLinks)), { audienceExclusions: { public: { label: 'Analyst evidence relationships', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } } }),
|
|
112
138
|
decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
|
|
113
139
|
actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
|
|
@@ -8,12 +8,12 @@
|
|
|
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 { caseEvidenceTimeline, compareCaseEvidence, currentCaseEvidence, publishedCaseEvidenceTimelineForVerification } from "./case-evidence-model.mjs";
|
|
11
|
+
import { caseEvidenceTimeline, caseEvidenceWithoutCollectionReceipts, compareCaseEvidence, currentCaseEvidence, publishedCaseEvidenceTimelineForVerification } from "./case-evidence-model.mjs";
|
|
12
12
|
import { httpSecurityHeaderLabel } from "./http-summary.mjs";
|
|
13
13
|
import { webCollectionScoreLimitation } from "../evidence/collection-quality.mjs";
|
|
14
14
|
import { analystInteroperabilityTags } from "../analysis/analyst-taxonomy.mjs";
|
|
15
15
|
import { caseEvidenceLinkIssues } from "./case-evidence-links.mjs";
|
|
16
|
-
import { caseTypeSummary } from "./case-
|
|
16
|
+
import { caseTypeSummary } from "./case-type-presentation.mjs";
|
|
17
17
|
import { caseClosureHistoryQualification } from "./case-response-outcomes.mjs";
|
|
18
18
|
import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../analysis/portable-generator.mjs";
|
|
19
19
|
import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
|
|
@@ -178,9 +178,10 @@ export function buildCaseReport(caseRecord, options = {}) {
|
|
|
178
178
|
const now = generatedAt || new Date().toISOString();
|
|
179
179
|
const generator = buildPortableGeneratorMetadata(options.applicationVersion);
|
|
180
180
|
// --- Build JSON report ---
|
|
181
|
-
const
|
|
181
|
+
const sharedEvidence = { ...caseRecord, evidenceHistory: caseEvidenceWithoutCollectionReceipts(caseRecord) };
|
|
182
|
+
const timeline = caseEvidenceTimeline(sharedEvidence.evidenceHistory);
|
|
182
183
|
const timelineEntries = projectReportTimeline(timeline);
|
|
183
|
-
const selection = currentCaseEvidence(
|
|
184
|
+
const selection = currentCaseEvidence(sharedEvidence);
|
|
184
185
|
const latest = selection.snapshot;
|
|
185
186
|
const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
|
|
186
187
|
const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
|