@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.
Files changed (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,20 +1,23 @@
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";
3
- 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";
6
- import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../frontend/src/lib/analysis/bulk-review-export.js";
7
- 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";
10
- import { SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, } from "./saved-lookup.mjs";
11
- import { DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
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 { verifyCaseResponsePacketIntegrity, } from "../packages/cases/case-response-packet.mjs";
6
+ import { CASE_PORTABILITY_VERIFIER_DISPATCH, CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_PACKET_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_SCHEMA, } from "../packages/contracts/case-portability.mjs";
7
+ import { ACQUISITION_DECISION_PACKET_SCHEMA, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_MAIL_EXPOSURE_SCHEMA, BULK_REVIEW_MANIFEST_SCHEMA, LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_VERSION, SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS, SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS, SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../packages/contracts/investigation-portability.mjs";
8
+ import { decryptWorkspaceArchive, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../packages/workspace/workspace-archive-crypto.mjs";
9
+ import { WORKSPACE_ARCHIVE_SCHEMA, previewWorkspaceArchive, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
10
+ import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, sha256ArtifactDigestFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
11
+ import { SAVED_LOOKUP_SCHEMA, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, parseCliLookupDocument, } from "./saved-lookup.mjs";
12
+ import { DOMAIN_CONTROL_MANIFEST_SCHEMA, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
13
+ import { DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, } from "../packages/contracts/domain-control-manifest.mjs";
12
14
  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";
15
+ import { INVESTIGATION_CAPSULE_SCHEMA, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, verifyInvestigationCapsule, } from "../packages/investigation/investigation-capsule.mjs";
14
16
  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";
17
+ import { verifyCliCasePack } from "./case-pack.mjs";
18
+ import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
16
19
  export const OFFLINE_ARTIFACT_VERIFICATION_SCHEMA = 'whoisleuth.offline-artifact-verification';
17
- export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 1;
20
+ export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 3;
18
21
  export const MAX_OFFLINE_ARTIFACT_BYTES = 15 * 1024 * 1024;
19
22
  export const MAX_OFFLINE_PASSPHRASE_FILE_BYTES = 1024;
20
23
  export class UnsupportedOfflineArtifactError extends TypeError {
@@ -24,38 +27,89 @@ export class UnsupportedOfflineArtifactError extends TypeError {
24
27
  this.name = 'UnsupportedOfflineArtifactError';
25
28
  }
26
29
  }
27
- 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]),
30
+ function currentCanonicalizationRoutes(versions) {
31
+ return Object.freeze(versions.map((version) => Object.freeze({
32
+ version,
33
+ canonicalization: SORTED_JSON_V2,
34
+ explicit: true,
35
+ })));
36
+ }
37
+ const SIGNED_ARTIFACT_ROUTES = Object.freeze({
38
+ [ACQUISITION_DECISION_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS),
39
+ [LOOKUP_CLAIM_PASSPORT_SCHEMA]: currentCanonicalizationRoutes([LOOKUP_CLAIM_PASSPORT_VERSION]),
40
+ [BULK_DOMAIN_COMPARISON_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS),
41
+ [BULK_MAIL_EXPOSURE_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS),
42
+ [BULK_REVIEW_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS),
43
+ [DOMAIN_CONTROL_MANIFEST_SCHEMA]: DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES,
44
+ [DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes([DOMAIN_CHANGE_PACKET_VERSION]),
45
+ [INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes([INVESTIGATION_MANIFEST_VERSION]),
35
46
  });
36
47
  function record(value) {
37
48
  return value && typeof value === 'object' && !Array.isArray(value)
38
49
  ? value
39
50
  : null;
40
51
  }
41
- 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.`);
52
+ function selectCasePortabilityVerifier(value) {
53
+ const packet = record(value.packet);
54
+ for (const descriptor of CASE_PORTABILITY_VERIFIER_DISPATCH) {
55
+ if (descriptor.id === 'case-response-packet'
56
+ && descriptor.discriminator === 'root_schema'
57
+ && value.schema === CASE_RESPONSE_PACKET_SCHEMA)
58
+ return descriptor;
59
+ if (descriptor.id === 'cli-case-pack'
60
+ && descriptor.discriminator === 'packet_schema'
61
+ && packet?.schema === CLI_CASE_PACK_SCHEMA)
62
+ return descriptor;
47
63
  }
48
- let parsed;
49
- try {
50
- parsed = JSON.parse(raw);
64
+ return null;
65
+ }
66
+ function casePortabilityVersion(descriptor, value) {
67
+ if (descriptor.versionField === 'schemaVersion')
68
+ return artifactVersion(value);
69
+ const packet = record(value.packet);
70
+ if (!packet)
71
+ throw new TypeError('The Case portability verifier discriminator is missing.');
72
+ return artifactVersion(packet);
73
+ }
74
+ function supportsCasePortabilityVersion(descriptor, version) {
75
+ return descriptor.supportedVersions.some((candidate) => candidate === version);
76
+ }
77
+ function rejectUnsupportedCasePortabilityVersion(label, version, currentVersion) {
78
+ if (Number.isSafeInteger(version) && version >= 1 && version < currentVersion) {
79
+ throw new UnsupportedOfflineArtifactError(`${label} version ${String(version)} is retired. Export it again as version ${String(currentVersion)} with the last broad-reader release; no data was changed.`);
51
80
  }
52
- catch {
53
- throw new TypeError('Artefact input is not valid JSON.');
81
+ if (Number.isSafeInteger(version) && version > currentVersion) {
82
+ throw new UnsupportedOfflineArtifactError(`${label} version ${String(version)} is newer than the supported version ${String(currentVersion)}; no data was changed.`);
54
83
  }
55
- const value = record(parsed);
56
- if (!value)
57
- throw new TypeError('Artefact input must contain one JSON object.');
58
- return value;
84
+ throw new UnsupportedOfflineArtifactError(`This ${label} version is malformed or unsupported; no data was changed.`);
85
+ }
86
+ function parseJson(raw) {
87
+ return parseBoundedJsonObject(raw, { maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
88
+ }
89
+ export function hasVerifiedArtifactStructure(report) {
90
+ return report.checks.structure === 'verified';
91
+ }
92
+ export function hasVerifiedApplicableIntegrity(report) {
93
+ return report.checks.contentIntegrity === 'verified'
94
+ && (report.checks.contentIntegrityScope === 'whole_artifact'
95
+ || report.checks.contentIntegrityScope === 'embedded_projections');
96
+ }
97
+ export function hasVerifiedWholeArtifactIntegrity(report) {
98
+ return report.checks.contentIntegrity === 'verified'
99
+ && report.checks.contentIntegrityScope === 'whole_artifact';
100
+ }
101
+ export function offlineArtifactSatisfiesAssurance(report, requirement) {
102
+ if (!hasVerifiedArtifactStructure(report))
103
+ return false;
104
+ if (requirement === 'structure')
105
+ return true;
106
+ if (requirement === 'applicable_integrity')
107
+ return hasVerifiedApplicableIntegrity(report);
108
+ return hasVerifiedWholeArtifactIntegrity(report);
109
+ }
110
+ export function isCompleteOfflineArtifactVerification(report) {
111
+ return (report.state === 'verified' || report.state === 'structure_valid')
112
+ && (report.manifestIdentity === null || report.manifestIdentity.state === 'identity_verified');
59
113
  }
60
114
  function artifactVersion(value) {
61
115
  const declared = value.version ?? value.schemaVersion;
@@ -70,7 +124,17 @@ function artifactVersion(value) {
70
124
  function inputBytes(raw) {
71
125
  return Buffer.byteLength(raw, 'utf8');
72
126
  }
73
- function archiveReport(raw, archive, encrypted, ciphertextBytes) {
127
+ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
128
+ const preview = await previewWorkspaceArchive(source, {});
129
+ const readySectionCount = preview.sections.filter((section) => section.status === 'ready').length;
130
+ const unsupportedSectionCount = preview.sections.filter((section) => section.status === 'unsupported').length;
131
+ const blockedSectionCount = preview.sections.filter((section) => section.status === 'blocked').length;
132
+ const skippedRecordCount = preview.sections.reduce((sum, section) => sum + section.skipped, 0);
133
+ const prunedRecordCount = preview.sections.reduce((sum, section) => sum + (section.pruned ?? 0), 0);
134
+ const fullyImportable = unsupportedSectionCount === 0
135
+ && blockedSectionCount === 0
136
+ && skippedRecordCount === 0
137
+ && prunedRecordCount === 0;
74
138
  return Object.freeze({
75
139
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
76
140
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
@@ -79,11 +143,11 @@ function archiveReport(raw, archive, encrypted, ciphertextBytes) {
79
143
  schema: WORKSPACE_ARCHIVE_SCHEMA,
80
144
  version: archive.sourceVersion,
81
145
  }),
82
- state: 'verified',
83
- valid: true,
146
+ state: encrypted ? 'verified' : 'integrity_valid',
84
147
  checks: Object.freeze({
85
148
  structure: 'verified',
86
149
  contentIntegrity: 'verified',
150
+ contentIntegrityScope: encrypted ? 'whole_artifact' : 'embedded_projections',
87
151
  authenticatedEncryption: encrypted ? 'verified' : 'not_applicable',
88
152
  }),
89
153
  summary: Object.freeze({
@@ -91,17 +155,29 @@ function archiveReport(raw, archive, encrypted, ciphertextBytes) {
91
155
  sectionCount: archive.sections.length,
92
156
  recordCount: archive.sections.reduce((sum, section) => sum + section.recordCount, 0),
93
157
  ciphertextBytes,
158
+ readySectionCount,
159
+ unsupportedSectionCount,
160
+ blockedSectionCount,
161
+ skippedRecordCount,
162
+ prunedRecordCount,
163
+ fullyImportable,
94
164
  }),
95
165
  limitations: Object.freeze([
96
- 'Verification checks the retained file against its declared versioned integrity contract; it does not establish that the original observations were accurate or remain current.',
166
+ ...(encrypted
167
+ ? ['Authenticated decryption and archive validation cover the complete encrypted workspace content; they do not establish that the original observations were accurate or remain current.']
168
+ : ['Section digest verification covers the archive projections, not root metadata such as generation time or archive limitations. It does not establish that the original observations were accurate or remain current.']),
169
+ ...(!fullyImportable
170
+ ? ['One or more integrity-valid archive sections cannot be imported completely by this version. Inspect the archive before selecting data to restore.']
171
+ : []),
97
172
  ]),
98
173
  });
99
174
  }
100
175
  async function verifySignedArtifact(raw, value, schema, version) {
101
- const supported = SIGNED_ARTIFACT_VERSIONS[schema];
102
- if (!supported?.has(version)) {
176
+ const routes = SIGNED_ARTIFACT_ROUTES[schema] ?? [];
177
+ if (!routes.some((route) => route.version === version)) {
103
178
  throw new UnsupportedOfflineArtifactError('This signed review-artifact schema or version is not supported.');
104
179
  }
180
+ validateSignedDigestArtifactStructure(schema, value);
105
181
  const integrity = record(value.integrity);
106
182
  if (!integrity
107
183
  || integrity.algorithm !== 'SHA-256'
@@ -109,8 +185,9 @@ async function verifySignedArtifact(raw, value, schema, version) {
109
185
  || !/^sha256:[a-f0-9]{64}$/u.test(integrity.digestSha256)) {
110
186
  throw new TypeError('The signed review artefact has a missing or malformed integrity envelope.');
111
187
  }
188
+ const canonicalization = resolveArtifactCanonicalization(version, integrity.canonicalization, routes, 'Signed review artefact');
112
189
  const { integrity: _integrity, ...unsigned } = value;
113
- if (await sha256ArtifactDigest(unsigned) !== integrity.digestSha256) {
190
+ if (await sha256ArtifactDigestFor(unsigned, canonicalization) !== integrity.digestSha256) {
114
191
  throw new TypeError('The signed review artefact failed its SHA-256 integrity check.');
115
192
  }
116
193
  if (schema === DOMAIN_CONTROL_MANIFEST_SCHEMA)
@@ -120,10 +197,10 @@ async function verifySignedArtifact(raw, value, schema, version) {
120
197
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
121
198
  artifact: Object.freeze({ kind: 'signed_review_artifact', schema, version }),
122
199
  state: 'verified',
123
- valid: true,
124
200
  checks: Object.freeze({
125
201
  structure: 'verified',
126
202
  contentIntegrity: 'verified',
203
+ contentIntegrityScope: 'whole_artifact',
127
204
  authenticatedEncryption: 'not_applicable',
128
205
  }),
129
206
  summary: Object.freeze({
@@ -137,10 +214,14 @@ async function verifySignedArtifact(raw, value, schema, version) {
137
214
  ]),
138
215
  });
139
216
  }
140
- export async function verifyOfflineArtifact(raw, options = {}) {
217
+ async function verifyOfflineArtifactCore(raw, options = {}) {
141
218
  const value = parseJson(raw);
142
- const casePackPacket = record(value.packet);
143
- if (casePackPacket?.schema === CLI_CASE_PACK_SCHEMA) {
219
+ const casePortabilityVerifier = selectCasePortabilityVerifier(value);
220
+ if (casePortabilityVerifier?.id === 'cli-case-pack') {
221
+ const casePackVersion = casePortabilityVersion(casePortabilityVerifier, value);
222
+ if (!supportsCasePortabilityVersion(casePortabilityVerifier, casePackVersion)) {
223
+ rejectUnsupportedCasePortabilityVersion('CLI Case-pack', casePackVersion, CLI_CASE_PACK_VERSION);
224
+ }
144
225
  const verified = verifyCliCasePack(value);
145
226
  return Object.freeze({
146
227
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
@@ -148,13 +229,13 @@ export async function verifyOfflineArtifact(raw, options = {}) {
148
229
  artifact: Object.freeze({
149
230
  kind: 'cli_case_pack',
150
231
  schema: CLI_CASE_PACK_SCHEMA,
151
- version: CLI_CASE_PACK_VERSION,
232
+ version: casePackVersion,
152
233
  }),
153
234
  state: 'verified',
154
- valid: true,
155
235
  checks: Object.freeze({
156
236
  structure: 'verified',
157
237
  contentIntegrity: 'verified',
238
+ contentIntegrityScope: 'whole_artifact',
158
239
  authenticatedEncryption: 'not_applicable',
159
240
  }),
160
241
  summary: Object.freeze({
@@ -180,10 +261,10 @@ export async function verifyOfflineArtifact(raw, options = {}) {
180
261
  version: artifactVersion(value),
181
262
  }),
182
263
  state: 'envelope_valid',
183
- valid: true,
184
264
  checks: Object.freeze({
185
265
  structure: 'verified',
186
266
  contentIntegrity: 'not_checked',
267
+ contentIntegrityScope: 'not_checked',
187
268
  authenticatedEncryption: 'not_checked',
188
269
  }),
189
270
  summary: Object.freeze({
@@ -200,17 +281,18 @@ export async function verifyOfflineArtifact(raw, options = {}) {
200
281
  }
201
282
  const decrypted = await decryptWorkspaceArchive(value, options.passphrase);
202
283
  const archive = await readWorkspaceArchive(decrypted);
203
- return archiveReport(raw, archive, true, inspected.ciphertextBytes);
284
+ return archiveReport(raw, decrypted, archive, true, inspected.ciphertextBytes);
204
285
  }
205
286
  const schema = typeof value.schema === 'string' ? value.schema : '';
206
287
  const version = artifactVersion(value);
207
288
  if (schema === WORKSPACE_ARCHIVE_SCHEMA) {
208
- return archiveReport(raw, await readWorkspaceArchive(value), false, null);
289
+ return archiveReport(raw, value, await readWorkspaceArchive(value), false, null);
209
290
  }
210
- if (schema === CASE_RESPONSE_PACKET_SCHEMA) {
211
- if (version !== CASE_RESPONSE_PACKET_VERSION) {
212
- throw new UnsupportedOfflineArtifactError('This case-response packet version is not supported.');
291
+ if (casePortabilityVerifier?.id === 'case-response-packet') {
292
+ if (!supportsCasePortabilityVersion(casePortabilityVerifier, version)) {
293
+ rejectUnsupportedCasePortabilityVersion('case-response packet', version, CASE_RESPONSE_PACKET_VERSION);
213
294
  }
295
+ validateOfflineArtifactStructure(CASE_RESPONSE_PACKET_SCHEMA, value);
214
296
  const integrity = record(value.integrity);
215
297
  if (!integrity || !await verifyCaseResponsePacketIntegrity(value)) {
216
298
  throw new TypeError('The case-response packet failed its manifest integrity check.');
@@ -218,12 +300,12 @@ export async function verifyOfflineArtifact(raw, options = {}) {
218
300
  return Object.freeze({
219
301
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
220
302
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
221
- artifact: Object.freeze({ kind: 'case_response_packet', schema, version }),
303
+ artifact: Object.freeze({ kind: 'case_response_packet', schema: CASE_RESPONSE_PACKET_SCHEMA, version }),
222
304
  state: 'verified',
223
- valid: true,
224
305
  checks: Object.freeze({
225
306
  structure: 'verified',
226
307
  contentIntegrity: 'verified',
308
+ contentIntegrityScope: 'whole_artifact',
227
309
  authenticatedEncryption: 'not_applicable',
228
310
  }),
229
311
  summary: Object.freeze({
@@ -238,9 +320,10 @@ export async function verifyOfflineArtifact(raw, options = {}) {
238
320
  });
239
321
  }
240
322
  if (schema === INVESTIGATION_CAPSULE_SCHEMA) {
241
- if (version !== INVESTIGATION_CAPSULE_VERSION) {
323
+ if (!SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS.some((candidate) => candidate === version)) {
242
324
  throw new UnsupportedOfflineArtifactError('This investigation-capsule version is not supported.');
243
325
  }
326
+ validateInvestigationCapsuleStructure(value);
244
327
  const verification = await verifyInvestigationCapsule(value);
245
328
  if (!verification.valid)
246
329
  throw new TypeError('The investigation capsule failed its embedded projection integrity checks.');
@@ -249,10 +332,10 @@ export async function verifyOfflineArtifact(raw, options = {}) {
249
332
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
250
333
  artifact: Object.freeze({ kind: 'investigation_capsule', schema, version }),
251
334
  state: 'verified',
252
- valid: true,
253
335
  checks: Object.freeze({
254
336
  structure: 'verified',
255
337
  contentIntegrity: 'verified',
338
+ contentIntegrityScope: 'whole_artifact',
256
339
  authenticatedEncryption: 'not_applicable',
257
340
  }),
258
341
  summary: Object.freeze({
@@ -262,25 +345,57 @@ export async function verifyOfflineArtifact(raw, options = {}) {
262
345
  ciphertextBytes: null,
263
346
  }),
264
347
  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.',
348
+ '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.',
266
349
  'Digest verification detects changed content but does not authenticate the analyst, signer, collection source, or truth of retained observations and assertions.',
267
350
  ]),
268
351
  });
269
352
  }
353
+ if (schema === LOOKUP_EVIDENCE_SCHEMA) {
354
+ if (!SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS.some((candidate) => candidate === version)) {
355
+ throw new UnsupportedOfflineArtifactError('This Lookup-evidence document version is not supported.');
356
+ }
357
+ if (inputBytes(raw) > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
358
+ throw new TypeError('Lookup-evidence documents are limited to 5 MiB for browser-compatible verification.');
359
+ }
360
+ validateLookupEvidenceArtifactStructure(value);
361
+ return Object.freeze({
362
+ schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
363
+ version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
364
+ artifact: Object.freeze({ kind: 'lookup_evidence', schema, version }),
365
+ state: 'structure_valid',
366
+ checks: Object.freeze({
367
+ structure: 'verified',
368
+ contentIntegrity: 'not_checked',
369
+ contentIntegrityScope: 'not_applicable',
370
+ authenticatedEncryption: 'not_applicable',
371
+ }),
372
+ summary: Object.freeze({
373
+ inputBytes: inputBytes(raw),
374
+ sectionCount: null,
375
+ recordCount: null,
376
+ ciphertextBytes: null,
377
+ }),
378
+ limitations: Object.freeze([
379
+ `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.`,
380
+ 'Use an integrity-verified investigation manifest and an exact manifest-entry identity check when retained-file byte identity must be established.',
381
+ 'The export may contain raw registry RDAP publication data, normalised WHOIS values, and bounded contact details; review it before sharing.',
382
+ ]),
383
+ });
384
+ }
270
385
  if (schema === SAVED_LOOKUP_SCHEMA) {
271
- if (version !== SAVED_LOOKUP_SCHEMA_VERSION) {
386
+ if (!SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS.some((supported) => supported === version)) {
272
387
  throw new UnsupportedOfflineArtifactError('This saved Lookup document version is not supported.');
273
388
  }
274
- const document = parseSavedLookupDocument(raw, { label: 'Saved Lookup artefact' });
389
+ parseCliLookupDocument(raw, { label: 'Saved Lookup artefact' });
275
390
  return Object.freeze({
276
391
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
277
392
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
278
393
  artifact: Object.freeze({ kind: 'saved_lookup', schema, version }),
279
394
  state: 'structure_valid',
280
- valid: true,
281
395
  checks: Object.freeze({
282
396
  structure: 'verified',
283
397
  contentIntegrity: 'not_checked',
398
+ contentIntegrityScope: 'not_applicable',
284
399
  authenticatedEncryption: 'not_applicable',
285
400
  }),
286
401
  summary: Object.freeze({
@@ -290,12 +405,88 @@ export async function verifyOfflineArtifact(raw, options = {}) {
290
405
  ciphertextBytes: null,
291
406
  }),
292
407
  limitations: Object.freeze([
293
- `The saved ${document.mode} Lookup matches its versioned structural contract, but this document format has no embedded checksum or signature. Structural validity does not prove that its evidence is accurate, current, or unchanged since collection.`,
408
+ 'The saved Lookup matches its versioned structural contract, but this document format has no embedded checksum or signature. Structural validity does not prove that its evidence is accurate, current, or unchanged since collection.',
294
409
  ]),
295
410
  });
296
411
  }
297
412
  return verifySignedArtifact(raw, value, schema, version);
298
413
  }
414
+ function rawContentDigest(raw) {
415
+ return `sha256:${createHash('sha256').update(raw, 'utf8').digest('hex')}`;
416
+ }
417
+ function artifactMetadata(value) {
418
+ const declaredVersion = value.version ?? value.schemaVersion;
419
+ return Object.freeze({
420
+ schema: typeof value.schema === 'string' ? value.schema : null,
421
+ version: Number.isSafeInteger(declaredVersion) && Number(declaredVersion) >= 1 && Number(declaredVersion) <= 1_000
422
+ ? Number(declaredVersion)
423
+ : null,
424
+ });
425
+ }
426
+ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
427
+ const manifestReport = await verifyOfflineArtifactCore(manifestRaw);
428
+ if (manifestReport.artifact.schema !== INVESTIGATION_MANIFEST_SCHEMA
429
+ || manifestReport.artifact.kind !== 'signed_review_artifact'
430
+ || manifestReport.state !== 'verified'
431
+ || manifestReport.checks.contentIntegrity !== 'verified'
432
+ || manifestReport.checks.contentIntegrityScope !== 'whole_artifact') {
433
+ throw new TypeError('The selected manifest is not a fully integrity-verified investigation manifest.');
434
+ }
435
+ const manifest = parseJson(manifestRaw);
436
+ const entries = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
437
+ const entry = entries.find((candidate) => record(candidate)?.id === entryId);
438
+ const item = record(entry);
439
+ if (!item)
440
+ throw new TypeError('The requested investigation manifest entry was not found.');
441
+ const artifactValue = parseJson(artifactRaw);
442
+ const metadata = artifactMetadata(artifactValue);
443
+ const actualByteLength = inputBytes(artifactRaw);
444
+ const expectedByteLength = Number(item.byteLength);
445
+ const checks = Object.freeze({
446
+ manifestIntegrity: 'verified',
447
+ byteLength: actualByteLength === expectedByteLength ? 'verified' : 'mismatch',
448
+ rawContentDigest: rawContentDigest(artifactRaw) === item.contentDigestSha256 ? 'verified' : 'mismatch',
449
+ canonicalDigest: rawContentDigest(canonicalArtifactJsonFor(artifactValue, SORTED_JSON_V2)) === item.canonicalDigestSha256 ? 'verified' : 'mismatch',
450
+ schema: metadata.schema === item.schema ? 'verified' : 'mismatch',
451
+ version: metadata.version === item.version ? 'verified' : 'mismatch',
452
+ });
453
+ const exact = checks.byteLength === 'verified'
454
+ && checks.rawContentDigest === 'verified'
455
+ && checks.canonicalDigest === 'verified'
456
+ && checks.schema === 'verified'
457
+ && checks.version === 'verified';
458
+ const canonicalOnly = !exact
459
+ && checks.canonicalDigest === 'verified'
460
+ && checks.schema === 'verified'
461
+ && checks.version === 'verified';
462
+ const state = exact
463
+ ? 'identity_verified'
464
+ : canonicalOnly
465
+ ? 'canonical_match_only'
466
+ : 'mismatch';
467
+ return Object.freeze({
468
+ state,
469
+ manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: INVESTIGATION_MANIFEST_VERSION, entryId }),
470
+ checks,
471
+ expectedByteLength,
472
+ actualByteLength,
473
+ limitations: Object.freeze([
474
+ ...(state === 'identity_verified'
475
+ ? ['The selected artefact matches the manifest entry byte-for-byte and by canonical JSON, schema, and version.']
476
+ : state === 'canonical_match_only'
477
+ ? ['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.']
478
+ : ['One or more manifest identity checks differ. This artefact is not the exact retained file identified by the selected manifest entry.']),
479
+ 'Manifest identity does not establish that the retained observations were accurate, complete, authorised, or remain current.',
480
+ ]),
481
+ });
482
+ }
483
+ export async function verifyOfflineArtifact(raw, options = {}) {
484
+ const report = await verifyOfflineArtifactCore(raw, options);
485
+ const manifestIdentity = options.manifest
486
+ ? await verifyManifestIdentity(raw, options.manifest.raw, options.manifest.entryId)
487
+ : null;
488
+ return Object.freeze({ ...report, manifestIdentity });
489
+ }
299
490
  export function formatOfflineArtifactVerification(report) {
300
491
  const lines = [
301
492
  'WHOISleuth offline artefact verification',
@@ -303,6 +494,7 @@ export function formatOfflineArtifactVerification(report) {
303
494
  `State: ${report.state}`,
304
495
  `Structure: ${report.checks.structure}`,
305
496
  `Content integrity: ${report.checks.contentIntegrity}`,
497
+ `Content integrity scope: ${report.checks.contentIntegrityScope}`,
306
498
  `Authenticated encryption: ${report.checks.authenticatedEncryption}`,
307
499
  `Input bytes: ${report.summary.inputBytes}`,
308
500
  ];
@@ -312,6 +504,36 @@ export function formatOfflineArtifactVerification(report) {
312
504
  lines.push(`Records: ${report.summary.recordCount}`);
313
505
  if (report.summary.ciphertextBytes !== null)
314
506
  lines.push(`Ciphertext bytes: ${report.summary.ciphertextBytes}`);
507
+ if (report.summary.readySectionCount !== undefined && report.summary.readySectionCount !== null) {
508
+ lines.push(`Import-ready sections: ${report.summary.readySectionCount}`);
509
+ }
510
+ if (report.summary.unsupportedSectionCount !== undefined && report.summary.unsupportedSectionCount !== null) {
511
+ lines.push(`Unsupported sections: ${report.summary.unsupportedSectionCount}`);
512
+ }
513
+ if (report.summary.blockedSectionCount !== undefined && report.summary.blockedSectionCount !== null) {
514
+ lines.push(`Blocked sections: ${report.summary.blockedSectionCount}`);
515
+ }
516
+ if (report.summary.skippedRecordCount !== undefined && report.summary.skippedRecordCount !== null) {
517
+ lines.push(`Skipped records: ${report.summary.skippedRecordCount}`);
518
+ }
519
+ if (report.summary.prunedRecordCount !== undefined && report.summary.prunedRecordCount !== null) {
520
+ lines.push(`Pruned records: ${report.summary.prunedRecordCount}`);
521
+ }
522
+ if (report.summary.fullyImportable !== undefined && report.summary.fullyImportable !== null) {
523
+ lines.push(`Importability: ${report.summary.fullyImportable ? 'complete' : 'partial'}`);
524
+ }
525
+ if (report.manifestIdentity) {
526
+ lines.push(`Manifest identity: ${report.manifestIdentity.state}`);
527
+ lines.push(`Manifest entry: ${report.manifestIdentity.manifest.entryId}`);
528
+ lines.push(`Manifest integrity: ${report.manifestIdentity.checks.manifestIntegrity}`);
529
+ lines.push(`Exact bytes: ${report.manifestIdentity.checks.rawContentDigest}`);
530
+ lines.push(`Byte length: ${report.manifestIdentity.checks.byteLength}`);
531
+ lines.push(`Canonical JSON: ${report.manifestIdentity.checks.canonicalDigest}`);
532
+ lines.push(`Schema identity: ${report.manifestIdentity.checks.schema}`);
533
+ lines.push(`Version identity: ${report.manifestIdentity.checks.version}`);
534
+ for (const limitation of report.manifestIdentity.limitations)
535
+ lines.push(`Manifest limitation: ${limitation}`);
536
+ }
315
537
  for (const limitation of report.limitations)
316
538
  lines.push(`Limitation: ${limitation}`);
317
539
  return `${lines.join('\n')}\n`;
@@ -0,0 +1 @@
1
+ export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";