@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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/NOTICE +6 -0
- package/README.md +20 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +112 -72
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- 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 +34 -3
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -8
- 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 +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-header-review.mjs +467 -0
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +77 -24
- package/docs/cli.md +300 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +51 -8
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/rdap-client.mjs +15 -7
- 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 +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +178 -151
- package/lib/structured-data-identity.mjs +1 -0
- 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 +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +153 -45
- package/lib/whois-authority.mjs +14 -2
- 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-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -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 +74 -572
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -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 +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -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 +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -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/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +15 -11
- 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 +183 -211
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +201 -33
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -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/external-findings-import.mjs +92 -51
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- 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/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +193 -165
- 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/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/cli/artifact-verify.mjs
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { readEditableCaseExport } from "../packages/cases/case-export-input.mjs";
|
|
4
|
+
import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "./bounded-json.mjs";
|
|
4
5
|
import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./offline-artifact-validation.mjs";
|
|
5
6
|
import { verifyCaseResponsePacketIntegrity, } from "../packages/cases/case-response-packet.mjs";
|
|
6
7
|
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
8
|
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 {
|
|
9
|
-
import { WORKSPACE_ARCHIVE_SCHEMA,
|
|
9
|
+
import { decryptWorkspaceArchiveWithMetadata, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, } from "../packages/workspace/workspace-archive-crypto.mjs";
|
|
10
|
+
import { WORKSPACE_ARCHIVE_SCHEMA, prepareWorkspaceArchive, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
|
|
10
11
|
import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, sha256ArtifactDigestFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
|
|
11
12
|
import { SAVED_LOOKUP_SCHEMA, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, parseCliLookupDocument, } from "./saved-lookup.mjs";
|
|
12
13
|
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";
|
|
14
|
-
import { DOMAIN_CHANGE_PACKET_SCHEMA,
|
|
14
|
+
import { DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, } from "../packages/contracts/domain-control-manifest.mjs";
|
|
15
|
+
import { DOMAIN_CHANGE_PACKET_SCHEMA, SUPPORTED_DOMAIN_CHANGE_PACKET_VERSIONS, } from "../lib/domain-change-packet.mjs";
|
|
15
16
|
import { INVESTIGATION_CAPSULE_SCHEMA, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, verifyInvestigationCapsule, } from "../packages/investigation/investigation-capsule.mjs";
|
|
16
|
-
import { INVESTIGATION_MANIFEST_SCHEMA,
|
|
17
|
+
import { INVESTIGATION_MANIFEST_SCHEMA, SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS, } from "./investigation-manifest.mjs";
|
|
17
18
|
import { verifyCliCasePack } from "./case-pack.mjs";
|
|
18
19
|
import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
|
|
19
20
|
export const OFFLINE_ARTIFACT_VERIFICATION_SCHEMA = 'whoisleuth.offline-artifact-verification';
|
|
20
|
-
export const OFFLINE_ARTIFACT_VERIFICATION_VERSION =
|
|
21
|
-
export const MAX_OFFLINE_ARTIFACT_BYTES =
|
|
21
|
+
export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 4;
|
|
22
|
+
export const MAX_OFFLINE_ARTIFACT_BYTES = Math.max(MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES);
|
|
22
23
|
export const MAX_OFFLINE_PASSPHRASE_FILE_BYTES = 1024;
|
|
23
24
|
export class UnsupportedOfflineArtifactError extends TypeError {
|
|
24
25
|
code = 'unsupported_artifact';
|
|
@@ -41,8 +42,8 @@ const SIGNED_ARTIFACT_ROUTES = Object.freeze({
|
|
|
41
42
|
[BULK_MAIL_EXPOSURE_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS),
|
|
42
43
|
[BULK_REVIEW_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS),
|
|
43
44
|
[DOMAIN_CONTROL_MANIFEST_SCHEMA]: DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES,
|
|
44
|
-
[DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes(
|
|
45
|
-
[INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(
|
|
45
|
+
[DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_DOMAIN_CHANGE_PACKET_VERSIONS),
|
|
46
|
+
[INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS),
|
|
46
47
|
});
|
|
47
48
|
function record(value) {
|
|
48
49
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -83,8 +84,16 @@ function rejectUnsupportedCasePortabilityVersion(label, version, currentVersion)
|
|
|
83
84
|
}
|
|
84
85
|
throw new UnsupportedOfflineArtifactError(`This ${label} version is malformed or unsupported; no data was changed.`);
|
|
85
86
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
87
|
+
export function parseOfflineArtifactJson(raw) {
|
|
88
|
+
const value = parseBoundedJsonObject(raw, {
|
|
89
|
+
maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES,
|
|
90
|
+
limits: boundedJsonLimitsForBytes(MAX_OFFLINE_ARTIFACT_BYTES),
|
|
91
|
+
});
|
|
92
|
+
if (value.schema !== WORKSPACE_ARCHIVE_SCHEMA && !isEncryptedWorkspaceArchive(value)
|
|
93
|
+
&& Buffer.byteLength(raw, 'utf8') > MAX_DOMAIN_CONTROL_MANIFEST_BYTES) {
|
|
94
|
+
throw new TypeError(`Non-workspace artefacts are limited to ${MAX_DOMAIN_CONTROL_MANIFEST_BYTES} bytes.`);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
88
97
|
}
|
|
89
98
|
export function hasVerifiedArtifactStructure(report) {
|
|
90
99
|
return report.checks.structure === 'verified';
|
|
@@ -92,6 +101,7 @@ export function hasVerifiedArtifactStructure(report) {
|
|
|
92
101
|
export function hasVerifiedApplicableIntegrity(report) {
|
|
93
102
|
return report.checks.contentIntegrity === 'verified'
|
|
94
103
|
&& (report.checks.contentIntegrityScope === 'whole_artifact'
|
|
104
|
+
|| report.checks.contentIntegrityScope === 'manifest_and_files'
|
|
95
105
|
|| report.checks.contentIntegrityScope === 'embedded_projections');
|
|
96
106
|
}
|
|
97
107
|
export function hasVerifiedWholeArtifactIntegrity(report) {
|
|
@@ -108,6 +118,9 @@ export function offlineArtifactSatisfiesAssurance(report, requirement) {
|
|
|
108
118
|
return hasVerifiedWholeArtifactIntegrity(report);
|
|
109
119
|
}
|
|
110
120
|
export function isCompleteOfflineArtifactVerification(report) {
|
|
121
|
+
if (report.artifact.kind === 'bagit')
|
|
122
|
+
return report.state === 'integrity_valid' && report.bagit?.state === 'valid'
|
|
123
|
+
&& report.checks.structure === 'verified' && report.checks.contentIntegrity === 'verified';
|
|
111
124
|
return (report.state === 'verified' || report.state === 'structure_valid')
|
|
112
125
|
&& (report.manifestIdentity === null || report.manifestIdentity.state === 'identity_verified');
|
|
113
126
|
}
|
|
@@ -124,8 +137,7 @@ function artifactVersion(value) {
|
|
|
124
137
|
function inputBytes(raw) {
|
|
125
138
|
return Buffer.byteLength(raw, 'utf8');
|
|
126
139
|
}
|
|
127
|
-
|
|
128
|
-
const preview = await previewWorkspaceArchive(source, {});
|
|
140
|
+
function archiveReport(raw, archive, preview, encrypted, ciphertextBytes) {
|
|
129
141
|
const readySectionCount = preview.sections.filter((section) => section.status === 'ready').length;
|
|
130
142
|
const unsupportedSectionCount = preview.sections.filter((section) => section.status === 'unsupported').length;
|
|
131
143
|
const blockedSectionCount = preview.sections.filter((section) => section.status === 'blocked').length;
|
|
@@ -172,6 +184,24 @@ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
|
|
|
172
184
|
]),
|
|
173
185
|
});
|
|
174
186
|
}
|
|
187
|
+
async function verifyWorkspaceArchiveValue(raw, value, passphrase) {
|
|
188
|
+
const encrypted = isEncryptedWorkspaceArchive(value);
|
|
189
|
+
if (encrypted && !passphrase) {
|
|
190
|
+
throw new TypeError('Encrypted archive inspection requires a separate passphrase file.');
|
|
191
|
+
}
|
|
192
|
+
if (!encrypted)
|
|
193
|
+
artifactVersion(value);
|
|
194
|
+
const decrypted = encrypted ? await decryptWorkspaceArchiveWithMetadata(value, passphrase) : null;
|
|
195
|
+
const source = decrypted ? decrypted.archive : value;
|
|
196
|
+
const prepared = await prepareWorkspaceArchive(source);
|
|
197
|
+
const archive = prepared.read();
|
|
198
|
+
const report = archiveReport(raw, archive, prepared.preview({}), encrypted, decrypted?.ciphertextBytes ?? null);
|
|
199
|
+
return { archive, report, encrypted };
|
|
200
|
+
}
|
|
201
|
+
/** Verify an archive once and retain its sections for bounded offline inspection. */
|
|
202
|
+
export async function verifyOfflineWorkspaceArchive(raw, options = {}) {
|
|
203
|
+
return verifyWorkspaceArchiveValue(raw, parseOfflineArtifactJson(raw), options.passphrase);
|
|
204
|
+
}
|
|
175
205
|
async function verifySignedArtifact(raw, value, schema, version) {
|
|
176
206
|
const routes = SIGNED_ARTIFACT_ROUTES[schema] ?? [];
|
|
177
207
|
if (!routes.some((route) => route.version === version)) {
|
|
@@ -215,7 +245,7 @@ async function verifySignedArtifact(raw, value, schema, version) {
|
|
|
215
245
|
});
|
|
216
246
|
}
|
|
217
247
|
async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
218
|
-
const value =
|
|
248
|
+
const value = parseOfflineArtifactJson(raw);
|
|
219
249
|
const casePortabilityVerifier = selectCasePortabilityVerifier(value);
|
|
220
250
|
if (casePortabilityVerifier?.id === 'cli-case-pack') {
|
|
221
251
|
const casePackVersion = casePortabilityVersion(casePortabilityVerifier, value);
|
|
@@ -249,9 +279,24 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
249
279
|
]),
|
|
250
280
|
});
|
|
251
281
|
}
|
|
282
|
+
if (value.schema === undefined && Array.isArray(value.cases)) {
|
|
283
|
+
const cases = readEditableCaseExport(raw);
|
|
284
|
+
return Object.freeze({
|
|
285
|
+
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
286
|
+
artifact: Object.freeze({ kind: 'case_export', schema: null, version: artifactVersion(value) }),
|
|
287
|
+
state: 'structure_valid',
|
|
288
|
+
checks: Object.freeze({ structure: 'verified', contentIntegrity: 'not_checked', contentIntegrityScope: 'not_applicable', authenticatedEncryption: 'not_applicable' }),
|
|
289
|
+
summary: Object.freeze({ inputBytes: inputBytes(raw), sectionCount: 1, recordCount: cases.length, ciphertextBytes: null }),
|
|
290
|
+
limitations: Object.freeze([
|
|
291
|
+
'The ordinary Case export matches its exact supported structure without repairing or removing retained records. It has no embedded checksum or signature.',
|
|
292
|
+
'Original-file references do not include original bytes. Verify a selected evidence package to check which references have matching bytes.',
|
|
293
|
+
'The Case includes private analyst content. Structural validity does not authenticate its author or establish factual accuracy, currentness or response authority.',
|
|
294
|
+
]),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
252
297
|
if (isEncryptedWorkspaceArchive(value)) {
|
|
253
|
-
const inspected = inspectEncryptedWorkspaceArchive(value);
|
|
254
298
|
if (!options.passphrase) {
|
|
299
|
+
const inspected = inspectEncryptedWorkspaceArchive(value);
|
|
255
300
|
return Object.freeze({
|
|
256
301
|
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
257
302
|
version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
@@ -279,14 +324,12 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
|
|
|
279
324
|
]),
|
|
280
325
|
});
|
|
281
326
|
}
|
|
282
|
-
|
|
283
|
-
const archive = await readWorkspaceArchive(decrypted);
|
|
284
|
-
return archiveReport(raw, decrypted, archive, true, inspected.ciphertextBytes);
|
|
327
|
+
return (await verifyWorkspaceArchiveValue(raw, value, options.passphrase)).report;
|
|
285
328
|
}
|
|
286
329
|
const schema = typeof value.schema === 'string' ? value.schema : '';
|
|
287
330
|
const version = artifactVersion(value);
|
|
288
331
|
if (schema === WORKSPACE_ARCHIVE_SCHEMA) {
|
|
289
|
-
return
|
|
332
|
+
return (await verifyWorkspaceArchiveValue(raw, value)).report;
|
|
290
333
|
}
|
|
291
334
|
if (casePortabilityVerifier?.id === 'case-response-packet') {
|
|
292
335
|
if (!supportsCasePortabilityVersion(casePortabilityVerifier, version)) {
|
|
@@ -432,13 +475,13 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
|
|
|
432
475
|
|| manifestReport.checks.contentIntegrityScope !== 'whole_artifact') {
|
|
433
476
|
throw new TypeError('The selected manifest is not a fully integrity-verified investigation manifest.');
|
|
434
477
|
}
|
|
435
|
-
const manifest =
|
|
478
|
+
const manifest = parseOfflineArtifactJson(manifestRaw);
|
|
436
479
|
const entries = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
437
480
|
const entry = entries.find((candidate) => record(candidate)?.id === entryId);
|
|
438
481
|
const item = record(entry);
|
|
439
482
|
if (!item)
|
|
440
483
|
throw new TypeError('The requested investigation manifest entry was not found.');
|
|
441
|
-
const artifactValue =
|
|
484
|
+
const artifactValue = parseOfflineArtifactJson(artifactRaw);
|
|
442
485
|
const metadata = artifactMetadata(artifactValue);
|
|
443
486
|
const actualByteLength = inputBytes(artifactRaw);
|
|
444
487
|
const expectedByteLength = Number(item.byteLength);
|
|
@@ -466,7 +509,7 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
|
|
|
466
509
|
: 'mismatch';
|
|
467
510
|
return Object.freeze({
|
|
468
511
|
state,
|
|
469
|
-
manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version:
|
|
512
|
+
manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: manifestReport.artifact.version, entryId }),
|
|
470
513
|
checks,
|
|
471
514
|
expectedByteLength,
|
|
472
515
|
actualByteLength,
|
|
@@ -490,8 +533,9 @@ export async function verifyOfflineArtifact(raw, options = {}) {
|
|
|
490
533
|
export function formatOfflineArtifactVerification(report) {
|
|
491
534
|
const lines = [
|
|
492
535
|
'WHOISleuth offline artefact verification',
|
|
493
|
-
`Artifact: ${report.artifact.kind} · ${report.artifact.schema} v${report.artifact.version}`,
|
|
536
|
+
`Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'no schema identifier'} v${report.artifact.version}`,
|
|
494
537
|
`State: ${report.state}`,
|
|
538
|
+
'Automation: use --strict-exit for exit 4 on incomplete verification; default exit 0 means the report was produced.',
|
|
495
539
|
`Structure: ${report.checks.structure}`,
|
|
496
540
|
`Content integrity: ${report.checks.contentIntegrity}`,
|
|
497
541
|
`Content integrity scope: ${report.checks.contentIntegrityScope}`,
|
|
@@ -534,6 +578,28 @@ export function formatOfflineArtifactVerification(report) {
|
|
|
534
578
|
for (const limitation of report.manifestIdentity.limitations)
|
|
535
579
|
lines.push(`Manifest limitation: ${limitation}`);
|
|
536
580
|
}
|
|
581
|
+
if (report.bagit) {
|
|
582
|
+
const bag = report.bagit;
|
|
583
|
+
lines.push(`BagIt: ${bag.state}`, `Payload: ${bag.entries.length} files; ${bag.payloadBytes} present bytes`, `Tag checksums: ${bag.verifiedTagFiles} of ${bag.tagFiles} tag files checked`, `Fetch declarations: ${bag.fetchEntries}; ${bag.fetchMissing} files missing; no requests made`, `Unsupported manifests: ${bag.unsupportedManifests}`);
|
|
584
|
+
for (const entry of bag.entries)
|
|
585
|
+
lines.push(`${entry.id}: ${entry.state} · ${entry.byteLength ?? 'unknown'} bytes`);
|
|
586
|
+
for (const issue of bag.issues)
|
|
587
|
+
lines.push(`BagIt issue: ${issue}`);
|
|
588
|
+
}
|
|
589
|
+
if (report.package) {
|
|
590
|
+
lines.push(`Package bytes: ${report.package.digestSha256}`, 'Storage: unchanged; inspection only', 'Audience: private; review selected file contents before sharing', 'Signature trust: not checked', 'Trusted timestamp: not checked', 'Factual accuracy: not established');
|
|
591
|
+
for (const entry of report.package.entries)
|
|
592
|
+
lines.push(`${entry.id}: ${entry.state} · ${entry.byteLength} bytes · ${entry.mediaType}${entry.issue ? ` · ${entry.issue}` : ''}`);
|
|
593
|
+
for (const link of report.package.links)
|
|
594
|
+
lines.push(`Source link ${link.capsuleEntryId}: ${link.state}${link.sourceEntryId ? ` (${link.sourceEntryId})` : ''}`);
|
|
595
|
+
for (const capture of report.package.captureManifests) {
|
|
596
|
+
lines.push(`Capture manifest ${capture.entryId}: ${capture.state}`);
|
|
597
|
+
for (const artifact of capture.artifacts)
|
|
598
|
+
lines.push(`Capture ${artifact.capture} ${artifact.kind}: ${artifact.state}${artifact.matchingIds.length ? ` (${artifact.matchingIds.join(', ')})` : ''}`);
|
|
599
|
+
}
|
|
600
|
+
for (const item of report.package.caseFiles)
|
|
601
|
+
lines.push(`Case files ${item.entryId} (${item.caseCount} Case${item.caseCount === 1 ? '' : 's'}): ${item.matched} of ${item.references} references matched; ${item.missing} missing`);
|
|
602
|
+
}
|
|
537
603
|
for (const limitation of report.limitations)
|
|
538
604
|
lines.push(`Limitation: ${limitation}`);
|
|
539
605
|
return `${lines.join('\n')}\n`;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { readBoundedRegularFile } from "../lib/bounded-file.mjs";
|
|
3
|
+
import { buildInvestigationPackage } from "../packages/investigation/investigation-package.mjs";
|
|
4
|
+
import { encryptInvestigationPackage } from "../packages/investigation/investigation-package-crypto.mjs";
|
|
5
|
+
import { writeInvestigationFolder, writePreparedEvidenceFolder } from "./investigation-folder.mjs";
|
|
6
|
+
import { buildInvestigationBagIt, prepareInvestigationBagIt } from "../packages/investigation/investigation-bagit.mjs";
|
|
2
7
|
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
3
8
|
import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
|
|
4
9
|
import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
|
|
@@ -12,7 +17,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
|
12
17
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
13
18
|
import { MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, formatExternalObservationMapping, mapExternalObservations, } from "./external-observation-mapping.mjs";
|
|
14
19
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
15
|
-
import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
|
|
20
|
+
import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, investigationFileMediaType, } from "./investigation-manifest.mjs";
|
|
16
21
|
import { buildOpenAssetModelBridge, formatOpenAssetModelBridge, } from "./open-asset-model-bridge.mjs";
|
|
17
22
|
import { ASSURANCE_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
18
23
|
import { MAX_SHARING_REVIEW_BYTES, buildSharingReview, formatSharingReview, } from "./sharing-review.mjs";
|
|
@@ -23,6 +28,15 @@ async function runManifestCommand(args, dependencies, context) {
|
|
|
23
28
|
let totalBytes = 0;
|
|
24
29
|
try {
|
|
25
30
|
for (const source of args.sources) {
|
|
31
|
+
if (args.package || args.folder) {
|
|
32
|
+
const content = dependencies.readBinaryArtifactInput ? await dependencies.readBinaryArtifactInput(source)
|
|
33
|
+
: await readBoundedRegularFile(source, { maximumBytes: MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, minimumBytes: 1, label: 'Package artefact input', ...(dependencies.signal ? { signal: dependencies.signal } : {}) });
|
|
34
|
+
if (content.byteLength > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES - totalBytes)
|
|
35
|
+
throw new CliUsageError('Package artefacts exceed the combined byte limit.');
|
|
36
|
+
totalBytes += content.byteLength;
|
|
37
|
+
artifacts.push({ content, mediaType: investigationFileMediaType(source) });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
26
40
|
const content = dependencies.readDiffInput
|
|
27
41
|
? await dependencies.readDiffInput(source)
|
|
28
42
|
: await context.readInput(source, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, 'Manifest artefact input');
|
|
@@ -40,11 +54,32 @@ async function runManifestCommand(args, dependencies, context) {
|
|
|
40
54
|
}
|
|
41
55
|
let document;
|
|
42
56
|
try {
|
|
43
|
-
|
|
57
|
+
if (args.package) {
|
|
58
|
+
const built = await (args.bagit ? buildInvestigationBagIt : buildInvestigationPackage)({ workflow: args.workflow, configurationDigestSha256: args.configurationDigestSha256, artifacts }, context.now(), context.packageVersion);
|
|
59
|
+
dependencies.signal?.throwIfAborted();
|
|
60
|
+
if (!context.writeBinaryOutput)
|
|
61
|
+
throw new CliUsageError('Package output requires an explicit file destination.');
|
|
62
|
+
const output = args.passphraseSource
|
|
63
|
+
? await encryptInvestigationPackage(built.bytes, await context.readPassphraseSource(args.passphraseSource))
|
|
64
|
+
: built.bytes;
|
|
65
|
+
dependencies.signal?.throwIfAborted();
|
|
66
|
+
context.writeBinaryOutput(output);
|
|
67
|
+
return EXIT_CODES.SUCCESS;
|
|
68
|
+
}
|
|
69
|
+
const input = {
|
|
44
70
|
workflow: args.workflow,
|
|
45
71
|
configurationDigestSha256: args.configurationDigestSha256,
|
|
46
72
|
artifacts,
|
|
47
|
-
}
|
|
73
|
+
};
|
|
74
|
+
if (args.bagit && args.folder) {
|
|
75
|
+
const prepared = await prepareInvestigationBagIt(input, context.now(), context.packageVersion);
|
|
76
|
+
await writePreparedEvidenceFolder(args.folder, prepared.files, 'data', 'tagmanifest-sha512.txt', dependencies.signal);
|
|
77
|
+
document = prepared.manifest;
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
document = args.folder
|
|
81
|
+
? await writeInvestigationFolder(args.folder, input, context.now(), context.packageVersion, dependencies.signal)
|
|
82
|
+
: await buildInvestigationManifest(input, context.now(), context.packageVersion);
|
|
48
83
|
}
|
|
49
84
|
catch (error) {
|
|
50
85
|
throw new CliUsageError(boundedCliErrorMessage(error, 'Investigation manifest input is invalid'));
|
|
@@ -52,7 +87,7 @@ async function runManifestCommand(args, dependencies, context) {
|
|
|
52
87
|
if (!args.quiet) {
|
|
53
88
|
context.writeStdout(args.output === 'json'
|
|
54
89
|
? formatJsonDocument(document)
|
|
55
|
-
: context.terminal(formatInvestigationManifest(document)
|
|
90
|
+
: context.terminal(`${args.folder ? 'Created a new private evidence folder.\n' : ''}${formatInvestigationManifest(document)}`, args.color));
|
|
56
91
|
}
|
|
57
92
|
return EXIT_CODES.SUCCESS;
|
|
58
93
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { lstat, opendir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readBoundedRegularFileWithin } from "../lib/bounded-file.mjs";
|
|
4
|
+
import { assertBagItSelection, bagItPath, bagItEntryMaximum, MAX_BAGIT_ENTRIES } from "../packages/interchange/bagit.mjs";
|
|
5
|
+
import { selectedEvidenceDirectory, evidenceDirectoryIdentity } from "./investigation-folder.mjs";
|
|
6
|
+
/** Bounded recursive enumeration; no symbolic links, special files or writes. */
|
|
7
|
+
export async function readBagItFolder(value, signal) {
|
|
8
|
+
const selected = selectedEvidenceDirectory(value);
|
|
9
|
+
const identity = await evidenceDirectoryIdentity(selected), root = await realpath(selected);
|
|
10
|
+
const directories = new Map([['', identity]]);
|
|
11
|
+
const entries = [];
|
|
12
|
+
const visit = async (relative) => {
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
const path = join(root, relative);
|
|
15
|
+
if (await realpath(path) !== path)
|
|
16
|
+
throw new TypeError('BagIt directories must not contain symbolic links.');
|
|
17
|
+
for await (const item of await opendir(path)) {
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
if (entries.length >= MAX_BAGIT_ENTRIES)
|
|
20
|
+
throw new TypeError('BagIt exceeds its entry limit.');
|
|
21
|
+
const name = `${relative ? `${relative}/` : ''}${item.name}`;
|
|
22
|
+
bagItPath(item.isDirectory() ? `${name}/` : name);
|
|
23
|
+
const stat = await lstat(join(root, name));
|
|
24
|
+
if (item.isDirectory() && stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
25
|
+
directories.set(name, await evidenceDirectoryIdentity(join(root, name)));
|
|
26
|
+
entries.push({ path: `${name}/`, byteLength: 0 });
|
|
27
|
+
await visit(name);
|
|
28
|
+
}
|
|
29
|
+
else if (item.isFile() && stat.isFile() && !stat.isSymbolicLink()) {
|
|
30
|
+
if (stat.size > bagItEntryMaximum(name))
|
|
31
|
+
throw new TypeError('BagIt entry exceeds its byte limit.');
|
|
32
|
+
entries.push({ path: name, byteLength: stat.size });
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
throw new TypeError('BagIt entries must be ordinary files and directories, not symbolic links or special files.');
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
await visit('');
|
|
39
|
+
assertBagItSelection(entries);
|
|
40
|
+
const files = new Map();
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
signal?.throwIfAborted();
|
|
43
|
+
files.set(entry.path, entry.path.endsWith('/') ? new Uint8Array() : await readBoundedRegularFileWithin(root, entry.path, {
|
|
44
|
+
maximumBytes: bagItEntryMaximum(entry.path), minimumBytes: 0, expectedBytes: entry.byteLength,
|
|
45
|
+
label: 'BagIt file', ...(signal ? { signal } : {}),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
for (const [path, before] of directories) {
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
const currentPath = path ? join(root, path) : selected;
|
|
51
|
+
const after = await evidenceDirectoryIdentity(currentPath);
|
|
52
|
+
if (await realpath(currentPath) !== join(root, path) || before.device !== after.device || before.inode !== after.inode
|
|
53
|
+
|| before.modified !== after.modified || before.changed !== after.changed)
|
|
54
|
+
throw new TypeError('BagIt folder changed while it was read.');
|
|
55
|
+
}
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { inspectBagItEntries, readBagItZip } from "../packages/interchange/bagit.mjs";
|
|
2
|
+
import { OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, OFFLINE_ARTIFACT_VERIFICATION_VERSION } from "./artifact-verify.mjs";
|
|
3
|
+
export async function verifyOfflineBagIt(input) {
|
|
4
|
+
const files = input instanceof Uint8Array ? readBagItZip(input) : input;
|
|
5
|
+
const zipBytes = input instanceof Uint8Array ? input.byteLength : null;
|
|
6
|
+
const { review, inputBytes } = await inspectBagItEntries(files);
|
|
7
|
+
return {
|
|
8
|
+
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
9
|
+
artifact: { kind: 'bagit', schema: null, version: '1.0' },
|
|
10
|
+
state: review.state === 'valid' ? 'integrity_valid' : 'partial',
|
|
11
|
+
checks: { structure: review.complete ? 'verified' : 'failed',
|
|
12
|
+
contentIntegrity: review.state === 'valid' ? 'verified' : review.state === 'invalid' ? 'failed' : 'not_checked',
|
|
13
|
+
contentIntegrityScope: 'manifest_and_files', authenticatedEncryption: 'not_applicable' },
|
|
14
|
+
summary: { inputBytes: zipBytes ?? inputBytes,
|
|
15
|
+
sectionCount: null, recordCount: review.entries.length, ciphertextBytes: null },
|
|
16
|
+
manifestIdentity: null, bagit: review,
|
|
17
|
+
limitations: [
|
|
18
|
+
'BagIt verifies declared file checksums, not payload schemas, factual accuracy, source identity, signatures or trusted timestamps.',
|
|
19
|
+
'Tag files without a checksum are not integrity-checked. No file is imported, executed or fetched; fetch.txt locations and original paths are omitted from this report.',
|
|
20
|
+
'This bounded reader supports BagIt 1.0, UTF-8 text tags, SHA-256 and SHA-512, portable relative paths and the selected-file byte limits. Other encodings, algorithms and unsafe or platform-ambiguous paths are not silently accepted.',
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
package/cli/bounded-json.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
|
|
1
|
+
export { boundedJsonLimitsForBytes, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
|
package/cli/bulk-checkpoint.mjs
CHANGED
|
@@ -54,6 +54,24 @@ function normalizeCompactResult(value) {
|
|
|
54
54
|
return null;
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
|
+
function normalizeCollectionContext(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const context = recordOrNull(value);
|
|
61
|
+
if (!context || Object.keys(context).some((key) => !['dnsResolver', 'resolverServers'].includes(key)))
|
|
62
|
+
return null;
|
|
63
|
+
const resolverServers = context.resolverServers;
|
|
64
|
+
if (!Array.isArray(resolverServers) || resolverServers.length > 3
|
|
65
|
+
|| resolverServers.some((server) => typeof server !== 'string' || !server || server.length > 45))
|
|
66
|
+
return null;
|
|
67
|
+
if (context.dnsResolver === 'system_default' && resolverServers.length === 0) {
|
|
68
|
+
return { dnsResolver: 'system_default', resolverServers: [] };
|
|
69
|
+
}
|
|
70
|
+
if (context.dnsResolver === 'analyst_selected' && resolverServers.length > 0) {
|
|
71
|
+
return { dnsResolver: 'analyst_selected', resolverServers: [...resolverServers] };
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
57
75
|
function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
58
76
|
const item = recordOrNull(value);
|
|
59
77
|
const index = item?.index;
|
|
@@ -67,10 +85,16 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
|
67
85
|
: normalizedTimestamp(item.observedAt) ?? undefined;
|
|
68
86
|
if (observedAt === undefined)
|
|
69
87
|
return null;
|
|
88
|
+
const collectionContext = normalizeCollectionContext(item.collectionContext);
|
|
89
|
+
if (collectionContext === null)
|
|
90
|
+
return null;
|
|
70
91
|
if (item.ok === false) {
|
|
71
92
|
if (typeof item.error !== 'string' || !item.error || item.error.length > 300)
|
|
72
93
|
return null;
|
|
73
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
index: Number(index), query, ok: false, error: item.error, observedAt,
|
|
96
|
+
...(collectionContext ? { collectionContext } : {}),
|
|
97
|
+
};
|
|
74
98
|
}
|
|
75
99
|
const result = normalizeCompactResult(item.result);
|
|
76
100
|
if (item.ok !== true || !result)
|
|
@@ -79,7 +103,10 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
|
|
|
79
103
|
const storedClassified = recordOrNull(item.classified);
|
|
80
104
|
if (storedClassified?.type !== classified.type || storedClassified.value !== classified.value)
|
|
81
105
|
return null;
|
|
82
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
index: Number(index), query, ok: true, classified, result, observedAt,
|
|
108
|
+
...(collectionContext ? { collectionContext } : {}),
|
|
109
|
+
};
|
|
83
110
|
}
|
|
84
111
|
function parseBulkCheckpoint(text, options) {
|
|
85
112
|
if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
|
|
@@ -214,8 +241,10 @@ async function createBulkCheckpointWriter(options) {
|
|
|
214
241
|
}
|
|
215
242
|
}
|
|
216
243
|
function scheduleWrite() {
|
|
244
|
+
if (writeFailure)
|
|
245
|
+
return;
|
|
217
246
|
dirty = true;
|
|
218
|
-
if (activeWrite
|
|
247
|
+
if (activeWrite)
|
|
219
248
|
return;
|
|
220
249
|
const pending = drainWrites();
|
|
221
250
|
activeWrite = pending;
|
|
@@ -250,6 +279,8 @@ async function createBulkCheckpointWriter(options) {
|
|
|
250
279
|
},
|
|
251
280
|
async flush() {
|
|
252
281
|
while (activeWrite || dirty) {
|
|
282
|
+
if (writeFailure && !activeWrite)
|
|
283
|
+
break;
|
|
253
284
|
if (!activeWrite)
|
|
254
285
|
scheduleWrite();
|
|
255
286
|
if (activeWrite)
|
|
@@ -113,6 +113,8 @@ async function runBulkCommand(args, dependencies, context) {
|
|
|
113
113
|
context.writeStdout(formatJsonLines(selectedItems, metadata));
|
|
114
114
|
else if (args.output === 'csv')
|
|
115
115
|
context.writeStdout(formatBulkCsv(selectedItems));
|
|
116
|
+
else if (args.output === 'csv_metadata')
|
|
117
|
+
context.writeStdout(formatBulkCsv(selectedItems, selectedDocument));
|
|
116
118
|
else if (args.output === 'domains')
|
|
117
119
|
context.writeStdout(formatBulkDomainList(selectedItems));
|
|
118
120
|
else if (args.output === 'queries')
|
package/cli/bulk-output.mjs
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { cliCsvCell } from "./csv.mjs";
|
|
2
|
+
import { lookupDiagnosticStates } from "../lib/lookup-diagnostics.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
2
4
|
const REGISTERED_STATES = new Set(['expiring', 'for_sale', 'registered']);
|
|
3
5
|
const MAX_DNS_VALUES_PER_TYPE = 100;
|
|
6
|
+
const BULK_CSV_METADATA_COLUMNS = Object.freeze([
|
|
7
|
+
'source_schema', 'source_version', 'observed_at', 'report_generated_at',
|
|
8
|
+
'collection_origin', 'scan_mode', 'diagnostics_version', 'source_health',
|
|
9
|
+
]);
|
|
10
|
+
function bulkCsvMetadataValues(item, metadata) {
|
|
11
|
+
const diagnostics = record(item.diagnostics);
|
|
12
|
+
return [
|
|
13
|
+
metadata.schema,
|
|
14
|
+
metadata.version,
|
|
15
|
+
normalizeExplicitIsoTimestamp(item.observedAt) ?? 'unknown',
|
|
16
|
+
normalizeExplicitIsoTimestamp(metadata.generatedAt) ?? 'unknown',
|
|
17
|
+
item.collectionOrigin === 'current_run' || item.collectionOrigin === 'resumed_checkpoint' ? item.collectionOrigin : 'unknown',
|
|
18
|
+
metadata.mode,
|
|
19
|
+
Number.isSafeInteger(diagnostics.version) && Number(diagnostics.version) > 0 ? diagnostics.version : 'unknown',
|
|
20
|
+
JSON.stringify(lookupDiagnosticStates(item.diagnostics)),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
4
23
|
function record(value) {
|
|
5
24
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
6
25
|
? value
|
|
@@ -78,12 +97,18 @@ function bulkDnsSummary(item) {
|
|
|
78
97
|
function booleanCell(value) {
|
|
79
98
|
return value === null ? 'unknown' : value ? 'observed' : 'not_observed';
|
|
80
99
|
}
|
|
81
|
-
function formatBulkCsv(items) {
|
|
100
|
+
function formatBulkCsv(items, metadata) {
|
|
101
|
+
if (metadata && metadata.results.length !== items.length)
|
|
102
|
+
throw new TypeError('CSV metadata does not match the selected result count.');
|
|
82
103
|
const header = [
|
|
83
104
|
'query', 'domain', 'outcome', 'availability', 'confidence', 'dns_status',
|
|
84
105
|
'a', 'aaaa', 'ns', 'mx', 'null_mx', 'spf', 'dmarc', 'error',
|
|
106
|
+
...(metadata ? BULK_CSV_METADATA_COLUMNS : []),
|
|
85
107
|
];
|
|
86
|
-
const rows = items.map((item) => {
|
|
108
|
+
const rows = items.map((item, index) => {
|
|
109
|
+
const retained = metadata?.results[index];
|
|
110
|
+
if (metadata && (!retained || retained.index !== item.index || retained.query !== item.query))
|
|
111
|
+
throw new TypeError('CSV metadata does not match the selected result identity.');
|
|
87
112
|
const availability = item.ok ? record(record(item.result).availability) : {};
|
|
88
113
|
const dns = bulkDnsSummary(item);
|
|
89
114
|
return [
|
|
@@ -101,6 +126,7 @@ function formatBulkCsv(items) {
|
|
|
101
126
|
booleanCell(dns.hasSpf),
|
|
102
127
|
booleanCell(dns.hasDmarc),
|
|
103
128
|
item.ok ? '' : item.error,
|
|
129
|
+
...(metadata && retained ? bulkCsvMetadataValues(retained, metadata) : []),
|
|
104
130
|
].map(cliCsvCell).join(',');
|
|
105
131
|
});
|
|
106
132
|
return `${[header.join(','), ...rows].join('\n')}\n`;
|
|
@@ -114,4 +140,4 @@ function formatBulkDomainList(items) {
|
|
|
114
140
|
function formatBulkQueryList(items) {
|
|
115
141
|
return items.length ? `${items.map((item) => item.query).join('\n')}\n` : '';
|
|
116
142
|
}
|
|
117
|
-
export { REGISTERED_STATES, availabilityState, bulkDnsSummary, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
|
|
143
|
+
export { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkDnsSummary, bulkCsvMetadataValues, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
|