@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
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// This protects a downloaded file while it is locked. It does not encrypt the
|
|
3
3
|
// active IndexedDB workspace or defend an unlocked page from same-origin code.
|
|
4
4
|
import { WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_VERSION, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, isSupportedWorkspaceArchiveVersion, readWorkspaceArchive, } from "./workspace-archive.mjs";
|
|
5
|
-
import { parseBoundedJson } from "../../lib/bounded-json.mjs";
|
|
5
|
+
import { boundedJsonLimitsForBytes, parseBoundedJson } from "../../lib/bounded-json.mjs";
|
|
6
6
|
import { ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA, ENCRYPTED_WORKSPACE_ARCHIVE_VERSION, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES, MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS, WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS, } from "../contracts/case-portability.mjs";
|
|
7
7
|
import { assertWorkspaceInputGraph } from "./hostile-input.mjs";
|
|
8
|
+
import { AES_KEY_BITS, arrayBuffer, assertPassphrase, cryptoProvider, deriveArchiveKey } from "../evidence/passphrase-encryption.mjs";
|
|
8
9
|
export { ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA, ENCRYPTED_WORKSPACE_ARCHIVE_VERSION, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES, MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS, WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS, };
|
|
9
10
|
const SALT_BYTES = 16;
|
|
10
11
|
const IV_BYTES = 12;
|
|
11
|
-
const AES_KEY_BITS = 256;
|
|
12
12
|
const AES_GCM_TAG_BITS = 128;
|
|
13
13
|
const MAX_CIPHERTEXT_BASE64URL_CHARACTERS = Math.ceil((MAX_WORKSPACE_ARCHIVE_BYTES + AES_GCM_TAG_BITS / 8) * 4 / 3);
|
|
14
14
|
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
@@ -25,25 +25,6 @@ function hasExactKeys(value, expected) {
|
|
|
25
25
|
const keys = Object.keys(value);
|
|
26
26
|
return keys.length === expected.length && keys.every((key) => expected.includes(key));
|
|
27
27
|
}
|
|
28
|
-
function arrayBuffer(bytes) {
|
|
29
|
-
return bytes.slice().buffer;
|
|
30
|
-
}
|
|
31
|
-
function assertPassphrase(passphrase) {
|
|
32
|
-
if (typeof passphrase !== 'string') {
|
|
33
|
-
throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
|
|
34
|
-
}
|
|
35
|
-
if (passphrase.length > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
|
|
36
|
-
throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
|
|
37
|
-
}
|
|
38
|
-
if (Array.from(passphrase).length < MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS) {
|
|
39
|
-
throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
|
|
40
|
-
}
|
|
41
|
-
const bytes = encoder.encode(passphrase);
|
|
42
|
-
if (bytes.byteLength > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
|
|
43
|
-
throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
|
|
44
|
-
}
|
|
45
|
-
return bytes;
|
|
46
|
-
}
|
|
47
28
|
function base64url(bytes) {
|
|
48
29
|
let encoded = '';
|
|
49
30
|
for (let index = 0; index < bytes.length; index += 3) {
|
|
@@ -112,7 +93,7 @@ function validTimestamp(value) {
|
|
|
112
93
|
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
113
94
|
}
|
|
114
95
|
function validateEnvelope(raw) {
|
|
115
|
-
assertWorkspaceInputGraph(raw, 'Encrypted workspace archive');
|
|
96
|
+
assertWorkspaceInputGraph(raw, 'Encrypted workspace archive', { maximumBytes: MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES });
|
|
116
97
|
const value = record(raw);
|
|
117
98
|
if (!value || value.schema !== ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA) {
|
|
118
99
|
throw new Error('This file is not an encrypted WHOISleuth workspace archive.');
|
|
@@ -197,26 +178,6 @@ function validateEnvelope(raw) {
|
|
|
197
178
|
ciphertext,
|
|
198
179
|
};
|
|
199
180
|
}
|
|
200
|
-
function cryptoProvider(provider = globalThis.crypto) {
|
|
201
|
-
if (!provider
|
|
202
|
-
|| typeof provider.getRandomValues !== 'function'
|
|
203
|
-
|| typeof provider.subtle?.importKey !== 'function'
|
|
204
|
-
|| typeof provider.subtle?.deriveKey !== 'function'
|
|
205
|
-
|| typeof provider.subtle?.encrypt !== 'function'
|
|
206
|
-
|| typeof provider.subtle?.decrypt !== 'function') {
|
|
207
|
-
throw new Error('Encrypted workspace archives are unavailable in this browser.');
|
|
208
|
-
}
|
|
209
|
-
return provider;
|
|
210
|
-
}
|
|
211
|
-
async function deriveArchiveKey(provider, passphraseBytes, salt, usages) {
|
|
212
|
-
const material = await provider.subtle.importKey('raw', arrayBuffer(passphraseBytes), 'PBKDF2', false, ['deriveKey']);
|
|
213
|
-
return provider.subtle.deriveKey({
|
|
214
|
-
name: 'PBKDF2',
|
|
215
|
-
hash: 'SHA-256',
|
|
216
|
-
salt: arrayBuffer(salt),
|
|
217
|
-
iterations: WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS,
|
|
218
|
-
}, material, { name: 'AES-GCM', length: AES_KEY_BITS }, false, usages);
|
|
219
|
-
}
|
|
220
181
|
export function isEncryptedWorkspaceArchive(raw) {
|
|
221
182
|
return record(raw)?.schema === ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA;
|
|
222
183
|
}
|
|
@@ -227,15 +188,19 @@ export function inspectEncryptedWorkspaceArchive(raw) {
|
|
|
227
188
|
}
|
|
228
189
|
/** Encrypt one already-built workspace archive using browser-native Web Crypto. */
|
|
229
190
|
export async function encryptWorkspaceArchive(archive, passphrase, provider) {
|
|
230
|
-
|
|
231
|
-
const archiveVersion = record(archive)?.version;
|
|
232
|
-
if (!isSupportedWorkspaceArchiveVersion(archiveVersion)) {
|
|
233
|
-
throw new Error('The workspace archive declares an unsupported content contract.');
|
|
234
|
-
}
|
|
191
|
+
assertWorkspaceInputGraph(archive, 'Workspace archive', { maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES });
|
|
235
192
|
const plaintext = JSON.stringify(archive);
|
|
236
193
|
const plaintextBytes = encoder.encode(plaintext);
|
|
237
194
|
if (plaintextBytes.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
|
|
238
|
-
throw new Error(
|
|
195
|
+
throw new Error(`Workspace archives are limited to ${MAX_WORKSPACE_ARCHIVE_BYTES / 1024 / 1024} MiB. Export smaller collections separately before trying again.`);
|
|
196
|
+
}
|
|
197
|
+
// Validation and encryption consume the same snapshot, including when the
|
|
198
|
+
// caller changes the original object while checksum verification is pending.
|
|
199
|
+
const snapshot = JSON.parse(plaintext);
|
|
200
|
+
await readWorkspaceArchive(snapshot);
|
|
201
|
+
const archiveVersion = record(snapshot)?.version;
|
|
202
|
+
if (!isSupportedWorkspaceArchiveVersion(archiveVersion)) {
|
|
203
|
+
throw new Error('The workspace archive declares an unsupported content contract.');
|
|
239
204
|
}
|
|
240
205
|
const crypto = cryptoProvider(provider);
|
|
241
206
|
const passphraseBytes = assertPassphrase(passphrase);
|
|
@@ -278,30 +243,45 @@ export async function encryptWorkspaceArchive(archive, passphrase, provider) {
|
|
|
278
243
|
}
|
|
279
244
|
/** Decrypt and parse one encrypted archive. The ordinary archive reader remains authoritative. */
|
|
280
245
|
export async function decryptWorkspaceArchive(raw, passphrase, provider) {
|
|
246
|
+
return (await decryptWorkspaceArchiveWithMetadata(raw, passphrase, provider)).archive;
|
|
247
|
+
}
|
|
248
|
+
/** Authenticate one envelope and return its inner JSON with verified byte metadata. */
|
|
249
|
+
export async function decryptWorkspaceArchiveWithMetadata(raw, passphrase, provider) {
|
|
281
250
|
const { envelope, salt, iv, ciphertext } = validateEnvelope(raw);
|
|
282
251
|
const crypto = cryptoProvider(provider);
|
|
283
252
|
const passphraseBytes = assertPassphrase(passphrase);
|
|
284
253
|
const { ciphertext: _ciphertext, ...metadata } = envelope;
|
|
285
254
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
255
|
+
let plaintext;
|
|
256
|
+
try {
|
|
257
|
+
const key = await deriveArchiveKey(crypto, passphraseBytes, salt, ['decrypt']);
|
|
258
|
+
plaintext = await crypto.subtle.decrypt({
|
|
259
|
+
name: 'AES-GCM',
|
|
260
|
+
iv: arrayBuffer(iv),
|
|
261
|
+
additionalData: arrayBuffer(encoder.encode(canonicalMetadata(metadata))),
|
|
262
|
+
tagLength: AES_GCM_TAG_BITS,
|
|
263
|
+
}, key, arrayBuffer(ciphertext));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Authentication failure cannot distinguish a wrong key from tampering.
|
|
267
|
+
throw new Error('The backup passphrase is incorrect or the encrypted file is corrupted.');
|
|
268
|
+
}
|
|
293
269
|
if (plaintext.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
|
|
294
270
|
throw new Error('The decrypted workspace archive exceeds its byte limit.');
|
|
295
271
|
}
|
|
296
|
-
|
|
272
|
+
let json;
|
|
273
|
+
try {
|
|
274
|
+
json = decoder.decode(plaintext);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
throw new Error('The backup decrypted, but its contents are not valid UTF-8.');
|
|
278
|
+
}
|
|
279
|
+
const archive = parseBoundedJson(json, {
|
|
297
280
|
label: 'Decrypted workspace archive',
|
|
298
281
|
maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES,
|
|
282
|
+
limits: boundedJsonLimitsForBytes(MAX_WORKSPACE_ARCHIVE_BYTES),
|
|
299
283
|
});
|
|
300
|
-
|
|
301
|
-
catch (cause) {
|
|
302
|
-
if (cause instanceof Error && cause.message === 'The decrypted workspace archive exceeds its byte limit.')
|
|
303
|
-
throw cause;
|
|
304
|
-
throw new Error('The backup passphrase is incorrect or the encrypted file is corrupted.');
|
|
284
|
+
return { archive, ciphertextBytes: ciphertext.byteLength };
|
|
305
285
|
}
|
|
306
286
|
finally {
|
|
307
287
|
passphraseBytes.fill(0);
|
|
@@ -18,6 +18,8 @@ import { buildAnalystReviewStateExport, emptyAnalystReviewStateStore, mergeAnaly
|
|
|
18
18
|
import { isSupportedWorkspaceArchiveVersion, MAX_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_SECTION_BYTES, MAX_WORKSPACE_ARCHIVE_SECTIONS, serialiseWorkspaceArchiveSection, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_CASE_SECTION, WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_VERSION, WORKSPACE_ARCHIVE_VERSION, WORKSPACE_SETTINGS_COMPATIBILITY, WORKSPACE_SETTINGS_SCHEMA, WORKSPACE_SETTINGS_VERSION, } from "../contracts/case-portability.mjs";
|
|
19
19
|
import { ANALYST_REVIEW_STATE_COMPATIBILITY } from "../contracts/analyst-review-state.mjs";
|
|
20
20
|
import { WORKSPACE_PORTABILITY_ARCHIVE_SECTION_REFERENCES } from "../contracts/workspace-portability.mjs";
|
|
21
|
+
import { CASE_VIEWS_COMPATIBILITY } from "../contracts/case-views.mjs";
|
|
22
|
+
import { emptyCaseViewsStore, mergeCaseViews, normalizeCaseViewsStore } from "./case-views.mjs";
|
|
21
23
|
export { isSupportedWorkspaceArchiveVersion, MAX_WORKSPACE_ARCHIVE_BYTES, MAX_WORKSPACE_ARCHIVE_SECTION_BYTES, MAX_WORKSPACE_ARCHIVE_SECTIONS, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_CASE_SECTION, WORKSPACE_ARCHIVE_SCHEMA, WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_SECTION_IDS, PUBLIC_WORKSPACE_ARCHIVE_VERSION, WORKSPACE_ARCHIVE_VERSION, WORKSPACE_SETTINGS_SCHEMA, WORKSPACE_SETTINGS_VERSION, };
|
|
22
24
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
23
25
|
const CHECKSUM_RE = /^sha256:[a-f0-9]{64}$/;
|
|
@@ -111,6 +113,7 @@ function workspaceArchiveSections(input, now) {
|
|
|
111
113
|
investigationTemplates: buildInvestigationTemplateExport(input.investigationTemplates, now),
|
|
112
114
|
bulkReview: buildBulkReviewExport(input.bulkReview),
|
|
113
115
|
analystReviewState: buildAnalystReviewStateExport(input.analystReviewState),
|
|
116
|
+
caseViews: normalizeCaseViewsStore(input.caseViews),
|
|
114
117
|
settings: settingsDocument(input),
|
|
115
118
|
};
|
|
116
119
|
}
|
|
@@ -150,7 +153,7 @@ const SECTION_DEFINITIONS = [
|
|
|
150
153
|
merge: (local, data) => {
|
|
151
154
|
const result = mergeCases(local.cases, data);
|
|
152
155
|
const bounded = enforceStoreBudget(result.cases);
|
|
153
|
-
return { ...result,
|
|
156
|
+
return { ...result, document: bounded.cases, pruned: bounded.pruned };
|
|
154
157
|
},
|
|
155
158
|
},
|
|
156
159
|
{
|
|
@@ -158,7 +161,7 @@ const SECTION_DEFINITIONS = [
|
|
|
158
161
|
count: (data) => arrayCount(data, 'campaigns'),
|
|
159
162
|
merge: (local, data) => {
|
|
160
163
|
const result = mergeCampaigns(local.campaigns, data);
|
|
161
|
-
return { ...result,
|
|
164
|
+
return { ...result, document: assertCampaignStoreBudget(result.campaigns).campaigns };
|
|
162
165
|
},
|
|
163
166
|
},
|
|
164
167
|
{
|
|
@@ -166,7 +169,8 @@ const SECTION_DEFINITIONS = [
|
|
|
166
169
|
count: (data) => arrayCount(data, 'profiles'),
|
|
167
170
|
merge: (local, data, now) => {
|
|
168
171
|
const result = mergeBrandProfiles(local.brandProfiles, data, { nowIso: now });
|
|
169
|
-
|
|
172
|
+
const profiles = assertBrandProfileStoreBudget(result.profiles).profiles;
|
|
173
|
+
return { ...result, profiles, document: profiles };
|
|
170
174
|
},
|
|
171
175
|
},
|
|
172
176
|
{
|
|
@@ -174,7 +178,7 @@ const SECTION_DEFINITIONS = [
|
|
|
174
178
|
count: (data) => objectCount(data, 'watchlists'),
|
|
175
179
|
merge: (local, data) => {
|
|
176
180
|
const result = mergeWatchlistStores(local.watchlists, data);
|
|
177
|
-
return { ...result,
|
|
181
|
+
return { ...result, document: assertWatchlistStoreBudget(result.watchlists).watchlists };
|
|
178
182
|
},
|
|
179
183
|
},
|
|
180
184
|
{
|
|
@@ -182,7 +186,7 @@ const SECTION_DEFINITIONS = [
|
|
|
182
186
|
count: (data) => arrayCount(data, 'entries'),
|
|
183
187
|
merge: (local, data) => {
|
|
184
188
|
const result = mergeShortlistStores(local.shortlist, data);
|
|
185
|
-
return { ...result,
|
|
189
|
+
return { ...result, document: assertShortlistStoreBudget(result.entries).entries };
|
|
186
190
|
},
|
|
187
191
|
},
|
|
188
192
|
{
|
|
@@ -190,14 +194,17 @@ const SECTION_DEFINITIONS = [
|
|
|
190
194
|
count: (data) => arrayCount(data, 'rules'),
|
|
191
195
|
merge: (local, data) => {
|
|
192
196
|
const result = mergeDetectionRules(local.detectionRules, data);
|
|
193
|
-
return { ...result,
|
|
197
|
+
return { ...result, document: assertDetectionRuleStoreBudget(result.rules).rules };
|
|
194
198
|
},
|
|
195
199
|
},
|
|
196
200
|
{
|
|
197
201
|
...sectionContract('relationshipObservations'),
|
|
198
202
|
label: 'Retained relationship observations',
|
|
199
203
|
count: (data) => arrayCount(data, 'observations'),
|
|
200
|
-
merge: (local, data) =>
|
|
204
|
+
merge: (local, data) => {
|
|
205
|
+
const result = mergeRelationshipObservations(local.relationshipObservations, data);
|
|
206
|
+
return { ...result, document: result.observations };
|
|
207
|
+
},
|
|
201
208
|
},
|
|
202
209
|
{
|
|
203
210
|
...sectionContract('bulkSessions'),
|
|
@@ -205,27 +212,36 @@ const SECTION_DEFINITIONS = [
|
|
|
205
212
|
count: (data) => arrayCount(data, 'sessions'),
|
|
206
213
|
merge: (local, data) => {
|
|
207
214
|
const result = mergeBulkSessions(local.bulkSessions, data);
|
|
208
|
-
enforceBulkSessionStoreBudget(result.sessions);
|
|
209
|
-
return result;
|
|
215
|
+
const bounded = enforceBulkSessionStoreBudget(result.sessions);
|
|
216
|
+
return { ...result, document: bounded.store.sessions };
|
|
210
217
|
},
|
|
211
218
|
},
|
|
212
219
|
{
|
|
213
220
|
...sectionContract('websiteSnapshots'),
|
|
214
221
|
label: 'Website profile snapshots',
|
|
215
222
|
count: (data) => arrayCount(data, 'snapshots'),
|
|
216
|
-
merge: (local, data) =>
|
|
223
|
+
merge: (local, data) => {
|
|
224
|
+
const result = mergeWebsiteSnapshots(local.websiteSnapshots, data);
|
|
225
|
+
return { ...result, document: result.snapshots };
|
|
226
|
+
},
|
|
217
227
|
},
|
|
218
228
|
{
|
|
219
229
|
...sectionContract('investigationTemplates'),
|
|
220
230
|
label: 'Investigation templates',
|
|
221
231
|
count: (data) => arrayCount(data, 'templates'),
|
|
222
|
-
merge: (local, data) =>
|
|
232
|
+
merge: (local, data) => {
|
|
233
|
+
const result = mergeInvestigationTemplates(local.investigationTemplates, data);
|
|
234
|
+
return { ...result, document: result.templates };
|
|
235
|
+
},
|
|
223
236
|
},
|
|
224
237
|
{
|
|
225
238
|
...sectionContract('bulkReview'),
|
|
226
239
|
label: 'Bulk saved views and review queue',
|
|
227
240
|
count: (data) => arrayCount(data, 'presets') + arrayCount(data, 'rows'),
|
|
228
|
-
merge: (local, data) =>
|
|
241
|
+
merge: (local, data) => {
|
|
242
|
+
const result = mergeBulkReviewStores(local.bulkReview, data);
|
|
243
|
+
return { ...result, document: result.store };
|
|
244
|
+
},
|
|
229
245
|
},
|
|
230
246
|
{
|
|
231
247
|
id: 'analystReviewState',
|
|
@@ -234,7 +250,19 @@ const SECTION_DEFINITIONS = [
|
|
|
234
250
|
version: ANALYST_REVIEW_STATE_COMPATIBILITY.currentVersion,
|
|
235
251
|
supportedVersions: ANALYST_REVIEW_STATE_COMPATIBILITY.supportedVersions,
|
|
236
252
|
count: (data) => arrayCount(data, 'records'),
|
|
237
|
-
merge: (local, data) =>
|
|
253
|
+
merge: (local, data) => {
|
|
254
|
+
const result = mergeAnalystReviewStateStores(local.analystReviewState, data);
|
|
255
|
+
return { ...result, document: result.store };
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: 'caseViews', label: 'Saved Case views', schema: compatibilitySchema(CASE_VIEWS_COMPATIBILITY),
|
|
260
|
+
version: CASE_VIEWS_COMPATIBILITY.currentVersion, supportedVersions: CASE_VIEWS_COMPATIBILITY.supportedVersions,
|
|
261
|
+
count: (data) => arrayCount(data, 'views'),
|
|
262
|
+
merge: (local, data) => {
|
|
263
|
+
const result = mergeCaseViews(local.caseViews, data);
|
|
264
|
+
return { ...result, document: result.store };
|
|
265
|
+
},
|
|
238
266
|
},
|
|
239
267
|
{
|
|
240
268
|
id: 'settings', label: 'Workspace settings', schema: compatibilitySchema(WORKSPACE_SETTINGS_COMPATIBILITY),
|
|
@@ -245,6 +273,19 @@ const SECTION_DEFINITIONS = [
|
|
|
245
273
|
},
|
|
246
274
|
];
|
|
247
275
|
const DEFINITION_BY_ID = new Map(SECTION_DEFINITIONS.map((definition) => [definition.id, definition]));
|
|
276
|
+
/** Apply previously verified data sections through the same owners as preview. */
|
|
277
|
+
export function mergeReadyWorkspaceArchiveData(localInput, sections, generatedAt) {
|
|
278
|
+
if (sections.length > MAX_WORKSPACE_ARCHIVE_SECTIONS || new Set(sections.map(section => section.id)).size !== sections.length) {
|
|
279
|
+
throw new Error('Workspace data sections exceed their count or contain duplicate identities.');
|
|
280
|
+
}
|
|
281
|
+
const local = normalizedInput(localInput);
|
|
282
|
+
return sections.map(section => {
|
|
283
|
+
const definition = DEFINITION_BY_ID.get(section.id);
|
|
284
|
+
if (section.status !== 'ready' || !definition?.merge)
|
|
285
|
+
throw new Error('Only verified, supported workspace data sections can be merged.');
|
|
286
|
+
return { ...definition.merge(local, section.data, generatedAt), id: section.id };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
248
289
|
const SECTION_ORDER = new Map(WORKSPACE_ARCHIVE_SECTION_IDS.map((id, index) => [id, index]));
|
|
249
290
|
function canonicalSectionOrder(left, right) {
|
|
250
291
|
const leftIndex = SECTION_ORDER.get(left.id) ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -266,6 +307,7 @@ function normalizedInput(input) {
|
|
|
266
307
|
investigationTemplates: Array.isArray(value.investigationTemplates) ? value.investigationTemplates : [],
|
|
267
308
|
bulkReview: record(value.bulkReview) || {},
|
|
268
309
|
analystReviewState: record(value.analystReviewState) || emptyAnalystReviewStateStore(),
|
|
310
|
+
caseViews: value.caseViews ?? emptyCaseViewsStore(),
|
|
269
311
|
settings: record(value.settings) || {},
|
|
270
312
|
};
|
|
271
313
|
}
|
|
@@ -273,7 +315,7 @@ function ensureArchiveBudget(value) {
|
|
|
273
315
|
const serialized = serialize(value);
|
|
274
316
|
const bytes = byteLength(serialized);
|
|
275
317
|
if (bytes > MAX_WORKSPACE_ARCHIVE_BYTES) {
|
|
276
|
-
throw new Error(
|
|
318
|
+
throw new Error(`Workspace archives are limited to ${MAX_WORKSPACE_ARCHIVE_BYTES / 1024 / 1024} MiB. Export smaller collections separately before trying again.`);
|
|
277
319
|
}
|
|
278
320
|
return { serialized, bytes };
|
|
279
321
|
}
|
|
@@ -290,7 +332,7 @@ export async function buildWorkspaceArchive(input, options = {}) {
|
|
|
290
332
|
const data = sections[definition.id];
|
|
291
333
|
const sectionBytes = byteLength(serialize(data));
|
|
292
334
|
if (sectionBytes > MAX_WORKSPACE_ARCHIVE_SECTION_BYTES) {
|
|
293
|
-
throw new Error(`${definition.label} exceeds the
|
|
335
|
+
throw new Error(`${definition.label} exceeds the ${MAX_WORKSPACE_ARCHIVE_SECTION_BYTES / 1024 / 1024} MiB workspace archive section limit.`);
|
|
294
336
|
}
|
|
295
337
|
const recordCount = definition.count(data);
|
|
296
338
|
manifestSections.push({
|
|
@@ -338,8 +380,11 @@ function manifestEntry(raw) {
|
|
|
338
380
|
}
|
|
339
381
|
/** Validate structure, section byte counts, and checksums without applying data. */
|
|
340
382
|
export async function readWorkspaceArchive(raw, options = {}) {
|
|
341
|
-
assertWorkspaceInputGraph(raw, 'Workspace archive');
|
|
342
|
-
|
|
383
|
+
assertWorkspaceInputGraph(raw, 'Workspace archive', { maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES });
|
|
384
|
+
// Snapshot before the first asynchronous digest: caller-owned data must not
|
|
385
|
+
// change between its checksum, contract checks and eventual import preview.
|
|
386
|
+
const { serialized, bytes } = ensureArchiveBudget(raw);
|
|
387
|
+
const value = record(JSON.parse(serialized));
|
|
343
388
|
if (!value || value.schema !== WORKSPACE_ARCHIVE_SCHEMA) {
|
|
344
389
|
throw new Error('This file is not a WHOISleuth workspace archive.');
|
|
345
390
|
}
|
|
@@ -355,11 +400,10 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
355
400
|
const sourceVersion = value.version;
|
|
356
401
|
const expectedSectionIds = sourceVersion === PUBLIC_WORKSPACE_ARCHIVE_VERSION
|
|
357
402
|
? PUBLIC_WORKSPACE_ARCHIVE_SECTION_IDS
|
|
358
|
-
: WORKSPACE_ARCHIVE_SECTION_IDS;
|
|
403
|
+
: sourceVersion < 9 ? WORKSPACE_ARCHIVE_SECTION_IDS.filter(id => id !== 'caseViews') : WORKSPACE_ARCHIVE_SECTION_IDS;
|
|
359
404
|
const generatedAt = timestamp(value.generatedAt);
|
|
360
405
|
if (!generatedAt)
|
|
361
406
|
throw new Error('The workspace archive generation time is invalid.');
|
|
362
|
-
const { bytes } = ensureArchiveBudget(value);
|
|
363
407
|
assertExactKeys(value, WORKSPACE_ARCHIVE_ROOT_KEYS, `version ${sourceVersion} envelope`);
|
|
364
408
|
const manifest = record(value.manifest);
|
|
365
409
|
const sectionValues = record(value.sections);
|
|
@@ -423,7 +467,7 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
423
467
|
else if (definition.count(data) !== entry.recordCount) {
|
|
424
468
|
throw new Error(`${entry.id} does not match its manifest record count.`);
|
|
425
469
|
}
|
|
426
|
-
sections.push({ ...entry, label: definition?.label || entry.id, status, reason, data
|
|
470
|
+
sections.push({ ...entry, label: definition?.label || entry.id, status, reason, data });
|
|
427
471
|
totalRecords += entry.recordCount;
|
|
428
472
|
}
|
|
429
473
|
if (Object.keys(sectionValues).some((id) => !seen.has(id))) {
|
|
@@ -450,6 +494,15 @@ export async function readWorkspaceArchive(raw, options = {}) {
|
|
|
450
494
|
data,
|
|
451
495
|
});
|
|
452
496
|
}
|
|
497
|
+
if (sourceVersion < 9) {
|
|
498
|
+
const data = emptyCaseViewsStore();
|
|
499
|
+
sections.push({
|
|
500
|
+
id: 'caseViews', label: 'Saved Case views', schema: compatibilitySchema(CASE_VIEWS_COMPATIBILITY),
|
|
501
|
+
version: CASE_VIEWS_COMPATIBILITY.currentVersion, recordCount: 0,
|
|
502
|
+
bytes: byteLength(serialize(data)), checksum: await checksum(data, options.cryptoProvider), status: 'ready',
|
|
503
|
+
reason: 'This archive predates saved Case views. Import supplies an empty section and does not remove existing views.', data,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
453
506
|
sections.sort(canonicalSectionOrder);
|
|
454
507
|
return {
|
|
455
508
|
schema: WORKSPACE_ARCHIVE_SCHEMA,
|
|
@@ -491,9 +544,20 @@ function settingsPreview(data, local, mergedProfiles) {
|
|
|
491
544
|
|| (skipped === 0 && activeProfileId !== retainedActiveProfileId));
|
|
492
545
|
return { added: 0, updated, skipped, reason, settings: { activeProfileId, theme } };
|
|
493
546
|
}
|
|
547
|
+
/** Retain one verified snapshot; each read or preview returns an independent copy. */
|
|
548
|
+
export async function prepareWorkspaceArchive(raw, options = {}) {
|
|
549
|
+
const archive = await readWorkspaceArchive(raw, options);
|
|
550
|
+
return Object.freeze({
|
|
551
|
+
read: () => clone(archive),
|
|
552
|
+
preview: (localInput, selection = {}) => previewVerifiedWorkspaceArchive(clone(archive), localInput, selection),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
494
555
|
/** Preview section-specific non-destructive merge outcomes without writing. */
|
|
495
556
|
export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
496
|
-
|
|
557
|
+
// A one-shot preview owns the reader result directly; retained readers copy it.
|
|
558
|
+
return previewVerifiedWorkspaceArchive(await readWorkspaceArchive(raw, options), localInput, options);
|
|
559
|
+
}
|
|
560
|
+
function previewVerifiedWorkspaceArchive(archive, localInput, options) {
|
|
497
561
|
const local = normalizedInput(localInput);
|
|
498
562
|
const results = [];
|
|
499
563
|
let mergedProfiles = local.brandProfiles;
|
|
@@ -528,6 +592,7 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
528
592
|
skipped: result.skipped ?? 0,
|
|
529
593
|
pruned: result.pruned ?? 0,
|
|
530
594
|
brandProfileReferencesOmitted: result.brandProfileReferencesOmitted ?? 0,
|
|
595
|
+
authoredHistoryOmitted: result.authoredHistoryOmitted ?? 0,
|
|
531
596
|
selected,
|
|
532
597
|
normalizedSettings: result.settings || null,
|
|
533
598
|
});
|
|
@@ -567,6 +632,7 @@ export async function previewWorkspaceArchive(raw, localInput, options = {}) {
|
|
|
567
632
|
skipped: dependentSection.recordCount,
|
|
568
633
|
pruned: 0,
|
|
569
634
|
brandProfileReferencesOmitted: 0,
|
|
635
|
+
authoredHistoryOmitted: 0,
|
|
570
636
|
selected: false,
|
|
571
637
|
normalizedSettings: dependentId === 'settings' ? null : dependentSection.normalizedSettings ?? null,
|
|
572
638
|
};
|
package/third-party-notices.txt
CHANGED
|
@@ -2,9 +2,9 @@ WHOISleuth CLI third-party production dependency notices
|
|
|
2
2
|
|
|
3
3
|
Generated deterministically from package-lock.json and the installed production
|
|
4
4
|
dependency packages. Do not edit this file directly; run npm run licenses:update.
|
|
5
|
-
The inventory is the exact transitive closure of the packaged
|
|
5
|
+
The inventory is the exact transitive closure of the packaged runtime dependencies.
|
|
6
6
|
|
|
7
|
-
Package count:
|
|
7
|
+
Package count: 28
|
|
8
8
|
|
|
9
9
|
================================================================================
|
|
10
10
|
@peculiar/asn1-cms@2.9.4
|
|
@@ -384,6 +384,33 @@ Redistributions in binary form must reproduce the above copyright notice, this l
|
|
|
384
384
|
THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
|
|
385
385
|
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
386
386
|
================================================================================
|
|
387
|
+
fflate@0.8.3
|
|
388
|
+
Relationship: direct production dependency
|
|
389
|
+
Declared licence: MIT
|
|
390
|
+
Licence source: LICENSE
|
|
391
|
+
|
|
392
|
+
MIT License
|
|
393
|
+
|
|
394
|
+
Copyright (c) 2026 Arjun Barrett
|
|
395
|
+
|
|
396
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
397
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
398
|
+
in the Software without restriction, including without limitation the rights
|
|
399
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
400
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
401
|
+
furnished to do so, subject to the following conditions:
|
|
402
|
+
|
|
403
|
+
The above copyright notice and this permission notice shall be included in all
|
|
404
|
+
copies or substantial portions of the Software.
|
|
405
|
+
|
|
406
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
407
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
408
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
409
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
410
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
411
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
412
|
+
SOFTWARE.
|
|
413
|
+
================================================================================
|
|
387
414
|
maxmind@5.0.7
|
|
388
415
|
Relationship: direct production dependency
|
|
389
416
|
Declared licence: MIT
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { CliUsageError } from "./errors.mjs";
|
|
2
|
-
export function parseInspectArchiveArguments(argv) {
|
|
3
|
-
let source = null;
|
|
4
|
-
let passphraseSource = null;
|
|
5
|
-
let search = null;
|
|
6
|
-
let reveal = false;
|
|
7
|
-
let requireMatch = false;
|
|
8
|
-
let expectedContentDigest = null;
|
|
9
|
-
let output = 'terminal';
|
|
10
|
-
let quiet = false;
|
|
11
|
-
let color = true;
|
|
12
|
-
for (let index = 0; index < argv.length; index++) {
|
|
13
|
-
const argument = argv[index];
|
|
14
|
-
if (argument === undefined)
|
|
15
|
-
break;
|
|
16
|
-
if (argument === '--json') {
|
|
17
|
-
if (output !== 'terminal')
|
|
18
|
-
throw new CliUsageError('--json may be supplied only once.');
|
|
19
|
-
output = 'json';
|
|
20
|
-
}
|
|
21
|
-
else if (argument === '--passphrase-file') {
|
|
22
|
-
if (passphraseSource !== null)
|
|
23
|
-
throw new CliUsageError('--passphrase-file may be supplied only once.');
|
|
24
|
-
const value = argv[++index];
|
|
25
|
-
if (!value || value.startsWith('-'))
|
|
26
|
-
throw new CliUsageError('--passphrase-file requires one bounded UTF-8 file.');
|
|
27
|
-
passphraseSource = value;
|
|
28
|
-
}
|
|
29
|
-
else if (argument === '--search') {
|
|
30
|
-
if (search !== null)
|
|
31
|
-
throw new CliUsageError('--search may be supplied only once.');
|
|
32
|
-
const value = argv[++index];
|
|
33
|
-
if (!value || value.startsWith('-'))
|
|
34
|
-
throw new CliUsageError('--search requires one exact bounded value.');
|
|
35
|
-
search = value;
|
|
36
|
-
}
|
|
37
|
-
else if (argument === '--reveal') {
|
|
38
|
-
if (reveal)
|
|
39
|
-
throw new CliUsageError('--reveal may be supplied only once.');
|
|
40
|
-
reveal = true;
|
|
41
|
-
}
|
|
42
|
-
else if (argument === '--require-match') {
|
|
43
|
-
if (requireMatch)
|
|
44
|
-
throw new CliUsageError('--require-match may be supplied only once.');
|
|
45
|
-
requireMatch = true;
|
|
46
|
-
}
|
|
47
|
-
else if (argument === '--expect-content-digest') {
|
|
48
|
-
if (expectedContentDigest !== null)
|
|
49
|
-
throw new CliUsageError('--expect-content-digest may be supplied only once.');
|
|
50
|
-
const value = argv[++index];
|
|
51
|
-
if (!value || !/^sha256:[a-f0-9]{64}$/u.test(value)) {
|
|
52
|
-
throw new CliUsageError('--expect-content-digest requires sha256 followed by 64 lowercase hexadecimal characters.');
|
|
53
|
-
}
|
|
54
|
-
expectedContentDigest = value;
|
|
55
|
-
}
|
|
56
|
-
else if (argument === '--quiet')
|
|
57
|
-
quiet = true;
|
|
58
|
-
else if (argument === '--no-color')
|
|
59
|
-
color = false;
|
|
60
|
-
else if (argument.startsWith('-'))
|
|
61
|
-
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
62
|
-
else if (source === null)
|
|
63
|
-
source = argument;
|
|
64
|
-
else
|
|
65
|
-
throw new CliUsageError('inspect-archive accepts one optional JSON file. Otherwise pipe one archive on stdin.');
|
|
66
|
-
}
|
|
67
|
-
if (reveal && search === null)
|
|
68
|
-
throw new CliUsageError('--reveal requires --search.');
|
|
69
|
-
if (requireMatch && search === null)
|
|
70
|
-
throw new CliUsageError('--require-match requires --search.');
|
|
71
|
-
if (quiet && output !== 'terminal')
|
|
72
|
-
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
73
|
-
return {
|
|
74
|
-
action: 'inspect-archive',
|
|
75
|
-
source,
|
|
76
|
-
passphraseSource,
|
|
77
|
-
search,
|
|
78
|
-
reveal,
|
|
79
|
-
requireMatch,
|
|
80
|
-
expectedContentDigest,
|
|
81
|
-
output,
|
|
82
|
-
quiet,
|
|
83
|
-
color,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
export function parseSignArtifactArguments(argv) {
|
|
87
|
-
let source = null;
|
|
88
|
-
let privateKeySource = null;
|
|
89
|
-
for (let index = 0; index < argv.length; index++) {
|
|
90
|
-
const argument = argv[index];
|
|
91
|
-
if (argument === undefined)
|
|
92
|
-
break;
|
|
93
|
-
if (argument === '--private-key-file') {
|
|
94
|
-
if (privateKeySource !== null)
|
|
95
|
-
throw new CliUsageError('--private-key-file may be supplied only once.');
|
|
96
|
-
const value = argv[++index];
|
|
97
|
-
if (!value || value.startsWith('-'))
|
|
98
|
-
throw new CliUsageError('--private-key-file requires one bounded PEM file.');
|
|
99
|
-
privateKeySource = value;
|
|
100
|
-
}
|
|
101
|
-
else if (argument.startsWith('-'))
|
|
102
|
-
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
103
|
-
else if (source === null)
|
|
104
|
-
source = argument;
|
|
105
|
-
else
|
|
106
|
-
throw new CliUsageError('sign-artifact accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
|
|
107
|
-
}
|
|
108
|
-
if (!privateKeySource)
|
|
109
|
-
throw new CliUsageError('sign-artifact requires --private-key-file.');
|
|
110
|
-
return { action: 'sign-artifact', source, privateKeySource };
|
|
111
|
-
}
|
|
112
|
-
export function parseVerifySignatureArguments(argv) {
|
|
113
|
-
let source = null;
|
|
114
|
-
let publicKeySource = null;
|
|
115
|
-
let output = 'terminal';
|
|
116
|
-
let quiet = false;
|
|
117
|
-
let color = true;
|
|
118
|
-
for (let index = 0; index < argv.length; index++) {
|
|
119
|
-
const argument = argv[index];
|
|
120
|
-
if (argument === undefined)
|
|
121
|
-
break;
|
|
122
|
-
if (argument === '--json') {
|
|
123
|
-
if (output !== 'terminal')
|
|
124
|
-
throw new CliUsageError('--json may be supplied only once.');
|
|
125
|
-
output = 'json';
|
|
126
|
-
}
|
|
127
|
-
else if (argument === '--public-key-file') {
|
|
128
|
-
if (publicKeySource !== null)
|
|
129
|
-
throw new CliUsageError('--public-key-file may be supplied only once.');
|
|
130
|
-
const value = argv[++index];
|
|
131
|
-
if (!value || value.startsWith('-'))
|
|
132
|
-
throw new CliUsageError('--public-key-file requires one bounded PEM file.');
|
|
133
|
-
publicKeySource = value;
|
|
134
|
-
}
|
|
135
|
-
else if (argument === '--quiet')
|
|
136
|
-
quiet = true;
|
|
137
|
-
else if (argument === '--no-color')
|
|
138
|
-
color = false;
|
|
139
|
-
else if (argument.startsWith('-'))
|
|
140
|
-
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
141
|
-
else if (source === null)
|
|
142
|
-
source = argument;
|
|
143
|
-
else
|
|
144
|
-
throw new CliUsageError('verify-signature accepts one optional JSON file. Otherwise pipe one package on stdin.');
|
|
145
|
-
}
|
|
146
|
-
if (quiet && output !== 'terminal')
|
|
147
|
-
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
148
|
-
return {
|
|
149
|
-
action: 'verify-signature',
|
|
150
|
-
source,
|
|
151
|
-
publicKeySource,
|
|
152
|
-
output,
|
|
153
|
-
quiet,
|
|
154
|
-
color,
|
|
155
|
-
};
|
|
156
|
-
}
|