@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.
- package/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- 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
|
|
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
|
-
|
|
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
|
-
:
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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(
|
|
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)
|