@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs}
RENAMED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
// Pure browser-local watchlist collection model. Watchlist history owns the
|
|
2
2
|
// evidence shape and diff semantics; this module owns collection names, schema
|
|
3
3
|
// migration, import merging, and exact serialized-byte accounting.
|
|
4
|
-
import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry } from "./watchlist-history.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
export const MAX_WATCHLIST_NAME_LENGTH = 100;
|
|
10
|
-
// Cases and other analyst stores share the same origin quota. A two-megabyte
|
|
11
|
-
// ceiling accommodates practical Bulk results while failing before the browser
|
|
12
|
-
// turns storage pressure into an opaque quota exception.
|
|
13
|
-
export const MAX_WATCHLIST_STORE_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry } from "./watchlist-history.mjs";
|
|
5
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
+
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
|
+
import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
8
|
+
export { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
14
9
|
const BLOCKED_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
|
|
15
10
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
16
11
|
function plainRecord(value) {
|
|
17
|
-
return value
|
|
18
|
-
? value
|
|
19
|
-
: null;
|
|
12
|
+
return ordinaryWorkspaceRecord(value, 'Watchlist input');
|
|
20
13
|
}
|
|
21
14
|
function isEnvelope(value) {
|
|
22
15
|
return Boolean(value && value.schema === WATCHLIST_SCHEMA && plainRecord(value.watchlists));
|
|
@@ -37,14 +30,23 @@ function watchlistMap(raw) {
|
|
|
37
30
|
}
|
|
38
31
|
export function watchlistStoreVersion(raw) {
|
|
39
32
|
const value = plainRecord(raw);
|
|
40
|
-
if (!value
|
|
41
|
-
return
|
|
33
|
+
if (!value)
|
|
34
|
+
return null;
|
|
35
|
+
if (!isEnvelope(value))
|
|
36
|
+
return WATCHLIST_SCHEMA_VERSION;
|
|
42
37
|
return typeof value.version === 'number' && Number.isFinite(value.version) && value.version > 0 ? value.version : null;
|
|
43
38
|
}
|
|
44
39
|
function defineEntry(target, name, entry) {
|
|
45
40
|
Object.defineProperty(target, name, { value: entry, writable: true, enumerable: true, configurable: true });
|
|
46
41
|
}
|
|
47
42
|
export function normalizeWatchlistStore(raw) {
|
|
43
|
+
assertWorkspaceInputGraph(raw, 'Watchlist store');
|
|
44
|
+
const root = plainRecord(raw);
|
|
45
|
+
if (root?.schema === WATCHLIST_SCHEMA)
|
|
46
|
+
assertWorkspaceDeclaredVersion(raw, 'Watchlist store');
|
|
47
|
+
if (root?.schema === WATCHLIST_SCHEMA && watchlistStoreVersion(root) !== WATCHLIST_SCHEMA_VERSION) {
|
|
48
|
+
throw new Error(`Watchlist schema ${String(root.version)} is unsupported; no data was changed.`);
|
|
49
|
+
}
|
|
48
50
|
const source = watchlistMap(raw);
|
|
49
51
|
const watchlists = {};
|
|
50
52
|
if (!source)
|
|
@@ -83,6 +85,9 @@ function validateImportShape(raw) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
export function mergeWatchlistStores(localRaw, importedRaw) {
|
|
88
|
+
assertWorkspaceInputGraph(localRaw, 'Local watchlist store');
|
|
89
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported watchlist document');
|
|
90
|
+
assertWorkspacePortableVersion(importedRaw, WATCHLIST_SCHEMA_VERSION, 'Imported watchlist document');
|
|
86
91
|
validateImportShape(importedRaw);
|
|
87
92
|
const importedVersion = watchlistStoreVersion(importedRaw);
|
|
88
93
|
if (importedVersion !== null && importedVersion > WATCHLIST_SCHEMA_VERSION) {
|
|
@@ -117,11 +122,13 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
|
|
|
117
122
|
return { watchlists: local, added, updated, skipped };
|
|
118
123
|
}
|
|
119
124
|
export function buildWatchlistExport(watchlists, nowIso = new Date().toISOString()) {
|
|
120
|
-
const
|
|
125
|
+
const exportedAt = normalizeExplicitIsoTimestamp(nowIso);
|
|
126
|
+
if (!exportedAt)
|
|
127
|
+
throw new Error('Watchlist export time must use an explicit timezone.');
|
|
121
128
|
return {
|
|
122
129
|
schema: WATCHLIST_SCHEMA,
|
|
123
130
|
version: WATCHLIST_SCHEMA_VERSION,
|
|
124
|
-
exportedAt
|
|
131
|
+
exportedAt,
|
|
125
132
|
watchlists: normalizeWatchlistStore(watchlists).watchlists,
|
|
126
133
|
};
|
|
127
134
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { normalizeDomain } from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export const MAX_WEBSITE_SNAPSHOT_STORE_BYTES = 512 * 1024;
|
|
7
|
-
export const MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES = 768 * 1024;
|
|
1
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
2
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
4
|
+
import { MAX_WEBSITE_SNAPSHOTS, MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN, MAX_WEBSITE_SNAPSHOT_STORE_BYTES, SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS, WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
5
|
+
export { MAX_WEBSITE_SNAPSHOTS, MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN, MAX_WEBSITE_SNAPSHOT_IMPORT_BYTES, MAX_WEBSITE_SNAPSHOT_STORE_BYTES, SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS, WEBSITE_SNAPSHOT_SCHEMA, WEBSITE_SNAPSHOT_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
8
6
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
9
7
|
const DIGEST_RE = /^[a-f0-9]{16,128}$/iu;
|
|
10
8
|
const SHA256_RE = /^[a-f0-9]{64}$/iu;
|
|
11
9
|
const SERIAL_RE = /^[a-f0-9]{1,128}$/iu;
|
|
12
10
|
function record(value) {
|
|
13
|
-
return value
|
|
11
|
+
return ordinaryWorkspaceRecord(value, 'Website-snapshot input');
|
|
14
12
|
}
|
|
15
13
|
function text(value, maximum) {
|
|
16
14
|
return typeof value === 'string' && value.length <= maximum && !CONTROL_RE.test(value)
|
|
@@ -18,9 +16,7 @@ function text(value, maximum) {
|
|
|
18
16
|
: '';
|
|
19
17
|
}
|
|
20
18
|
function timestamp(value) {
|
|
21
|
-
|
|
22
|
-
const parsed = Date.parse(candidate);
|
|
23
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
19
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
24
20
|
}
|
|
25
21
|
function digest(value) {
|
|
26
22
|
const candidate = text(value, 128).toLowerCase();
|
|
@@ -183,13 +179,20 @@ export function normalizeWebsiteProfileSnapshot(raw) {
|
|
|
183
179
|
};
|
|
184
180
|
}
|
|
185
181
|
export function normalizeWebsiteSnapshotStore(raw) {
|
|
182
|
+
assertWorkspaceInputGraph(raw, 'Website-snapshot store');
|
|
183
|
+
assertWorkspaceDeclaredVersion(raw, 'Website-snapshot store');
|
|
186
184
|
const value = record(raw);
|
|
187
185
|
if (value?.schema === WEBSITE_SNAPSHOT_SCHEMA
|
|
188
186
|
&& Number(value?.version) > WEBSITE_SNAPSHOT_SCHEMA_VERSION) {
|
|
189
187
|
throw new Error('This website-snapshot collection uses a newer schema and cannot be read safely.');
|
|
190
188
|
}
|
|
189
|
+
if (value?.schema === WEBSITE_SNAPSHOT_SCHEMA
|
|
190
|
+
&& Number.isSafeInteger(value.version)
|
|
191
|
+
&& !SUPPORTED_WEBSITE_SNAPSHOT_SCHEMA_VERSIONS.includes(Number(value.version))) {
|
|
192
|
+
throw new Error('This website-snapshot collection uses an unsupported schema and cannot be read safely.');
|
|
193
|
+
}
|
|
191
194
|
const sourceValues = Array.isArray(raw) ? raw : Array.isArray(value?.snapshots) ? value.snapshots : [];
|
|
192
|
-
const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, normalizeWebsiteProfileSnapshot);
|
|
195
|
+
const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, (candidate) => normalizeWebsiteProfileSnapshot(candidate));
|
|
193
196
|
const perDomain = new Map();
|
|
194
197
|
const retained = [];
|
|
195
198
|
for (const snapshot of snapshots.sort((left, right) => right.savedAt.localeCompare(left.savedAt))) {
|
|
@@ -230,6 +233,9 @@ export function buildWebsiteSnapshotExport(raw, generatedAt = new Date().toISOSt
|
|
|
230
233
|
return { ...store, generatedAt: timestamp(generatedAt) || new Date().toISOString() };
|
|
231
234
|
}
|
|
232
235
|
export function mergeWebsiteSnapshots(localRaw, incomingRaw) {
|
|
236
|
+
assertWorkspaceInputGraph(localRaw, 'Local website-snapshot store');
|
|
237
|
+
assertWorkspaceInputGraph(incomingRaw, 'Imported website-snapshot document');
|
|
238
|
+
assertWorkspacePortableVersion(incomingRaw, WEBSITE_SNAPSHOT_SCHEMA_VERSION, 'Imported website-snapshot document');
|
|
233
239
|
const local = normalizeWebsiteSnapshotStore(localRaw).snapshots;
|
|
234
240
|
const incoming = normalizeWebsiteSnapshotStore(incomingRaw).snapshots;
|
|
235
241
|
const byId = new Map(local.map((item) => [item.id, item]));
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
// Browser-local authenticated encryption for portable workspace archives.
|
|
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
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
export const MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES = 1024;
|
|
10
|
-
export const MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES = Math.ceil(MAX_WORKSPACE_ARCHIVE_BYTES * 4 / 3) + 4096;
|
|
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";
|
|
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
|
+
import { assertWorkspaceInputGraph } from "./hostile-input.mjs";
|
|
8
|
+
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, };
|
|
11
9
|
const SALT_BYTES = 16;
|
|
12
10
|
const IV_BYTES = 12;
|
|
13
11
|
const AES_KEY_BITS = 256;
|
|
14
12
|
const AES_GCM_TAG_BITS = 128;
|
|
15
13
|
const MAX_CIPHERTEXT_BASE64URL_CHARACTERS = Math.ceil((MAX_WORKSPACE_ARCHIVE_BYTES + AES_GCM_TAG_BITS / 8) * 4 / 3);
|
|
16
14
|
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
15
|
+
const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
17
16
|
const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
18
17
|
const encoder = new TextEncoder();
|
|
19
18
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
@@ -46,11 +45,20 @@ function assertPassphrase(passphrase) {
|
|
|
46
45
|
return bytes;
|
|
47
46
|
}
|
|
48
47
|
function base64url(bytes) {
|
|
49
|
-
let
|
|
50
|
-
for (let index = 0; index < bytes.length; index +=
|
|
51
|
-
|
|
48
|
+
let encoded = '';
|
|
49
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
50
|
+
const first = bytes[index] ?? 0;
|
|
51
|
+
const second = bytes[index + 1] ?? 0;
|
|
52
|
+
const third = bytes[index + 2] ?? 0;
|
|
53
|
+
const aggregate = (first << 16) | (second << 8) | third;
|
|
54
|
+
encoded += BASE64URL_ALPHABET[(aggregate >>> 18) & 63];
|
|
55
|
+
encoded += BASE64URL_ALPHABET[(aggregate >>> 12) & 63];
|
|
56
|
+
if (index + 1 < bytes.length)
|
|
57
|
+
encoded += BASE64URL_ALPHABET[(aggregate >>> 6) & 63];
|
|
58
|
+
if (index + 2 < bytes.length)
|
|
59
|
+
encoded += BASE64URL_ALPHABET[aggregate & 63];
|
|
52
60
|
}
|
|
53
|
-
return
|
|
61
|
+
return encoded;
|
|
54
62
|
}
|
|
55
63
|
function decodeBase64url(value, expectedBytes, label) {
|
|
56
64
|
const expectedCharacters = expectedBytes === null ? null : Math.ceil(expectedBytes * 4 / 3);
|
|
@@ -60,15 +68,25 @@ function decodeBase64url(value, expectedBytes, label) {
|
|
|
60
68
|
|| !BASE64URL_RE.test(value)) {
|
|
61
69
|
throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
|
|
62
70
|
}
|
|
63
|
-
|
|
64
|
-
let binary = '';
|
|
65
|
-
try {
|
|
66
|
-
binary = atob(value.replaceAll('-', '+').replaceAll('_', '/') + '='.repeat(padding));
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
71
|
+
if (value.length % 4 === 1) {
|
|
69
72
|
throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
|
|
70
73
|
}
|
|
71
|
-
const bytes = Uint8Array.
|
|
74
|
+
const bytes = new Uint8Array(Math.floor(value.length * 6 / 8));
|
|
75
|
+
let outputIndex = 0;
|
|
76
|
+
let accumulator = 0;
|
|
77
|
+
let bits = 0;
|
|
78
|
+
for (const character of value) {
|
|
79
|
+
const decoded = BASE64URL_ALPHABET.indexOf(character);
|
|
80
|
+
if (decoded < 0)
|
|
81
|
+
throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
|
|
82
|
+
accumulator = (accumulator << 6) | decoded;
|
|
83
|
+
bits += 6;
|
|
84
|
+
if (bits >= 8) {
|
|
85
|
+
bits -= 8;
|
|
86
|
+
bytes[outputIndex] = (accumulator >>> bits) & 0xff;
|
|
87
|
+
outputIndex += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
72
90
|
if (expectedBytes !== null && bytes.byteLength !== expectedBytes) {
|
|
73
91
|
throw new Error(`The encrypted workspace archive has an invalid ${label}.`);
|
|
74
92
|
}
|
|
@@ -94,6 +112,7 @@ function validTimestamp(value) {
|
|
|
94
112
|
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
95
113
|
}
|
|
96
114
|
function validateEnvelope(raw) {
|
|
115
|
+
assertWorkspaceInputGraph(raw, 'Encrypted workspace archive');
|
|
97
116
|
const value = record(raw);
|
|
98
117
|
if (!value || value.schema !== ENCRYPTED_WORKSPACE_ARCHIVE_SCHEMA) {
|
|
99
118
|
throw new Error('This file is not an encrypted WHOISleuth workspace archive.');
|
|
@@ -111,6 +130,14 @@ function validateEnvelope(raw) {
|
|
|
111
130
|
throw new Error('The encrypted workspace archive has an invalid creation time.');
|
|
112
131
|
}
|
|
113
132
|
const content = record(value.content);
|
|
133
|
+
if (content?.schema === WORKSPACE_ARCHIVE_SCHEMA && Number.isSafeInteger(content.version)) {
|
|
134
|
+
if (content.version < Math.min(...SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS)) {
|
|
135
|
+
throw new Error(`Encrypted workspace content schema ${String(content.version)} is retired. Decrypt and re-export it as schema ${WORKSPACE_ARCHIVE_VERSION} with the last broad-reader release; no data was changed.`);
|
|
136
|
+
}
|
|
137
|
+
if (content.version > WORKSPACE_ARCHIVE_VERSION) {
|
|
138
|
+
throw new Error(`Encrypted workspace content schema ${String(content.version)} is newer than the supported schema ${WORKSPACE_ARCHIVE_VERSION}; no data was changed.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
114
141
|
if (!content
|
|
115
142
|
|| !hasExactKeys(content, ['schema', 'version'])
|
|
116
143
|
|| content.schema !== WORKSPACE_ARCHIVE_SCHEMA
|
|
@@ -266,7 +293,10 @@ export async function decryptWorkspaceArchive(raw, passphrase, provider) {
|
|
|
266
293
|
if (plaintext.byteLength > MAX_WORKSPACE_ARCHIVE_BYTES) {
|
|
267
294
|
throw new Error('The decrypted workspace archive exceeds its byte limit.');
|
|
268
295
|
}
|
|
269
|
-
return
|
|
296
|
+
return parseBoundedJson(decoder.decode(plaintext), {
|
|
297
|
+
label: 'Decrypted workspace archive',
|
|
298
|
+
maximumBytes: MAX_WORKSPACE_ARCHIVE_BYTES,
|
|
299
|
+
});
|
|
270
300
|
}
|
|
271
301
|
catch (cause) {
|
|
272
302
|
if (cause instanceof Error && cause.message === 'The decrypted workspace archive exceeds its byte limit.')
|