@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -1,13 +1,103 @@
|
|
|
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.mjs";
|
|
4
|
+
import { MAX_WATCHLIST_DOMAINS, normalizeWatchlistEntry, compactWatchlistResults, appendWatchlistScan, mergeWatchlistBaseline, watchlistActiveDomains, assertWatchlistEditable } from "./watchlist-history.mjs";
|
|
5
|
+
import { mergeWatchDomainMetadata, normalizeWatchDomainMetadata } from "./brand-candidate-workflow.mjs";
|
|
5
6
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
7
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
|
-
import {
|
|
8
|
+
import { boundedJsonLimitsForBytes, parseBoundedJson } from "../analysis/bounded-json.mjs";
|
|
9
|
+
import { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, MAX_WATCHLIST_PORTABLE_BYTES, WATCHLIST_RECOVERY_METADATA_BYTES, serialiseWorkspacePortableJson, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, WATCHLIST_BROWSER_SUPPORTED_VERSIONS, WATCHLIST_EXPORT_SUPPORTED_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
8
10
|
export { MAX_WATCHLIST_INPUTS, MAX_WATCHLIST_NAME_LENGTH, MAX_WATCHLISTS, MAX_WATCHLIST_STORE_BYTES, WATCHLIST_SCHEMA, WATCHLIST_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
9
11
|
const BLOCKED_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
|
|
10
12
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
13
|
+
/** Restore evidence while retaining every local active member and its context. */
|
|
14
|
+
export function planHostedWatchlistRestore(current, name, input) {
|
|
15
|
+
const normalizedName = normalizeWatchlistName(name);
|
|
16
|
+
if (!normalizedName)
|
|
17
|
+
throw new Error('Hosted watchlist name is invalid.');
|
|
18
|
+
const target = resolveWatchlistMutationTarget(normalizeWatchlistStore(current).watchlists, normalizedName);
|
|
19
|
+
const snapshot = normalizeWatchlistEntry(input);
|
|
20
|
+
assertWatchlistEditable(snapshot);
|
|
21
|
+
const before = new Set(target.previous ? watchlistActiveDomains(target.previous) : []);
|
|
22
|
+
const metadata = normalizeWatchDomainMetadata(target.previous?.domainMetadata, snapshot.results.map(record => record.domain));
|
|
23
|
+
const after = new Set(metadata.map(record => record.domain));
|
|
24
|
+
// Check bytes as well as membership before requesting consent.
|
|
25
|
+
assertWatchlistStoreBudget({ ...current, [target.name]: { ...snapshot, domainMetadata: metadata } });
|
|
26
|
+
return freezePreview(structuredClone({ name: target.name, previous: target.previous, snapshot,
|
|
27
|
+
retained: [...before].sort(), added: [...after].filter(domain => !before.has(domain)).sort(),
|
|
28
|
+
removed: [], capacity: MAX_WATCHLIST_DOMAINS }));
|
|
29
|
+
}
|
|
30
|
+
export function applyReviewedHostedWatchlistRestore(current, reviewed) {
|
|
31
|
+
const fresh = planHostedWatchlistRestore(current, reviewed.name, reviewed.snapshot);
|
|
32
|
+
if (JSON.stringify(fresh) !== JSON.stringify(reviewed))
|
|
33
|
+
throw new Error('The watchlist changed after review. Preview its membership again; nothing was overwritten.');
|
|
34
|
+
return assertWatchlistStoreBudget({ ...current, [fresh.name]: { ...fresh.snapshot,
|
|
35
|
+
domainMetadata: normalizeWatchDomainMetadata(fresh.previous?.domainMetadata, fresh.snapshot.results.map(record => record.domain)) } }).watchlists;
|
|
36
|
+
}
|
|
37
|
+
export function resolveWatchlistMutationTarget(current, requestedName) {
|
|
38
|
+
const existingName = Object.keys(current).find(candidate => candidate.toLowerCase() === requestedName.toLowerCase());
|
|
39
|
+
const previous = existingName ? current[existingName] ?? null : null;
|
|
40
|
+
assertWatchlistEditable(previous);
|
|
41
|
+
if (!previous && Object.keys(current).length >= MAX_WATCHLISTS)
|
|
42
|
+
throw new Error('Watchlist storage is full. Export and remove a watchlist before saving more.');
|
|
43
|
+
return { name: existingName || requestedName, previous };
|
|
44
|
+
}
|
|
45
|
+
function freezePreview(value) {
|
|
46
|
+
if (value && typeof value === 'object') {
|
|
47
|
+
for (const child of Object.values(value))
|
|
48
|
+
freezePreview(child);
|
|
49
|
+
Object.freeze(value);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/** Review current membership and the exact compact submitted evidence together. */
|
|
54
|
+
export function planWatchlistUpdate(current, name, input, mode, operation) {
|
|
55
|
+
const normalizedName = normalizeWatchlistName(name);
|
|
56
|
+
if (!normalizedName)
|
|
57
|
+
throw new Error('Watchlist names must be 1–100 characters and use a safe name.');
|
|
58
|
+
if (!Array.isArray(input) || input.length > MAX_WATCHLIST_DOMAINS)
|
|
59
|
+
throw new Error(`Watchlists are limited to ${MAX_WATCHLIST_DOMAINS} domains.`);
|
|
60
|
+
if (!['fast', 'deep'].includes(mode) || !['merge', 'replace'].includes(operation))
|
|
61
|
+
throw new Error('Choose a supported Monitor update operation.');
|
|
62
|
+
const target = resolveWatchlistMutationTarget(normalizeWatchlistStore(current).watchlists, normalizedName);
|
|
63
|
+
const submitted = compactWatchlistResults(input);
|
|
64
|
+
if (!submitted.length || submitted.length !== input.length)
|
|
65
|
+
throw new Error('The selected Monitor results contain invalid or repeated domains. Nothing was saved.');
|
|
66
|
+
const before = new Set(target.previous ? watchlistActiveDomains(target.previous) : []);
|
|
67
|
+
const after = new Set([...(operation === 'merge' ? before : []), ...submitted.map(record => record.domain)]);
|
|
68
|
+
if (after.size > MAX_WATCHLIST_DOMAINS)
|
|
69
|
+
throw new Error(`The merged watchlist exceeds its ${MAX_WATCHLIST_DOMAINS}-domain limit. Existing members were preserved.`);
|
|
70
|
+
return freezePreview(structuredClone({ name: target.name, operation, mode, previous: target.previous, input: submitted,
|
|
71
|
+
retained: [...before].filter(domain => after.has(domain)).sort(),
|
|
72
|
+
added: [...after].filter(domain => !before.has(domain)).sort(),
|
|
73
|
+
removed: [...before].filter(domain => !after.has(domain)).sort() }));
|
|
74
|
+
}
|
|
75
|
+
/** Reconcile consent inside the transaction, then retain only bounded history. */
|
|
76
|
+
export function applyReviewedWatchlistUpdate(current, reviewed, checkedAt = new Date().toISOString()) {
|
|
77
|
+
const fresh = planWatchlistUpdate(current, reviewed.name, reviewed.input, reviewed.mode, reviewed.operation);
|
|
78
|
+
if (JSON.stringify(fresh) !== JSON.stringify(reviewed))
|
|
79
|
+
throw new Error('The watchlist changed after review. Preview its membership again; nothing was overwritten.');
|
|
80
|
+
// An explicit replacement removes active membership, not retained history.
|
|
81
|
+
const selected = new Set(fresh.input.map(record => record.domain));
|
|
82
|
+
const previous = fresh.operation === 'replace' && fresh.previous
|
|
83
|
+
? { ...fresh.previous, results: fresh.previous.results.filter(record => selected.has(record.domain)),
|
|
84
|
+
domainMetadata: fresh.previous.domainMetadata.filter(record => selected.has(record.domain)) }
|
|
85
|
+
: fresh.previous;
|
|
86
|
+
const appended = appendWatchlistScan(previous, fresh.input, { mode: fresh.mode, checkedAt });
|
|
87
|
+
let entry = appended.entry;
|
|
88
|
+
if (fresh.operation === 'merge' && fresh.previous) {
|
|
89
|
+
const records = new Map(fresh.previous.results.map(record => [record.domain, record]));
|
|
90
|
+
for (const record of fresh.input)
|
|
91
|
+
records.set(record.domain, record);
|
|
92
|
+
const results = [...records.values()];
|
|
93
|
+
entry = normalizeWatchlistEntry({ ...entry, results,
|
|
94
|
+
baseline: mergeWatchlistBaseline(fresh.previous.baseline, results),
|
|
95
|
+
domainMetadata: fresh.previous.domainMetadata });
|
|
96
|
+
}
|
|
97
|
+
const all = { ...current };
|
|
98
|
+
Object.defineProperty(all, fresh.name, { value: entry, enumerable: true, writable: true, configurable: true });
|
|
99
|
+
return { watchlists: assertWatchlistStoreBudget(all).watchlists, changes: appended.changes };
|
|
100
|
+
}
|
|
11
101
|
function plainRecord(value) {
|
|
12
102
|
return ordinaryWorkspaceRecord(value, 'Watchlist input');
|
|
13
103
|
}
|
|
@@ -39,12 +129,34 @@ export function watchlistStoreVersion(raw) {
|
|
|
39
129
|
function defineEntry(target, name, entry) {
|
|
40
130
|
Object.defineProperty(target, name, { value: entry, writable: true, enumerable: true, configurable: true });
|
|
41
131
|
}
|
|
132
|
+
function assertHistoricalQuality(entry, version) {
|
|
133
|
+
if (version !== null && version < 6 && Object.hasOwn(entry, 'membershipRecovery'))
|
|
134
|
+
throw new TypeError('Watchlist membership recovery requires schema 6.');
|
|
135
|
+
if (version !== null && version < 5 && Object.hasOwn(entry, 'domainMetadata')) {
|
|
136
|
+
throw new TypeError('Domain watch metadata requires Watchlist schema 5; historical evidence was not reinterpreted.');
|
|
137
|
+
}
|
|
138
|
+
if (version !== null && version < 5) {
|
|
139
|
+
const values = [entry.results, entry.baseline, ...((Array.isArray(entry.history) ? entry.history : []).map(event => plainRecord(event)?.changes))];
|
|
140
|
+
if (values.some(rows => Array.isArray(rows) && rows.some(value => {
|
|
141
|
+
const row = plainRecord(value);
|
|
142
|
+
return row && (Object.hasOwn(row, 'hasExternalPasswordForm') || row.field === 'hasExternalPasswordForm');
|
|
143
|
+
})))
|
|
144
|
+
throw new TypeError('Password-form attribution requires Watchlist schema 5; historical evidence was not reinterpreted.');
|
|
145
|
+
}
|
|
146
|
+
if (version !== 2)
|
|
147
|
+
return;
|
|
148
|
+
for (const values of [entry.results, entry.baseline]) {
|
|
149
|
+
if (Array.isArray(values) && values.some(value => plainRecord(value)?.webCollectionQuality !== undefined)) {
|
|
150
|
+
throw new TypeError('Web collection quality requires Watchlist schema 3 or later; historical evidence was not reinterpreted.');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
42
154
|
export function normalizeWatchlistStore(raw) {
|
|
43
155
|
assertWorkspaceInputGraph(raw, 'Watchlist store');
|
|
44
156
|
const root = plainRecord(raw);
|
|
45
157
|
if (root?.schema === WATCHLIST_SCHEMA)
|
|
46
158
|
assertWorkspaceDeclaredVersion(raw, 'Watchlist store');
|
|
47
|
-
if (root?.schema === WATCHLIST_SCHEMA && watchlistStoreVersion(root)
|
|
159
|
+
if (root?.schema === WATCHLIST_SCHEMA && !WATCHLIST_BROWSER_SUPPORTED_VERSIONS.includes(Number(watchlistStoreVersion(root)))) {
|
|
48
160
|
throw new Error(`Watchlist schema ${String(root.version)} is unsupported; no data was changed.`);
|
|
49
161
|
}
|
|
50
162
|
const source = watchlistMap(raw);
|
|
@@ -56,7 +168,8 @@ export function normalizeWatchlistStore(raw) {
|
|
|
56
168
|
const entry = plainRecord(rawEntry);
|
|
57
169
|
if (!name || !entry || !Array.isArray(entry.results) || entry.results.length > MAX_WATCHLIST_DOMAINS)
|
|
58
170
|
continue;
|
|
59
|
-
|
|
171
|
+
assertHistoricalQuality(entry, root?.schema === WATCHLIST_SCHEMA ? watchlistStoreVersion(root) : null);
|
|
172
|
+
defineEntry(watchlists, name, normalizeWatchlistEntry(entry, { recoverLegacyMembership: root?.schema === WATCHLIST_SCHEMA && root.version === 5 }));
|
|
60
173
|
if (Object.keys(watchlists).length >= MAX_WATCHLISTS)
|
|
61
174
|
break;
|
|
62
175
|
}
|
|
@@ -67,7 +180,8 @@ function byteLength(value) {
|
|
|
67
180
|
}
|
|
68
181
|
export function assertWatchlistStoreBudget(watchlists) {
|
|
69
182
|
const store = normalizeWatchlistStore(watchlists);
|
|
70
|
-
|
|
183
|
+
const recoveryBytes = Object.values(store.watchlists).filter(entry => entry.membershipRecovery).length * WATCHLIST_RECOVERY_METADATA_BYTES;
|
|
184
|
+
if (byteLength(JSON.stringify(store)) - recoveryBytes > MAX_WATCHLIST_STORE_BYTES) {
|
|
71
185
|
throw new Error('Watchlist storage is full. Export and remove a watchlist before saving more.');
|
|
72
186
|
}
|
|
73
187
|
return store;
|
|
@@ -93,8 +207,8 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
|
|
|
93
207
|
if (importedVersion !== null && importedVersion > WATCHLIST_SCHEMA_VERSION) {
|
|
94
208
|
throw new Error(`This watchlist file uses newer schema ${importedVersion}. Update the app before importing it.`);
|
|
95
209
|
}
|
|
96
|
-
if (importedVersion
|
|
97
|
-
throw new Error(
|
|
210
|
+
if (!WATCHLIST_EXPORT_SUPPORTED_VERSIONS.includes(Number(importedVersion))) {
|
|
211
|
+
throw new Error('Expected a supported WHOISleuth watchlist export.');
|
|
98
212
|
}
|
|
99
213
|
const local = normalizeWatchlistStore(localRaw).watchlists;
|
|
100
214
|
const source = watchlistMap(importedRaw) || {};
|
|
@@ -109,12 +223,29 @@ export function mergeWatchlistStores(localRaw, importedRaw) {
|
|
|
109
223
|
skipped++;
|
|
110
224
|
continue;
|
|
111
225
|
}
|
|
112
|
-
|
|
226
|
+
assertHistoricalQuality(entry, importedVersion);
|
|
227
|
+
const normalized = normalizeWatchlistEntry(entry, { recoverLegacyMembership: importedVersion === 5 });
|
|
113
228
|
if (Object.prototype.hasOwnProperty.call(local, name)) {
|
|
114
|
-
if (normalized.
|
|
115
|
-
|
|
229
|
+
if (normalized.membershipRecovery || local[name].membershipRecovery) {
|
|
230
|
+
if (JSON.stringify(normalized) === JSON.stringify(local[name])) {
|
|
231
|
+
skipped++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
throw new Error('A watchlist awaiting membership recovery cannot be merged or replaced. Import it under a separate name to preserve both records.');
|
|
235
|
+
}
|
|
236
|
+
const localTime = local[name].updatedAt;
|
|
237
|
+
const previous = local[name];
|
|
238
|
+
const domainMetadata = mergeWatchDomainMetadata(previous.domainMetadata, Object.hasOwn(entry, 'domainMetadata') ? normalized.domainMetadata : []);
|
|
239
|
+
if (!normalized.updatedAt || !localTime || normalized.updatedAt <= localTime) {
|
|
240
|
+
if (JSON.stringify(domainMetadata) !== JSON.stringify(previous.domainMetadata)) {
|
|
241
|
+
defineEntry(local, name, { ...previous, domainMetadata });
|
|
242
|
+
updated++;
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
skipped++;
|
|
116
246
|
continue;
|
|
117
247
|
}
|
|
248
|
+
normalized.domainMetadata = domainMetadata;
|
|
118
249
|
updated++;
|
|
119
250
|
}
|
|
120
251
|
else if (Object.keys(local).length >= MAX_WATCHLISTS) {
|
|
@@ -135,6 +266,16 @@ export function buildWatchlistExport(watchlists, nowIso = new Date().toISOString
|
|
|
135
266
|
schema: WATCHLIST_SCHEMA,
|
|
136
267
|
version: WATCHLIST_SCHEMA_VERSION,
|
|
137
268
|
exportedAt,
|
|
138
|
-
watchlists:
|
|
269
|
+
watchlists: assertWatchlistStoreBudget(watchlists).watchlists,
|
|
139
270
|
};
|
|
140
271
|
}
|
|
272
|
+
/** Preserve readable formatting when it fits; maximum stores need compact JSON. */
|
|
273
|
+
export function serializeWatchlistExport(watchlists, nowIso) {
|
|
274
|
+
const value = buildWatchlistExport(watchlists, nowIso);
|
|
275
|
+
const formatted = serialiseWorkspacePortableJson(value);
|
|
276
|
+
return byteLength(formatted) <= MAX_WATCHLIST_PORTABLE_BYTES ? formatted : JSON.stringify(value);
|
|
277
|
+
}
|
|
278
|
+
export function parseWatchlistExport(json) {
|
|
279
|
+
return parseBoundedJson(json, { label: 'Watchlist import', maximumBytes: MAX_WATCHLIST_PORTABLE_BYTES,
|
|
280
|
+
limits: boundedJsonLimitsForBytes(MAX_WATCHLIST_PORTABLE_BYTES) });
|
|
281
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
3
|
import { validWebObservationMode } from "../evidence/lookup-target.mjs";
|
|
4
4
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
@@ -302,17 +302,42 @@ export function mergeWebsiteSnapshots(localRaw, incomingRaw) {
|
|
|
302
302
|
const local = normalizeWebsiteSnapshotStore(localRaw).snapshots;
|
|
303
303
|
const incoming = normalizeWebsiteSnapshotStore(incomingRaw).snapshots;
|
|
304
304
|
const byId = new Map(local.map((item) => [item.id, item]));
|
|
305
|
+
const perDomain = new Map();
|
|
306
|
+
for (const item of local)
|
|
307
|
+
perDomain.set(item.domain, (perDomain.get(item.domain) ?? 0) + 1);
|
|
308
|
+
const byteLength = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
309
|
+
let bytes = byteLength({ schema: WEBSITE_SNAPSHOT_SCHEMA, version: WEBSITE_SNAPSHOT_SCHEMA_VERSION, snapshots: local });
|
|
310
|
+
if (bytes > MAX_WEBSITE_SNAPSHOT_STORE_BYTES)
|
|
311
|
+
throw new Error('Existing website snapshots exceed the storage limit; no imported changes were applied.');
|
|
305
312
|
let added = 0;
|
|
306
313
|
let updated = 0;
|
|
314
|
+
const supplied = record(incomingRaw)?.snapshots;
|
|
315
|
+
let skipped = Array.isArray(supplied) ? supplied.length - incoming.length : 0;
|
|
316
|
+
let capacitySkipped = 0;
|
|
307
317
|
for (const item of incoming) {
|
|
308
|
-
|
|
318
|
+
const existing = byId.get(item.id);
|
|
319
|
+
const targetCount = (perDomain.get(item.domain) ?? 0) - Number(existing?.domain === item.domain);
|
|
320
|
+
const delta = byteLength(item) - (existing ? byteLength(existing) : 0) + (!existing && byId.size ? 1 : 0);
|
|
321
|
+
if ((!existing && byId.size >= MAX_WEBSITE_SNAPSHOTS)
|
|
322
|
+
|| targetCount >= MAX_WEBSITE_SNAPSHOTS_PER_DOMAIN
|
|
323
|
+
|| bytes + delta > MAX_WEBSITE_SNAPSHOT_STORE_BYTES) {
|
|
324
|
+
skipped += 1;
|
|
325
|
+
capacitySkipped += 1;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (existing) {
|
|
309
329
|
updated += 1;
|
|
330
|
+
perDomain.set(existing.domain, (perDomain.get(existing.domain) ?? 1) - 1);
|
|
331
|
+
}
|
|
310
332
|
else
|
|
311
333
|
added += 1;
|
|
312
334
|
byId.set(item.id, item);
|
|
335
|
+
perDomain.set(item.domain, (perDomain.get(item.domain) ?? 0) + 1);
|
|
336
|
+
bytes += delta;
|
|
313
337
|
}
|
|
314
338
|
const snapshots = normalizeWebsiteSnapshotStore([...byId.values()]).snapshots;
|
|
315
|
-
return { snapshots, added, updated, skipped
|
|
339
|
+
return { snapshots, added, updated, skipped,
|
|
340
|
+
reason: capacitySkipped ? `${capacitySkipped} imported website snapshots did not fit the remaining per-domain or total storage capacity. Existing local snapshots are preserved.` : '' };
|
|
316
341
|
}
|
|
317
342
|
const MAX_RECONCILED_WEBSITE_SOURCES = 16;
|
|
318
343
|
const UNAVAILABLE_WEBSITE_SOURCE_STATES = new Set([
|
|
@@ -2,7 +2,7 @@
|
|
|
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 { boundedJsonLimitsForBytes, parseBoundedJson } from "
|
|
5
|
+
import { boundedJsonLimitsForBytes, parseBoundedJson } from "../analysis/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
8
|
import { AES_KEY_BITS, arrayBuffer, assertPassphrase, cryptoProvider, deriveArchiveKey } from "../evidence/passphrase-encryption.mjs";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Pure, bounded workspace archive composition. Existing typed store models remain the
|
|
2
2
|
// authority for normalization and merge semantics; this module only packages
|
|
3
3
|
// their portable contracts, verifies integrity, and previews conflicts.
|
|
4
|
-
import { buildCaseExport, enforceStoreBudget
|
|
4
|
+
import { buildCaseExport, enforceStoreBudget } from "../cases/case-storage-model.mjs";
|
|
5
|
+
import { mergeCases } from "../cases/case-migration-model.mjs";
|
|
5
6
|
import { assertBrandProfileStoreBudget, buildBrandProfileExport, mergeBrandProfiles, normalizeBrandProfileId, } from "./brand-profile-model.mjs";
|
|
6
7
|
import { assertCampaignStoreBudget, buildCampaignExport, mergeCampaigns, } from "./campaign-model.mjs";
|
|
7
8
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
@@ -584,15 +585,14 @@ function previewVerifiedWorkspaceArchive(archive, localInput, options) {
|
|
|
584
585
|
results.push({
|
|
585
586
|
...section,
|
|
586
587
|
status: 'ready',
|
|
587
|
-
reason:
|
|
588
|
-
? result.reason ?? 'The imported workspace preference was skipped. Existing browser-local context is preserved.'
|
|
589
|
-
: section.reason,
|
|
588
|
+
reason: selected && result.reason ? result.reason : section.reason,
|
|
590
589
|
added: result.added ?? 0,
|
|
591
590
|
updated: result.updated ?? 0,
|
|
592
591
|
skipped: result.skipped ?? 0,
|
|
593
592
|
pruned: result.pruned ?? 0,
|
|
594
593
|
brandProfileReferencesOmitted: result.brandProfileReferencesOmitted ?? 0,
|
|
595
594
|
authoredHistoryOmitted: result.authoredHistoryOmitted ?? 0,
|
|
595
|
+
evidenceHistoryOmitted: result.evidenceHistoryOmitted ?? 0,
|
|
596
596
|
selected,
|
|
597
597
|
normalizedSettings: result.settings || null,
|
|
598
598
|
});
|
|
@@ -633,6 +633,7 @@ function previewVerifiedWorkspaceArchive(archive, localInput, options) {
|
|
|
633
633
|
pruned: 0,
|
|
634
634
|
brandProfileReferencesOmitted: 0,
|
|
635
635
|
authoredHistoryOmitted: 0,
|
|
636
|
+
evidenceHistoryOmitted: 0,
|
|
636
637
|
selected: false,
|
|
637
638
|
normalizedSettings: dependentId === 'settings' ? null : dependentSection.normalizedSettings ?? null,
|
|
638
639
|
};
|