@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/artifact-verify.mjs
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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,
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
throw new
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
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
|
-
|
|
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
|
|
102
|
-
if (!
|
|
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
|
|
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
|
-
|
|
217
|
+
async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
141
218
|
const value = parseJson(raw);
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
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:
|
|
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 (
|
|
211
|
-
if (version
|
|
212
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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";
|