@slicedearth/whoisleuth-cli 1.45.0 → 1.47.4

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.
Files changed (133) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +59 -6
  4. package/bin/whoisleuth.mjs +5 -3
  5. package/cli/arguments.mjs +256 -9
  6. package/cli/artifact-structure.mjs +1833 -0
  7. package/cli/artifact-verify.mjs +242 -49
  8. package/cli/bounded-json.mjs +1 -0
  9. package/cli/bulk-checkpoint.mjs +2 -3
  10. package/cli/bulk-command-runner.mjs +2 -5
  11. package/cli/case-pack.mjs +819 -34
  12. package/cli/command-reference.mjs +48 -24
  13. package/cli/completion.mjs +16 -8
  14. package/cli/ct-event-intake.mjs +32 -2
  15. package/cli/discovery-scan-command-runner.mjs +4 -5
  16. package/cli/discovery-workflow.mjs +2 -3
  17. package/cli/doctor.mjs +2 -2
  18. package/cli/domain-control-monitor.mjs +21 -15
  19. package/cli/domain-control-observations.mjs +25 -2
  20. package/cli/evidence-command-runner.mjs +17 -11
  21. package/cli/evidence-signing.mjs +73 -45
  22. package/cli/export-evidence.mjs +11 -4
  23. package/cli/formatters/html.mjs +3 -3
  24. package/cli/formatters/terminal.mjs +3 -0
  25. package/cli/input.mjs +57 -0
  26. package/cli/interchange-report.mjs +226 -0
  27. package/cli/investigation-manifest.mjs +16 -17
  28. package/cli/investigation-run.mjs +71 -23
  29. package/cli/local-mmdb-review.mjs +45 -14
  30. package/cli/lookup-command-runner.mjs +2 -2
  31. package/cli/lookup-diff.mjs +9 -0
  32. package/cli/network-command-runner.mjs +78 -0
  33. package/cli/offline-evidence-review.mjs +28 -8
  34. package/cli/page-compare.mjs +8 -5
  35. package/cli/registry-cohort.mjs +385 -61
  36. package/cli/registry-support.mjs +2 -1
  37. package/cli/retained-artifact-diff.mjs +376 -0
  38. package/cli/risk-calibration.mjs +7 -5
  39. package/cli/runner.mjs +147 -185
  40. package/cli/saved-lookup.mjs +26 -3
  41. package/cli/sharing-review.mjs +12 -6
  42. package/docs/cli-reference.md +444 -70
  43. package/docs/cli.md +24 -5
  44. package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
  45. package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
  46. package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
  47. package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
  48. package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
  49. package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
  50. package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
  51. package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
  52. package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
  53. package/frontend/src/lib/analysis/bulk-sort.js +4 -0
  54. package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
  55. package/frontend/src/lib/analysis/case-evidence-model.js +8 -0
  56. package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
  57. package/frontend/src/lib/analysis/case-migration-model.js +83 -31
  58. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  59. package/frontend/src/lib/analysis/case-record-model.js +2 -0
  60. package/frontend/src/lib/analysis/case-record-operations.js +42 -12
  61. package/frontend/src/lib/analysis/case-report.js +39 -6
  62. package/frontend/src/lib/analysis/case-response-model.js +66 -7
  63. package/frontend/src/lib/analysis/case-response-packet.js +15 -5
  64. package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
  65. package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
  66. package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
  67. package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
  68. package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
  69. package/frontend/src/lib/analysis/domain-control-records.js +88 -0
  70. package/frontend/src/lib/analysis/external-findings-import.js +70 -6
  71. package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
  72. package/frontend/src/lib/analysis/investigation-guide.js +52 -10
  73. package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
  74. package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
  75. package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
  76. package/frontend/src/lib/analysis/lookup-decision-support.js +61 -6
  77. package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
  78. package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
  79. package/frontend/src/lib/analysis/utils.js +26 -3
  80. package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
  81. package/frontend/src/lib/analysis/visualization-models.js +63 -94
  82. package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
  83. package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
  84. package/frontend/src/lib/analysis/workspace-archive.js +104 -14
  85. package/lib/application-version.mjs +3 -0
  86. package/lib/availability.mjs +14 -12
  87. package/lib/bounded-file.mjs +13 -0
  88. package/lib/bounded-json-response.mjs +17 -4
  89. package/lib/bounded-json.mjs +249 -0
  90. package/lib/certificate-extension-profile.mjs +2 -1
  91. package/lib/cryptographic-assurance.mjs +155 -0
  92. package/lib/ct-search.mjs +87 -47
  93. package/lib/dns-intelligence.mjs +3 -2
  94. package/lib/dns-resolver-selection.mjs +6 -4
  95. package/lib/dnssec-chain-validation.mjs +1053 -0
  96. package/lib/dnssec-evidence-validation.mjs +10 -3
  97. package/lib/domain-change-packet.mjs +7 -5
  98. package/lib/domain-control-manifest.mjs +17 -129
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/evidence-export.mjs +751 -78
  101. package/lib/evidence-report-markdown.mjs +3 -0
  102. package/lib/evidence-report.mjs +5 -0
  103. package/lib/favicon.mjs +8 -5
  104. package/lib/http-evidence-bounds.mjs +3 -0
  105. package/lib/http-intelligence.mjs +2 -5
  106. package/lib/interchange-fidelity-registry.mjs +122 -0
  107. package/lib/local-geoip-evidence.mjs +2 -1
  108. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  109. package/lib/lookup-response-contract.mjs +691 -3
  110. package/lib/lookup.mjs +2 -2
  111. package/lib/observation.mjs +7 -6
  112. package/lib/outbound-identity.mjs +2 -3
  113. package/lib/perceptual-hash.mjs +12 -4
  114. package/lib/portable-generator.mjs +21 -0
  115. package/lib/provider-policy-admission.mjs +9 -4
  116. package/lib/rdap-normalization.mjs +18 -5
  117. package/lib/rdap-registrar.mjs +5 -1
  118. package/lib/registry-capabilities.mjs +3 -0
  119. package/lib/registry-capability-catalogue.mjs +12 -2
  120. package/lib/registry-comparison.mjs +28 -11
  121. package/lib/risk-calibration-summary.mjs +338 -0
  122. package/lib/rpki-evidence.mjs +9 -8
  123. package/lib/safe-fetch.mjs +59 -10
  124. package/lib/security-txt.mjs +26 -3
  125. package/lib/service-binding-dns.mjs +6 -1
  126. package/lib/smtp-transport-review.mjs +784 -0
  127. package/lib/sslbl-certificates.generated.mjs +136 -134
  128. package/lib/threat-intelligence-runtime.mjs +3 -11
  129. package/lib/threat-intelligence-types.mjs +18 -1
  130. package/lib/tls-intelligence.mjs +11 -5
  131. package/lib/tlsa-evidence.mjs +37 -7
  132. package/package.json +1 -1
  133. package/third-party-notices.txt +95 -5817
@@ -1,20 +1,26 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, verifyCaseResponsePacketIntegrity, } from "../frontend/src/lib/analysis/case-response-packet.js";
2
+ import { createHash } from 'node:crypto';
3
+ import { parseBoundedJsonObject } from "./bounded-json.mjs";
4
+ import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./artifact-structure.mjs";
5
+ import { CASE_RESPONSE_PACKET_SCHEMA, CASE_RESPONSE_PACKET_VERSION, LEGACY_CASE_RESPONSE_PACKET_VERSION, verifyCaseResponsePacketIntegrity, } from "../frontend/src/lib/analysis/case-response-packet.js";
3
6
  import { ACQUISITION_DECISION_PACKET_SCHEMA, ACQUISITION_DECISION_PACKET_VERSION, } from "../frontend/src/lib/analysis/acquisition-decision-packet.js";
4
- import { BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_VERSION, } from "../frontend/src/lib/analysis/bulk-domain-comparison.js";
5
- import { BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_VERSION, } from "../frontend/src/lib/analysis/bulk-mail-exposure.js";
7
+ import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_VERSION, } from "../frontend/src/lib/analysis/lookup-claim-passport.js";
8
+ import { BULK_DOMAIN_COMPARISON_SCHEMA, BULK_DOMAIN_COMPARISON_EXPORT_VERSION, } from "../frontend/src/lib/analysis/bulk-domain-comparison.js";
9
+ import { BULK_MAIL_EXPOSURE_SCHEMA, BULK_MAIL_EXPOSURE_EXPORT_VERSION, } from "../frontend/src/lib/analysis/bulk-mail-exposure.js";
6
10
  import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../frontend/src/lib/analysis/bulk-review-export.js";
7
11
  import { decryptWorkspaceArchive, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive-crypto.js";
8
- import { WORKSPACE_ARCHIVE_SCHEMA, readWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive.js";
9
- import { sha256ArtifactDigest } from "../frontend/src/lib/analysis/artifact-integrity.js";
12
+ import { WORKSPACE_ARCHIVE_SCHEMA, previewWorkspaceArchive, readWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive.js";
13
+ import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, sha256ArtifactDigestFor, SORTED_JSON_V1, SORTED_JSON_V2, } from "../frontend/src/lib/analysis/artifact-integrity.js";
10
14
  import { SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, } from "./saved-lookup.mjs";
11
15
  import { DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
16
+ import { DOMAIN_CONTROL_PASSPORT_VERSION } from "../frontend/src/lib/analysis/domain-control-manifest-core.js";
12
17
  import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION, } from "../lib/domain-change-packet.mjs";
13
- import { INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, verifyInvestigationCapsule, } from "../frontend/src/lib/analysis/investigation-capsule.js";
18
+ import { INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, LEGACY_INVESTIGATION_CAPSULE_VERSION, verifyInvestigationCapsule, } from "../frontend/src/lib/analysis/investigation-capsule.js";
14
19
  import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, } from "./investigation-manifest.mjs";
15
- import { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, verifyCliCasePack, } from "./case-pack.mjs";
20
+ import { CLI_CASE_PACK_SCHEMA, verifyCliCasePack, } from "./case-pack.mjs";
21
+ import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
16
22
  export const OFFLINE_ARTIFACT_VERIFICATION_SCHEMA = 'whoisleuth.offline-artifact-verification';
17
- export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 1;
23
+ export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 3;
18
24
  export const MAX_OFFLINE_ARTIFACT_BYTES = 15 * 1024 * 1024;
19
25
  export const MAX_OFFLINE_PASSPHRASE_FILE_BYTES = 1024;
20
26
  export class UnsupportedOfflineArtifactError extends TypeError {
@@ -25,13 +31,24 @@ export class UnsupportedOfflineArtifactError extends TypeError {
25
31
  }
26
32
  }
27
33
  const SIGNED_ARTIFACT_VERSIONS = Object.freeze({
28
- [ACQUISITION_DECISION_PACKET_SCHEMA]: new Set([ACQUISITION_DECISION_PACKET_VERSION]),
29
- [BULK_DOMAIN_COMPARISON_SCHEMA]: new Set([BULK_DOMAIN_COMPARISON_VERSION]),
30
- [BULK_MAIL_EXPOSURE_SCHEMA]: new Set([BULK_MAIL_EXPOSURE_VERSION]),
31
- [BULK_REVIEW_MANIFEST_SCHEMA]: new Set([BULK_REVIEW_MANIFEST_VERSION]),
32
- [DOMAIN_CONTROL_MANIFEST_SCHEMA]: new Set([DOMAIN_CONTROL_MANIFEST_VERSION]),
33
- [DOMAIN_CHANGE_PACKET_SCHEMA]: new Set([DOMAIN_CHANGE_PACKET_VERSION]),
34
- [INVESTIGATION_MANIFEST_SCHEMA]: new Set([INVESTIGATION_MANIFEST_VERSION]),
34
+ [ACQUISITION_DECISION_PACKET_SCHEMA]: new Set([1, ACQUISITION_DECISION_PACKET_VERSION]),
35
+ [LOOKUP_CLAIM_PASSPORT_SCHEMA]: new Set([LOOKUP_CLAIM_PASSPORT_VERSION]),
36
+ [BULK_DOMAIN_COMPARISON_SCHEMA]: new Set([3, BULK_DOMAIN_COMPARISON_EXPORT_VERSION]),
37
+ [BULK_MAIL_EXPOSURE_SCHEMA]: new Set([1, BULK_MAIL_EXPOSURE_EXPORT_VERSION]),
38
+ [BULK_REVIEW_MANIFEST_SCHEMA]: new Set([1, BULK_REVIEW_MANIFEST_VERSION]),
39
+ [DOMAIN_CONTROL_MANIFEST_SCHEMA]: new Set([DOMAIN_CONTROL_PASSPORT_VERSION, DOMAIN_CONTROL_MANIFEST_VERSION]),
40
+ [DOMAIN_CHANGE_PACKET_SCHEMA]: new Set([1, DOMAIN_CHANGE_PACKET_VERSION]),
41
+ [INVESTIGATION_MANIFEST_SCHEMA]: new Set([1, INVESTIGATION_MANIFEST_VERSION]),
42
+ });
43
+ const SIGNED_ARTIFACT_CANONICALIZATION = Object.freeze({
44
+ [ACQUISITION_DECISION_PACKET_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: ACQUISITION_DECISION_PACKET_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
45
+ [LOOKUP_CLAIM_PASSPORT_SCHEMA]: [{ version: LOOKUP_CLAIM_PASSPORT_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
46
+ [BULK_DOMAIN_COMPARISON_SCHEMA]: [{ version: 3, canonicalization: SORTED_JSON_V1, explicit: false }, { version: BULK_DOMAIN_COMPARISON_EXPORT_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
47
+ [BULK_MAIL_EXPOSURE_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: BULK_MAIL_EXPOSURE_EXPORT_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
48
+ [BULK_REVIEW_MANIFEST_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: BULK_REVIEW_MANIFEST_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
49
+ [DOMAIN_CONTROL_MANIFEST_SCHEMA]: [{ version: DOMAIN_CONTROL_PASSPORT_VERSION, canonicalization: SORTED_JSON_V1, explicit: true }, { version: DOMAIN_CONTROL_MANIFEST_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
50
+ [DOMAIN_CHANGE_PACKET_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: DOMAIN_CHANGE_PACKET_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
51
+ [INVESTIGATION_MANIFEST_SCHEMA]: [{ version: 1, canonicalization: SORTED_JSON_V1, explicit: false }, { version: INVESTIGATION_MANIFEST_VERSION, canonicalization: SORTED_JSON_V2, explicit: true }],
35
52
  });
36
53
  function record(value) {
37
54
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -39,23 +56,32 @@ function record(value) {
39
56
  : null;
40
57
  }
41
58
  function parseJson(raw) {
42
- if (typeof raw !== 'string')
43
- throw new TypeError('Artefact input must be UTF-8 JSON text.');
44
- const bytes = Buffer.byteLength(raw, 'utf8');
45
- if (bytes === 0 || bytes > MAX_OFFLINE_ARTIFACT_BYTES) {
46
- throw new TypeError(`Artifact input must be between 1 byte and ${MAX_OFFLINE_ARTIFACT_BYTES} bytes.`);
47
- }
48
- let parsed;
49
- try {
50
- parsed = JSON.parse(raw);
51
- }
52
- catch {
53
- throw new TypeError('Artefact input is not valid JSON.');
54
- }
55
- const value = record(parsed);
56
- if (!value)
57
- throw new TypeError('Artefact input must contain one JSON object.');
58
- return value;
59
+ return parseBoundedJsonObject(raw, { maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
60
+ }
61
+ export function hasVerifiedArtifactStructure(report) {
62
+ return report.checks.structure === 'verified';
63
+ }
64
+ export function hasVerifiedApplicableIntegrity(report) {
65
+ return report.checks.contentIntegrity === 'verified'
66
+ && (report.checks.contentIntegrityScope === 'whole_artifact'
67
+ || report.checks.contentIntegrityScope === 'embedded_projections');
68
+ }
69
+ export function hasVerifiedWholeArtifactIntegrity(report) {
70
+ return report.checks.contentIntegrity === 'verified'
71
+ && report.checks.contentIntegrityScope === 'whole_artifact';
72
+ }
73
+ export function offlineArtifactSatisfiesAssurance(report, requirement) {
74
+ if (!hasVerifiedArtifactStructure(report))
75
+ return false;
76
+ if (requirement === 'structure')
77
+ return true;
78
+ if (requirement === 'applicable_integrity')
79
+ return hasVerifiedApplicableIntegrity(report);
80
+ return hasVerifiedWholeArtifactIntegrity(report);
81
+ }
82
+ export function isCompleteOfflineArtifactVerification(report) {
83
+ return (report.state === 'verified' || report.state === 'structure_valid')
84
+ && (report.manifestIdentity === null || report.manifestIdentity.state === 'identity_verified');
59
85
  }
60
86
  function artifactVersion(value) {
61
87
  const declared = value.version ?? value.schemaVersion;
@@ -70,7 +96,17 @@ function artifactVersion(value) {
70
96
  function inputBytes(raw) {
71
97
  return Buffer.byteLength(raw, 'utf8');
72
98
  }
73
- function archiveReport(raw, archive, encrypted, ciphertextBytes) {
99
+ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
100
+ const preview = await previewWorkspaceArchive(source, {});
101
+ const readySectionCount = preview.sections.filter((section) => section.status === 'ready').length;
102
+ const unsupportedSectionCount = preview.sections.filter((section) => section.status === 'unsupported').length;
103
+ const blockedSectionCount = preview.sections.filter((section) => section.status === 'blocked').length;
104
+ const skippedRecordCount = preview.sections.reduce((sum, section) => sum + section.skipped, 0);
105
+ const prunedRecordCount = preview.sections.reduce((sum, section) => sum + (section.pruned ?? 0), 0);
106
+ const fullyImportable = unsupportedSectionCount === 0
107
+ && blockedSectionCount === 0
108
+ && skippedRecordCount === 0
109
+ && prunedRecordCount === 0;
74
110
  return Object.freeze({
75
111
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
76
112
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
@@ -80,10 +116,10 @@ function archiveReport(raw, archive, encrypted, ciphertextBytes) {
80
116
  version: archive.sourceVersion,
81
117
  }),
82
118
  state: 'verified',
83
- valid: true,
84
119
  checks: Object.freeze({
85
120
  structure: 'verified',
86
121
  contentIntegrity: 'verified',
122
+ contentIntegrityScope: 'whole_artifact',
87
123
  authenticatedEncryption: encrypted ? 'verified' : 'not_applicable',
88
124
  }),
89
125
  summary: Object.freeze({
@@ -91,9 +127,18 @@ function archiveReport(raw, archive, encrypted, ciphertextBytes) {
91
127
  sectionCount: archive.sections.length,
92
128
  recordCount: archive.sections.reduce((sum, section) => sum + section.recordCount, 0),
93
129
  ciphertextBytes,
130
+ readySectionCount,
131
+ unsupportedSectionCount,
132
+ blockedSectionCount,
133
+ skippedRecordCount,
134
+ prunedRecordCount,
135
+ fullyImportable,
94
136
  }),
95
137
  limitations: Object.freeze([
96
138
  'Verification checks the retained file against its declared versioned integrity contract; it does not establish that the original observations were accurate or remain current.',
139
+ ...(!fullyImportable
140
+ ? ['One or more integrity-valid archive sections cannot be imported completely by this version. Inspect the archive before selecting data to restore.']
141
+ : []),
97
142
  ]),
98
143
  });
99
144
  }
@@ -102,6 +147,7 @@ async function verifySignedArtifact(raw, value, schema, version) {
102
147
  if (!supported?.has(version)) {
103
148
  throw new UnsupportedOfflineArtifactError('This signed review-artifact schema or version is not supported.');
104
149
  }
150
+ validateSignedDigestArtifactStructure(schema, value);
105
151
  const integrity = record(value.integrity);
106
152
  if (!integrity
107
153
  || integrity.algorithm !== 'SHA-256'
@@ -109,8 +155,9 @@ async function verifySignedArtifact(raw, value, schema, version) {
109
155
  || !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)) {
110
156
  throw new TypeError('The signed review artefact has a missing or malformed integrity envelope.');
111
157
  }
158
+ const canonicalization = resolveArtifactCanonicalization(version, integrity.canonicalization, SIGNED_ARTIFACT_CANONICALIZATION[schema] ?? [], 'Signed review artefact');
112
159
  const { integrity: _integrity, ...unsigned } = value;
113
- if (await sha256ArtifactDigest(unsigned) !== integrity.digestSha256) {
160
+ if (await sha256ArtifactDigestFor(unsigned, canonicalization) !== integrity.digestSha256) {
114
161
  throw new TypeError('The signed review artefact failed its SHA-256 integrity check.');
115
162
  }
116
163
  if (schema === DOMAIN_CONTROL_MANIFEST_SCHEMA)
@@ -120,10 +167,10 @@ async function verifySignedArtifact(raw, value, schema, version) {
120
167
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
121
168
  artifact: Object.freeze({ kind: 'signed_review_artifact', schema, version }),
122
169
  state: 'verified',
123
- valid: true,
124
170
  checks: Object.freeze({
125
171
  structure: 'verified',
126
172
  contentIntegrity: 'verified',
173
+ contentIntegrityScope: 'whole_artifact',
127
174
  authenticatedEncryption: 'not_applicable',
128
175
  }),
129
176
  summary: Object.freeze({
@@ -137,7 +184,7 @@ async function verifySignedArtifact(raw, value, schema, version) {
137
184
  ]),
138
185
  });
139
186
  }
140
- export async function verifyOfflineArtifact(raw, options = {}) {
187
+ async function verifyOfflineArtifactCore(raw, options = {}) {
141
188
  const value = parseJson(raw);
142
189
  const casePackPacket = record(value.packet);
143
190
  if (casePackPacket?.schema === CLI_CASE_PACK_SCHEMA) {
@@ -148,13 +195,13 @@ export async function verifyOfflineArtifact(raw, options = {}) {
148
195
  artifact: Object.freeze({
149
196
  kind: 'cli_case_pack',
150
197
  schema: CLI_CASE_PACK_SCHEMA,
151
- version: CLI_CASE_PACK_VERSION,
198
+ version: Number(casePackPacket.version),
152
199
  }),
153
200
  state: 'verified',
154
- valid: true,
155
201
  checks: Object.freeze({
156
202
  structure: 'verified',
157
203
  contentIntegrity: 'verified',
204
+ contentIntegrityScope: 'whole_artifact',
158
205
  authenticatedEncryption: 'not_applicable',
159
206
  }),
160
207
  summary: Object.freeze({
@@ -180,10 +227,10 @@ export async function verifyOfflineArtifact(raw, options = {}) {
180
227
  version: artifactVersion(value),
181
228
  }),
182
229
  state: 'envelope_valid',
183
- valid: true,
184
230
  checks: Object.freeze({
185
231
  structure: 'verified',
186
232
  contentIntegrity: 'not_checked',
233
+ contentIntegrityScope: 'not_checked',
187
234
  authenticatedEncryption: 'not_checked',
188
235
  }),
189
236
  summary: Object.freeze({
@@ -200,17 +247,18 @@ export async function verifyOfflineArtifact(raw, options = {}) {
200
247
  }
201
248
  const decrypted = await decryptWorkspaceArchive(value, options.passphrase);
202
249
  const archive = await readWorkspaceArchive(decrypted);
203
- return archiveReport(raw, archive, true, inspected.ciphertextBytes);
250
+ return archiveReport(raw, decrypted, archive, true, inspected.ciphertextBytes);
204
251
  }
205
252
  const schema = typeof value.schema === 'string' ? value.schema : '';
206
253
  const version = artifactVersion(value);
207
254
  if (schema === WORKSPACE_ARCHIVE_SCHEMA) {
208
- return archiveReport(raw, await readWorkspaceArchive(value), false, null);
255
+ return archiveReport(raw, value, await readWorkspaceArchive(value), false, null);
209
256
  }
210
257
  if (schema === CASE_RESPONSE_PACKET_SCHEMA) {
211
- if (version !== CASE_RESPONSE_PACKET_VERSION) {
258
+ if (version !== LEGACY_CASE_RESPONSE_PACKET_VERSION && version !== CASE_RESPONSE_PACKET_VERSION) {
212
259
  throw new UnsupportedOfflineArtifactError('This case-response packet version is not supported.');
213
260
  }
261
+ validateOfflineArtifactStructure(schema, value);
214
262
  const integrity = record(value.integrity);
215
263
  if (!integrity || !await verifyCaseResponsePacketIntegrity(value)) {
216
264
  throw new TypeError('The case-response packet failed its manifest integrity check.');
@@ -220,10 +268,10 @@ export async function verifyOfflineArtifact(raw, options = {}) {
220
268
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
221
269
  artifact: Object.freeze({ kind: 'case_response_packet', schema, version }),
222
270
  state: 'verified',
223
- valid: true,
224
271
  checks: Object.freeze({
225
272
  structure: 'verified',
226
273
  contentIntegrity: 'verified',
274
+ contentIntegrityScope: 'whole_artifact',
227
275
  authenticatedEncryption: 'not_applicable',
228
276
  }),
229
277
  summary: Object.freeze({
@@ -238,21 +286,23 @@ export async function verifyOfflineArtifact(raw, options = {}) {
238
286
  });
239
287
  }
240
288
  if (schema === INVESTIGATION_CAPSULE_SCHEMA) {
241
- if (version !== INVESTIGATION_CAPSULE_VERSION) {
289
+ if (version !== LEGACY_INVESTIGATION_CAPSULE_VERSION && version !== INVESTIGATION_CAPSULE_VERSION) {
242
290
  throw new UnsupportedOfflineArtifactError('This investigation-capsule version is not supported.');
243
291
  }
292
+ validateInvestigationCapsuleStructure(value);
244
293
  const verification = await verifyInvestigationCapsule(value);
245
294
  if (!verification.valid)
246
295
  throw new TypeError('The investigation capsule failed its embedded projection integrity checks.');
296
+ const current = version === INVESTIGATION_CAPSULE_VERSION;
247
297
  return Object.freeze({
248
298
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
249
299
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
250
300
  artifact: Object.freeze({ kind: 'investigation_capsule', schema, version }),
251
- state: 'verified',
252
- valid: true,
301
+ state: current ? 'verified' : 'integrity_valid',
253
302
  checks: Object.freeze({
254
303
  structure: 'verified',
255
304
  contentIntegrity: 'verified',
305
+ contentIntegrityScope: current ? 'whole_artifact' : 'embedded_projections',
256
306
  authenticatedEncryption: 'not_applicable',
257
307
  }),
258
308
  summary: Object.freeze({
@@ -262,11 +312,45 @@ export async function verifyOfflineArtifact(raw, options = {}) {
262
312
  ciphertextBytes: null,
263
313
  }),
264
314
  limitations: Object.freeze([
265
- 'The embedded brief, graph, and optional analyst-record projections match their declared digests; the linked Lookup evidence is not embedded and must be verified separately.',
315
+ ...(current
316
+ ? ['The whole capsule matches its declared digest, including metadata and the linked source-contract projection digests. The non-embedded Lookup evidence remains linked by digest and must be retained separately.']
317
+ : ['The embedded brief, graph, and optional analyst-record projections match their declared digests. Capsule metadata and the linked Lookup evidence are outside those projection digests and must not be treated as whole-file integrity verified.']),
266
318
  'Digest verification detects changed content but does not authenticate the analyst, signer, collection source, or truth of retained observations and assertions.',
267
319
  ]),
268
320
  });
269
321
  }
322
+ if (schema === LOOKUP_EVIDENCE_SCHEMA) {
323
+ if (!SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS.some((candidate) => candidate === version)) {
324
+ throw new UnsupportedOfflineArtifactError('This Lookup-evidence document version is not supported.');
325
+ }
326
+ if (inputBytes(raw) > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
327
+ throw new TypeError('Lookup-evidence documents are limited to 5 MiB for browser-compatible verification.');
328
+ }
329
+ validateLookupEvidenceArtifactStructure(value);
330
+ return Object.freeze({
331
+ schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
332
+ version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
333
+ artifact: Object.freeze({ kind: 'lookup_evidence', schema, version }),
334
+ state: 'structure_valid',
335
+ checks: Object.freeze({
336
+ structure: 'verified',
337
+ contentIntegrity: 'not_checked',
338
+ contentIntegrityScope: 'not_applicable',
339
+ authenticatedEncryption: 'not_applicable',
340
+ }),
341
+ summary: Object.freeze({
342
+ inputBytes: inputBytes(raw),
343
+ sectionCount: null,
344
+ recordCount: null,
345
+ ciphertextBytes: null,
346
+ }),
347
+ limitations: Object.freeze([
348
+ `Lookup-evidence schema ${version} matches its bounded browser-importable structural contract, but this export format has no embedded checksum or signature. Structural validity does not prove that its evidence is accurate, current, or unchanged since collection.`,
349
+ 'Use an integrity-verified investigation manifest and an exact manifest-entry identity check when retained-file byte identity must be established.',
350
+ 'The export may contain raw registry RDAP publication data, normalised WHOIS values, and bounded contact details; review it before sharing.',
351
+ ]),
352
+ });
353
+ }
270
354
  if (schema === SAVED_LOOKUP_SCHEMA) {
271
355
  if (version !== SAVED_LOOKUP_SCHEMA_VERSION) {
272
356
  throw new UnsupportedOfflineArtifactError('This saved Lookup document version is not supported.');
@@ -277,10 +361,10 @@ export async function verifyOfflineArtifact(raw, options = {}) {
277
361
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
278
362
  artifact: Object.freeze({ kind: 'saved_lookup', schema, version }),
279
363
  state: 'structure_valid',
280
- valid: true,
281
364
  checks: Object.freeze({
282
365
  structure: 'verified',
283
366
  contentIntegrity: 'not_checked',
367
+ contentIntegrityScope: 'not_applicable',
284
368
  authenticatedEncryption: 'not_applicable',
285
369
  }),
286
370
  summary: Object.freeze({
@@ -296,6 +380,84 @@ export async function verifyOfflineArtifact(raw, options = {}) {
296
380
  }
297
381
  return verifySignedArtifact(raw, value, schema, version);
298
382
  }
383
+ function rawContentDigest(raw) {
384
+ return `sha256:${createHash('sha256').update(raw, 'utf8').digest('hex')}`;
385
+ }
386
+ function artifactMetadata(value) {
387
+ const declaredVersion = value.version ?? value.schemaVersion;
388
+ return Object.freeze({
389
+ schema: typeof value.schema === 'string' ? value.schema : null,
390
+ version: Number.isSafeInteger(declaredVersion) && Number(declaredVersion) >= 1 && Number(declaredVersion) <= 1_000
391
+ ? Number(declaredVersion)
392
+ : null,
393
+ });
394
+ }
395
+ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
396
+ const manifestReport = await verifyOfflineArtifactCore(manifestRaw);
397
+ if (manifestReport.artifact.schema !== INVESTIGATION_MANIFEST_SCHEMA
398
+ || manifestReport.artifact.kind !== 'signed_review_artifact'
399
+ || manifestReport.state !== 'verified'
400
+ || manifestReport.checks.contentIntegrity !== 'verified'
401
+ || manifestReport.checks.contentIntegrityScope !== 'whole_artifact') {
402
+ throw new TypeError('The selected manifest is not a fully integrity-verified investigation manifest.');
403
+ }
404
+ const manifest = parseJson(manifestRaw);
405
+ const entries = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
406
+ const entry = entries.find((candidate) => record(candidate)?.id === entryId);
407
+ const item = record(entry);
408
+ if (!item)
409
+ throw new TypeError('The requested investigation manifest entry was not found.');
410
+ const artifactValue = parseJson(artifactRaw);
411
+ const metadata = artifactMetadata(artifactValue);
412
+ const manifestVersion = artifactVersion(manifest);
413
+ const canonicalization = manifestVersion === 1 ? SORTED_JSON_V1 : SORTED_JSON_V2;
414
+ const actualByteLength = inputBytes(artifactRaw);
415
+ const expectedByteLength = Number(item.byteLength);
416
+ const checks = Object.freeze({
417
+ manifestIntegrity: 'verified',
418
+ byteLength: actualByteLength === expectedByteLength ? 'verified' : 'mismatch',
419
+ rawContentDigest: rawContentDigest(artifactRaw) === item.contentDigestSha256 ? 'verified' : 'mismatch',
420
+ canonicalDigest: rawContentDigest(canonicalArtifactJsonFor(artifactValue, canonicalization)) === item.canonicalDigestSha256 ? 'verified' : 'mismatch',
421
+ schema: metadata.schema === item.schema ? 'verified' : 'mismatch',
422
+ version: metadata.version === item.version ? 'verified' : 'mismatch',
423
+ });
424
+ const exact = checks.byteLength === 'verified'
425
+ && checks.rawContentDigest === 'verified'
426
+ && checks.canonicalDigest === 'verified'
427
+ && checks.schema === 'verified'
428
+ && checks.version === 'verified';
429
+ const canonicalOnly = !exact
430
+ && checks.canonicalDigest === 'verified'
431
+ && checks.schema === 'verified'
432
+ && checks.version === 'verified';
433
+ const state = exact
434
+ ? 'identity_verified'
435
+ : canonicalOnly
436
+ ? 'canonical_match_only'
437
+ : 'mismatch';
438
+ return Object.freeze({
439
+ state,
440
+ manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: manifestVersion, entryId }),
441
+ checks,
442
+ expectedByteLength,
443
+ actualByteLength,
444
+ limitations: Object.freeze([
445
+ ...(state === 'identity_verified'
446
+ ? ['The selected artefact matches the manifest entry byte-for-byte and by canonical JSON, schema, and version.']
447
+ : state === 'canonical_match_only'
448
+ ? ['Canonical JSON, schema, and version match, but the retained UTF-8 bytes or byte length differ from the manifest entry. This is not exact retained-file identity.']
449
+ : ['One or more manifest identity checks differ. This artefact is not the exact retained file identified by the selected manifest entry.']),
450
+ 'Manifest identity does not establish that the retained observations were accurate, complete, authorised, or remain current.',
451
+ ]),
452
+ });
453
+ }
454
+ export async function verifyOfflineArtifact(raw, options = {}) {
455
+ const report = await verifyOfflineArtifactCore(raw, options);
456
+ const manifestIdentity = options.manifest
457
+ ? await verifyManifestIdentity(raw, options.manifest.raw, options.manifest.entryId)
458
+ : null;
459
+ return Object.freeze({ ...report, manifestIdentity });
460
+ }
299
461
  export function formatOfflineArtifactVerification(report) {
300
462
  const lines = [
301
463
  'WHOISleuth offline artefact verification',
@@ -303,6 +465,7 @@ export function formatOfflineArtifactVerification(report) {
303
465
  `State: ${report.state}`,
304
466
  `Structure: ${report.checks.structure}`,
305
467
  `Content integrity: ${report.checks.contentIntegrity}`,
468
+ `Content integrity scope: ${report.checks.contentIntegrityScope}`,
306
469
  `Authenticated encryption: ${report.checks.authenticatedEncryption}`,
307
470
  `Input bytes: ${report.summary.inputBytes}`,
308
471
  ];
@@ -312,6 +475,36 @@ export function formatOfflineArtifactVerification(report) {
312
475
  lines.push(`Records: ${report.summary.recordCount}`);
313
476
  if (report.summary.ciphertextBytes !== null)
314
477
  lines.push(`Ciphertext bytes: ${report.summary.ciphertextBytes}`);
478
+ if (report.summary.readySectionCount !== undefined && report.summary.readySectionCount !== null) {
479
+ lines.push(`Import-ready sections: ${report.summary.readySectionCount}`);
480
+ }
481
+ if (report.summary.unsupportedSectionCount !== undefined && report.summary.unsupportedSectionCount !== null) {
482
+ lines.push(`Unsupported sections: ${report.summary.unsupportedSectionCount}`);
483
+ }
484
+ if (report.summary.blockedSectionCount !== undefined && report.summary.blockedSectionCount !== null) {
485
+ lines.push(`Blocked sections: ${report.summary.blockedSectionCount}`);
486
+ }
487
+ if (report.summary.skippedRecordCount !== undefined && report.summary.skippedRecordCount !== null) {
488
+ lines.push(`Skipped records: ${report.summary.skippedRecordCount}`);
489
+ }
490
+ if (report.summary.prunedRecordCount !== undefined && report.summary.prunedRecordCount !== null) {
491
+ lines.push(`Pruned records: ${report.summary.prunedRecordCount}`);
492
+ }
493
+ if (report.summary.fullyImportable !== undefined && report.summary.fullyImportable !== null) {
494
+ lines.push(`Importability: ${report.summary.fullyImportable ? 'complete' : 'partial'}`);
495
+ }
496
+ if (report.manifestIdentity) {
497
+ lines.push(`Manifest identity: ${report.manifestIdentity.state}`);
498
+ lines.push(`Manifest entry: ${report.manifestIdentity.manifest.entryId}`);
499
+ lines.push(`Manifest integrity: ${report.manifestIdentity.checks.manifestIntegrity}`);
500
+ lines.push(`Exact bytes: ${report.manifestIdentity.checks.rawContentDigest}`);
501
+ lines.push(`Byte length: ${report.manifestIdentity.checks.byteLength}`);
502
+ lines.push(`Canonical JSON: ${report.manifestIdentity.checks.canonicalDigest}`);
503
+ lines.push(`Schema identity: ${report.manifestIdentity.checks.schema}`);
504
+ lines.push(`Version identity: ${report.manifestIdentity.checks.version}`);
505
+ for (const limitation of report.manifestIdentity.limitations)
506
+ lines.push(`Manifest limitation: ${limitation}`);
507
+ }
315
508
  for (const limitation of report.limitations)
316
509
  lines.push(`Limitation: ${limitation}`);
317
510
  return `${lines.join('\n')}\n`;
@@ -0,0 +1 @@
1
+ export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { open } from 'node:fs/promises';
2
+ import { lstat } from 'node:fs/promises';
3
3
  import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
4
4
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
5
5
  import { boundedCliInputError, CliUsageError } from "./errors.mjs";
@@ -143,8 +143,7 @@ async function readCheckpointFile(path) {
143
143
  }
144
144
  async function checkpointFileExists(path) {
145
145
  try {
146
- const handle = await open(path, 'r');
147
- await handle.close();
146
+ await lstat(path);
148
147
  return true;
149
148
  }
150
149
  catch (error) {
@@ -1,8 +1,7 @@
1
- import { createReadStream } from 'node:fs';
2
1
  import { classifyQuery } from "../lib/classify.mjs";
3
2
  import { runUnifiedLookup } from "../lib/lookup.mjs";
4
3
  import { createBulkCheckpointWriter } from "./bulk-checkpoint.mjs";
5
- import { MAX_BULK_INPUT_BYTES, parseBulkQueries, readTextStreamBounded, runBulkLookups, } from "./bulk.mjs";
4
+ import { MAX_BULK_INPUT_BYTES, parseBulkQueries, runBulkLookups, } from "./bulk.mjs";
6
5
  import { formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems } from "./bulk-output.mjs";
7
6
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
8
7
  import EXIT_CODES from "./exit-codes.mjs";
@@ -24,9 +23,7 @@ async function runBulkCommand(args, dependencies, context) {
24
23
  try {
25
24
  input = dependencies.readBulkInput
26
25
  ? await dependencies.readBulkInput(args.source)
27
- : await readTextStreamBounded(args.source
28
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
29
- : dependencies.stdin || process.stdin, MAX_BULK_INPUT_BYTES, dependencies.signal);
26
+ : await context.readInput(args.source, MAX_BULK_INPUT_BYTES, 'Bulk input');
30
27
  }
31
28
  catch (error) {
32
29
  if (error instanceof CliUsageError)