@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- 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 +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- 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 +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -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 +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- 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-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/project-metadata.mjs +1 -1
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- 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-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- 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 +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- 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 +13 -5
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- 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 -21
- 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} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- 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/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -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/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- 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} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- 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/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- 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} +71 -48
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -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} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- 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} +13 -13
- 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} +110 -89
- package/third-party-notices.txt +14 -14
- 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 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- 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 -255
- 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 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /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-history.js → packages/workspace/watchlist-history.mjs}
RENAMED
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
// the watchlist entry for rescanning, while history stores compact changes
|
|
3
3
|
// against a last-known baseline so browser-local storage does not grow by one complete
|
|
4
4
|
// result set on every check.
|
|
5
|
-
import { explainRiskScore, normalizeRiskModelVersion } from "
|
|
6
|
-
import { HTTP_SECURITY_HEADER_TOKENS, normalizeHttpSummary } from "
|
|
7
|
-
import { normalizeDomain } from "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
export const MAX_WATCHLIST_INPUT_RECORDS = MAX_WATCHLIST_DOMAINS * 2;
|
|
12
|
-
export const MAX_WATCHLIST_NAMESERVERS = 12;
|
|
13
|
-
export const MAX_WATCHLIST_MUTATION_TYPES = 30;
|
|
14
|
-
export const MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS = MAX_WATCHLIST_DOMAINS;
|
|
5
|
+
import { explainRiskScore, normalizeRiskModelVersion } from "../../lib/risk-scoring.mjs";
|
|
6
|
+
import { HTTP_SECURITY_HEADER_TOKENS, normalizeHttpSummary } from "../cases/http-summary.mjs";
|
|
7
|
+
import { normalizeDomain } from "../cases/case-model.mjs";
|
|
8
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
9
|
+
import { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
|
|
10
|
+
export { MAX_WATCHLIST_CHANGES_PER_EVENT, MAX_WATCHLIST_DOMAINS, MAX_WATCHLIST_HISTORY_DOMAIN_OPTIONS, MAX_WATCHLIST_HISTORY_EVENTS, MAX_WATCHLIST_INPUT_RECORDS, MAX_WATCHLIST_MUTATION_TYPES, MAX_WATCHLIST_NAMESERVERS, } from "../contracts/workspace-portability.mjs";
|
|
15
11
|
const MAX_TEXT_LENGTH = 300;
|
|
16
12
|
const MAX_TITLE_LENGTH = 200;
|
|
17
13
|
const MAX_MUTATION_TYPE_LENGTH = 60;
|
|
@@ -149,10 +145,7 @@ function boundedText(value, maximum = MAX_TEXT_LENGTH, allowNull = true) {
|
|
|
149
145
|
return normalized || (allowNull ? null : '');
|
|
150
146
|
}
|
|
151
147
|
function isoTimestamp(value, fallback = new Date(0).toISOString()) {
|
|
152
|
-
|
|
153
|
-
return fallback;
|
|
154
|
-
const parsed = Date.parse(value);
|
|
155
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
|
|
148
|
+
return normalizeExplicitIsoTimestamp(value) ?? fallback;
|
|
156
149
|
}
|
|
157
150
|
function boundedInteger(value, maximum, fallback = 0) {
|
|
158
151
|
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum
|
|
@@ -487,7 +480,7 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
487
480
|
: [];
|
|
488
481
|
const rawMode = event.mode;
|
|
489
482
|
return {
|
|
490
|
-
checkedAt: isoTimestamp(event.checkedAt),
|
|
483
|
+
checkedAt: isoTimestamp(event.checkedAt, new Date(0).toISOString()),
|
|
491
484
|
mode: typeof rawMode === 'string' && ['fast', 'deep', 'saved'].includes(rawMode)
|
|
492
485
|
? rawMode
|
|
493
486
|
: 'saved',
|
|
@@ -500,7 +493,7 @@ export function normalizeWatchlistEntry(entry) {
|
|
|
500
493
|
}).slice(-MAX_WATCHLIST_HISTORY_EVENTS)
|
|
501
494
|
: [];
|
|
502
495
|
const normalized = {
|
|
503
|
-
updatedAt: isoTimestamp(input.updatedAt),
|
|
496
|
+
updatedAt: isoTimestamp(input.updatedAt, new Date(0).toISOString()),
|
|
504
497
|
results,
|
|
505
498
|
baseline,
|
|
506
499
|
history,
|
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,17 +1,14 @@
|
|
|
1
|
-
import { normalizeDomain } from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export const MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN = 12;
|
|
7
|
-
export const MAX_WEBSITE_SNAPSHOT_STORE_BYTES = 512 * 1024;
|
|
8
|
-
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";
|
|
9
6
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
10
7
|
const DIGEST_RE = /^[a-f0-9]{16,128}$/iu;
|
|
11
8
|
const SHA256_RE = /^[a-f0-9]{64}$/iu;
|
|
12
9
|
const SERIAL_RE = /^[a-f0-9]{1,128}$/iu;
|
|
13
10
|
function record(value) {
|
|
14
|
-
return value
|
|
11
|
+
return ordinaryWorkspaceRecord(value, 'Website-snapshot input');
|
|
15
12
|
}
|
|
16
13
|
function text(value, maximum) {
|
|
17
14
|
return typeof value === 'string' && value.length <= maximum && !CONTROL_RE.test(value)
|
|
@@ -19,9 +16,7 @@ function text(value, maximum) {
|
|
|
19
16
|
: '';
|
|
20
17
|
}
|
|
21
18
|
function timestamp(value) {
|
|
22
|
-
|
|
23
|
-
const parsed = Date.parse(candidate);
|
|
24
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '';
|
|
19
|
+
return normalizeExplicitIsoTimestamp(value) ?? '';
|
|
25
20
|
}
|
|
26
21
|
function digest(value) {
|
|
27
22
|
const candidate = text(value, 128).toLowerCase();
|
|
@@ -184,6 +179,8 @@ export function normalizeWebsiteProfileSnapshot(raw) {
|
|
|
184
179
|
};
|
|
185
180
|
}
|
|
186
181
|
export function normalizeWebsiteSnapshotStore(raw) {
|
|
182
|
+
assertWorkspaceInputGraph(raw, 'Website-snapshot store');
|
|
183
|
+
assertWorkspaceDeclaredVersion(raw, 'Website-snapshot store');
|
|
187
184
|
const value = record(raw);
|
|
188
185
|
if (value?.schema === WEBSITE_SNAPSHOT_SCHEMA
|
|
189
186
|
&& Number(value?.version) > WEBSITE_SNAPSHOT_SCHEMA_VERSION) {
|
|
@@ -195,7 +192,7 @@ export function normalizeWebsiteSnapshotStore(raw) {
|
|
|
195
192
|
throw new Error('This website-snapshot collection uses an unsupported schema and cannot be read safely.');
|
|
196
193
|
}
|
|
197
194
|
const sourceValues = Array.isArray(raw) ? raw : Array.isArray(value?.snapshots) ? value.snapshots : [];
|
|
198
|
-
const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, normalizeWebsiteProfileSnapshot);
|
|
195
|
+
const snapshots = values(sourceValues, MAX_WEBSITE_SNAPSHOTS * 2, (candidate) => normalizeWebsiteProfileSnapshot(candidate));
|
|
199
196
|
const perDomain = new Map();
|
|
200
197
|
const retained = [];
|
|
201
198
|
for (const snapshot of snapshots.sort((left, right) => right.savedAt.localeCompare(left.savedAt))) {
|
|
@@ -236,6 +233,9 @@ export function buildWebsiteSnapshotExport(raw, generatedAt = new Date().toISOSt
|
|
|
236
233
|
return { ...store, generatedAt: timestamp(generatedAt) || new Date().toISOString() };
|
|
237
234
|
}
|
|
238
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');
|
|
239
239
|
const local = normalizeWebsiteSnapshotStore(localRaw).snapshots;
|
|
240
240
|
const incoming = normalizeWebsiteSnapshotStore(incomingRaw).snapshots;
|
|
241
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.')
|