@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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 +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/case-pack.mjs
CHANGED
|
@@ -1,97 +1,44 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import { canonicalArtifactJson,
|
|
4
|
-
import { buildCaseReport
|
|
3
|
+
import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
+
import { buildCaseReport } from "../packages/cases/case-report.mjs";
|
|
5
5
|
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { assertBoundedJsonStructure, scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
7
|
+
import { assertCaseBrandProfileIds, normalizeDomain, normalizeCaseStore, safeId, } from "../packages/cases/case-model.mjs";
|
|
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
|
-
export
|
|
11
|
-
export const CLI_CASE_PACK_VERSION = 2;
|
|
12
|
-
export const LEGACY_CLI_CASE_PACK_VERSION = 1;
|
|
13
|
-
export const MAX_CASE_PACK_INPUT_BYTES = 4 * 1024 * 1024;
|
|
14
|
-
export const MAX_CASE_PACK_CASES = 25;
|
|
10
|
+
export { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, };
|
|
15
11
|
const MAX_CASE_PACK_REFERENCE_SCAN_DEPTH = 64;
|
|
16
|
-
const LEGACY_CASE_REPORT_VERSIONS = Object.freeze([1, 2, 3, 4, 5, 6, 7]);
|
|
17
12
|
const INTERNAL_EXCLUSIONS = Object.freeze(['Raw upstream payloads and credentials are outside the case schema.']);
|
|
18
13
|
const TRUSTED_EXCLUSIONS = Object.freeze(['Case notes', 'Recipient values', 'Manual trail targets', 'Raw upstream payloads and credentials']);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'This local package is browser-importable through its top-level case collection and does not upload or submit evidence.',
|
|
29
|
-
'The reviewed flag records a deliberate CLI choice; it does not prove recipient authorisation, factual correctness, or legal sufficiency.',
|
|
30
|
-
'Importing the package does not restore fields excluded by its audience profile.',
|
|
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',
|
|
31
23
|
]);
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
[1, LEGACY_RISK_MODEL_LIMITATION],
|
|
37
|
-
[2, LEGACY_RISK_MODEL_LIMITATION],
|
|
38
|
-
[3, LEGACY_RISK_MODEL_LIMITATION],
|
|
39
|
-
[4, LEGACY_RISK_MODEL_LIMITATION],
|
|
40
|
-
[5, LEGACY_SCORING_MODEL_LIMITATION],
|
|
41
|
-
[6, LEGACY_PORTABLE_LIMITATION],
|
|
42
|
-
[7, LEGACY_PORTABLE_LIMITATION],
|
|
43
|
-
]);
|
|
44
|
-
const HISTORICAL_BASE_MATERIAL_FIELDS = Object.freeze([
|
|
45
|
-
'scanDepth',
|
|
46
|
-
'availability', 'confidence', 'riskModelVersion', 'riskScore', 'opportunityScore',
|
|
47
|
-
'riskFactors', 'opportunityFactors',
|
|
48
|
-
'registrar', 'createdDate', 'expiryDate', 'nameservers',
|
|
49
|
-
'hasMx', 'hasSpf', 'hasDmarc',
|
|
50
|
-
'activityStatus', 'websiteProbeDetail', 'pageTitle',
|
|
51
|
-
'httpSummaryVersion', 'httpEvidenceStatus', 'httpFinalOrigin', 'httpResponseStatus', 'httpTransportSecurity', 'httpRedirectCount',
|
|
52
|
-
'httpCrossOriginRedirect', 'httpHttpsDowngrade', 'httpContentType', 'httpSecurityHeaders',
|
|
53
|
-
'faviconMatch', 'faviconNearMatch', 'reusesOfficialAssets', 'hasPasswordField', 'phishingLanguageMatch',
|
|
54
|
-
'mutationTypes',
|
|
55
|
-
]);
|
|
56
|
-
const HISTORICAL_EXTERNAL_FORM_MATERIAL_FIELDS = Object.freeze([
|
|
57
|
-
...HISTORICAL_BASE_MATERIAL_FIELDS.slice(0, -2),
|
|
58
|
-
'hasExternalFormAction',
|
|
59
|
-
...HISTORICAL_BASE_MATERIAL_FIELDS.slice(-2),
|
|
60
|
-
]);
|
|
61
|
-
const HISTORICAL_CONTEXT_MATERIAL_FIELDS = Object.freeze([
|
|
62
|
-
'scanDepth',
|
|
63
|
-
'availability', 'confidence', 'riskModelVersion', 'riskScore', 'opportunityModelVersion', 'opportunityScore',
|
|
64
|
-
'riskFactors', 'opportunityFactors',
|
|
65
|
-
'registrar', 'createdDate', 'expiryDate', 'nameservers',
|
|
66
|
-
'hasMx', 'hasSpf', 'hasDmarc',
|
|
67
|
-
'activityStatus', 'websiteProbeDetail', 'pageTitle',
|
|
68
|
-
'httpSummaryVersion', 'httpEvidenceStatus', 'httpFinalOrigin', 'httpResponseStatus', 'httpTransportSecurity', 'httpRedirectCount',
|
|
69
|
-
'httpCrossOriginRedirect', 'httpHttpsDowngrade', 'httpContentType', 'httpSecurityHeaders',
|
|
70
|
-
'faviconMatch', 'faviconNearMatch', 'reusesOfficialAssets', 'hasPasswordField', 'hasExternalFormAction', 'phishingLanguageMatch',
|
|
71
|
-
'privacyProtected', 'idnReferenceMatch', 'pageBaselineMatch', 'hasActiveBrandProfile',
|
|
72
|
-
'mutationTypes',
|
|
73
|
-
]);
|
|
74
|
-
const HISTORICAL_CHANGE_ORDER = Object.freeze([
|
|
75
|
-
'availability', 'confidence', 'riskScore', 'riskFactors', 'opportunityScore', 'opportunityFactors',
|
|
76
|
-
'registrar', 'createdDate', 'expiryDate', 'nameservers', 'hasMx', 'hasSpf', 'hasDmarc',
|
|
77
|
-
'activityStatus', 'websiteProbeDetail', 'pageTitle', 'httpEvidenceStatus', 'httpFinalOrigin',
|
|
78
|
-
'httpResponseStatus', 'httpTransportSecurity', 'httpRedirectCount', 'httpCrossOriginRedirect',
|
|
79
|
-
'httpHttpsDowngrade', 'httpContentType', 'httpSecurityHeaders', 'faviconMatch', 'faviconNearMatch',
|
|
80
|
-
'reusesOfficialAssets', 'hasPasswordField', 'hasExternalFormAction', 'phishingLanguageMatch', 'mutationTypes',
|
|
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',
|
|
81
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.';
|
|
82
31
|
function record(value) {
|
|
83
32
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
84
33
|
? value
|
|
85
34
|
: null;
|
|
86
35
|
}
|
|
87
|
-
function expectedExclusions(audience, caseVersion) {
|
|
36
|
+
function expectedExclusions(audience, caseVersion = CASE_SCHEMA_VERSION) {
|
|
88
37
|
if (audience === 'internal')
|
|
89
38
|
return INTERNAL_EXCLUSIONS;
|
|
90
39
|
if (audience === 'trusted')
|
|
91
40
|
return TRUSTED_EXCLUSIONS;
|
|
92
|
-
|
|
93
|
-
return PUBLIC_EXCLUSIONS;
|
|
94
|
-
return caseVersion === 11 ? PUBLIC_EXCLUSIONS_V11 : PUBLIC_EXCLUSIONS_V10;
|
|
41
|
+
return caseVersion === PUBLIC_CASE_SCHEMA_VERSION ? PUBLIC_V12_EXCLUSIONS : PUBLIC_EXCLUSIONS;
|
|
95
42
|
}
|
|
96
43
|
function stringListsMatch(value, expected) {
|
|
97
44
|
return Array.isArray(value)
|
|
@@ -104,60 +51,11 @@ function assertOnlyKeys(value, allowed, label) {
|
|
|
104
51
|
throw new TypeError(`The CLI case pack contains an unexpected ${label} field.`);
|
|
105
52
|
}
|
|
106
53
|
}
|
|
107
|
-
function
|
|
108
|
-
if (value === null)
|
|
109
|
-
return 'null';
|
|
110
|
-
if (Array.isArray(value))
|
|
111
|
-
return 'array';
|
|
112
|
-
return typeof value === 'object' ? 'object' : 'scalar';
|
|
113
|
-
}
|
|
114
|
-
/** Historical records may omit later object fields, but every retained value must match the bounded projection. */
|
|
115
|
-
function assertProjectionKeys(actual, projection, path, depth = 0) {
|
|
116
|
-
if (depth > MAX_CASE_PACK_REFERENCE_SCAN_DEPTH) {
|
|
117
|
-
throw new TypeError('The CLI case pack exceeds its bounded projection depth.');
|
|
118
|
-
}
|
|
119
|
-
const actualKind = jsonNodeKind(actual);
|
|
120
|
-
const projectionKind = jsonNodeKind(projection);
|
|
121
|
-
if (actualKind !== projectionKind) {
|
|
122
|
-
throw new TypeError(`The CLI case pack contains an invalid ${path} projection node.`);
|
|
123
|
-
}
|
|
124
|
-
if (actualKind === 'array') {
|
|
125
|
-
const actualArray = actual;
|
|
126
|
-
const projectionArray = projection;
|
|
127
|
-
if (actualArray.length > projectionArray.length) {
|
|
128
|
-
throw new TypeError(`The CLI case pack contains data outside its normalised ${path} projection.`);
|
|
129
|
-
}
|
|
130
|
-
for (let index = 0; index < actualArray.length; index++) {
|
|
131
|
-
const expected = projectionArray[index];
|
|
132
|
-
if (expected === undefined) {
|
|
133
|
-
throw new TypeError(`The CLI case pack contains data outside its normalised ${path} projection.`);
|
|
134
|
-
}
|
|
135
|
-
assertProjectionKeys(actualArray[index], expected, `${path}[${index}]`, depth + 1);
|
|
136
|
-
}
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (actualKind === 'object') {
|
|
140
|
-
const actualRecord = actual;
|
|
141
|
-
const projectionRecord = record(projection);
|
|
142
|
-
if (!projectionRecord)
|
|
143
|
-
throw new TypeError(`The CLI case pack contains an invalid ${path} projection.`);
|
|
144
|
-
for (const key of Object.keys(actualRecord)) {
|
|
145
|
-
if (!Object.hasOwn(projectionRecord, key)) {
|
|
146
|
-
throw new TypeError(`The CLI case pack contains an unexpected ${path}.${key} field.`);
|
|
147
|
-
}
|
|
148
|
-
assertProjectionKeys(actualRecord[key], projectionRecord[key], `${path}.${key}`, depth + 1);
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (!canonicalValuesMatch(actual, projection)) {
|
|
153
|
-
throw new TypeError(`The CLI case pack contains a changed or unbounded ${path} projection value.`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function assertClosedEnvelopes(root, packet, integrity, redactionManifest, reports, current) {
|
|
54
|
+
function assertClosedEnvelopes(root, packet, integrity, redactionManifest, reports, caseVersion) {
|
|
157
55
|
assertOnlyKeys(root, ROOT_KEYS, 'root envelope');
|
|
158
56
|
assertOnlyKeys(packet, PACKET_KEYS, 'packet envelope');
|
|
159
57
|
assertOnlyKeys(integrity, INTEGRITY_KEYS, 'integrity envelope');
|
|
160
|
-
assertOnlyKeys(redactionManifest,
|
|
58
|
+
assertOnlyKeys(redactionManifest, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, 'redaction manifest');
|
|
161
59
|
if (!stringListsMatch(packet.limitations, PACKET_LIMITATIONS)) {
|
|
162
60
|
throw new TypeError('The CLI case pack contains an invalid packet limitation manifest.');
|
|
163
61
|
}
|
|
@@ -165,7 +63,7 @@ function assertClosedEnvelopes(root, packet, integrity, redactionManifest, repor
|
|
|
165
63
|
const report = record(value);
|
|
166
64
|
if (!report)
|
|
167
65
|
throw new TypeError('The CLI case pack contains an invalid Case report envelope.');
|
|
168
|
-
assertOnlyKeys(report, REPORT_KEYS, 'Case report envelope');
|
|
66
|
+
assertOnlyKeys(report, caseVersion === PUBLIC_CASE_SCHEMA_VERSION ? CLI_CASE_PACK_PUBLIC_REPORT_KEYS : REPORT_KEYS, 'Case report envelope');
|
|
169
67
|
}
|
|
170
68
|
}
|
|
171
69
|
function topCasePath(path) {
|
|
@@ -184,12 +82,12 @@ function listItemPath(path, list) {
|
|
|
184
82
|
|| (path.length === 6 && path[0] === 'packet' && path[1] === 'reports' && typeof path[2] === 'number'
|
|
185
83
|
&& path[3] === 'analystResponse' && path[4] === list && typeof path[5] === 'number');
|
|
186
84
|
}
|
|
187
|
-
function allowedSensitiveFieldPath(key, path
|
|
85
|
+
function allowedSensitiveFieldPath(key, path) {
|
|
188
86
|
if (key === 'brandProfileIds')
|
|
189
|
-
return
|
|
87
|
+
return topCasePath(path) || reportCasePath(path);
|
|
190
88
|
if (key === 'notes')
|
|
191
89
|
return topCasePath(path) || reportCasePath(path);
|
|
192
|
-
if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'branches') {
|
|
90
|
+
if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
|
|
193
91
|
return topCasePath(path) || analystResponsePath(path);
|
|
194
92
|
}
|
|
195
93
|
if (key === 'recipient')
|
|
@@ -199,7 +97,7 @@ function allowedSensitiveFieldPath(key, path, current) {
|
|
|
199
97
|
return true;
|
|
200
98
|
}
|
|
201
99
|
/** Rejects hidden audience-sensitive copies with a budget proven by the serialized byte bound. */
|
|
202
|
-
function assertSensitiveFieldPlacement(root,
|
|
100
|
+
function assertSensitiveFieldPlacement(root, serializedLength) {
|
|
203
101
|
const stack = [{ value: root, path: [], depth: 0 }];
|
|
204
102
|
const visited = new Set();
|
|
205
103
|
let inspected = 0;
|
|
@@ -208,7 +106,7 @@ function assertSensitiveFieldPlacement(root, current, serializedLength) {
|
|
|
208
106
|
if (!entry || !entry.value || typeof entry.value !== 'object')
|
|
209
107
|
continue;
|
|
210
108
|
for (const key of SENSITIVE_CASE_PACK_FIELDS) {
|
|
211
|
-
if (Object.hasOwn(entry.value, key) && !allowedSensitiveFieldPath(key, entry.path
|
|
109
|
+
if (Object.hasOwn(entry.value, key) && !allowedSensitiveFieldPath(key, entry.path)) {
|
|
212
110
|
throw new TypeError('The CLI case pack contains audience-sensitive data outside its versioned case or report fields.');
|
|
213
111
|
}
|
|
214
112
|
}
|
|
@@ -274,63 +172,61 @@ function assertCanonicalCaseIdentities(cases, label) {
|
|
|
274
172
|
function assertCurrentCaseProjection(rawCases, normalised, label) {
|
|
275
173
|
if (rawCases.length !== normalised.length
|
|
276
174
|
|| rawCases.some((item, index) => !canonicalValuesMatch(item, normalised[index]))) {
|
|
277
|
-
throw new TypeError(`${label} contains a schema
|
|
175
|
+
throw new TypeError(`${label} contains a schema ${CASE_SCHEMA_VERSION} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
|
|
278
176
|
}
|
|
279
177
|
}
|
|
280
|
-
function
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
throw new TypeError(`The CLI case pack contains an invalid historical Case at cases[${index}].`);
|
|
294
|
-
const missing = required.find((key) => !Object.hasOwn(item, key));
|
|
295
|
-
if (missing) {
|
|
296
|
-
throw new TypeError(`The CLI case pack is missing required historical Case field cases[${index}].${missing}.`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
178
|
+
function publicObservedEffectHistory(preV13HistoryUnavailable) {
|
|
179
|
+
return {
|
|
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
|
+
};
|
|
299
191
|
}
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (caseVersion === 9)
|
|
314
|
-
return reportVersion === 4;
|
|
315
|
-
if (caseVersion === 10)
|
|
316
|
-
return reportVersion === 5 || reportVersion === 6;
|
|
317
|
-
return caseVersion === 11 && reportVersion === 7;
|
|
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
|
+
};
|
|
318
205
|
}
|
|
319
|
-
function assertAudienceFields(value, audience
|
|
320
|
-
if (!supportsBranches && Object.hasOwn(value, 'branches')) {
|
|
321
|
-
throw new TypeError('The CLI case pack contains an Investigation branch outside its versioned fields.');
|
|
322
|
-
}
|
|
206
|
+
function assertAudienceFields(value, audience) {
|
|
323
207
|
if (audience === 'internal')
|
|
324
208
|
return;
|
|
325
209
|
if (Object.hasOwn(value, 'notes') && (!Array.isArray(value.notes) || value.notes.length !== 0)) {
|
|
326
210
|
throw new TypeError('The CLI case pack contains Case notes excluded by its audience.');
|
|
327
211
|
}
|
|
328
212
|
if (audience === 'public') {
|
|
329
|
-
for (const field of ['actions', 'assertions',
|
|
213
|
+
for (const field of ['actions', 'assertions', 'branches']) {
|
|
330
214
|
if (Object.hasOwn(value, field) && (!Array.isArray(value[field]) || value[field].length !== 0)) {
|
|
331
215
|
throw new TypeError(`The public CLI case pack contains ${field} excluded by its audience.`);
|
|
332
216
|
}
|
|
333
217
|
}
|
|
218
|
+
for (const [field, list] of [['observedEffects', 'reviews'], ['closures', 'records']]) {
|
|
219
|
+
if (Object.hasOwn(value, field)) {
|
|
220
|
+
const history = record(value[field]);
|
|
221
|
+
const expected = field === 'observedEffects'
|
|
222
|
+
? publicObservedEffectHistory(history?.preV13HistoryUnavailable === true)
|
|
223
|
+
: publicClosureHistory(history?.preV13HistoryUnavailable === true);
|
|
224
|
+
if (!history || !Array.isArray(history[list]) || history[list].length !== 0
|
|
225
|
+
|| !canonicalValuesMatch(history, expected)) {
|
|
226
|
+
throw new TypeError(`The public CLI case pack contains ${field} excluded by its audience.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
334
230
|
}
|
|
335
231
|
else if (Object.hasOwn(value, 'actions')) {
|
|
336
232
|
if (!Array.isArray(value.actions) || value.actions.some((entry) => {
|
|
@@ -365,306 +261,25 @@ function assertCurrentReportProjection(report, rawCase) {
|
|
|
365
261
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report projection.');
|
|
366
262
|
}
|
|
367
263
|
}
|
|
368
|
-
function historicalMaterialFields(caseVersion) {
|
|
369
|
-
if (caseVersion <= 7)
|
|
370
|
-
return HISTORICAL_BASE_MATERIAL_FIELDS;
|
|
371
|
-
if (caseVersion <= 9)
|
|
372
|
-
return HISTORICAL_EXTERNAL_FORM_MATERIAL_FIELDS;
|
|
373
|
-
return HISTORICAL_CONTEXT_MATERIAL_FIELDS;
|
|
374
|
-
}
|
|
375
|
-
function historicalMaterialValue(field, snapshot) {
|
|
376
|
-
switch (field) {
|
|
377
|
-
case 'availability':
|
|
378
|
-
return typeof snapshot.availability === 'string' && CONCLUSIVE_AVAILABILITY.has(snapshot.availability)
|
|
379
|
-
? snapshot.availability
|
|
380
|
-
: null;
|
|
381
|
-
case 'registrar':
|
|
382
|
-
return String(snapshot.registrar ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() || null;
|
|
383
|
-
case 'createdDate':
|
|
384
|
-
case 'expiryDate': {
|
|
385
|
-
const value = snapshot[field];
|
|
386
|
-
return typeof value === 'string' ? value.slice(0, 10) : null;
|
|
387
|
-
}
|
|
388
|
-
case 'riskFactors':
|
|
389
|
-
case 'opportunityFactors':
|
|
390
|
-
return snapshot[field].map((factor) => [factor.label, factor.points]);
|
|
391
|
-
default:
|
|
392
|
-
return snapshot[field] ?? null;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function historicalMaterialIsEmpty(value) {
|
|
396
|
-
if (value === null || value === undefined)
|
|
397
|
-
return true;
|
|
398
|
-
if (Array.isArray(value))
|
|
399
|
-
return value.length === 0;
|
|
400
|
-
return typeof value === 'string' ? value.trim() === '' : false;
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Recreates the exact material identity used by each historical Case epoch.
|
|
404
|
-
* Current migration happens separately so adding schema-12 fields can never
|
|
405
|
-
* invalidate an authentic old fingerprint or make a forged one look valid.
|
|
406
|
-
*/
|
|
407
|
-
function normalizeHistoricalEvidenceHistory(rawHistory, caseVersion) {
|
|
408
|
-
if (!Array.isArray(rawHistory) || rawHistory.length > MAX_EVIDENCE_SNAPSHOTS_PER_CASE) {
|
|
409
|
-
throw new TypeError('The CLI case pack contains an invalid historical evidence collection.');
|
|
410
|
-
}
|
|
411
|
-
const fields = historicalMaterialFields(caseVersion);
|
|
412
|
-
const materialSeen = new Set();
|
|
413
|
-
const built = rawHistory.map((raw, index) => {
|
|
414
|
-
const rawSnapshot = record(raw);
|
|
415
|
-
const normalized = normalizeSnapshot(raw, { fallback: null, sourceVersion: caseVersion });
|
|
416
|
-
if (!rawSnapshot || !normalized) {
|
|
417
|
-
throw new TypeError(`The CLI case pack contains an invalid historical evidence snapshot ${index}.`);
|
|
418
|
-
}
|
|
419
|
-
const materialRecord = {};
|
|
420
|
-
let hasEvidence = false;
|
|
421
|
-
for (const field of fields) {
|
|
422
|
-
const value = historicalMaterialValue(field, normalized);
|
|
423
|
-
materialRecord[field] = value;
|
|
424
|
-
if (field !== 'scanDepth' && field !== 'confidence' && !historicalMaterialIsEmpty(value))
|
|
425
|
-
hasEvidence = true;
|
|
426
|
-
}
|
|
427
|
-
if (!hasEvidence) {
|
|
428
|
-
throw new TypeError(`The CLI case pack contains an empty historical evidence snapshot ${index}.`);
|
|
429
|
-
}
|
|
430
|
-
const material = JSON.stringify(materialRecord);
|
|
431
|
-
if (materialSeen.has(material)) {
|
|
432
|
-
throw new TypeError('The CLI case pack contains duplicate historical evidence material.');
|
|
433
|
-
}
|
|
434
|
-
materialSeen.add(material);
|
|
435
|
-
const fingerprint = hashString(material);
|
|
436
|
-
return {
|
|
437
|
-
material,
|
|
438
|
-
snapshot: {
|
|
439
|
-
id: `ev-${fingerprint}`,
|
|
440
|
-
fingerprint,
|
|
441
|
-
firstCapturedAt: normalized.firstCapturedAt,
|
|
442
|
-
capturedAt: normalized.capturedAt,
|
|
443
|
-
source: normalized.source,
|
|
444
|
-
...Object.fromEntries(fields.map((field) => [field, normalized[field] ?? null])),
|
|
445
|
-
},
|
|
446
|
-
rawSnapshot,
|
|
447
|
-
};
|
|
448
|
-
});
|
|
449
|
-
built.sort((left, right) => (Date.parse(String(left.snapshot.capturedAt)) - Date.parse(String(right.snapshot.capturedAt))
|
|
450
|
-
|| Date.parse(String(left.snapshot.firstCapturedAt)) - Date.parse(String(right.snapshot.firstCapturedAt))
|
|
451
|
-
|| String(left.snapshot.fingerprint).localeCompare(String(right.snapshot.fingerprint))));
|
|
452
|
-
const usedIds = new Set();
|
|
453
|
-
for (const item of built) {
|
|
454
|
-
const base = String(item.snapshot.id);
|
|
455
|
-
let id = base;
|
|
456
|
-
let suffix = 2;
|
|
457
|
-
while (usedIds.has(id))
|
|
458
|
-
id = `${base}-${suffix++}`;
|
|
459
|
-
usedIds.add(id);
|
|
460
|
-
item.snapshot.id = id;
|
|
461
|
-
}
|
|
462
|
-
const projected = built.map((item) => item.snapshot);
|
|
463
|
-
assertProjectionKeys(rawHistory, projected, 'historical evidenceHistory');
|
|
464
|
-
for (let index = 0; index < built.length; index++) {
|
|
465
|
-
const item = built[index];
|
|
466
|
-
if (!item
|
|
467
|
-
|| item.rawSnapshot.id !== item.snapshot.id
|
|
468
|
-
|| item.rawSnapshot.fingerprint !== item.snapshot.fingerprint) {
|
|
469
|
-
throw new TypeError(`The CLI case pack contains an invalid historical evidence identity at snapshot ${index}.`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return projected;
|
|
473
|
-
}
|
|
474
|
-
function historicalEvidenceChanges(previous, current, caseVersion) {
|
|
475
|
-
const previousSnapshot = previous;
|
|
476
|
-
const currentSnapshot = current;
|
|
477
|
-
const changes = compareCaseEvidence(previousSnapshot, currentSnapshot)
|
|
478
|
-
.filter((change) => caseVersion >= 10 || !['opportunityScore', 'opportunityFactors'].includes(change.field));
|
|
479
|
-
if (caseVersion <= 9 && previous.opportunityScore !== current.opportunityScore) {
|
|
480
|
-
changes.push({
|
|
481
|
-
field: 'opportunityScore',
|
|
482
|
-
label: 'Opportunity score',
|
|
483
|
-
before: previous.opportunityScore ?? null,
|
|
484
|
-
after: current.opportunityScore ?? null,
|
|
485
|
-
tone: 'neutral',
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
if (caseVersion <= 9 && !canonicalValuesMatch(previous.opportunityFactors, current.opportunityFactors)) {
|
|
489
|
-
changes.push({
|
|
490
|
-
field: 'opportunityFactors',
|
|
491
|
-
label: 'Opportunity factors',
|
|
492
|
-
before: previous.opportunityFactors,
|
|
493
|
-
after: current.opportunityFactors,
|
|
494
|
-
tone: 'neutral',
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
const order = new Map(HISTORICAL_CHANGE_ORDER.map((field, index) => [field, index]));
|
|
498
|
-
return changes
|
|
499
|
-
.sort((left, right) => (order.get(left.field) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.field) ?? Number.MAX_SAFE_INTEGER))
|
|
500
|
-
.slice(0, 40);
|
|
501
|
-
}
|
|
502
|
-
function historicalEvidenceTimeline(history, caseVersion) {
|
|
503
|
-
return history.map((snapshot, index) => {
|
|
504
|
-
if (index === 0) {
|
|
505
|
-
return {
|
|
506
|
-
snapshot,
|
|
507
|
-
isBaseline: true,
|
|
508
|
-
hasRepeatedObservation: snapshot.firstCapturedAt !== snapshot.capturedAt,
|
|
509
|
-
changes: null,
|
|
510
|
-
hasIncomparableChange: false,
|
|
511
|
-
incomparableReasons: [],
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
const previous = history[index - 1];
|
|
515
|
-
if (!previous)
|
|
516
|
-
throw new TypeError('The CLI case pack contains an invalid historical evidence timeline.');
|
|
517
|
-
const changes = historicalEvidenceChanges(previous, snapshot, caseVersion);
|
|
518
|
-
let incomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot).filter((reason) => caseVersion >= 10 || reason !== 'opportunity-model');
|
|
519
|
-
if (changes.length === 0 && snapshot.fingerprint !== previous.fingerprint && incomparableReasons.length === 0) {
|
|
520
|
-
incomparableReasons = ['other'];
|
|
521
|
-
}
|
|
522
|
-
return {
|
|
523
|
-
snapshot,
|
|
524
|
-
isBaseline: false,
|
|
525
|
-
hasRepeatedObservation: snapshot.firstCapturedAt !== snapshot.capturedAt,
|
|
526
|
-
changes: changes.length ? changes : null,
|
|
527
|
-
hasIncomparableChange: incomparableReasons.length > 0,
|
|
528
|
-
incomparableReasons,
|
|
529
|
-
};
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
function pickHistoricalFields(value, fields) {
|
|
533
|
-
return Object.fromEntries(fields.map((field) => [field, structuredClone(value[field])]));
|
|
534
|
-
}
|
|
535
|
-
function historicalAnalystResponse(caseRecord, caseVersion, reportVersion) {
|
|
536
|
-
if (reportVersion === 1)
|
|
537
|
-
return null;
|
|
538
|
-
const oldPinFields = ['id', 'label', 'value', 'source', 'observedAt', 'completeness', 'limitations', 'createdAt'];
|
|
539
|
-
const expandedPinFields = [
|
|
540
|
-
'id', 'checkpointId', 'field', 'category', 'label', 'value', 'source', 'sourceState', 'sourceSchema',
|
|
541
|
-
'observedAt', 'collectionDepth', 'completeness', 'truncated',
|
|
542
|
-
...(caseVersion >= 7 ? ['transitionExpectation'] : []),
|
|
543
|
-
'limitations', 'createdAt',
|
|
544
|
-
];
|
|
545
|
-
const evidencePins = caseRecord.evidencePins.map((item) => pickHistoricalFields(item, caseVersion >= 5 ? expandedPinFields : oldPinFields));
|
|
546
|
-
const decisions = caseRecord.decisions.map((item) => pickHistoricalFields(item, ['id', 'summary', 'rationale', 'evidencePinIds', 'createdAt']));
|
|
547
|
-
const actions = caseRecord.actions.map((item) => pickHistoricalFields(item, ['id', 'type', 'recipient', 'contactSource', 'contactLimitations', 'dueAt', 'state', 'reference', 'followUpAt', 'outcome', 'createdAt', 'updatedAt']));
|
|
548
|
-
const assertions = caseRecord.assertions.map((item) => {
|
|
549
|
-
const projected = pickHistoricalFields(item, [
|
|
550
|
-
'id', 'kind', 'statement', 'rationale', 'evidencePinIds',
|
|
551
|
-
...(reportVersion >= 6 ? ['evidenceRelations'] : []),
|
|
552
|
-
'state', 'createdAt', 'updatedAt',
|
|
553
|
-
]);
|
|
554
|
-
if (caseVersion >= 7 && item.provenance)
|
|
555
|
-
projected.provenance = structuredClone(item.provenance);
|
|
556
|
-
return projected;
|
|
557
|
-
});
|
|
558
|
-
const manualTrail = caseRecord.manualTrail.map((item) => pickHistoricalFields(item, ['id', 'kind', 'summary', 'target', 'createdAt']));
|
|
559
|
-
const sightings = caseRecord.sightings.map((item) => pickHistoricalFields(item, ['id', 'state', 'sourceClass', 'category', 'source', 'observedAt', 'completeness', 'evidencePinId', 'limitations', 'createdAt']));
|
|
560
|
-
const branches = (caseRecord.branches ?? []).map((item) => pickHistoricalFields(item, ['id', 'name', 'state', 'evidencePinIds', 'checkpointIds', 'assertionIds', 'actionIds', 'createdAt', 'updatedAt']));
|
|
561
|
-
return {
|
|
562
|
-
evidencePins,
|
|
563
|
-
decisions,
|
|
564
|
-
actions,
|
|
565
|
-
...(reportVersion >= 3 ? { assertions, manualTrail } : {}),
|
|
566
|
-
...(reportVersion >= 4 ? { sightings } : {}),
|
|
567
|
-
...(reportVersion >= 7 ? { branches } : {}),
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
function assertHistoricalCaseResponseEpoch(rawCase, caseRecord, caseVersion, reportVersion, path) {
|
|
571
|
-
const expected = historicalAnalystResponse(caseRecord, caseVersion, reportVersion);
|
|
572
|
-
const introductions = new Map([
|
|
573
|
-
['evidencePins', 3], ['decisions', 3], ['actions', 3],
|
|
574
|
-
['assertions', 4], ['manualTrail', 4], ['sightings', 9], ['branches', 11],
|
|
575
|
-
]);
|
|
576
|
-
for (const [field, introduced] of introductions) {
|
|
577
|
-
if (caseVersion < introduced) {
|
|
578
|
-
if (Object.hasOwn(rawCase, field)) {
|
|
579
|
-
throw new TypeError(`The CLI case pack contains ${path}.${field} outside its historical Case epoch.`);
|
|
580
|
-
}
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
if (Object.hasOwn(rawCase, field)) {
|
|
584
|
-
const projection = record(expected)?.[field];
|
|
585
|
-
assertProjectionKeys(rawCase[field], projection, `${path}.${field}`);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
if (caseVersion < 10 && Object.hasOwn(rawCase, 'reviewReasonCode')) {
|
|
589
|
-
throw new TypeError(`The CLI case pack contains ${path}.reviewReasonCode outside its historical Case epoch.`);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
function historicalReportProjection(caseRecord, evidenceHistory, report, caseVersion, reportVersion, generatedAt) {
|
|
593
|
-
const limitation = LEGACY_REPORT_LIMITATION_BY_VERSION.get(reportVersion);
|
|
594
|
-
if (!limitation)
|
|
595
|
-
throw new TypeError('The CLI case pack contains an unsupported historical Case report.');
|
|
596
|
-
const reportCase = {
|
|
597
|
-
id: caseRecord.id,
|
|
598
|
-
domain: caseRecord.domain,
|
|
599
|
-
status: caseRecord.status,
|
|
600
|
-
disposition: caseRecord.disposition,
|
|
601
|
-
...(reportVersion >= 5 ? { reviewReasonCode: caseRecord.reviewReasonCode ?? null } : {}),
|
|
602
|
-
...(reportVersion >= 6 ? { interoperabilityTags: analystInteroperabilityTags(caseRecord.disposition, caseRecord.reviewReasonCode) } : {}),
|
|
603
|
-
tags: [...caseRecord.tags],
|
|
604
|
-
source: caseRecord.source,
|
|
605
|
-
openedAt: caseRecord.createdAt,
|
|
606
|
-
updatedAt: caseRecord.updatedAt,
|
|
607
|
-
notesIncluded: false,
|
|
608
|
-
};
|
|
609
|
-
const response = historicalAnalystResponse(caseRecord, caseVersion, reportVersion);
|
|
610
|
-
return {
|
|
611
|
-
schema: CASE_REPORT_SCHEMA,
|
|
612
|
-
schemaVersion: reportVersion,
|
|
613
|
-
generatedAt,
|
|
614
|
-
application: reportVersion >= 6
|
|
615
|
-
? buildPortableGeneratorMetadata(reportApplicationVersion(report))
|
|
616
|
-
: { name: 'WHOISleuth' },
|
|
617
|
-
case: reportCase,
|
|
618
|
-
currentAssessment: evidenceHistory.at(-1) ?? null,
|
|
619
|
-
evidenceTimeline: historicalEvidenceTimeline(evidenceHistory, caseVersion),
|
|
620
|
-
...(response ? { analystResponse: response } : {}),
|
|
621
|
-
limitations: limitation,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
function assertLegacyProfileContextEpoch(rawCase, report) {
|
|
625
|
-
const assertSnapshot = (value, path) => {
|
|
626
|
-
const snapshot = record(value);
|
|
627
|
-
if (snapshot && (Object.hasOwn(snapshot, 'profileContextState') || Object.hasOwn(snapshot, 'profileContextLimitation'))) {
|
|
628
|
-
throw new TypeError(`The CLI case pack contains schema-12 profile context fields outside the historical ${path} epoch.`);
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
if (Array.isArray(rawCase.evidenceHistory)) {
|
|
632
|
-
rawCase.evidenceHistory.forEach((snapshot, index) => assertSnapshot(snapshot, `cases evidence snapshot ${index}`));
|
|
633
|
-
}
|
|
634
|
-
assertSnapshot(report.currentAssessment, 'current assessment');
|
|
635
|
-
if (Array.isArray(report.evidenceTimeline)) {
|
|
636
|
-
report.evidenceTimeline.forEach((entry, index) => {
|
|
637
|
-
const timelineEntry = record(entry);
|
|
638
|
-
assertSnapshot(timelineEntry?.snapshot, `report timeline snapshot ${index}`);
|
|
639
|
-
if (!Array.isArray(timelineEntry?.changes))
|
|
640
|
-
return;
|
|
641
|
-
timelineEntry.changes.forEach((value, changeIndex) => {
|
|
642
|
-
const change = record(value);
|
|
643
|
-
if (!change)
|
|
644
|
-
return;
|
|
645
|
-
const field = change.field;
|
|
646
|
-
const label = change.label;
|
|
647
|
-
const carriesProfileContextField = ['profileContextState', 'profileContextLimitation'].some((key) => (Object.hasOwn(change, key)
|
|
648
|
-
|| field === key
|
|
649
|
-
|| [change.before, change.after].some((candidate) => Boolean(record(candidate) && Object.hasOwn(record(candidate), key)))));
|
|
650
|
-
if (carriesProfileContextField
|
|
651
|
-
|| (typeof label === 'string' && /profile[ -]?context/iu.test(label))) {
|
|
652
|
-
throw new TypeError(`The CLI case pack contains schema-12 profile context fields outside the historical report timeline change ${index}.${changeIndex} epoch.`);
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
264
|
function redactedCase(record, audience) {
|
|
659
265
|
if (audience === 'internal')
|
|
660
266
|
return structuredClone(record);
|
|
661
267
|
return {
|
|
662
268
|
...structuredClone(record),
|
|
269
|
+
status: audience === 'public' && record.status === 'resolved' && record.closures.records.length
|
|
270
|
+
? 'reviewing'
|
|
271
|
+
: record.status,
|
|
663
272
|
brandProfileIds: audience === 'public' ? [] : [...record.brandProfileIds],
|
|
664
273
|
notes: [],
|
|
665
274
|
actions: audience === 'public' ? [] : record.actions.map((item) => ({ ...item, recipient: '[redacted]' })),
|
|
666
275
|
manualTrail: record.manualTrail.map((item) => ({ ...item, target: null })),
|
|
667
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),
|
|
668
283
|
branches: audience === 'public' ? [] : structuredClone(record.branches ?? []),
|
|
669
284
|
};
|
|
670
285
|
}
|
|
@@ -673,33 +288,39 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
673
288
|
throw new CliUsageError('case-pack requires --reviewed after the audience-specific output has been checked.');
|
|
674
289
|
if (Buffer.byteLength(input, 'utf8') > MAX_CASE_PACK_INPUT_BYTES)
|
|
675
290
|
throw new CliUsageError('Case-pack input is limited to 4 MiB.');
|
|
291
|
+
const normalized = input.replace(/^\uFEFF/u, '');
|
|
676
292
|
let parsed;
|
|
677
293
|
try {
|
|
678
|
-
|
|
294
|
+
scanBoundedJson(normalized);
|
|
295
|
+
parsed = JSON.parse(normalized);
|
|
679
296
|
}
|
|
680
297
|
catch {
|
|
681
|
-
throw new CliUsageError('Case-pack input must be valid JSON.');
|
|
298
|
+
throw new CliUsageError('Case-pack input must be valid bounded JSON without duplicate keys.');
|
|
682
299
|
}
|
|
683
300
|
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
684
|
-
if (
|
|
685
|
-
throw new CliUsageError(`Case
|
|
301
|
+
if (Number.isSafeInteger(root.version) && root.version < CASE_SCHEMA_VERSION) {
|
|
302
|
+
throw new CliUsageError(`Case schema ${String(root.version)} is retired. Export it as schema ${CASE_SCHEMA_VERSION} with the last broad-reader release before packaging; no data was changed.`);
|
|
686
303
|
}
|
|
687
|
-
if (root.version
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
304
|
+
if (Number.isSafeInteger(root.version) && root.version > CASE_SCHEMA_VERSION) {
|
|
305
|
+
throw new CliUsageError(`Case schema ${String(root.version)} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
306
|
+
}
|
|
307
|
+
if (root.version !== CASE_SCHEMA_VERSION || !Array.isArray(root.cases)) {
|
|
308
|
+
throw new CliUsageError(`Case-pack input must be a well-formed WHOISleuth Case schema ${CASE_SCHEMA_VERSION} export.`);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
assertCanonicalCaseIdentities(root.cases, 'Case-pack input');
|
|
312
|
+
for (const item of root.cases) {
|
|
313
|
+
const rawCase = record(item);
|
|
314
|
+
if (!rawCase || !Object.hasOwn(rawCase, 'brandProfileIds'))
|
|
315
|
+
throw new Error('missing');
|
|
316
|
+
assertCaseBrandProfileIds(rawCase.brandProfileIds);
|
|
696
317
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
throw new CliUsageError('Case-pack schema 12 input requires exact canonical Case identities and an exact, unique, bounded brandProfileIds array on every case.');
|
|
318
|
+
}
|
|
319
|
+
catch (cause) {
|
|
320
|
+
if (cause instanceof TypeError && /Case identity/u.test(cause.message)) {
|
|
321
|
+
throw new CliUsageError(cause.message);
|
|
702
322
|
}
|
|
323
|
+
throw new CliUsageError(`Case-pack schema ${CASE_SCHEMA_VERSION} input requires exact canonical Case identities and an exact, unique, bounded brandProfileIds array on every case.`);
|
|
703
324
|
}
|
|
704
325
|
const normalised = normalizeCaseStore(root).cases;
|
|
705
326
|
if (!normalised.length)
|
|
@@ -710,13 +331,11 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
710
331
|
if (normalised.length > MAX_CASE_PACK_CASES) {
|
|
711
332
|
throw new CliUsageError(`Case packs are limited to ${MAX_CASE_PACK_CASES} reviewed cases. Export a smaller selected set so no case is silently omitted.`);
|
|
712
333
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
throw new CliUsageError(cause instanceof Error ? cause.message : 'Case-pack schema 12 input is not exact.');
|
|
719
|
-
}
|
|
334
|
+
try {
|
|
335
|
+
assertCurrentCaseProjection(root.cases, normalised, 'Case-pack input');
|
|
336
|
+
}
|
|
337
|
+
catch (cause) {
|
|
338
|
+
throw new CliUsageError(cause instanceof Error ? cause.message : `Case-pack schema ${CASE_SCHEMA_VERSION} input is not exact.`);
|
|
720
339
|
}
|
|
721
340
|
const cases = normalised.map((item) => redactedCase(item, options.audience));
|
|
722
341
|
const brandProfileReferencesOmitted = options.audience === 'public'
|
|
@@ -727,7 +346,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
727
346
|
includeNotes: false,
|
|
728
347
|
generatedAt,
|
|
729
348
|
}).json);
|
|
730
|
-
const exclusions = expectedExclusions(options.audience
|
|
349
|
+
const exclusions = expectedExclusions(options.audience);
|
|
731
350
|
const packet = Object.freeze({
|
|
732
351
|
schema: CLI_CASE_PACK_SCHEMA,
|
|
733
352
|
version: CLI_CASE_PACK_VERSION,
|
|
@@ -756,10 +375,31 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
756
375
|
return document;
|
|
757
376
|
}
|
|
758
377
|
export function verifyCliCasePack(input) {
|
|
378
|
+
assertBoundedJsonStructure(input, 'CLI case pack');
|
|
759
379
|
const root = record(input);
|
|
380
|
+
const packet = record(root?.packet);
|
|
381
|
+
const packetVersion = packet?.version;
|
|
382
|
+
if (packet?.schema === CLI_CASE_PACK_SCHEMA && Number.isSafeInteger(packetVersion)) {
|
|
383
|
+
if (packetVersion < CLI_CASE_PACK_VERSION) {
|
|
384
|
+
throw new TypeError(`CLI case-pack version ${String(packetVersion)} is retired. Export it again as version ${CLI_CASE_PACK_VERSION} before importing; no data was changed.`);
|
|
385
|
+
}
|
|
386
|
+
if (packetVersion > CLI_CASE_PACK_VERSION) {
|
|
387
|
+
throw new TypeError(`CLI case-pack version ${String(packetVersion)} is newer than the supported version ${CLI_CASE_PACK_VERSION}; no data was changed.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const caseVersion = root?.version;
|
|
391
|
+
if (Number.isSafeInteger(caseVersion)) {
|
|
392
|
+
if (caseVersion < CASE_SCHEMA_VERSION
|
|
393
|
+
&& !CASE_IMPORT_VERSIONS.includes(caseVersion)) {
|
|
394
|
+
throw new TypeError(`Case schema ${String(caseVersion)} in this CLI case pack is not part of the public compatibility boundary; no data was changed.`);
|
|
395
|
+
}
|
|
396
|
+
if (caseVersion > CASE_SCHEMA_VERSION) {
|
|
397
|
+
throw new TypeError(`Case schema ${String(caseVersion)} in this CLI case pack is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
760
400
|
let serialized;
|
|
761
401
|
try {
|
|
762
|
-
serialized = JSON.stringify(
|
|
402
|
+
serialized = JSON.stringify(input);
|
|
763
403
|
}
|
|
764
404
|
catch {
|
|
765
405
|
throw new TypeError('The CLI case pack structure is not serializable.');
|
|
@@ -767,15 +407,16 @@ export function verifyCliCasePack(input) {
|
|
|
767
407
|
if (Buffer.byteLength(serialized, 'utf8') > MAX_CASE_IMPORT_BYTES) {
|
|
768
408
|
throw new TypeError('The CLI case pack exceeds the browser import limit.');
|
|
769
409
|
}
|
|
770
|
-
const packet = record(root?.packet);
|
|
771
410
|
const integrity = record(root?.integrity);
|
|
772
411
|
const redactionManifest = record(packet?.redactionManifest);
|
|
773
412
|
if (!root
|
|
774
413
|
|| !packet
|
|
775
414
|
|| packet.schema !== CLI_CASE_PACK_SCHEMA
|
|
776
|
-
||
|
|
415
|
+
|| packet.version !== CLI_CASE_PACK_VERSION
|
|
416
|
+
|| typeof root.version !== 'number'
|
|
417
|
+
|| !CASE_IMPORT_VERSIONS.includes(root.version)
|
|
777
418
|
|| packet.reviewed !== true
|
|
778
|
-
||
|
|
419
|
+
|| (packet.audience !== 'internal' && packet.audience !== 'public' && packet.audience !== 'trusted')
|
|
779
420
|
|| !Array.isArray(packet.reports)
|
|
780
421
|
|| !redactionManifest
|
|
781
422
|
|| !Array.isArray(root.cases)
|
|
@@ -783,121 +424,84 @@ export function verifyCliCasePack(input) {
|
|
|
783
424
|
|| root.cases.length > MAX_CASE_PACK_CASES
|
|
784
425
|
|| packet.reports.length !== root.cases.length
|
|
785
426
|
|| typeof root.exportedAt !== 'string'
|
|
786
|
-
|| typeof root.version !== 'number'
|
|
787
|
-
|| !CASE_IMPORT_VERSIONS.includes(root.version)
|
|
788
427
|
|| !integrity
|
|
789
428
|
|| integrity.algorithm !== 'SHA-256'
|
|
429
|
+
|| integrity.canonicalization !== SORTED_JSON_V2
|
|
790
430
|
|| typeof integrity.digestSha256 !== 'string'
|
|
791
431
|
|| !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)) {
|
|
792
|
-
throw new TypeError('The CLI case pack structure or integrity envelope is
|
|
793
|
-
}
|
|
794
|
-
let canonicalization;
|
|
795
|
-
try {
|
|
796
|
-
canonicalization = resolveArtifactCanonicalization(packet.version, integrity.canonicalization, [
|
|
797
|
-
{ version: LEGACY_CLI_CASE_PACK_VERSION, canonicalization: SORTED_JSON_V1, explicit: true },
|
|
798
|
-
{ version: CLI_CASE_PACK_VERSION, canonicalization: SORTED_JSON_V2, explicit: true },
|
|
799
|
-
], 'CLI case pack');
|
|
432
|
+
throw new TypeError('The CLI case pack structure or integrity envelope is malformed.');
|
|
800
433
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
const { integrity: _integrity, ...unsigned } = root;
|
|
434
|
+
assertClosedEnvelopes(root, packet, integrity, redactionManifest, packet.reports, root.version);
|
|
435
|
+
assertSensitiveFieldPlacement(root, serialized.length);
|
|
805
436
|
const audience = packet.audience;
|
|
806
|
-
const current = root.version === CASE_SCHEMA_VERSION;
|
|
807
|
-
assertClosedEnvelopes(root, packet, integrity, redactionManifest, packet.reports, current);
|
|
808
|
-
assertSensitiveFieldPlacement(root, current, serialized.length);
|
|
809
437
|
if (!Number.isSafeInteger(redactionManifest.sourceCaseCount)
|
|
810
|
-
||
|
|
438
|
+
|| redactionManifest.sourceCaseCount !== root.cases.length
|
|
811
439
|
|| !stringListsMatch(redactionManifest.excluded, expectedExclusions(audience, root.version))) {
|
|
812
440
|
throw new TypeError('The CLI case pack has an invalid audience redaction manifest.');
|
|
813
441
|
}
|
|
814
|
-
const caseReferenceLists = [];
|
|
815
|
-
const reportReferenceLists = [];
|
|
816
|
-
if (!current)
|
|
817
|
-
assertHistoricalCaseOwnKeys(root.cases, root.version);
|
|
818
442
|
assertCanonicalCaseIdentities(root.cases, 'The CLI case pack');
|
|
819
443
|
const normalised = normalizeCaseStore(root).cases;
|
|
820
444
|
if (normalised.length !== root.cases.length) {
|
|
821
445
|
throw new TypeError('The CLI case pack contains an invalid case collection.');
|
|
822
446
|
}
|
|
823
|
-
|
|
824
|
-
if (current)
|
|
447
|
+
if (root.version === CASE_SCHEMA_VERSION)
|
|
825
448
|
assertCurrentCaseProjection(root.cases, normalised, 'The CLI case pack');
|
|
826
|
-
|
|
449
|
+
const normalisedByDomain = new Map(normalised.map((item) => [item.domain, item]));
|
|
450
|
+
const caseReferenceLists = [];
|
|
451
|
+
const reportReferenceLists = [];
|
|
452
|
+
for (let index = 0; index < root.cases.length; index += 1) {
|
|
827
453
|
const rawCase = record(root.cases[index]);
|
|
828
454
|
const report = record(packet.reports[index]);
|
|
829
455
|
const reportCase = record(report?.case);
|
|
830
|
-
const reportVersion = report?.schemaVersion;
|
|
831
456
|
if (!rawCase
|
|
832
457
|
|| !report
|
|
833
458
|
|| !reportCase
|
|
834
459
|
|| typeof rawCase.domain !== 'string'
|
|
835
460
|
|| report.schema !== CASE_REPORT_SCHEMA
|
|
836
|
-
|| !
|
|
461
|
+
|| !caseReportVersionMatchesCase(root.version, report.schemaVersion)
|
|
837
462
|
|| reportCase.id !== rawCase.id
|
|
838
|
-
|| reportCase.domain !== rawCase.domain
|
|
463
|
+
|| reportCase.domain !== rawCase.domain
|
|
464
|
+
|| !normalisedByDomain.has(rawCase.domain)) {
|
|
839
465
|
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
|
|
840
466
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (current) {
|
|
848
|
-
try {
|
|
849
|
-
if (!Object.hasOwn(rawCase, 'brandProfileIds') || !Object.hasOwn(reportCase, 'brandProfileIds'))
|
|
850
|
-
throw new Error('missing');
|
|
851
|
-
caseReferenceLists.push(assertCaseBrandProfileIds(rawCase.brandProfileIds));
|
|
852
|
-
reportReferenceLists.push(assertCaseBrandProfileIds(reportCase.brandProfileIds));
|
|
853
|
-
}
|
|
854
|
-
catch {
|
|
855
|
-
throw new TypeError('The CLI case pack contains invalid Brand Profile references.');
|
|
856
|
-
}
|
|
857
|
-
assertCurrentReportProjection(report, rawCase);
|
|
467
|
+
assertAudienceFields(rawCase, audience);
|
|
468
|
+
try {
|
|
469
|
+
if (!Object.hasOwn(rawCase, 'brandProfileIds') || !Object.hasOwn(reportCase, 'brandProfileIds'))
|
|
470
|
+
throw new Error('missing');
|
|
471
|
+
caseReferenceLists.push(assertCaseBrandProfileIds(rawCase.brandProfileIds));
|
|
472
|
+
reportReferenceLists.push(assertCaseBrandProfileIds(reportCase.brandProfileIds));
|
|
858
473
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
assertProjectionKeys(rawCase, { ...normalisedCase, evidenceHistory: historicalHistory }, `cases[${index}]`);
|
|
862
|
-
assertHistoricalCaseResponseEpoch(rawCase, normalisedCase, root.version, reportVersion, `cases[${index}]`);
|
|
863
|
-
if (report.generatedAt !== root.exportedAt) {
|
|
864
|
-
throw new TypeError('The CLI case pack contains an invalid or mismatched historical report generation time.');
|
|
865
|
-
}
|
|
866
|
-
const expectedReport = historicalReportProjection(normalisedCase, historicalHistory, report, root.version, reportVersion, root.exportedAt);
|
|
867
|
-
if (!canonicalValuesMatch(report, expectedReport)) {
|
|
868
|
-
throw new TypeError('The CLI case pack contains an invalid or mismatched historical Case report projection.');
|
|
869
|
-
}
|
|
474
|
+
catch {
|
|
475
|
+
throw new TypeError('The CLI case pack contains invalid Brand Profile references.');
|
|
870
476
|
}
|
|
477
|
+
if (root.version === CASE_SCHEMA_VERSION)
|
|
478
|
+
assertCurrentReportProjection(report, rawCase);
|
|
871
479
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
throw new TypeError('The public CLI case pack has an invalid Brand Profile redaction manifest.');
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
else if (Number(omitted) !== 0 || !referencesMatch) {
|
|
892
|
-
throw new TypeError('The trusted or internal CLI case pack has inconsistent Brand Profile references.');
|
|
480
|
+
const omitted = redactionManifest.brandProfileReferencesOmitted;
|
|
481
|
+
if (!Object.hasOwn(redactionManifest, 'brandProfileReferencesOmitted')
|
|
482
|
+
|| !Number.isSafeInteger(omitted)
|
|
483
|
+
|| omitted < 0
|
|
484
|
+
|| omitted > MAX_CASE_PACK_CASES * 8) {
|
|
485
|
+
throw new TypeError('The CLI case pack has an invalid Brand Profile redaction manifest.');
|
|
486
|
+
}
|
|
487
|
+
const referencesMatch = caseReferenceLists.every((references, index) => {
|
|
488
|
+
const reportReferences = reportReferenceLists[index];
|
|
489
|
+
return reportReferences?.length === references.length
|
|
490
|
+
&& references.every((reference, referenceIndex) => reportReferences[referenceIndex] === reference);
|
|
491
|
+
});
|
|
492
|
+
if (audience === 'public') {
|
|
493
|
+
if (caseReferenceLists.some((references) => references.length !== 0)
|
|
494
|
+
|| reportReferenceLists.some((references) => references.length !== 0)) {
|
|
495
|
+
throw new TypeError('The public CLI case pack has an invalid Brand Profile redaction manifest.');
|
|
893
496
|
}
|
|
894
497
|
}
|
|
895
|
-
else if (
|
|
896
|
-
throw new TypeError('The
|
|
498
|
+
else if (omitted !== 0 || !referencesMatch) {
|
|
499
|
+
throw new TypeError('The trusted or internal CLI case pack has inconsistent Brand Profile references.');
|
|
897
500
|
}
|
|
501
|
+
const { integrity: _integrity, ...unsigned } = root;
|
|
898
502
|
let calculated;
|
|
899
503
|
try {
|
|
900
|
-
calculated = `sha256:${createHash('sha256').update(
|
|
504
|
+
calculated = `sha256:${createHash('sha256').update(canonicalArtifactJsonV2(unsigned)).digest('hex')}`;
|
|
901
505
|
}
|
|
902
506
|
catch {
|
|
903
507
|
throw new TypeError('The CLI case pack structure is not serializable.');
|