@slicedearth/whoisleuth-cli 1.45.0 → 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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- 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 +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- 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 +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -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 +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- 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-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- 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-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- 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 +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- 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/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- 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 -19
- 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} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- 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/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -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/packages/evidence/artifact-integrity.mjs +65 -0
- 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} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- 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/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- 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} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -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} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- 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} +18 -12
- 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} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- 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 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- 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 -232
- 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 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /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,22 +1,286 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import { canonicalArtifactJson } from "../
|
|
4
|
-
import { buildCaseReport } from "../
|
|
5
|
-
import {
|
|
3
|
+
import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
4
|
+
import { buildCaseReport } from "../packages/cases/case-report.mjs";
|
|
5
|
+
import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
|
|
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";
|
|
6
9
|
import { CliUsageError } from "./errors.mjs";
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
export { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, };
|
|
11
|
+
const MAX_CASE_PACK_REFERENCE_SCAN_DEPTH = 64;
|
|
12
|
+
const INTERNAL_EXCLUSIONS = Object.freeze(['Raw upstream payloads and credentials are outside the case schema.']);
|
|
13
|
+
const TRUSTED_EXCLUSIONS = Object.freeze(['Case notes', 'Recipient values', 'Manual trail targets', 'Raw upstream payloads and credentials']);
|
|
14
|
+
const PUBLIC_EXCLUSIONS = Object.freeze([
|
|
15
|
+
'Case notes',
|
|
16
|
+
'Brand Profile references',
|
|
17
|
+
'Actions and recipient values',
|
|
18
|
+
'Analyst assertions',
|
|
19
|
+
'Investigation branches',
|
|
20
|
+
'Manual trail targets',
|
|
21
|
+
'Raw upstream payloads and credentials',
|
|
22
|
+
'Independent observed-effect reviews and closure history',
|
|
23
|
+
]);
|
|
24
|
+
const PUBLIC_V12_EXCLUSIONS = Object.freeze(PUBLIC_EXCLUSIONS.slice(0, -1));
|
|
25
|
+
const SENSITIVE_CASE_PACK_FIELDS = Object.freeze([
|
|
26
|
+
'brandProfileIds', 'notes', 'actions', 'assertions', 'manualTrail', 'observedEffects',
|
|
27
|
+
'closures', 'branches', 'recipient', 'target',
|
|
28
|
+
]);
|
|
29
|
+
const PUBLIC_OBSERVED_EFFECT_LIMITATION = 'Independent observed-effect review records were excluded from this public Case pack.';
|
|
30
|
+
const PUBLIC_CLOSURE_LIMITATION = 'Deliberate closure records were excluded from this public Case pack.';
|
|
31
|
+
function record(value) {
|
|
32
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
+
? value
|
|
34
|
+
: null;
|
|
35
|
+
}
|
|
36
|
+
function expectedExclusions(audience, caseVersion = CASE_SCHEMA_VERSION) {
|
|
37
|
+
if (audience === 'internal')
|
|
38
|
+
return INTERNAL_EXCLUSIONS;
|
|
39
|
+
if (audience === 'trusted')
|
|
40
|
+
return TRUSTED_EXCLUSIONS;
|
|
41
|
+
return caseVersion === PUBLIC_CASE_SCHEMA_VERSION ? PUBLIC_V12_EXCLUSIONS : PUBLIC_EXCLUSIONS;
|
|
42
|
+
}
|
|
43
|
+
function stringListsMatch(value, expected) {
|
|
44
|
+
return Array.isArray(value)
|
|
45
|
+
&& value.length === expected.length
|
|
46
|
+
&& value.every((item, index) => item === expected[index]);
|
|
47
|
+
}
|
|
48
|
+
function assertOnlyKeys(value, allowed, label) {
|
|
49
|
+
const allowedKeys = new Set(allowed);
|
|
50
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
|
|
51
|
+
throw new TypeError(`The CLI case pack contains an unexpected ${label} field.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function assertClosedEnvelopes(root, packet, integrity, redactionManifest, reports, caseVersion) {
|
|
55
|
+
assertOnlyKeys(root, ROOT_KEYS, 'root envelope');
|
|
56
|
+
assertOnlyKeys(packet, PACKET_KEYS, 'packet envelope');
|
|
57
|
+
assertOnlyKeys(integrity, INTEGRITY_KEYS, 'integrity envelope');
|
|
58
|
+
assertOnlyKeys(redactionManifest, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, 'redaction manifest');
|
|
59
|
+
if (!stringListsMatch(packet.limitations, PACKET_LIMITATIONS)) {
|
|
60
|
+
throw new TypeError('The CLI case pack contains an invalid packet limitation manifest.');
|
|
61
|
+
}
|
|
62
|
+
for (const value of reports) {
|
|
63
|
+
const report = record(value);
|
|
64
|
+
if (!report)
|
|
65
|
+
throw new TypeError('The CLI case pack contains an invalid Case report envelope.');
|
|
66
|
+
assertOnlyKeys(report, caseVersion === PUBLIC_CASE_SCHEMA_VERSION ? CLI_CASE_PACK_PUBLIC_REPORT_KEYS : REPORT_KEYS, 'Case report envelope');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function topCasePath(path) {
|
|
70
|
+
return path.length === 2 && path[0] === 'cases' && typeof path[1] === 'number';
|
|
71
|
+
}
|
|
72
|
+
function reportCasePath(path) {
|
|
73
|
+
return path.length === 4 && path[0] === 'packet' && path[1] === 'reports'
|
|
74
|
+
&& typeof path[2] === 'number' && path[3] === 'case';
|
|
75
|
+
}
|
|
76
|
+
function analystResponsePath(path) {
|
|
77
|
+
return path.length === 4 && path[0] === 'packet' && path[1] === 'reports'
|
|
78
|
+
&& typeof path[2] === 'number' && path[3] === 'analystResponse';
|
|
79
|
+
}
|
|
80
|
+
function listItemPath(path, list) {
|
|
81
|
+
return (path.length === 4 && path[0] === 'cases' && typeof path[1] === 'number' && path[2] === list && typeof path[3] === 'number')
|
|
82
|
+
|| (path.length === 6 && path[0] === 'packet' && path[1] === 'reports' && typeof path[2] === 'number'
|
|
83
|
+
&& path[3] === 'analystResponse' && path[4] === list && typeof path[5] === 'number');
|
|
84
|
+
}
|
|
85
|
+
function allowedSensitiveFieldPath(key, path) {
|
|
86
|
+
if (key === 'brandProfileIds')
|
|
87
|
+
return topCasePath(path) || reportCasePath(path);
|
|
88
|
+
if (key === 'notes')
|
|
89
|
+
return topCasePath(path) || reportCasePath(path);
|
|
90
|
+
if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
|
|
91
|
+
return topCasePath(path) || analystResponsePath(path);
|
|
92
|
+
}
|
|
93
|
+
if (key === 'recipient')
|
|
94
|
+
return listItemPath(path, 'actions');
|
|
95
|
+
if (key === 'target')
|
|
96
|
+
return listItemPath(path, 'manualTrail');
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/** Rejects hidden audience-sensitive copies with a budget proven by the serialized byte bound. */
|
|
100
|
+
function assertSensitiveFieldPlacement(root, serializedLength) {
|
|
101
|
+
const stack = [{ value: root, path: [], depth: 0 }];
|
|
102
|
+
const visited = new Set();
|
|
103
|
+
let inspected = 0;
|
|
104
|
+
while (stack.length) {
|
|
105
|
+
const entry = stack.pop();
|
|
106
|
+
if (!entry || !entry.value || typeof entry.value !== 'object')
|
|
107
|
+
continue;
|
|
108
|
+
for (const key of SENSITIVE_CASE_PACK_FIELDS) {
|
|
109
|
+
if (Object.hasOwn(entry.value, key) && !allowedSensitiveFieldPath(key, entry.path)) {
|
|
110
|
+
throw new TypeError('The CLI case pack contains audience-sensitive data outside its versioned case or report fields.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (visited.has(entry.value))
|
|
114
|
+
continue;
|
|
115
|
+
visited.add(entry.value);
|
|
116
|
+
inspected += 1;
|
|
117
|
+
if (inspected > serializedLength + 1 || entry.depth > MAX_CASE_PACK_REFERENCE_SCAN_DEPTH) {
|
|
118
|
+
throw new TypeError('The CLI case pack exceeds its bounded audience-field scan.');
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(entry.value)) {
|
|
121
|
+
for (let index = entry.value.length - 1; index >= 0; index--) {
|
|
122
|
+
inspected += 1;
|
|
123
|
+
if (inspected > serializedLength + 1)
|
|
124
|
+
throw new TypeError('The CLI case pack exceeds its bounded audience-field scan.');
|
|
125
|
+
stack.push({ value: entry.value[index], path: [...entry.path, index], depth: entry.depth + 1 });
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
for (const key in entry.value) {
|
|
131
|
+
if (!Object.hasOwn(entry.value, key))
|
|
132
|
+
continue;
|
|
133
|
+
inspected += 1;
|
|
134
|
+
if (inspected > serializedLength + 1)
|
|
135
|
+
throw new TypeError('The CLI case pack exceeds its bounded audience-field scan.');
|
|
136
|
+
stack.push({ value: entry.value[key], path: [...entry.path, key], depth: entry.depth + 1 });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (cause) {
|
|
140
|
+
if (cause instanceof TypeError && /bounded audience-field scan/u.test(cause.message))
|
|
141
|
+
throw cause;
|
|
142
|
+
throw new TypeError('The CLI case pack could not be inspected safely for audience-sensitive data.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function canonicalValuesMatch(left, right) {
|
|
147
|
+
try {
|
|
148
|
+
return canonicalArtifactJson(left) === canonicalArtifactJson(right);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function assertCanonicalCaseIdentities(cases, label) {
|
|
155
|
+
const ids = new Set();
|
|
156
|
+
const domains = new Set();
|
|
157
|
+
for (const value of cases) {
|
|
158
|
+
const item = record(value);
|
|
159
|
+
if (!item
|
|
160
|
+
|| typeof item.id !== 'string'
|
|
161
|
+
|| safeId(item.id) !== item.id
|
|
162
|
+
|| typeof item.domain !== 'string'
|
|
163
|
+
|| normalizeDomain(item.domain) !== item.domain
|
|
164
|
+
|| ids.has(item.id)
|
|
165
|
+
|| domains.has(item.domain)) {
|
|
166
|
+
throw new TypeError(`${label} contains a missing, unsafe, non-canonical, or duplicate Case identity.`);
|
|
167
|
+
}
|
|
168
|
+
ids.add(item.id);
|
|
169
|
+
domains.add(item.domain);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function assertCurrentCaseProjection(rawCases, normalised, label) {
|
|
173
|
+
if (rawCases.length !== normalised.length
|
|
174
|
+
|| rawCases.some((item, index) => !canonicalValuesMatch(item, normalised[index]))) {
|
|
175
|
+
throw new TypeError(`${label} contains a schema ${CASE_SCHEMA_VERSION} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
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
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function publicClosureHistory(preV13HistoryUnavailable) {
|
|
193
|
+
return {
|
|
194
|
+
records: [],
|
|
195
|
+
omitted: 0,
|
|
196
|
+
preV13HistoryUnavailable,
|
|
197
|
+
limitations: [
|
|
198
|
+
...(preV13HistoryUnavailable
|
|
199
|
+
? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.']
|
|
200
|
+
: []),
|
|
201
|
+
'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
|
|
202
|
+
PUBLIC_CLOSURE_LIMITATION,
|
|
203
|
+
].sort(),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function assertAudienceFields(value, audience) {
|
|
207
|
+
if (audience === 'internal')
|
|
208
|
+
return;
|
|
209
|
+
if (Object.hasOwn(value, 'notes') && (!Array.isArray(value.notes) || value.notes.length !== 0)) {
|
|
210
|
+
throw new TypeError('The CLI case pack contains Case notes excluded by its audience.');
|
|
211
|
+
}
|
|
212
|
+
if (audience === 'public') {
|
|
213
|
+
for (const field of ['actions', 'assertions', 'branches']) {
|
|
214
|
+
if (Object.hasOwn(value, field) && (!Array.isArray(value[field]) || value[field].length !== 0)) {
|
|
215
|
+
throw new TypeError(`The public CLI case pack contains ${field} excluded by its audience.`);
|
|
216
|
+
}
|
|
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
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (Object.hasOwn(value, 'actions')) {
|
|
232
|
+
if (!Array.isArray(value.actions) || value.actions.some((entry) => {
|
|
233
|
+
const action = record(entry);
|
|
234
|
+
return !action || (Object.hasOwn(action, 'recipient') && action.recipient !== '[redacted]');
|
|
235
|
+
})) {
|
|
236
|
+
throw new TypeError('The trusted CLI case pack contains an unredacted action recipient.');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Object.hasOwn(value, 'manualTrail')) {
|
|
240
|
+
if (!Array.isArray(value.manualTrail) || value.manualTrail.some((entry) => {
|
|
241
|
+
const event = record(entry);
|
|
242
|
+
return !event || (Object.hasOwn(event, 'target') && event.target !== null);
|
|
243
|
+
})) {
|
|
244
|
+
throw new TypeError('The CLI case pack contains a manual-trail target excluded by its audience.');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function reportApplicationVersion(report) {
|
|
249
|
+
return record(report.application)?.version;
|
|
250
|
+
}
|
|
251
|
+
function assertCurrentReportProjection(report, rawCase) {
|
|
252
|
+
if (typeof report.generatedAt !== 'string') {
|
|
253
|
+
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
|
|
254
|
+
}
|
|
255
|
+
const expected = buildCaseReport(rawCase, {
|
|
256
|
+
applicationVersion: reportApplicationVersion(report),
|
|
257
|
+
includeNotes: false,
|
|
258
|
+
generatedAt: report.generatedAt,
|
|
259
|
+
}).json;
|
|
260
|
+
if (!canonicalValuesMatch(report, expected)) {
|
|
261
|
+
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report projection.');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
11
264
|
function redactedCase(record, audience) {
|
|
12
265
|
if (audience === 'internal')
|
|
13
266
|
return structuredClone(record);
|
|
14
267
|
return {
|
|
15
268
|
...structuredClone(record),
|
|
269
|
+
status: audience === 'public' && record.status === 'resolved' && record.closures.records.length
|
|
270
|
+
? 'reviewing'
|
|
271
|
+
: record.status,
|
|
272
|
+
brandProfileIds: audience === 'public' ? [] : [...record.brandProfileIds],
|
|
16
273
|
notes: [],
|
|
17
274
|
actions: audience === 'public' ? [] : record.actions.map((item) => ({ ...item, recipient: '[redacted]' })),
|
|
18
275
|
manualTrail: record.manualTrail.map((item) => ({ ...item, target: null })),
|
|
19
276
|
assertions: audience === 'public' ? [] : structuredClone(record.assertions),
|
|
277
|
+
observedEffects: audience === 'public'
|
|
278
|
+
? publicObservedEffectHistory(record.observedEffects.preV13HistoryUnavailable)
|
|
279
|
+
: structuredClone(record.observedEffects),
|
|
280
|
+
closures: audience === 'public'
|
|
281
|
+
? publicClosureHistory(record.closures.preV13HistoryUnavailable)
|
|
282
|
+
: structuredClone(record.closures),
|
|
283
|
+
branches: audience === 'public' ? [] : structuredClone(record.branches ?? []),
|
|
20
284
|
};
|
|
21
285
|
}
|
|
22
286
|
export function buildCliCasePack(input, options, generatedAt = new Date().toISOString()) {
|
|
@@ -24,16 +288,39 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
24
288
|
throw new CliUsageError('case-pack requires --reviewed after the audience-specific output has been checked.');
|
|
25
289
|
if (Buffer.byteLength(input, 'utf8') > MAX_CASE_PACK_INPUT_BYTES)
|
|
26
290
|
throw new CliUsageError('Case-pack input is limited to 4 MiB.');
|
|
291
|
+
const normalized = input.replace(/^\uFEFF/u, '');
|
|
27
292
|
let parsed;
|
|
28
293
|
try {
|
|
29
|
-
|
|
294
|
+
scanBoundedJson(normalized);
|
|
295
|
+
parsed = JSON.parse(normalized);
|
|
30
296
|
}
|
|
31
297
|
catch {
|
|
32
|
-
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.');
|
|
33
299
|
}
|
|
34
300
|
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
35
|
-
if (
|
|
36
|
-
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.`);
|
|
303
|
+
}
|
|
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);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (cause) {
|
|
320
|
+
if (cause instanceof TypeError && /Case identity/u.test(cause.message)) {
|
|
321
|
+
throw new CliUsageError(cause.message);
|
|
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.`);
|
|
37
324
|
}
|
|
38
325
|
const normalised = normalizeCaseStore(root).cases;
|
|
39
326
|
if (!normalised.length)
|
|
@@ -44,33 +331,42 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
44
331
|
if (normalised.length > MAX_CASE_PACK_CASES) {
|
|
45
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.`);
|
|
46
333
|
}
|
|
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.`);
|
|
339
|
+
}
|
|
47
340
|
const cases = normalised.map((item) => redactedCase(item, options.audience));
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
341
|
+
const brandProfileReferencesOmitted = options.audience === 'public'
|
|
342
|
+
? normalised.reduce((count, item) => count + item.brandProfileIds.length, 0)
|
|
343
|
+
: 0;
|
|
344
|
+
const reports = cases.map((item) => buildCaseReport(item, {
|
|
345
|
+
applicationVersion: WHOISLEUTH_APPLICATION_VERSION,
|
|
346
|
+
includeNotes: false,
|
|
347
|
+
generatedAt,
|
|
348
|
+
}).json);
|
|
349
|
+
const exclusions = expectedExclusions(options.audience);
|
|
54
350
|
const packet = Object.freeze({
|
|
55
351
|
schema: CLI_CASE_PACK_SCHEMA,
|
|
56
352
|
version: CLI_CASE_PACK_VERSION,
|
|
57
353
|
audience: options.audience,
|
|
58
354
|
reviewed: true,
|
|
59
355
|
reports: Object.freeze(reports),
|
|
60
|
-
redactionManifest: Object.freeze({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
356
|
+
redactionManifest: Object.freeze({
|
|
357
|
+
excluded: Object.freeze(exclusions),
|
|
358
|
+
sourceCaseCount: normalised.length,
|
|
359
|
+
brandProfileReferencesOmitted,
|
|
360
|
+
}),
|
|
361
|
+
limitations: Object.freeze(PACKET_LIMITATIONS),
|
|
66
362
|
});
|
|
67
363
|
const unsigned = { version: CASE_SCHEMA_VERSION, exportedAt: generatedAt, cases, packet };
|
|
68
364
|
const document = Object.freeze({
|
|
69
365
|
...unsigned,
|
|
70
366
|
integrity: Object.freeze({
|
|
71
367
|
algorithm: 'SHA-256',
|
|
72
|
-
canonicalization:
|
|
73
|
-
digestSha256: `sha256:${createHash('sha256').update(
|
|
368
|
+
canonicalization: SORTED_JSON_V2,
|
|
369
|
+
digestSha256: `sha256:${createHash('sha256').update(canonicalArtifactJsonV2(unsigned)).digest('hex')}`,
|
|
74
370
|
}),
|
|
75
371
|
});
|
|
76
372
|
if (Buffer.byteLength(JSON.stringify(document), 'utf8') > MAX_CASE_IMPORT_BYTES) {
|
|
@@ -79,47 +375,140 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
|
|
|
79
375
|
return document;
|
|
80
376
|
}
|
|
81
377
|
export function verifyCliCasePack(input) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
378
|
+
assertBoundedJsonStructure(input, 'CLI case pack');
|
|
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
|
+
}
|
|
400
|
+
let serialized;
|
|
401
|
+
try {
|
|
402
|
+
serialized = JSON.stringify(input);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
throw new TypeError('The CLI case pack structure is not serializable.');
|
|
406
|
+
}
|
|
407
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_CASE_IMPORT_BYTES) {
|
|
408
|
+
throw new TypeError('The CLI case pack exceeds the browser import limit.');
|
|
409
|
+
}
|
|
410
|
+
const integrity = record(root?.integrity);
|
|
411
|
+
const redactionManifest = record(packet?.redactionManifest);
|
|
91
412
|
if (!root
|
|
92
413
|
|| !packet
|
|
93
414
|
|| packet.schema !== CLI_CASE_PACK_SCHEMA
|
|
94
415
|
|| packet.version !== CLI_CASE_PACK_VERSION
|
|
416
|
+
|| typeof root.version !== 'number'
|
|
417
|
+
|| !CASE_IMPORT_VERSIONS.includes(root.version)
|
|
95
418
|
|| packet.reviewed !== true
|
|
96
|
-
||
|
|
419
|
+
|| (packet.audience !== 'internal' && packet.audience !== 'public' && packet.audience !== 'trusted')
|
|
97
420
|
|| !Array.isArray(packet.reports)
|
|
421
|
+
|| !redactionManifest
|
|
98
422
|
|| !Array.isArray(root.cases)
|
|
99
423
|
|| root.cases.length < 1
|
|
100
424
|
|| root.cases.length > MAX_CASE_PACK_CASES
|
|
101
425
|
|| packet.reports.length !== root.cases.length
|
|
102
|
-
|| typeof root.
|
|
103
|
-
|| !CASE_IMPORT_VERSIONS.includes(root.version)
|
|
426
|
+
|| typeof root.exportedAt !== 'string'
|
|
104
427
|
|| !integrity
|
|
105
428
|
|| integrity.algorithm !== 'SHA-256'
|
|
106
|
-
|| integrity.canonicalization !==
|
|
429
|
+
|| integrity.canonicalization !== SORTED_JSON_V2
|
|
107
430
|
|| typeof integrity.digestSha256 !== 'string'
|
|
108
431
|
|| !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)) {
|
|
109
|
-
throw new TypeError('The CLI case pack structure or integrity envelope is
|
|
432
|
+
throw new TypeError('The CLI case pack structure or integrity envelope is malformed.');
|
|
110
433
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
throw new TypeError('The CLI case pack
|
|
434
|
+
assertClosedEnvelopes(root, packet, integrity, redactionManifest, packet.reports, root.version);
|
|
435
|
+
assertSensitiveFieldPlacement(root, serialized.length);
|
|
436
|
+
const audience = packet.audience;
|
|
437
|
+
if (!Number.isSafeInteger(redactionManifest.sourceCaseCount)
|
|
438
|
+
|| redactionManifest.sourceCaseCount !== root.cases.length
|
|
439
|
+
|| !stringListsMatch(redactionManifest.excluded, expectedExclusions(audience, root.version))) {
|
|
440
|
+
throw new TypeError('The CLI case pack has an invalid audience redaction manifest.');
|
|
118
441
|
}
|
|
442
|
+
assertCanonicalCaseIdentities(root.cases, 'The CLI case pack');
|
|
119
443
|
const normalised = normalizeCaseStore(root).cases;
|
|
120
444
|
if (normalised.length !== root.cases.length) {
|
|
121
445
|
throw new TypeError('The CLI case pack contains an invalid case collection.');
|
|
122
446
|
}
|
|
447
|
+
if (root.version === CASE_SCHEMA_VERSION)
|
|
448
|
+
assertCurrentCaseProjection(root.cases, normalised, 'The CLI case pack');
|
|
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) {
|
|
453
|
+
const rawCase = record(root.cases[index]);
|
|
454
|
+
const report = record(packet.reports[index]);
|
|
455
|
+
const reportCase = record(report?.case);
|
|
456
|
+
if (!rawCase
|
|
457
|
+
|| !report
|
|
458
|
+
|| !reportCase
|
|
459
|
+
|| typeof rawCase.domain !== 'string'
|
|
460
|
+
|| report.schema !== CASE_REPORT_SCHEMA
|
|
461
|
+
|| !caseReportVersionMatchesCase(root.version, report.schemaVersion)
|
|
462
|
+
|| reportCase.id !== rawCase.id
|
|
463
|
+
|| reportCase.domain !== rawCase.domain
|
|
464
|
+
|| !normalisedByDomain.has(rawCase.domain)) {
|
|
465
|
+
throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
|
|
466
|
+
}
|
|
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));
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
throw new TypeError('The CLI case pack contains invalid Brand Profile references.');
|
|
476
|
+
}
|
|
477
|
+
if (root.version === CASE_SCHEMA_VERSION)
|
|
478
|
+
assertCurrentReportProjection(report, rawCase);
|
|
479
|
+
}
|
|
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.');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (omitted !== 0 || !referencesMatch) {
|
|
499
|
+
throw new TypeError('The trusted or internal CLI case pack has inconsistent Brand Profile references.');
|
|
500
|
+
}
|
|
501
|
+
const { integrity: _integrity, ...unsigned } = root;
|
|
502
|
+
let calculated;
|
|
503
|
+
try {
|
|
504
|
+
calculated = `sha256:${createHash('sha256').update(canonicalArtifactJsonV2(unsigned)).digest('hex')}`;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
throw new TypeError('The CLI case pack structure is not serializable.');
|
|
508
|
+
}
|
|
509
|
+
if (calculated !== integrity.digestSha256) {
|
|
510
|
+
throw new TypeError('The CLI case pack failed its SHA-256 integrity check.');
|
|
511
|
+
}
|
|
123
512
|
return Object.freeze({ caseCount: normalised.length });
|
|
124
513
|
}
|
|
125
514
|
export function formatCliCasePack(document) {
|
package/cli/ci-report.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WHOISLEUTH_REQUEST_POLICY_URL } from "../lib/project-metadata.mjs";
|
|
2
|
+
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
|
|
2
3
|
function record(value) {
|
|
3
4
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
4
5
|
}
|
|
@@ -27,13 +28,17 @@ function junitCases(document) {
|
|
|
27
28
|
inconclusive ? { name: 'authority conclusions', failure: `${inconclusive} target state(s) remained inconclusive.` } : { name: 'authority conclusions' },
|
|
28
29
|
];
|
|
29
30
|
}
|
|
30
|
-
if (schema ===
|
|
31
|
+
if (schema === CLI_DOMAIN_CONTROL_MONITOR_SCHEMA) {
|
|
31
32
|
const collection = record(document.collection);
|
|
32
33
|
const summary = record(record(document.flightRecorder).summary);
|
|
34
|
+
const review = record(document.review);
|
|
33
35
|
const failed = Number(collection.failed ?? 0);
|
|
34
36
|
const drift = Number(summary.unexpectedChanges ?? 0);
|
|
37
|
+
const reviewState = String(review.state || 'partial').toLowerCase();
|
|
38
|
+
const reviewFailure = ['drift', 'partial', 'expired'].includes(reviewState);
|
|
35
39
|
return [
|
|
36
40
|
failed ? { name: 'domain control collection', failure: `${failed} owned-domain collection(s) failed.` } : { name: 'domain control collection' },
|
|
41
|
+
reviewFailure ? { name: 'desired domain control state', failure: `Review state: ${reviewState}` } : { name: 'desired domain control state' },
|
|
37
42
|
drift ? { name: 'unexpected domain control changes', failure: `${drift} complete observation change(s) require review.` } : { name: 'unexpected domain control changes' },
|
|
38
43
|
];
|
|
39
44
|
}
|
|
@@ -2,7 +2,7 @@ export const CLI_COLLECTION_PREFLIGHT_SCHEMA = 'whoisleuth.cli.collection-prefli
|
|
|
2
2
|
export const CLI_COLLECTION_PREFLIGHT_VERSION = 1;
|
|
3
3
|
export function buildCollectionPreflight(options) {
|
|
4
4
|
const sourceFamilies = options.deep
|
|
5
|
-
? ['RDAP', 'WHOIS', 'DNS', 'HTTP and static page identity', 'TLS'
|
|
5
|
+
? ['RDAP', 'WHOIS', 'DNS', 'HTTP and static page identity', 'TLS']
|
|
6
6
|
: ['RDAP', 'conditional DNS authority evidence'];
|
|
7
7
|
return Object.freeze({
|
|
8
8
|
schema: CLI_COLLECTION_PREFLIGHT_SCHEMA,
|
|
@@ -21,7 +21,7 @@ export function buildCollectionPreflight(options) {
|
|
|
21
21
|
disclosure: Object.freeze([
|
|
22
22
|
'Targets are sent only to the source families required by the selected mode and permitted by registry/provider policy.',
|
|
23
23
|
...(options.customResolvers ? ['Selected public DNS resolvers receive the DNS questions needed for collection.'] : ['The runtime DNS resolver receives DNS questions when DNS collection is required.']),
|
|
24
|
-
...(options.deep ? ['Target web infrastructure receives bounded homepage, favicon and TLS requests
|
|
24
|
+
...(options.deep ? ['Target web infrastructure receives bounded homepage, favicon and TLS requests.'] : []),
|
|
25
25
|
]),
|
|
26
26
|
persistence: Object.freeze({
|
|
27
27
|
output: options.output,
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { CLI_COMMAND_CATALOGUE_SCHEMA, CLI_COMMAND_CATALOGUE_VERSION, } from "../packages/contracts/cli-command-catalogue.mjs";
|
|
2
|
+
export { CLI_COMMAND_CATALOGUE_SCHEMA, CLI_COMMAND_CATALOGUE_VERSION, };
|
|
3
|
+
function selectCliCommands(definitions, filter) {
|
|
4
|
+
return Object.freeze(definitions
|
|
5
|
+
.filter((definition) => !filter.common || definition.documentation.common)
|
|
6
|
+
.filter((definition) => filter.group === null || definition.help.group === filter.group)
|
|
7
|
+
.filter((definition) => filter.mode === null || definition.collection.mode === filter.mode)
|
|
8
|
+
.map((definition) => definition.command));
|
|
9
|
+
}
|
|
3
10
|
function buildCliCommandCatalogue(options) {
|
|
4
11
|
return Object.freeze({
|
|
5
12
|
schema: CLI_COMMAND_CATALOGUE_SCHEMA,
|
|
@@ -27,7 +34,9 @@ function formatCliCommandCatalogue(catalogue) {
|
|
|
27
34
|
for (const entry of catalogue.commands) {
|
|
28
35
|
lines.push(`${entry.command} [${entry.collection.mode}]`, ` ${entry.description}`, ` ${entry.usage}`);
|
|
29
36
|
}
|
|
37
|
+
if (catalogue.commands.length === 0)
|
|
38
|
+
lines.push('No commands match the selected filters.');
|
|
30
39
|
lines.push('', 'Run "whoisleuth <command> --help" for examples and collection boundaries.');
|
|
31
40
|
return `${lines.join('\n')}\n`;
|
|
32
41
|
}
|
|
33
|
-
export { buildCliCommandCatalogue, formatCliCommandCatalogue };
|
|
42
|
+
export { buildCliCommandCatalogue, formatCliCommandCatalogue, selectCliCommands };
|