@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { array, enumeration, exact } from "../evidence/artifact-structure.mjs";
|
|
2
|
+
import { IDENTITY_ACTIONS } from "../contracts/message-intake.mjs";
|
|
3
|
+
/** Reported actions select recovery guidance; they do not establish compromise. */
|
|
4
|
+
export function reviewIdentityIncident(value) {
|
|
5
|
+
const input = exact(value, ['reportedActions'], 'Identity incident review');
|
|
6
|
+
const allowed = IDENTITY_ACTIONS.map(action => action.id);
|
|
7
|
+
const reportedActions = [...new Set(array(input.reportedActions, 'Reported actions', allowed.length)
|
|
8
|
+
.map(item => enumeration(item, allowed, 'Reported action')))];
|
|
9
|
+
const has = (...actions) => actions.some(action => reportedActions.includes(action));
|
|
10
|
+
const nextSteps = [];
|
|
11
|
+
const add = (id, title, detail) => nextSteps.push({ id, title, detail });
|
|
12
|
+
if (reportedActions.length)
|
|
13
|
+
add('preserve', 'Preserve the message and sign-in timeline', 'Record the reported time and action, retain relevant account audit events and keep the original privately. Use an independently known account portal, not the supplied link.');
|
|
14
|
+
if (has('entered_password', 'approved_signin', 'entered_device_code')) {
|
|
15
|
+
add('sessions', 'Review and revoke suspicious account sessions', 'Use the identity provider’s documented response process. Review recent sign-ins, new authentication methods and registered devices; resetting a password alone may not invalidate existing sessions.');
|
|
16
|
+
}
|
|
17
|
+
if (has('entered_password'))
|
|
18
|
+
add('password', 'Change exposed and reused credentials', 'Change the affected password through a trusted route. Review other accounts where the same credential was used, without testing the supplied site.');
|
|
19
|
+
if (has('granted_consent', 'entered_device_code'))
|
|
20
|
+
add('grants', 'Review application grants and tokens', 'Identify the application/client ID in account audit records. Revoke unwanted consent and refresh tokens using the provider’s controls; check accessed mail and files and newly created inbox rules.');
|
|
21
|
+
if (has('executed_command'))
|
|
22
|
+
add('endpoint', 'Escalate for endpoint containment', 'Stop using the affected device for account recovery. Follow the organisation’s endpoint incident process, preserve the command as inert evidence and use a separate trusted device.');
|
|
23
|
+
if (has('entered_password', 'approved_signin', 'entered_device_code', 'granted_consent'))
|
|
24
|
+
add('persistence', 'Check persistence and downstream access', 'Review forwarding rules, delegates, newly authorised applications and affected data. Record the findings independently of public domain evidence.');
|
|
25
|
+
if (reportedActions.length)
|
|
26
|
+
add('followup', 'Record recovery actions and a follow-up', 'Track account recovery separately from provider reports and domain availability. A removed page does not show that an account session or application grant was revoked.');
|
|
27
|
+
return { reportedActions, nextSteps };
|
|
28
|
+
}
|
|
29
|
+
/** A deliberate follow-up request, never a completed recovery or compromise claim. */
|
|
30
|
+
export function identityRecoveryFollowUp(reportedActions, stepId) {
|
|
31
|
+
const review = reviewIdentityIncident({ reportedActions });
|
|
32
|
+
const step = review.nextSteps.find(item => item.id === stepId);
|
|
33
|
+
if (!step)
|
|
34
|
+
throw new TypeError('Select a recommendation supported by the reported actions.');
|
|
35
|
+
return {
|
|
36
|
+
statement: `Requested follow-up: ${step.title}`,
|
|
37
|
+
rationale: `Conditional analyst request based on reported actions, not independently verified account telemetry. ${step.detail} No recovery action or independent result is established by this request.`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CASE_PIN_COMPLETENESS } from "../cases/case-response-records.mjs";
|
|
2
|
+
import { MAX_RESPONSE_VALUE_LENGTH, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH } from "../contracts/case-portability.mjs";
|
|
3
|
+
import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS } from "../contracts/context-review.mjs";
|
|
4
|
+
import { array, domain, enumeration, exact, HEX_DIGEST_RE, iso, text } from "../evidence/artifact-structure.mjs";
|
|
5
|
+
export { INCIDENT_SEQUENCE_INPUT_SCHEMA, INCIDENT_SEQUENCE_INPUT_VERSION } from "../contracts/context-review.mjs";
|
|
6
|
+
export const INCIDENT_STAGE_KINDS = ['message', 'navigation', 'identity_prompt', 'credential_entry', 'consent', 'browser_instruction', 'clipboard', 'local_execution', 'payment', 'recovery', 'other'];
|
|
7
|
+
export const INCIDENT_STAGE_BASES = ['retained_observation', 'imported_record', 'reported_action'];
|
|
8
|
+
export function readIncidentStages(raw) {
|
|
9
|
+
const stages = array(raw, 'Incident stages', MAX_CONTEXT_RECORDS).map(value => {
|
|
10
|
+
const row = exact(value, ['id', 'kind', 'basis', 'description', 'occurredAt', 'hostname', 'source', 'reference', 'referenceSha256', 'completeness', 'limitations'], 'Incident stage');
|
|
11
|
+
const id = text(row.id, 'Stage ID', 64);
|
|
12
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(id))
|
|
13
|
+
throw new TypeError('Stage ID must be a bounded identifier.');
|
|
14
|
+
iso(row.occurredAt, 'Stage occurrence time', true);
|
|
15
|
+
if (row.hostname !== null)
|
|
16
|
+
domain(row.hostname, 'Stage hostname');
|
|
17
|
+
if (row.referenceSha256 !== null && (typeof row.referenceSha256 !== 'string' || !HEX_DIGEST_RE.test(row.referenceSha256)))
|
|
18
|
+
throw new TypeError('Stage reference digest must be hexadecimal SHA-256 or null.');
|
|
19
|
+
return { id, kind: enumeration(row.kind, INCIDENT_STAGE_KINDS, 'Stage kind'), basis: enumeration(row.basis, INCIDENT_STAGE_BASES, 'Stage basis'),
|
|
20
|
+
description: text(row.description, 'Stage description', MAX_RESPONSE_VALUE_LENGTH), occurredAt: row.occurredAt, hostname: row.hostname,
|
|
21
|
+
source: text(row.source, 'Stage source', MAX_RESPONSE_LABEL_LENGTH), reference: text(row.reference, 'Stage reference', 500), referenceSha256: row.referenceSha256,
|
|
22
|
+
completeness: enumeration(row.completeness, CASE_PIN_COMPLETENESS, 'Stage completeness'),
|
|
23
|
+
limitations: array(row.limitations, 'Stage limitations', MAX_RESPONSE_LIMITATIONS).map(item => text(item, 'Stage limitation', MAX_RESPONSE_LIMITATION_LENGTH)), };
|
|
24
|
+
});
|
|
25
|
+
if (new Set(stages.map(row => row.id)).size !== stages.length)
|
|
26
|
+
throw new TypeError('Incident stage IDs must be unique.');
|
|
27
|
+
return stages;
|
|
28
|
+
}
|
|
29
|
+
/** Copy the selected pin's provenance without giving it stronger assurance. */
|
|
30
|
+
export function incidentStageFromPin(pin, kind, id) {
|
|
31
|
+
return readIncidentStages([{ id, kind, basis: 'retained_observation', description: pin.value, occurredAt: pin.observedAt,
|
|
32
|
+
hostname: pin.observationHostname ?? null, source: pin.source, reference: pin.id, referenceSha256: pin.importContentSha256 ?? null,
|
|
33
|
+
completeness: pin.truncated === true && pin.completeness === 'complete' ? 'partial' : pin.completeness, limitations: [...pin.limitations],
|
|
34
|
+
}])[0];
|
|
35
|
+
}
|
|
36
|
+
export function reviewIncidentSequence(raw, reviewedAt) {
|
|
37
|
+
iso(reviewedAt, 'Review time');
|
|
38
|
+
const stages = readIncidentStages(raw);
|
|
39
|
+
const counts = Object.fromEntries(INCIDENT_STAGE_BASES.map(basis => [basis, stages.filter(row => row.basis === basis).length]));
|
|
40
|
+
let priorTime = null, reversedTimes = 0;
|
|
41
|
+
for (const row of stages) {
|
|
42
|
+
if (row.occurredAt === null)
|
|
43
|
+
continue;
|
|
44
|
+
const time = Date.parse(row.occurredAt);
|
|
45
|
+
if (priorTime !== null && time < priorTime)
|
|
46
|
+
reversedTimes++;
|
|
47
|
+
priorTime = time;
|
|
48
|
+
}
|
|
49
|
+
return { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'incident_sequence', reviewedAt, title: 'Incident sequence',
|
|
50
|
+
state: !stages.length || stages.some(row => row.occurredAt === null || row.completeness !== 'complete') || reversedTimes ? 'partial' : 'reviewed',
|
|
51
|
+
summary: `Analyst-ordered stages: ${stages.length} (retained observations: ${counts.retained_observation}; imported records: ${counts.imported_record}; reported actions: ${counts.reported_action}).${reversedTimes ? ` Timestamp reversals needing review: ${reversedTimes}; the selected order was preserved.` : ''}`,
|
|
52
|
+
observations: stages.map((row, index) => ({ label: `${index + 1}. ${row.kind.replaceAll('_', ' ')} · ${row.basis.replaceAll('_', ' ')}`,
|
|
53
|
+
state: row.completeness === 'complete' ? 'reported' : 'partial', detail: `${row.description} · Source completeness: ${row.completeness}.${row.limitations.length ? ` ${row.limitations.join(' ')}` : ''}`,
|
|
54
|
+
source: `${row.source} · reference ${row.reference}${row.referenceSha256 ? ` · reference SHA-256 ${row.referenceSha256}` : ''}`, observedAt: row.occurredAt, hostname: row.hostname })),
|
|
55
|
+
nextSteps: ['Compare stages with the selected message and capture records; retain gaps and conflicting source times.', 'Use reported credential entry, consent or local execution to select the relevant account or device recovery review, separately from domain reporting.'],
|
|
56
|
+
limitations: ['Order is analyst-selected, not proof of causation or a verified attack chain. An instruction, prompt or clipboard attempt does not establish that a person followed it.', 'Sources, occurrence times and reference digests retain their supplied meaning; importing this input does not authenticate them. Unknown times are not replaced by review or save time.'],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { sha256ArtifactBytes } from "../evidence/artifact-integrity.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { MESSAGE_INTAKE_KINDS, MESSAGE_INTAKE_SCHEMA, MESSAGE_INTAKE_VERSION, MAX_MESSAGE_INTAKE_BYTES } from "../contracts/message-intake.mjs";
|
|
4
|
+
export function assertMessageBytes(bytes) {
|
|
5
|
+
if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer) || !bytes.byteLength || bytes.byteLength > MAX_MESSAGE_INTAKE_BYTES) {
|
|
6
|
+
throw new TypeError('Select one non-empty input of at most 16 MiB. No input was retained.');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function createIntakeReport(bytes, kind, reviewedAt) {
|
|
10
|
+
assertMessageBytes(bytes);
|
|
11
|
+
const instant = normalizeExplicitIsoTimestamp(reviewedAt);
|
|
12
|
+
if (!instant || !MESSAGE_INTAKE_KINDS.includes(kind))
|
|
13
|
+
throw new TypeError('Input review requires a supported format and a timestamp with an explicit timezone.');
|
|
14
|
+
return { schema: MESSAGE_INTAKE_SCHEMA, schemaVersion: MESSAGE_INTAKE_VERSION, reviewedAt: instant,
|
|
15
|
+
source: { kind, digestSha256: await sha256ArtifactBytes(bytes), byteLength: bytes.byteLength },
|
|
16
|
+
coverage: { state: 'reviewed', reviewedParts: 0, unreviewedAttachments: 0, rejectedLinks: 0, boundsReached: [] },
|
|
17
|
+
identities: [], authenticationClaims: [], authenticationReview: { headers: [], omittedHeaders: 0 }, messageParts: [],
|
|
18
|
+
links: [], actionHints: [], identityRecovery: { reportedActions: [], nextSteps: [] } };
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { prepareBagItEntries, encodeBagItEntries } from "../interchange/bagit.mjs";
|
|
2
|
+
import { prepareInvestigationPackageEntries, INVESTIGATION_PACKAGE_MANIFEST_PATH } from "./investigation-package.mjs";
|
|
3
|
+
/** The existing manifest keeps source declarations; BagIt preserves the bytes. */
|
|
4
|
+
export async function prepareInvestigationBagIt(input, generatedAt, version) {
|
|
5
|
+
const prepared = await prepareInvestigationPackageEntries(input, generatedAt, version);
|
|
6
|
+
const payload = new Map([...prepared.files].filter(([path]) => path !== INVESTIGATION_PACKAGE_MANIFEST_PATH)
|
|
7
|
+
.map(([path, bytes]) => [`data/${path.slice('artifacts/'.length)}`, bytes]));
|
|
8
|
+
const tags = new Map([['whoisleuth-manifest.json', prepared.files.get(INVESTIGATION_PACKAGE_MANIFEST_PATH)]]);
|
|
9
|
+
return { manifest: prepared.manifest, files: await prepareBagItEntries(payload, tags) };
|
|
10
|
+
}
|
|
11
|
+
export async function buildInvestigationBagIt(input, generatedAt, version) {
|
|
12
|
+
const prepared = await prepareInvestigationBagIt(input, generatedAt, version);
|
|
13
|
+
return { manifest: prepared.manifest, bytes: encodeBagItEntries(prepared.files) };
|
|
14
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { LOOKUP_EVIDENCE_SCHEMA } from "../contracts/lookup-evidence.mjs";
|
|
1
2
|
import { canonicalArtifactJsonV2, sha256ArtifactDigestV2, SORTED_JSON_V2, } from "../evidence/artifact-integrity.mjs";
|
|
2
3
|
import { LOOKUP_ASSET_GRAPH_SCHEMA, } from "./lookup-asset-graph.mjs";
|
|
3
4
|
import { LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, } from "./lookup-investigation-brief.mjs";
|
|
4
|
-
import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, } from "../contracts/investigation-portability.mjs";
|
|
5
|
+
import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, investigationCapsuleContracts, } from "../contracts/investigation-portability.mjs";
|
|
5
6
|
export { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_SCHEMA, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, };
|
|
6
7
|
function safeVersion(value) {
|
|
7
8
|
return typeof value === 'string' && /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value)
|
|
@@ -45,7 +46,21 @@ function analystProjection(record) {
|
|
|
45
46
|
})),
|
|
46
47
|
};
|
|
47
48
|
}
|
|
49
|
+
/** Links admitted projections; this does not authenticate their observations. */
|
|
50
|
+
export function investigationCapsuleProjectionsMatch(target, brief, graph) {
|
|
51
|
+
const targets = graph.nodes.filter((node) => node.kind === 'target');
|
|
52
|
+
return target.value === brief.target && target.type === brief.targetType
|
|
53
|
+
&& targets.length === 1 && targets[0]?.id === graph.targetId
|
|
54
|
+
&& targets[0]?.label === target.value
|
|
55
|
+
&& brief.relationships.nodes === graph.nodes.length
|
|
56
|
+
&& brief.relationships.edges === graph.edges.length
|
|
57
|
+
&& brief.relationships.truncated === graph.truncated;
|
|
58
|
+
}
|
|
48
59
|
export async function buildInvestigationCapsule(input) {
|
|
60
|
+
const target = { value: input.brief.target, type: input.brief.targetType };
|
|
61
|
+
if (!investigationCapsuleProjectionsMatch(target, input.brief, input.graph)) {
|
|
62
|
+
throw new TypeError('The investigation brief and graph must describe the same target and retained relationships.');
|
|
63
|
+
}
|
|
49
64
|
const analystRecords = input.includeAnalystRecords ? analystProjection(input.caseRecord) : null;
|
|
50
65
|
const [evidenceDigest, briefDigest, graphDigest, analystRecordsDigest] = await Promise.all([
|
|
51
66
|
sha256ArtifactDigestV2(input.lookupEvidence),
|
|
@@ -60,7 +75,7 @@ export async function buildInvestigationCapsule(input) {
|
|
|
60
75
|
schemaVersion: INVESTIGATION_CAPSULE_VERSION,
|
|
61
76
|
generatedAt: iso(input.generatedAt),
|
|
62
77
|
application: { name: 'WHOISleuth', version: safeVersion(input.applicationVersion) },
|
|
63
|
-
target
|
|
78
|
+
target,
|
|
64
79
|
sourceContracts: [
|
|
65
80
|
{ id: 'lookup-evidence', schema: evidenceSchema, version: evidenceVersion, digest: evidenceDigest, embedded: false },
|
|
66
81
|
{ id: 'investigation-brief', schema: LOOKUP_INVESTIGATION_BRIEF_SCHEMA, version: LOOKUP_INVESTIGATION_BRIEF_VERSION, digest: briefDigest, embedded: true },
|
|
@@ -100,11 +115,24 @@ export async function buildInvestigationCapsule(input) {
|
|
|
100
115
|
},
|
|
101
116
|
};
|
|
102
117
|
}
|
|
118
|
+
/** Cross-file linkage after the caller has bounded and hashed the source file.
|
|
119
|
+
* This is neither a source-format validator nor an authenticity decision. */
|
|
120
|
+
export function investigationCapsuleSourceIdentity(target, reference, source) {
|
|
121
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
122
|
+
const expected = object(reference);
|
|
123
|
+
const capsuleTarget = object(target);
|
|
124
|
+
const sourceQuery = object(source.query);
|
|
125
|
+
const validReference = expected?.id === 'lookup-evidence' && expected.embedded === false;
|
|
126
|
+
const schema = validReference && expected.schema === LOOKUP_EVIDENCE_SCHEMA && expected.schema === source.schema;
|
|
127
|
+
const version = validReference && Number.isSafeInteger(expected.version) && expected.version === source.version;
|
|
128
|
+
const targetMatches = typeof capsuleTarget?.value === 'string' && capsuleTarget.value.length > 0
|
|
129
|
+
&& capsuleTarget.value === sourceQuery?.submitted && capsuleTarget.type === sourceQuery.type;
|
|
130
|
+
const contentDigest = validReference && typeof source.digest === 'string' && /^sha256:[a-f0-9]{64}$/u.test(source.digest) && expected.digest === source.digest;
|
|
131
|
+
return Object.freeze({ schema, version, target: targetMatches, contentDigest, linked: schema && version && targetMatches && contentDigest });
|
|
132
|
+
}
|
|
103
133
|
export async function verifyInvestigationCapsule(capsule) {
|
|
104
|
-
const current = capsule.schemaVersion === INVESTIGATION_CAPSULE_VERSION;
|
|
105
|
-
const publicVersion = capsule.schemaVersion === PUBLIC_INVESTIGATION_CAPSULE_VERSION;
|
|
106
134
|
const integrityRecord = capsule.integrity;
|
|
107
|
-
if ((
|
|
135
|
+
if (!investigationCapsuleContracts(capsule.schemaVersion)
|
|
108
136
|
|| integrityRecord.canonicalization !== SORTED_JSON_V2
|
|
109
137
|
|| integrityRecord.scope !== 'capsule excluding integrity') {
|
|
110
138
|
return { valid: false, brief: false, graph: false, analystRecords: null, whole: false };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { boundedJsonLimitsForBytes, parseBoundedJson, parseBoundedJsonObject } from "../analysis/bounded-json.mjs";
|
|
2
|
+
import { normalizeBoundedSemanticVersion } from "../analysis/semantic-version.mjs";
|
|
3
|
+
import { canonicalArtifactJsonV2, sha256ArtifactBytes, sha256ArtifactDigestV2, SORTED_JSON_V2 } from "../evidence/artifact-integrity.mjs";
|
|
4
|
+
import { array, digest, enumeration, exact, fail, integer, iso, strings, text, validateIntegrity } from "../evidence/artifact-structure.mjs";
|
|
5
|
+
import { SELECTED_FILE_MEDIA_TYPES as INVESTIGATION_FILE_MEDIA_TYPES } from "../contracts/selected-file-limits.mjs";
|
|
6
|
+
import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES } from "../contracts/investigation-package-limits.mjs";
|
|
7
|
+
export { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, INVESTIGATION_FILE_MEDIA_TYPES };
|
|
8
|
+
export const INVESTIGATION_MANIFEST_SCHEMA = 'whoisleuth.investigation-manifest';
|
|
9
|
+
export const INVESTIGATION_MANIFEST_VERSION = 3;
|
|
10
|
+
export const SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS = [2, INVESTIGATION_MANIFEST_VERSION];
|
|
11
|
+
// Independently bounds the path-free metadata, not the selected file content.
|
|
12
|
+
import { MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES } from "../contracts/investigation-package-limits.mjs";
|
|
13
|
+
export { MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES } from "../contracts/investigation-package-limits.mjs";
|
|
14
|
+
/** A filename suggests a declaration only; it never validates file contents. */
|
|
15
|
+
export function investigationFileMediaType(name) {
|
|
16
|
+
const extension = name.toLowerCase().split('.').at(-1);
|
|
17
|
+
switch (extension) {
|
|
18
|
+
case 'json': return 'application/json';
|
|
19
|
+
case 'png': return 'image/png';
|
|
20
|
+
case 'jpg':
|
|
21
|
+
case 'jpeg': return 'image/jpeg';
|
|
22
|
+
case 'webp': return 'image/webp';
|
|
23
|
+
case 'gif': return 'image/gif';
|
|
24
|
+
case 'pdf': return 'application/pdf';
|
|
25
|
+
default: return 'application/octet-stream';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function sourceDeclaration(source) {
|
|
29
|
+
const identity = source?.identity ?? null;
|
|
30
|
+
const observedAt = source?.observedAt ?? null;
|
|
31
|
+
if (identity !== null)
|
|
32
|
+
text(identity, 'Artefact source identity', 240);
|
|
33
|
+
iso(observedAt, 'Artefact source observation time', true);
|
|
34
|
+
return Object.freeze({ identity, observedAt });
|
|
35
|
+
}
|
|
36
|
+
export function investigationJsonMetadata(content, maximumBytes = MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES) {
|
|
37
|
+
const value = parseBoundedJson(content, {
|
|
38
|
+
label: 'Manifest artefact', maximumBytes,
|
|
39
|
+
limits: boundedJsonLimitsForBytes(maximumBytes),
|
|
40
|
+
});
|
|
41
|
+
const object = value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
42
|
+
let schema = null;
|
|
43
|
+
// Arbitrary valid JSON remains packageable. An unusable schema declaration
|
|
44
|
+
// is not copied into metadata; the exact source bytes still retain it.
|
|
45
|
+
if (typeof object?.schema === 'string') {
|
|
46
|
+
try {
|
|
47
|
+
schema = text(object.schema, 'Artefact schema', 160);
|
|
48
|
+
}
|
|
49
|
+
catch { /* Unknown marker; not source-format admission. */ }
|
|
50
|
+
}
|
|
51
|
+
const candidate = object?.version ?? object?.schemaVersion;
|
|
52
|
+
const version = Number.isSafeInteger(candidate) && Number(candidate) > 0 && Number(candidate) <= 1_000 ? Number(candidate) : null;
|
|
53
|
+
return { value, object, schema, version };
|
|
54
|
+
}
|
|
55
|
+
export async function prepareInvestigationManifest(input, generatedAt, applicationVersionValue) {
|
|
56
|
+
if (!Array.isArray(input.artifacts) || input.artifacts.length < 1 || input.artifacts.length > MAX_INVESTIGATION_MANIFEST_ARTIFACTS) {
|
|
57
|
+
throw new TypeError(`A manifest requires between 1 and ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} artefacts.`);
|
|
58
|
+
}
|
|
59
|
+
iso(generatedAt, 'Manifest generatedAt');
|
|
60
|
+
const workflow = text(input.workflow, 'Manifest workflow', 160);
|
|
61
|
+
const applicationVersion = normalizeBoundedSemanticVersion(applicationVersionValue, 'Application');
|
|
62
|
+
const configurationDigestSha256 = input.configurationDigestSha256;
|
|
63
|
+
if (configurationDigestSha256 !== null)
|
|
64
|
+
digest(configurationDigestSha256, 'configurationDigestSha256');
|
|
65
|
+
let totalBytes = 0;
|
|
66
|
+
// Capture all mutable bytes and declarations before the first asynchronous
|
|
67
|
+
// digest. The package writer uses these same owned bytes, not the caller's view.
|
|
68
|
+
const selected = input.artifacts.map((artifact, index) => {
|
|
69
|
+
if (typeof artifact.content !== 'string' && !(artifact.content instanceof Uint8Array))
|
|
70
|
+
throw new TypeError(`Artefact ${index + 1} requires text or bytes.`);
|
|
71
|
+
if (artifact.content.length > MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES || artifact.content.length > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES - totalBytes) {
|
|
72
|
+
throw new TypeError('Manifest artefacts exceed the per-file or combined limit.');
|
|
73
|
+
}
|
|
74
|
+
const bytes = typeof artifact.content === 'string' ? new TextEncoder().encode(artifact.content) : new Uint8Array(artifact.content);
|
|
75
|
+
if (bytes.byteLength < 1 || bytes.byteLength > MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES)
|
|
76
|
+
throw new TypeError(`Artefact ${index + 1} must be between 1 byte and ${MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES} bytes.`);
|
|
77
|
+
if (bytes.byteLength > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES - totalBytes)
|
|
78
|
+
throw new TypeError('Manifest artefacts exceed the combined limit.');
|
|
79
|
+
totalBytes += bytes.byteLength;
|
|
80
|
+
const mediaType = enumeration(artifact.mediaType ?? (typeof artifact.content === 'string' ? 'application/json' : 'application/octet-stream'), INVESTIGATION_FILE_MEDIA_TYPES, 'Artefact media type');
|
|
81
|
+
return { bytes, mediaType, source: sourceDeclaration(artifact.source) };
|
|
82
|
+
});
|
|
83
|
+
const artifacts = [];
|
|
84
|
+
for (const [index, file] of selected.entries()) {
|
|
85
|
+
const metadata = file.mediaType === 'application/json'
|
|
86
|
+
? investigationJsonMetadata(new TextDecoder('utf-8', { fatal: true }).decode(file.bytes)) : null;
|
|
87
|
+
artifacts.push(Object.freeze({
|
|
88
|
+
sequence: index + 1, id: `artifact-${index + 1}`,
|
|
89
|
+
schema: metadata?.schema ?? null, version: metadata?.version ?? null,
|
|
90
|
+
byteLength: file.bytes.byteLength,
|
|
91
|
+
contentDigestSha256: await sha256ArtifactBytes(file.bytes),
|
|
92
|
+
canonicalDigestSha256: metadata ? await sha256ArtifactDigestV2(metadata.value) : null,
|
|
93
|
+
mediaType: file.mediaType, source: file.source,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
const unsigned = Object.freeze({
|
|
97
|
+
schema: INVESTIGATION_MANIFEST_SCHEMA, version: INVESTIGATION_MANIFEST_VERSION,
|
|
98
|
+
generatedAt, application: Object.freeze({ name: 'WHOISleuth', version: applicationVersion }),
|
|
99
|
+
workflow, audience: 'private',
|
|
100
|
+
configuration: Object.freeze({ digestSha256: configurationDigestSha256 }),
|
|
101
|
+
artifacts: Object.freeze(artifacts),
|
|
102
|
+
steps: Object.freeze(artifacts.map((artifact) => Object.freeze({
|
|
103
|
+
sequence: artifact.sequence, artifactId: artifact.id, contentDigestSha256: artifact.contentDigestSha256,
|
|
104
|
+
action: 'packaged', occurredAt: generatedAt,
|
|
105
|
+
}))),
|
|
106
|
+
summary: Object.freeze({ artifactCount: artifacts.length, totalBytes }),
|
|
107
|
+
limitations: Object.freeze([
|
|
108
|
+
'Selected file bytes are unchanged and are not automatically redacted. Review their contents before sharing; original filenames and paths are not retained.',
|
|
109
|
+
'Source identities, observation times and media types are declarations, not independently authenticated evidence. Missing source times remain unknown.',
|
|
110
|
+
'Packaging events use the local clock. They do not establish earlier custody, a trusted timestamp, a signature, source accuracy or current validity.',
|
|
111
|
+
'Digests establish content identity. JSON parsing and declared schema metadata do not establish that a source format is supported or its claims are true.',
|
|
112
|
+
]),
|
|
113
|
+
});
|
|
114
|
+
const manifest = Object.freeze({ ...unsigned, integrity: Object.freeze({
|
|
115
|
+
algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2,
|
|
116
|
+
digestSha256: await sha256ArtifactDigestV2(unsigned),
|
|
117
|
+
}) });
|
|
118
|
+
if (new TextEncoder().encode(canonicalArtifactJsonV2(manifest)).byteLength > MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES)
|
|
119
|
+
throw new TypeError('Investigation manifest metadata exceeds its byte limit.');
|
|
120
|
+
return Object.freeze({ manifest, contents: Object.freeze(selected.map((file) => file.bytes)) });
|
|
121
|
+
}
|
|
122
|
+
export async function buildInvestigationManifest(input, generatedAt, applicationVersion) {
|
|
123
|
+
return (await prepareInvestigationManifest(input, generatedAt, applicationVersion)).manifest;
|
|
124
|
+
}
|
|
125
|
+
export function validateInvestigationManifest(value) {
|
|
126
|
+
const version = integer(value.version, 'Investigation manifest version', 2, INVESTIGATION_MANIFEST_VERSION);
|
|
127
|
+
const current = version === INVESTIGATION_MANIFEST_VERSION;
|
|
128
|
+
const root = exact(value, ['schema', 'version', 'generatedAt', 'application', 'workflow', 'configuration', 'artifacts', 'steps', 'summary', 'limitations', 'integrity', ...(current ? ['audience'] : [])], 'Investigation manifest');
|
|
129
|
+
if (root.schema !== INVESTIGATION_MANIFEST_SCHEMA)
|
|
130
|
+
fail('Investigation manifest schema');
|
|
131
|
+
iso(root.generatedAt, 'Investigation manifest generatedAt');
|
|
132
|
+
const application = exact(root.application, ['name', 'version'], 'Investigation manifest application');
|
|
133
|
+
if (application.name !== (current ? 'WHOISleuth' : 'WHOISleuth CLI'))
|
|
134
|
+
fail('Investigation manifest application');
|
|
135
|
+
normalizeBoundedSemanticVersion(application.version, 'Application');
|
|
136
|
+
text(root.workflow, 'Investigation manifest workflow', 160);
|
|
137
|
+
if (current && root.audience !== 'private')
|
|
138
|
+
fail('Investigation manifest audience');
|
|
139
|
+
const configuration = exact(root.configuration, ['digestSha256'], 'Investigation manifest configuration');
|
|
140
|
+
if (configuration.digestSha256 !== null)
|
|
141
|
+
digest(configuration.digestSha256, 'Investigation manifest configuration');
|
|
142
|
+
const maximumFiles = current ? MAX_INVESTIGATION_MANIFEST_ARTIFACTS : 16;
|
|
143
|
+
const maximumFileBytes = current ? MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES : 16 * 1024 * 1024;
|
|
144
|
+
const maximumTotalBytes = current ? MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES : 32 * 1024 * 1024;
|
|
145
|
+
const artifacts = array(root.artifacts, 'Investigation manifest artifacts', maximumFiles, 1);
|
|
146
|
+
const steps = array(root.steps, 'Investigation manifest steps', maximumFiles, 1);
|
|
147
|
+
if (steps.length !== artifacts.length)
|
|
148
|
+
fail('Investigation manifest step count');
|
|
149
|
+
let totalBytes = 0;
|
|
150
|
+
for (const [index, candidate] of artifacts.entries()) {
|
|
151
|
+
const item = exact(candidate, ['sequence', 'id', 'schema', 'version', 'byteLength', 'contentDigestSha256', 'canonicalDigestSha256', ...(current ? ['mediaType', 'source'] : [])], `Investigation manifest artifact ${index + 1}`);
|
|
152
|
+
if (integer(item.sequence, 'Investigation manifest artifact sequence', 1, artifacts.length) !== index + 1 || item.id !== `artifact-${index + 1}`)
|
|
153
|
+
fail('Investigation manifest artifact order');
|
|
154
|
+
if (item.schema !== null)
|
|
155
|
+
text(item.schema, 'Investigation manifest artifact schema', 160);
|
|
156
|
+
if (item.version !== null)
|
|
157
|
+
integer(item.version, 'Investigation manifest artifact version', 1, 1_000);
|
|
158
|
+
totalBytes += integer(item.byteLength, 'Investigation manifest artifact bytes', 1, maximumFileBytes);
|
|
159
|
+
digest(item.contentDigestSha256, 'Investigation manifest content digest');
|
|
160
|
+
const mediaType = current ? enumeration(item.mediaType, INVESTIGATION_FILE_MEDIA_TYPES, 'Artefact media type') : 'application/json';
|
|
161
|
+
if (mediaType === 'application/json')
|
|
162
|
+
digest(item.canonicalDigestSha256, 'Investigation manifest canonical digest');
|
|
163
|
+
else if (item.canonicalDigestSha256 !== null || item.schema !== null || item.version !== null)
|
|
164
|
+
fail('Opaque artefact JSON metadata');
|
|
165
|
+
if (current) {
|
|
166
|
+
const source = exact(item.source, ['identity', 'observedAt'], 'Artefact source');
|
|
167
|
+
if (source.identity !== null)
|
|
168
|
+
text(source.identity, 'Artefact source identity', 240);
|
|
169
|
+
iso(source.observedAt, 'Artefact source observation time', true);
|
|
170
|
+
}
|
|
171
|
+
const step = exact(steps[index], ['sequence', 'artifactId', 'contentDigestSha256', ...(current ? ['action', 'occurredAt'] : [])], `Investigation manifest step ${index + 1}`);
|
|
172
|
+
if (step.sequence !== item.sequence || step.artifactId !== item.id || step.contentDigestSha256 !== item.contentDigestSha256)
|
|
173
|
+
fail('Investigation manifest step linkage');
|
|
174
|
+
if (current && (step.action !== 'packaged' || step.occurredAt !== root.generatedAt))
|
|
175
|
+
fail('Investigation manifest packaging event');
|
|
176
|
+
}
|
|
177
|
+
const summary = exact(root.summary, ['artifactCount', 'totalBytes'], 'Investigation manifest summary');
|
|
178
|
+
if (integer(summary.artifactCount, 'Investigation manifest artifact count', 1, maximumFiles) !== artifacts.length
|
|
179
|
+
|| integer(summary.totalBytes, 'Investigation manifest total bytes', 1, maximumTotalBytes) !== totalBytes)
|
|
180
|
+
fail('Investigation manifest summary');
|
|
181
|
+
strings(root.limitations, 'Investigation manifest limitations', 8, 600);
|
|
182
|
+
validateIntegrity(root.integrity, 'Investigation manifest integrity', root.version, version);
|
|
183
|
+
}
|
|
184
|
+
export async function readInvestigationManifest(raw) {
|
|
185
|
+
const value = parseBoundedJsonObject(raw, { label: 'Investigation manifest', maximumBytes: MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES });
|
|
186
|
+
validateInvestigationManifest(value);
|
|
187
|
+
const { integrity, ...unsigned } = value;
|
|
188
|
+
if (await sha256ArtifactDigestV2(unsigned) !== integrity.digestSha256)
|
|
189
|
+
throw new TypeError('Investigation manifest failed its integrity check.');
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
export function formatInvestigationManifest(manifest) {
|
|
193
|
+
const lines = ['Investigation manifest', `Workflow ${manifest.workflow}`, `Tool version ${manifest.application.version}`,
|
|
194
|
+
`Artifacts ${manifest.summary.artifactCount}`, `Total bytes ${manifest.summary.totalBytes}`,
|
|
195
|
+
`Configuration ${manifest.configuration.digestSha256 ?? 'not supplied'}`, `Integrity ${manifest.integrity.digestSha256}`, '', 'Ordered artefacts:'];
|
|
196
|
+
for (const artifact of manifest.artifacts)
|
|
197
|
+
lines.push(` ${artifact.sequence}. ${artifact.schema ?? (artifact.canonicalDigestSha256 ? 'unversioned JSON' : 'opaque file')}${artifact.version ? ` v${artifact.version}` : ''} · ${artifact.contentDigestSha256}`);
|
|
198
|
+
lines.push('', 'No source file paths or artefact contents are retained in this manifest.', '');
|
|
199
|
+
return lines.join('\n');
|
|
200
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS } from "../contracts/case-portability.mjs";
|
|
2
|
+
import { ENCRYPTED_INVESTIGATION_PACKAGE_MAGIC, ENCRYPTED_INVESTIGATION_PACKAGE_VERSION, ENCRYPTED_INVESTIGATION_PACKAGE_HEADER_BYTES, MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES, MAX_INVESTIGATION_PACKAGE_BYTES, hasEncryptedInvestigationPackagePrefix, } from "../contracts/investigation-package-limits.mjs";
|
|
3
|
+
import { arrayBuffer, assertPassphrase, cryptoProvider, deriveArchiveKey } from "../evidence/passphrase-encryption.mjs";
|
|
4
|
+
import { inspectInvestigationPackage } from "./investigation-package.mjs";
|
|
5
|
+
const VERSION_OFFSET = ENCRYPTED_INVESTIGATION_PACKAGE_MAGIC.length;
|
|
6
|
+
const ITERATIONS_OFFSET = VERSION_OFFSET + 1;
|
|
7
|
+
const SALT_OFFSET = ITERATIONS_OFFSET + 4;
|
|
8
|
+
const IV_OFFSET = SALT_OFFSET + 16;
|
|
9
|
+
const LENGTH_OFFSET = IV_OFFSET + 12;
|
|
10
|
+
const HEADER_BYTES = ENCRYPTED_INVESTIGATION_PACKAGE_HEADER_BYTES;
|
|
11
|
+
const TAG_BYTES = 16;
|
|
12
|
+
function boundedSnapshot(input, maximum) {
|
|
13
|
+
if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer)
|
|
14
|
+
|| input.byteLength < 22 || input.byteLength > maximum)
|
|
15
|
+
throw new TypeError('Evidence package exceeds its input byte limit or uses shared memory.');
|
|
16
|
+
return new Uint8Array(input);
|
|
17
|
+
}
|
|
18
|
+
/** Fixed-width binary metadata. No targets, file identities or source claims are exposed. */
|
|
19
|
+
export function inspectEncryptedInvestigationPackage(input) {
|
|
20
|
+
if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer) || !hasEncryptedInvestigationPackagePrefix(input)
|
|
21
|
+
|| input.byteLength < HEADER_BYTES + TAG_BYTES + 22 || input.byteLength > MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES) {
|
|
22
|
+
throw new TypeError('The encrypted evidence package is malformed or exceeds its byte limit.');
|
|
23
|
+
}
|
|
24
|
+
if (input[VERSION_OFFSET] !== ENCRYPTED_INVESTIGATION_PACKAGE_VERSION)
|
|
25
|
+
throw new TypeError('Unsupported encrypted evidence package version. No file was decrypted.');
|
|
26
|
+
const header = input.slice(0, HEADER_BYTES);
|
|
27
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
28
|
+
if (view.getUint32(ITERATIONS_OFFSET) !== WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS)
|
|
29
|
+
throw new TypeError('Unsupported encrypted evidence package key-derivation parameters.');
|
|
30
|
+
const plaintextBytes = view.getUint32(LENGTH_OFFSET);
|
|
31
|
+
if (plaintextBytes < 22 || plaintextBytes > MAX_INVESTIGATION_PACKAGE_BYTES
|
|
32
|
+
|| input.byteLength !== HEADER_BYTES + plaintextBytes + TAG_BYTES)
|
|
33
|
+
throw new TypeError('Encrypted evidence package length does not match its declaration.');
|
|
34
|
+
return Object.freeze({ version: ENCRYPTED_INVESTIGATION_PACKAGE_VERSION, plaintextBytes, ciphertextBytes: plaintextBytes + TAG_BYTES });
|
|
35
|
+
}
|
|
36
|
+
/** Encrypt an independently verified package, never a partly accepted archive. */
|
|
37
|
+
export async function encryptInvestigationPackage(input, passphrase, provider) {
|
|
38
|
+
const plaintext = boundedSnapshot(input, MAX_INVESTIGATION_PACKAGE_BYTES);
|
|
39
|
+
let secret = null;
|
|
40
|
+
try {
|
|
41
|
+
secret = assertPassphrase(passphrase);
|
|
42
|
+
const inspected = await inspectInvestigationPackage(plaintext);
|
|
43
|
+
if (!inspected.identityVerified)
|
|
44
|
+
throw new TypeError('The evidence package contains an unverified file. Nothing was encrypted.');
|
|
45
|
+
const crypto = cryptoProvider(provider);
|
|
46
|
+
const salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12));
|
|
47
|
+
const header = new Uint8Array(HEADER_BYTES), view = new DataView(header.buffer);
|
|
48
|
+
header.set(new TextEncoder().encode(ENCRYPTED_INVESTIGATION_PACKAGE_MAGIC));
|
|
49
|
+
header[VERSION_OFFSET] = ENCRYPTED_INVESTIGATION_PACKAGE_VERSION;
|
|
50
|
+
view.setUint32(ITERATIONS_OFFSET, WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS);
|
|
51
|
+
header.set(salt, SALT_OFFSET);
|
|
52
|
+
header.set(iv, IV_OFFSET);
|
|
53
|
+
view.setUint32(LENGTH_OFFSET, plaintext.byteLength);
|
|
54
|
+
const key = await deriveArchiveKey(crypto, secret, salt, ['encrypt']);
|
|
55
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: arrayBuffer(iv), additionalData: header, tagLength: TAG_BYTES * 8 }, key, plaintext);
|
|
56
|
+
const output = new Uint8Array(HEADER_BYTES + encrypted.byteLength);
|
|
57
|
+
output.set(header);
|
|
58
|
+
output.set(new Uint8Array(encrypted), HEADER_BYTES);
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
secret?.fill(0);
|
|
63
|
+
plaintext.fill(0);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Authenticate all container bytes before passing the plaintext to the ZIP reader. */
|
|
67
|
+
export async function decryptInvestigationPackage(input, passphrase, provider) {
|
|
68
|
+
const snapshot = boundedSnapshot(input, MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES);
|
|
69
|
+
let secret = null;
|
|
70
|
+
let plaintext = null;
|
|
71
|
+
try {
|
|
72
|
+
const metadata = inspectEncryptedInvestigationPackage(snapshot);
|
|
73
|
+
secret = assertPassphrase(passphrase);
|
|
74
|
+
const crypto = cryptoProvider(provider);
|
|
75
|
+
try {
|
|
76
|
+
const key = await deriveArchiveKey(crypto, secret, snapshot.slice(SALT_OFFSET, IV_OFFSET), ['decrypt']);
|
|
77
|
+
plaintext = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: snapshot.slice(IV_OFFSET, LENGTH_OFFSET),
|
|
78
|
+
additionalData: snapshot.slice(0, HEADER_BYTES), tagLength: TAG_BYTES * 8 }, key, snapshot.slice(HEADER_BYTES)));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error('The package passphrase is incorrect or the encrypted file is corrupted.');
|
|
82
|
+
}
|
|
83
|
+
if (plaintext.byteLength !== metadata.plaintextBytes)
|
|
84
|
+
throw new TypeError('Decrypted evidence package length is invalid.');
|
|
85
|
+
const review = await inspectInvestigationPackage(plaintext);
|
|
86
|
+
if (!review.identityVerified)
|
|
87
|
+
throw new TypeError('The decrypted package contains an unverified file. No evidence was accepted.');
|
|
88
|
+
return { bytes: plaintext, review, ciphertextBytes: metadata.ciphertextBytes };
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
plaintext?.fill(0);
|
|
92
|
+
throw cause;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
secret?.fill(0);
|
|
96
|
+
snapshot.fill(0);
|
|
97
|
+
}
|
|
98
|
+
}
|