@slicedearth/whoisleuth-cli 2.3.0 → 2.5.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/NOTICE +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- 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 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- 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-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -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 +345 -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-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -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 +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- 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 +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -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/intake-report.mjs +19 -0
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -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/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { CASE_DISPOSITIONS as CASE_DISPOSITION_OPTIONS } from "../cases/case-record-contracts.mjs";
|
|
3
|
-
import { normalizeBulkPresentationSortKey } from "./bulk-sort.mjs";
|
|
3
|
+
import { BULK_SORT_KEYS, normalizeBulkPresentationSortKey } from "./bulk-sort.mjs";
|
|
4
|
+
import { normalizeBulkResultColumns } from "./bulk-columns.mjs";
|
|
4
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
5
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
6
|
-
import { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
7
|
+
import { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, BULK_REVIEW_EXPORT_SUPPORTED_VERSIONS, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
7
8
|
export { BULK_REVIEW_SCHEMA, BULK_REVIEW_SCHEMA_VERSION, MAX_BULK_REVIEW_NAME_LENGTH, MAX_BULK_REVIEW_PRESETS, MAX_BULK_REVIEW_ROWS, MAX_BULK_REVIEW_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
8
9
|
export const BULK_REVIEW_STATES = ['unreviewed', 'reviewing', 'reviewed', 'deferred'];
|
|
9
10
|
export const BULK_SOURCE_FILTERS = ['', 'complete', 'limited', 'unrecorded'];
|
|
@@ -19,7 +20,7 @@ const LIFECYCLE_FILTERS = new Set(BULK_LIFECYCLE_FILTERS);
|
|
|
19
20
|
const AGE_FILTERS = new Set(BULK_AGE_FILTERS);
|
|
20
21
|
const MAIL_FILTERS = new Set(BULK_MAIL_FILTERS);
|
|
21
22
|
const GROUPS = new Set(BULK_GROUP_OPTIONS);
|
|
22
|
-
const SORT_KEYS = new Set(
|
|
23
|
+
const SORT_KEYS = new Set(BULK_SORT_KEYS);
|
|
23
24
|
const REVIEW_STATES = new Set(BULK_REVIEW_STATES);
|
|
24
25
|
const CASE_DISPOSITIONS = new Set(['', 'untracked', ...CASE_DISPOSITION_OPTIONS.map((item) => item.value)]);
|
|
25
26
|
const SIGNAL_FILTERS = new Set(['favicon', 'password', 'phishing', 'asset_reuse', 'idn']);
|
|
@@ -67,6 +68,7 @@ function normalizeView(raw) {
|
|
|
67
68
|
groupBy: setValue(value.groupBy, GROUPS),
|
|
68
69
|
sortKey,
|
|
69
70
|
sortDirection: value.sortDirection === 1 ? 1 : -1,
|
|
71
|
+
columns: normalizeBulkResultColumns(value.columns),
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
function normalizePreset(raw, fallback) {
|
|
@@ -113,6 +115,10 @@ function sourceLists(raw) {
|
|
|
113
115
|
export function normalizeBulkReviewStore(raw) {
|
|
114
116
|
assertWorkspaceInputGraph(raw, 'Bulk-review store');
|
|
115
117
|
assertWorkspaceDeclaredVersion(raw, 'Bulk-review store');
|
|
118
|
+
const declaredVersion = record(raw).version;
|
|
119
|
+
if (typeof declaredVersion === 'number' && declaredVersion > BULK_REVIEW_SCHEMA_VERSION) {
|
|
120
|
+
throw new Error(`This Bulk review store uses newer schema ${declaredVersion}. Update the app before changing it.`);
|
|
121
|
+
}
|
|
116
122
|
const fallback = new Date(0).toISOString();
|
|
117
123
|
const source = sourceLists(raw);
|
|
118
124
|
const presets = new Map();
|
|
@@ -154,7 +160,7 @@ export function enforceBulkReviewBudget(raw) {
|
|
|
154
160
|
while (byteLength(JSON.stringify(store)) > MAX_BULK_REVIEW_STORE_BYTES && store.presets.length > 1)
|
|
155
161
|
store.presets.pop();
|
|
156
162
|
if (byteLength(JSON.stringify(store)) > MAX_BULK_REVIEW_STORE_BYTES) {
|
|
157
|
-
throw new Error('Bulk review preferences exceed the
|
|
163
|
+
throw new Error('Bulk review preferences exceed the workspace storage limit.');
|
|
158
164
|
}
|
|
159
165
|
return store;
|
|
160
166
|
}
|
|
@@ -217,7 +223,7 @@ export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
|
217
223
|
if (importedRecord.schema !== BULK_REVIEW_SCHEMA) {
|
|
218
224
|
throw new Error('This file is not a WHOISleuth Bulk review export.');
|
|
219
225
|
}
|
|
220
|
-
if (
|
|
226
|
+
if (!BULK_REVIEW_EXPORT_SUPPORTED_VERSIONS.some((version) => version === importedRecord.version)) {
|
|
221
227
|
if (typeof importedRecord.version === 'number' && importedRecord.version > BULK_REVIEW_SCHEMA_VERSION) {
|
|
222
228
|
throw new Error(`This Bulk review export uses newer schema ${importedRecord.version}. Update the app before importing it.`);
|
|
223
229
|
}
|
|
@@ -229,37 +235,38 @@ export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
|
229
235
|
const rows = new Map(local.rows.map((item) => [item.domain, item]));
|
|
230
236
|
let added = 0;
|
|
231
237
|
let updated = 0;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
updated += 1;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
238
|
+
const supplied = sourceLists(importedRaw);
|
|
239
|
+
let skipped = supplied.presets.length + supplied.rows.length - imported.presets.length - imported.rows.length;
|
|
240
|
+
let capacitySkipped = 0;
|
|
241
|
+
let bytes = byteLength(JSON.stringify(local));
|
|
242
|
+
if (bytes > MAX_BULK_REVIEW_STORE_BYTES)
|
|
243
|
+
throw new Error('Existing Bulk review preferences exceed the workspace storage limit; no imported changes were applied.');
|
|
244
|
+
function admit(target, key, candidate, maximum) {
|
|
245
|
+
const existing = target.get(key);
|
|
246
|
+
if (existing && candidate.updatedAt <= existing.updatedAt) {
|
|
244
247
|
skipped += 1;
|
|
248
|
+
return;
|
|
245
249
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!existing) {
|
|
250
|
-
rows.set(candidate.domain, candidate);
|
|
251
|
-
added += 1;
|
|
252
|
-
}
|
|
253
|
-
else if (candidate.updatedAt > existing.updatedAt) {
|
|
254
|
-
rows.set(candidate.domain, candidate);
|
|
255
|
-
updated += 1;
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
250
|
+
const delta = byteLength(JSON.stringify(candidate)) - (existing ? byteLength(JSON.stringify(existing)) : 0)
|
|
251
|
+
+ (!existing && target.size ? 1 : 0);
|
|
252
|
+
if ((!existing && target.size >= maximum) || bytes + delta > MAX_BULK_REVIEW_STORE_BYTES) {
|
|
258
253
|
skipped += 1;
|
|
254
|
+
capacitySkipped += 1;
|
|
255
|
+
return;
|
|
259
256
|
}
|
|
257
|
+
target.set(key, candidate);
|
|
258
|
+
bytes += delta;
|
|
259
|
+
if (existing)
|
|
260
|
+
updated += 1;
|
|
261
|
+
else
|
|
262
|
+
added += 1;
|
|
260
263
|
}
|
|
264
|
+
for (const candidate of imported.presets)
|
|
265
|
+
admit(presets, candidate.id, candidate, MAX_BULK_REVIEW_PRESETS);
|
|
266
|
+
for (const candidate of imported.rows)
|
|
267
|
+
admit(rows, candidate.domain, candidate, MAX_BULK_REVIEW_ROWS);
|
|
261
268
|
return {
|
|
262
|
-
store:
|
|
269
|
+
store: normalizeBulkReviewStore({
|
|
263
270
|
schema: BULK_REVIEW_SCHEMA,
|
|
264
271
|
version: BULK_REVIEW_SCHEMA_VERSION,
|
|
265
272
|
presets: [...presets.values()],
|
|
@@ -268,5 +275,6 @@ export function mergeBulkReviewStores(localRaw, importedRaw) {
|
|
|
268
275
|
added,
|
|
269
276
|
updated,
|
|
270
277
|
skipped,
|
|
278
|
+
reason: capacitySkipped ? `${capacitySkipped} imported Bulk review records did not fit the remaining storage capacity. Existing local records are preserved.` : '',
|
|
271
279
|
};
|
|
272
280
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Pure, bounded persistence model for analyst-saved Bulk investigations.
|
|
2
2
|
// Sessions retain compact derived rows and source states, never raw registry,
|
|
3
3
|
// WHOIS, HTTP, TLS, page, or provider payloads.
|
|
4
|
-
import { normalizeDomain } from "../
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
5
|
import { normalizeCaaCritical } from "./dns-record-normalization.mjs";
|
|
6
6
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
7
|
+
import { normalizeWebCollectionQuality, webCollectionAllowsComparison } from "../evidence/collection-quality.mjs";
|
|
8
|
+
import { RELATIONSHIP_EVIDENCE_VERSION, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS } from "../contracts/offline-comparison.mjs";
|
|
9
|
+
import { normalizeRelationshipSourceProjection } from "../comparison/relationship-provenance.mjs";
|
|
7
10
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
8
11
|
import { BULK_SESSION_SCHEMA, BULK_SESSION_SCHEMA_VERSION, MAX_BULK_PROFILE_CONTEXT_LIMITATION_LENGTH, MAX_BULK_SESSIONS, MAX_BULK_SESSION_ARRAY_VALUES, MAX_BULK_SESSION_NAME_LENGTH, MAX_BULK_SESSION_ROWS, MAX_BULK_SESSION_SOURCES, MAX_BULK_SESSION_STORE_BYTES, MAX_BULK_SESSION_TEXT_LENGTH, SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
9
12
|
export { BULK_SESSION_SCHEMA, BULK_SESSION_SCHEMA_VERSION, MAX_BULK_PROFILE_CONTEXT_LIMITATION_LENGTH, MAX_BULK_SESSIONS, MAX_BULK_SESSION_ARRAY_VALUES, MAX_BULK_SESSION_NAME_LENGTH, MAX_BULK_SESSION_ROWS, MAX_BULK_SESSION_SOURCES, MAX_BULK_SESSION_STORE_BYTES, MAX_BULK_SESSION_TEXT_LENGTH, SUPPORTED_BULK_SESSION_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
@@ -92,7 +95,7 @@ export function normalizeBulkProfileContext(value, fallbackLimitation = BULK_PRO
|
|
|
92
95
|
}
|
|
93
96
|
return unavailableBulkProfileContext(profileContextLimitation(item?.limitation, fallbackLimitation));
|
|
94
97
|
}
|
|
95
|
-
function
|
|
98
|
+
export function bulkProfileContextsMatch(left, right) {
|
|
96
99
|
return left.sourceState === right.sourceState
|
|
97
100
|
&& left.activeProfileId === right.activeProfileId
|
|
98
101
|
&& left.profileUpdatedAt === right.profileUpdatedAt
|
|
@@ -102,7 +105,7 @@ export function summarizeBulkProfileContexts(rows) {
|
|
|
102
105
|
const first = rows[0]?.profileContext;
|
|
103
106
|
if (!first)
|
|
104
107
|
return unavailableBulkProfileContext('This saved Bulk session contains no settled row with profile-context provenance.');
|
|
105
|
-
if (rows.every((row) =>
|
|
108
|
+
if (rows.every((row) => bulkProfileContextsMatch(first, row.profileContext)))
|
|
106
109
|
return { ...first };
|
|
107
110
|
return {
|
|
108
111
|
sourceState: 'mixed',
|
|
@@ -175,7 +178,7 @@ function normalizeSourceCoverage(value) {
|
|
|
175
178
|
if (!SOURCE_RE.test(source) || !SOURCE_STATES.has(state) || seen.has(source))
|
|
176
179
|
continue;
|
|
177
180
|
seen.add(source);
|
|
178
|
-
output.push({ source, state: state });
|
|
181
|
+
output.push({ source, state: state, observedAt: normalizeExplicitIsoTimestamp(item?.observedAt) });
|
|
179
182
|
if (output.length >= MAX_BULK_SESSION_SOURCES)
|
|
180
183
|
break;
|
|
181
184
|
}
|
|
@@ -198,8 +201,11 @@ function nullableHash(value, expression) {
|
|
|
198
201
|
}
|
|
199
202
|
function normalizeRelationship(value) {
|
|
200
203
|
const item = record(value);
|
|
204
|
+
if (item?.version !== undefined && !SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS.some(version => version === item.version)) {
|
|
205
|
+
throw new TypeError('This Bulk relationship projection uses an unsupported version. No source evidence was interpreted.');
|
|
206
|
+
}
|
|
201
207
|
return {
|
|
202
|
-
version:
|
|
208
|
+
version: RELATIONSHIP_EVIDENCE_VERSION,
|
|
203
209
|
nameservers: boundedStrings(item?.nameservers, 20),
|
|
204
210
|
ipAddresses: boundedStrings(item?.ipAddresses, 50),
|
|
205
211
|
trackingIdentifiers: boundedTrackingIdentifiers(item?.trackingIdentifiers),
|
|
@@ -207,6 +213,7 @@ function normalizeRelationship(value) {
|
|
|
207
213
|
faviconHash: nullableHash(item?.faviconHash, HASH_64_RE),
|
|
208
214
|
faviconPHash: nullableHash(item?.faviconPHash, PHASH_RE),
|
|
209
215
|
certificateFingerprint: nullableHash(item?.certificateFingerprint, HASH_64_RE),
|
|
216
|
+
sourceEvidence: Number(item?.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? normalizeRelationshipSourceProjection(item?.sourceEvidence) : {},
|
|
210
217
|
truncated: item?.truncated === true,
|
|
211
218
|
};
|
|
212
219
|
}
|
|
@@ -270,14 +277,14 @@ export function normalizeBulkComparisonEvidence(value) {
|
|
|
270
277
|
},
|
|
271
278
|
};
|
|
272
279
|
}
|
|
273
|
-
export function normalizeBulkSessionResult(value) {
|
|
280
|
+
export function normalizeBulkSessionResult(value, inheritedProfileContext) {
|
|
274
281
|
const item = record(value);
|
|
275
282
|
const domain = normalizeDomain(item?.domain);
|
|
276
283
|
const status = boundedText(item?.status, 20);
|
|
277
284
|
const scanDepth = boundedText(item?.scanDepth, 10);
|
|
278
285
|
if (!item || !domain || !RESULT_STATES.has(status) || !MODES.has(scanDepth))
|
|
279
286
|
return null;
|
|
280
|
-
const rawProfileContext = record(item.profileContext);
|
|
287
|
+
const rawProfileContext = record(Object.hasOwn(item, 'profileContext') ? item.profileContext : inheritedProfileContext);
|
|
281
288
|
if (!rawProfileContext || !['ready', 'unavailable'].includes(String(rawProfileContext.sourceState)))
|
|
282
289
|
return null;
|
|
283
290
|
const profileContext = normalizeBulkProfileContext(rawProfileContext);
|
|
@@ -298,6 +305,7 @@ export function normalizeBulkSessionResult(value) {
|
|
|
298
305
|
const profileClaimsUsable = profileContextReady && !impossibleNoProfileClaim;
|
|
299
306
|
if (!profileClaimsUsable || readyWithoutActiveProfile)
|
|
300
307
|
relationship.officialAssetHosts = [];
|
|
308
|
+
const webCollectionQuality = normalizeWebCollectionQuality(item.webCollectionQuality, scanDepth);
|
|
301
309
|
return {
|
|
302
310
|
domain,
|
|
303
311
|
status: status,
|
|
@@ -311,6 +319,8 @@ export function normalizeBulkSessionResult(value) {
|
|
|
311
319
|
trusted: profileClaimsUsable && !readyWithoutActiveProfile && TRUST_STATES.has(trusted) ? trusted : null,
|
|
312
320
|
error: boundedText(item.error),
|
|
313
321
|
scanDepth: scanDepth,
|
|
322
|
+
...(webCollectionQuality ? { webCollectionQuality } : {}),
|
|
323
|
+
observedAt: normalizeExplicitIsoTimestamp(item.observedAt),
|
|
314
324
|
createdDate: boundedText(item.createdDate, 64) || null,
|
|
315
325
|
expiryDate: boundedText(item.expiryDate, 64) || null,
|
|
316
326
|
privacyProtected: nullableBoolean(item.privacyProtected),
|
|
@@ -343,7 +353,7 @@ export function normalizeBulkSessionResult(value) {
|
|
|
343
353
|
profileContext,
|
|
344
354
|
};
|
|
345
355
|
}
|
|
346
|
-
export function normalizeBulkSession(value) {
|
|
356
|
+
export function normalizeBulkSession(value, sourceStoreVersion) {
|
|
347
357
|
const item = record(value);
|
|
348
358
|
const id = boundedText(item?.id, 128);
|
|
349
359
|
const name = boundedText(item?.name, MAX_BULK_SESSION_NAME_LENGTH);
|
|
@@ -367,24 +377,47 @@ export function normalizeBulkSession(value) {
|
|
|
367
377
|
return null;
|
|
368
378
|
if (!Array.isArray(item.results))
|
|
369
379
|
return null;
|
|
380
|
+
const rawProfileContext = record(item.profileContext);
|
|
381
|
+
if (!rawProfileContext)
|
|
382
|
+
return null;
|
|
383
|
+
const declaredProfileContext = normalizeBulkProfileContext(rawProfileContext);
|
|
384
|
+
// Only an explicitly versioned current document may omit identical row
|
|
385
|
+
// context. Bare runtime rows and public schema 4 must carry their own context;
|
|
386
|
+
// an explicit invalid/null row value never falls back to the session.
|
|
387
|
+
const inheritedProfileContext = sourceStoreVersion !== undefined && sourceStoreVersion >= 5
|
|
388
|
+
&& declaredProfileContext.sourceState !== 'mixed'
|
|
389
|
+
&& rawProfileContext.sourceState === declaredProfileContext.sourceState
|
|
390
|
+
? declaredProfileContext
|
|
391
|
+
: undefined;
|
|
370
392
|
const allowed = new Set(domains);
|
|
371
393
|
const results = [];
|
|
372
394
|
const seen = new Set();
|
|
373
395
|
for (const candidate of Array.isArray(item.results) ? item.results.slice(0, MAX_BULK_SESSION_ROWS * 2) : []) {
|
|
374
|
-
|
|
396
|
+
if (sourceStoreVersion !== undefined && sourceStoreVersion < 6 && record(candidate)?.webCollectionQuality !== undefined) {
|
|
397
|
+
throw new TypeError('Web collection quality requires Bulk schema 6 or later; historical evidence was not reinterpreted.');
|
|
398
|
+
}
|
|
399
|
+
const publicLegacyRow = sourceStoreVersion === 4;
|
|
400
|
+
if (publicLegacyRow && Number(record(record(candidate)?.relationship)?.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION) {
|
|
401
|
+
throw new TypeError('Bulk schema 4 cannot contain newer relationship source evidence; no evidence was interpreted.');
|
|
402
|
+
}
|
|
403
|
+
const result = normalizeBulkSessionResult(candidate, inheritedProfileContext);
|
|
375
404
|
if (!result || !allowed.has(result.domain) || seen.has(result.domain)) {
|
|
376
405
|
return null;
|
|
377
406
|
}
|
|
407
|
+
// Public schema 4 never recorded a row observation time or qualified
|
|
408
|
+
// relationship sources. Ignore added fields rather than backdating them.
|
|
409
|
+
if (publicLegacyRow) {
|
|
410
|
+
result.observedAt = null;
|
|
411
|
+
result.relationship.sourceEvidence = {};
|
|
412
|
+
result.sourceCoverage = result.sourceCoverage.map((source) => ({ ...source, observedAt: null }));
|
|
413
|
+
}
|
|
378
414
|
seen.add(result.domain);
|
|
379
415
|
results.push(result);
|
|
380
416
|
}
|
|
381
417
|
if ((state === 'complete') !== (results.length === domains.length))
|
|
382
418
|
return null;
|
|
383
419
|
const profileContext = summarizeBulkProfileContexts(results);
|
|
384
|
-
if (!
|
|
385
|
-
return null;
|
|
386
|
-
const declaredProfileContext = normalizeBulkProfileContext(item.profileContext);
|
|
387
|
-
if (!sameProfileContext(declaredProfileContext, profileContext))
|
|
420
|
+
if (!bulkProfileContextsMatch(declaredProfileContext, profileContext))
|
|
388
421
|
return null;
|
|
389
422
|
return {
|
|
390
423
|
id,
|
|
@@ -407,7 +440,9 @@ export function bulkSessionStoreVersion(raw) {
|
|
|
407
440
|
: null;
|
|
408
441
|
}
|
|
409
442
|
export function normalizeBulkSessionStore(raw) {
|
|
410
|
-
|
|
443
|
+
// Preparation may combine an existing store and one incoming store before
|
|
444
|
+
// whole-session eviction. The persisted document still has its 4 MiB cap.
|
|
445
|
+
assertWorkspaceInputGraph(raw, 'Bulk-session store', { maximumBytes: MAX_BULK_SESSION_STORE_BYTES * 2 });
|
|
411
446
|
assertWorkspaceDeclaredVersion(raw, 'Bulk-session store');
|
|
412
447
|
const value = record(raw);
|
|
413
448
|
if (value?.schema === BULK_SESSION_SCHEMA
|
|
@@ -423,7 +458,7 @@ export function normalizeBulkSessionStore(raw) {
|
|
|
423
458
|
: [];
|
|
424
459
|
const byId = new Map();
|
|
425
460
|
for (const candidate of candidates.slice(0, MAX_BULK_SESSIONS * 4)) {
|
|
426
|
-
const session = normalizeBulkSession(candidate);
|
|
461
|
+
const session = normalizeBulkSession(candidate, bulkSessionStoreVersion(raw) ?? undefined);
|
|
427
462
|
if (!session)
|
|
428
463
|
continue;
|
|
429
464
|
const existing = byId.get(session.id);
|
|
@@ -438,8 +473,21 @@ export function normalizeBulkSessionStore(raw) {
|
|
|
438
473
|
.slice(0, MAX_BULK_SESSIONS),
|
|
439
474
|
};
|
|
440
475
|
}
|
|
476
|
+
/** Current wire representation of an already-normalized session. */
|
|
477
|
+
export function bulkSessionStorageValue(session) {
|
|
478
|
+
return {
|
|
479
|
+
...session,
|
|
480
|
+
results: session.results.map(({ profileContext, ...row }) => bulkProfileContextsMatch(profileContext, session.profileContext)
|
|
481
|
+
? row
|
|
482
|
+
: { ...row, profileContext }),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Current wire bytes for sessions already admitted by the collection owner. */
|
|
486
|
+
export function serializeNormalizedBulkSessions(sessions) {
|
|
487
|
+
return JSON.stringify({ schema: BULK_SESSION_SCHEMA, version: BULK_SESSION_SCHEMA_VERSION, sessions: sessions.map(bulkSessionStorageValue) });
|
|
488
|
+
}
|
|
441
489
|
export function serializeBulkSessionStore(raw) {
|
|
442
|
-
return
|
|
490
|
+
return serializeNormalizedBulkSessions(normalizeBulkSessionStore(raw).sessions);
|
|
443
491
|
}
|
|
444
492
|
function byteLength(value) {
|
|
445
493
|
return new TextEncoder().encode(value).byteLength;
|
|
@@ -447,11 +495,16 @@ function byteLength(value) {
|
|
|
447
495
|
export function enforceBulkSessionStoreBudget(raw) {
|
|
448
496
|
const store = normalizeBulkSessionStore(raw);
|
|
449
497
|
let pruned = 0;
|
|
450
|
-
|
|
498
|
+
const sessionBytes = store.sessions.map((session) => byteLength(JSON.stringify(bulkSessionStorageValue(session))));
|
|
499
|
+
let bytes = byteLength(JSON.stringify({ ...store, sessions: [] }))
|
|
500
|
+
+ sessionBytes.reduce((total, size) => total + size, 0)
|
|
501
|
+
+ Math.max(0, sessionBytes.length - 1);
|
|
502
|
+
while (bytes > MAX_BULK_SESSION_STORE_BYTES && store.sessions.length > 1) {
|
|
503
|
+
bytes -= sessionBytes.pop() + 1;
|
|
451
504
|
store.sessions.pop();
|
|
452
505
|
pruned += 1;
|
|
453
506
|
}
|
|
454
|
-
if (
|
|
507
|
+
if (bytes > MAX_BULK_SESSION_STORE_BYTES) {
|
|
455
508
|
throw new Error('This Bulk session exceeds the 4 MiB browser-local session limit.');
|
|
456
509
|
}
|
|
457
510
|
return { store, pruned };
|
|
@@ -474,9 +527,29 @@ export function upsertBulkSession(raw, sessionRaw) {
|
|
|
474
527
|
sessions: bounded.store.sessions,
|
|
475
528
|
session,
|
|
476
529
|
added: index < 0,
|
|
477
|
-
pruned: bounded.
|
|
530
|
+
pruned: Math.max(0, next.length - bounded.store.sessions.length),
|
|
478
531
|
};
|
|
479
532
|
}
|
|
533
|
+
/** Ephemeral retention preview, never part of the stored or exported format. */
|
|
534
|
+
export function prepareBulkSessionSave(raw, sessionRaw) {
|
|
535
|
+
const current = normalizeBulkSessionStore(raw).sessions;
|
|
536
|
+
const result = upsertBulkSession(current, sessionRaw);
|
|
537
|
+
if (!result.sessions.some((session) => session.id === result.session.id)) {
|
|
538
|
+
throw new Error('This Bulk session cannot fit alongside the saved sessions. Export or remove saved sessions before trying again. Nothing was changed.');
|
|
539
|
+
}
|
|
540
|
+
const retained = new Set(result.sessions.map((session) => session.id));
|
|
541
|
+
const removed = current.filter((session) => !retained.has(session.id));
|
|
542
|
+
return {
|
|
543
|
+
...result,
|
|
544
|
+
current,
|
|
545
|
+
removed,
|
|
546
|
+
removedBytes: removed.reduce((total, session) => total + byteLength(JSON.stringify(bulkSessionStorageValue(session))), 0),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/** Confirmation applies to the exact records displayed, not just their IDs. */
|
|
550
|
+
export function bulkSessionSavePreviewIsCurrent(raw, preview) {
|
|
551
|
+
return serializeBulkSessionStore(raw) === serializeNormalizedBulkSessions(preview.current);
|
|
552
|
+
}
|
|
480
553
|
export function deleteBulkSession(raw, idRaw) {
|
|
481
554
|
const id = boundedText(idRaw, 128);
|
|
482
555
|
return normalizeBulkSessionStore({
|
|
@@ -491,8 +564,10 @@ function sourceStateMap(value) {
|
|
|
491
564
|
export function areBulkRiskScoresComparable(previous, current) {
|
|
492
565
|
return previous.profileContext.sourceState === 'ready'
|
|
493
566
|
&& current.profileContext.sourceState === 'ready'
|
|
494
|
-
&&
|
|
567
|
+
&& bulkProfileContextsMatch(previous.profileContext, current.profileContext)
|
|
495
568
|
&& previous.scanDepth === current.scanDepth
|
|
569
|
+
&& webCollectionAllowsComparison('riskScore', previous.webCollectionQuality, previous.scanDepth)
|
|
570
|
+
&& webCollectionAllowsComparison('riskScore', current.webCollectionQuality, current.scanDepth)
|
|
496
571
|
&& previous.risk !== null
|
|
497
572
|
&& current.risk !== null
|
|
498
573
|
&& previous.riskModelVersion !== null
|
|
@@ -507,7 +582,9 @@ function resultChanges(previous, current) {
|
|
|
507
582
|
}
|
|
508
583
|
if (previous.registrar !== current.registrar)
|
|
509
584
|
changes.push(`Registrar: ${previous.registrar} → ${current.registrar}`);
|
|
510
|
-
if (previous.activity !== current.activity
|
|
585
|
+
if (previous.activity !== current.activity
|
|
586
|
+
&& webCollectionAllowsComparison('activityStatus', previous.webCollectionQuality, previous.scanDepth)
|
|
587
|
+
&& webCollectionAllowsComparison('activityStatus', current.webCollectionQuality, current.scanDepth))
|
|
511
588
|
changes.push(`Website: ${previous.activity} → ${current.activity}`);
|
|
512
589
|
const previousComparison = previous.comparisonEvidence;
|
|
513
590
|
const currentComparison = current.comparisonEvidence;
|
|
@@ -577,7 +654,7 @@ export function compareBulkSessions(baselineRaw, currentRaw) {
|
|
|
577
654
|
'A source-state change may reflect collection availability rather than a change to the domain.',
|
|
578
655
|
'A missing row means it was not completed in that saved session; it does not establish domain removal.',
|
|
579
656
|
...(riskIncomparable
|
|
580
|
-
? ['Risk deltas are omitted unless both rows retain the same ready Brand Profile provenance, scan depth,
|
|
657
|
+
? ['Risk deltas are omitted unless both rows retain the same ready Brand Profile provenance, scan depth, versioned Risk model and complete web collection for Deep observations.']
|
|
581
658
|
: []),
|
|
582
659
|
],
|
|
583
660
|
};
|
|
@@ -588,7 +665,7 @@ export function buildBulkSessionExport(sessions, generatedAt = new Date().toISOS
|
|
|
588
665
|
schema: BULK_SESSION_SCHEMA,
|
|
589
666
|
version: BULK_SESSION_SCHEMA_VERSION,
|
|
590
667
|
generatedAt: timestamp(generatedAt, new Date().toISOString()),
|
|
591
|
-
sessions: store.sessions,
|
|
668
|
+
sessions: store.sessions.map(bulkSessionStorageValue),
|
|
592
669
|
limitations: [
|
|
593
670
|
'Compact Bulk results and source states only; raw source payloads and contact records are excluded.',
|
|
594
671
|
'Import is non-destructive and does not resume network collection automatically.',
|
|
@@ -596,8 +673,8 @@ export function buildBulkSessionExport(sessions, generatedAt = new Date().toISOS
|
|
|
596
673
|
};
|
|
597
674
|
}
|
|
598
675
|
export function mergeBulkSessions(localRaw, importedRaw) {
|
|
599
|
-
assertWorkspaceInputGraph(localRaw, 'Local Bulk-session store');
|
|
600
|
-
assertWorkspaceInputGraph(importedRaw, 'Imported Bulk-session document');
|
|
676
|
+
assertWorkspaceInputGraph(localRaw, 'Local Bulk-session store', { maximumBytes: MAX_BULK_SESSION_STORE_BYTES * 2 });
|
|
677
|
+
assertWorkspaceInputGraph(importedRaw, 'Imported Bulk-session document', { maximumBytes: MAX_BULK_SESSION_STORE_BYTES * 2 });
|
|
601
678
|
assertWorkspacePortableVersion(importedRaw, BULK_SESSION_SCHEMA_VERSION, 'Imported Bulk-session document');
|
|
602
679
|
const imported = record(importedRaw);
|
|
603
680
|
if (!imported || imported.schema !== BULK_SESSION_SCHEMA || !Array.isArray(imported.sessions)) {
|
|
@@ -614,7 +691,7 @@ export function mergeBulkSessions(localRaw, importedRaw) {
|
|
|
614
691
|
let updated = 0;
|
|
615
692
|
let skipped = 0;
|
|
616
693
|
for (const candidate of imported.sessions.slice(0, MAX_BULK_SESSIONS * 4)) {
|
|
617
|
-
const normalized = normalizeBulkSession(candidate);
|
|
694
|
+
const normalized = normalizeBulkSession(candidate, Number(imported.version));
|
|
618
695
|
if (!normalized) {
|
|
619
696
|
skipped += 1;
|
|
620
697
|
continue;
|
|
@@ -649,8 +726,16 @@ export function mergeBulkSessions(localRaw, importedRaw) {
|
|
|
649
726
|
skipped += 1;
|
|
650
727
|
continue;
|
|
651
728
|
}
|
|
729
|
+
// An import is additive: count and byte pressure must not silently remove
|
|
730
|
+
// a local session (or an earlier admitted import). The preview reports
|
|
731
|
+
// the skipped candidate and the analyst can export/remove data explicitly.
|
|
732
|
+
const merged = upsertBulkSession(sessions, session);
|
|
733
|
+
if (merged.pruned > 0 || !merged.sessions.some((item) => item.id === session.id)) {
|
|
734
|
+
skipped += 1;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
652
737
|
added += 1;
|
|
653
|
-
sessions =
|
|
738
|
+
sessions = merged.sessions;
|
|
654
739
|
}
|
|
655
740
|
const bounded = enforceBulkSessionStoreBudget({
|
|
656
741
|
schema: BULK_SESSION_SCHEMA,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const BULK_SORT_KEYS = ['domain', 'availability', 'confidence', 'risk', 'opportunity', 'activity', 'registrar', 'mutation'];
|
|
1
2
|
/** Opportunity remains a retained data/export field, but is no longer a Bulk presentation sort. */
|
|
2
3
|
export function normalizeBulkPresentationSortKey(key) {
|
|
3
4
|
return key === 'opportunity' ? 'risk' : key;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure, framework-neutral browser-local campaign model. Campaigns deliberately
|
|
2
2
|
// reference normalized case domains rather than copying case evidence, notes,
|
|
3
3
|
// or mutable case ids. The browser wrapper owns persistence and downloads.
|
|
4
|
-
import { normalizeDomain } from "../
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
5
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
7
|
import { CAMPAIGN_SCHEMA, CAMPAIGN_SCHEMA_VERSION, MAX_CAMPAIGNS, MAX_CAMPAIGN_DESCRIPTION_LENGTH, MAX_CAMPAIGN_DOMAINS, MAX_CAMPAIGN_INPUT_RECORDS, MAX_CAMPAIGN_NAME_LENGTH, MAX_CAMPAIGN_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CASE_VIEWS_SCHEMA, CASE_VIEWS_VERSION, CASE_VIEW_SORTS, MAX_CASE_VIEWS, MAX_CASE_VIEWS_BYTES, MAX_CASE_VIEW_NAME_LENGTH, MAX_CASE_VIEW_SEARCH_LENGTH, } from "../contracts/case-views-contract.mjs";
|
|
2
|
+
import { isValidDisposition, isValidStatus } from "../cases/case-record-decisions.mjs";
|
|
3
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
|
+
import { assertWorkspaceInputGraph, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const control = /[\u0000-\u001f\u007f]/u;
|
|
7
|
+
function object(raw, keys, label) {
|
|
8
|
+
const value = ordinaryWorkspaceRecord(raw, label);
|
|
9
|
+
if (!value || Object.keys(value).length !== keys.length || Object.keys(value).some(key => !keys.includes(key)))
|
|
10
|
+
throw new Error(`${label} has missing or unsupported fields.`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function text(raw, maximum, label) {
|
|
14
|
+
if (typeof raw !== 'string' || raw.length > maximum || control.test(raw))
|
|
15
|
+
throw new Error(`${label} is invalid or exceeds ${maximum} characters.`);
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
function time(raw) {
|
|
19
|
+
const value = normalizeExplicitIsoTimestamp(raw);
|
|
20
|
+
if (!value)
|
|
21
|
+
throw new Error('A saved Case view has an invalid timestamp.');
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeCaseViewFilters(raw) {
|
|
25
|
+
const value = object(raw, ['status', 'disposition', 'search', 'sort'], 'Case filters');
|
|
26
|
+
if (value.status !== '' && !isValidStatus(value.status))
|
|
27
|
+
throw new Error('Unsupported Case status filter.');
|
|
28
|
+
if (value.disposition !== '' && !isValidDisposition(value.disposition))
|
|
29
|
+
throw new Error('Unsupported Case disposition filter.');
|
|
30
|
+
const sort = CASE_VIEW_SORTS.find(option => option.value === value.sort)?.value;
|
|
31
|
+
if (!sort)
|
|
32
|
+
throw new Error('Unsupported Case sort order.');
|
|
33
|
+
return { status: value.status, disposition: value.disposition, search: text(value.search, MAX_CASE_VIEW_SEARCH_LENGTH, 'Case search'), sort };
|
|
34
|
+
}
|
|
35
|
+
export function emptyCaseViewsStore() {
|
|
36
|
+
return { schema: CASE_VIEWS_SCHEMA, version: CASE_VIEWS_VERSION, views: [] };
|
|
37
|
+
}
|
|
38
|
+
export function caseViewsStoreVersion(raw) {
|
|
39
|
+
const value = ordinaryWorkspaceRecord(raw, 'Case views');
|
|
40
|
+
return value?.schema === CASE_VIEWS_SCHEMA && Number.isSafeInteger(value.version) ? value.version : null;
|
|
41
|
+
}
|
|
42
|
+
/** Strict, lossless admission: invalid views and capacity excess never prune saved work. */
|
|
43
|
+
export function normalizeCaseViewsStore(raw) {
|
|
44
|
+
if (raw === null || raw === undefined)
|
|
45
|
+
return emptyCaseViewsStore();
|
|
46
|
+
assertWorkspaceInputGraph(raw, 'Saved Case views', { maximumBytes: MAX_CASE_VIEWS_BYTES });
|
|
47
|
+
const store = object(raw, ['schema', 'version', 'views'], 'Case views');
|
|
48
|
+
if (store.schema !== CASE_VIEWS_SCHEMA || store.version !== CASE_VIEWS_VERSION)
|
|
49
|
+
throw new Error('Unsupported saved Case view format. No views were changed.');
|
|
50
|
+
if (!Array.isArray(store.views) || store.views.length > MAX_CASE_VIEWS)
|
|
51
|
+
throw new Error(`Keep at most ${MAX_CASE_VIEWS} saved Case views. Delete unwanted views explicitly before adding more.`);
|
|
52
|
+
const ids = new Set();
|
|
53
|
+
const views = [];
|
|
54
|
+
let bytes = encoder.encode(JSON.stringify(emptyCaseViewsStore())).byteLength;
|
|
55
|
+
for (const candidate of store.views) {
|
|
56
|
+
const value = object(candidate, ['id', 'name', 'filters', 'createdAt', 'updatedAt'], 'Saved Case view');
|
|
57
|
+
if (typeof value.id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/u.test(value.id) || ids.has(value.id))
|
|
58
|
+
throw new Error('Invalid or duplicate saved Case view identity.');
|
|
59
|
+
ids.add(value.id);
|
|
60
|
+
const name = text(value.name, MAX_CASE_VIEW_NAME_LENGTH, 'Case view name').trim();
|
|
61
|
+
if (!name)
|
|
62
|
+
throw new Error('Name this Case view.');
|
|
63
|
+
const createdAt = time(value.createdAt);
|
|
64
|
+
const updatedAt = time(value.updatedAt);
|
|
65
|
+
if (Date.parse(updatedAt) < Date.parse(createdAt))
|
|
66
|
+
throw new Error('A saved Case view update precedes its creation.');
|
|
67
|
+
const view = { id: value.id, name, filters: normalizeCaseViewFilters(value.filters), createdAt, updatedAt };
|
|
68
|
+
bytes += encoder.encode(JSON.stringify(view)).byteLength + (views.length ? 1 : 0);
|
|
69
|
+
if (bytes > MAX_CASE_VIEWS_BYTES)
|
|
70
|
+
throw new Error('Saved Case views exceed their storage limit. Existing views were not removed.');
|
|
71
|
+
views.push(view);
|
|
72
|
+
}
|
|
73
|
+
return { ...emptyCaseViewsStore(), views };
|
|
74
|
+
}
|
|
75
|
+
export function serializeCaseViewsStore(raw) {
|
|
76
|
+
return JSON.stringify(normalizeCaseViewsStore(raw));
|
|
77
|
+
}
|
|
78
|
+
export function serialiseCaseViewsJson(raw) {
|
|
79
|
+
return `${JSON.stringify(normalizeCaseViewsStore(raw), null, 2)}\n`;
|
|
80
|
+
}
|
|
81
|
+
export function mergeCaseViews(local, incoming) {
|
|
82
|
+
const current = normalizeCaseViewsStore(local);
|
|
83
|
+
const imported = normalizeCaseViewsStore(incoming);
|
|
84
|
+
const views = new Map(current.views.map(view => [view.id, view]));
|
|
85
|
+
let added = 0, updated = 0, skipped = 0;
|
|
86
|
+
for (const view of imported.views) {
|
|
87
|
+
const existing = views.get(view.id);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
views.set(view.id, view);
|
|
90
|
+
added += 1;
|
|
91
|
+
}
|
|
92
|
+
else if (JSON.stringify(existing) === JSON.stringify(view))
|
|
93
|
+
continue;
|
|
94
|
+
else if (Date.parse(view.updatedAt) > Date.parse(existing.updatedAt) && view.createdAt === existing.createdAt) {
|
|
95
|
+
views.set(view.id, view);
|
|
96
|
+
updated += 1;
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
skipped += 1;
|
|
100
|
+
}
|
|
101
|
+
return { store: normalizeCaseViewsStore({ ...current, views: [...views.values()] }), added, updated, skipped };
|
|
102
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure browser-local custom detection-rule model. Rules are deliberately a
|
|
2
2
|
// small structured language: field names and operators come from allowlists,
|
|
3
3
|
// so imported rules cannot execute code or reach outside bounded case evidence.
|
|
4
|
-
import { latestCaseEvidence } from "../cases/case-model.mjs";
|
|
4
|
+
import { latestCaseEvidence } from "../cases/case-evidence-model.mjs";
|
|
5
5
|
import { CASE_DISPOSITIONS, CASE_STATUSES } from "../cases/case-record-contracts.mjs";
|
|
6
6
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
7
7
|
import { DETECTION_RULE_SCHEMA, DETECTION_RULE_SCHEMA_VERSION, MAX_CONDITION_VALUE_LENGTH, MAX_CUSTOM_RISK_TOTAL, MAX_DETECTION_RULES, MAX_RULE_CONDITIONS, MAX_RULE_INPUT_RECORDS, MAX_RULE_NAME_LENGTH, MAX_RULE_RISK_DELTA, MAX_RULE_STORE_BYTES, MAX_RULE_TAG_LENGTH, } from "../contracts/workspace-portability.mjs";
|