@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,4 +1,5 @@
|
|
|
1
1
|
import { MAX_WORKSPACE_INPUT_ARRAY_LENGTH, MAX_WORKSPACE_INPUT_GRAPH_DEPTH, MAX_WORKSPACE_INPUT_GRAPH_NODES, MAX_WORKSPACE_INPUT_OBJECT_KEYS, MAX_WORKSPACE_INPUT_STRING_CODE_UNITS, } from "../contracts/workspace-portability.mjs";
|
|
2
|
+
import { boundedJsonLimitsForBytes } from "../analysis/bounded-json.mjs";
|
|
2
3
|
function inputError(label, detail) {
|
|
3
4
|
return new TypeError(`${label} must be bounded ordinary JSON data; ${detail}.`);
|
|
4
5
|
}
|
|
@@ -27,9 +28,7 @@ function ownDataDescriptors(value, label) {
|
|
|
27
28
|
return [key, descriptor];
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
32
|
-
return null;
|
|
31
|
+
function ordinaryRecordDescriptors(value, label) {
|
|
33
32
|
let prototype;
|
|
34
33
|
try {
|
|
35
34
|
prototype = Object.getPrototypeOf(value);
|
|
@@ -47,19 +46,33 @@ export function ordinaryWorkspaceRecord(value, label = 'Workspace input') {
|
|
|
47
46
|
if (descriptors.some(([, descriptor]) => !descriptor.enumerable)) {
|
|
48
47
|
throw inputError(label, 'non-enumerable object fields are not supported');
|
|
49
48
|
}
|
|
49
|
+
return descriptors;
|
|
50
|
+
}
|
|
51
|
+
export function ordinaryWorkspaceRecord(value, label = 'Workspace input') {
|
|
52
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
53
|
+
return null;
|
|
54
|
+
ordinaryRecordDescriptors(value, label);
|
|
50
55
|
return value;
|
|
51
56
|
}
|
|
52
|
-
export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
|
|
57
|
+
export function assertWorkspaceInputGraph(value, label = 'Workspace input', options = {}) {
|
|
58
|
+
const limits = options.maximumBytes === undefined ? null : boundedJsonLimitsForBytes(options.maximumBytes);
|
|
59
|
+
const maximumStringCodeUnits = limits?.maximumStringCodeUnits ?? MAX_WORKSPACE_INPUT_STRING_CODE_UNITS;
|
|
53
60
|
const pending = [{ value, depth: 0, label }];
|
|
54
61
|
const seen = new WeakSet();
|
|
55
62
|
let nodes = 0;
|
|
63
|
+
let values = 0;
|
|
64
|
+
let keys = 0;
|
|
56
65
|
let stringCodeUnits = 0;
|
|
57
66
|
while (pending.length) {
|
|
58
67
|
const current = pending.pop();
|
|
59
68
|
const candidate = current.value;
|
|
69
|
+
values += 1;
|
|
70
|
+
if (limits && values > limits.maximumValues) {
|
|
71
|
+
throw inputError(label, 'the graph exceeds the byte-budgeted value ceiling');
|
|
72
|
+
}
|
|
60
73
|
if (typeof candidate === 'string') {
|
|
61
74
|
stringCodeUnits += candidate.length;
|
|
62
|
-
if (stringCodeUnits >
|
|
75
|
+
if (stringCodeUnits > maximumStringCodeUnits) {
|
|
63
76
|
throw inputError(label, 'aggregate text exceeds the string ceiling');
|
|
64
77
|
}
|
|
65
78
|
continue;
|
|
@@ -76,7 +89,7 @@ export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
|
|
|
76
89
|
throw inputError(label, 'the nesting depth exceeds the graph ceiling');
|
|
77
90
|
}
|
|
78
91
|
nodes += 1;
|
|
79
|
-
if (nodes > MAX_WORKSPACE_INPUT_GRAPH_NODES) {
|
|
92
|
+
if (!limits && nodes > MAX_WORKSPACE_INPUT_GRAPH_NODES) {
|
|
80
93
|
throw inputError(label, 'the graph exceeds the node ceiling');
|
|
81
94
|
}
|
|
82
95
|
if (seen.has(candidate))
|
|
@@ -118,8 +131,16 @@ export function assertWorkspaceInputGraph(value, label = 'Workspace input') {
|
|
|
118
131
|
}
|
|
119
132
|
continue;
|
|
120
133
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
134
|
+
const descriptors = ordinaryRecordDescriptors(candidate, current.label);
|
|
135
|
+
if (limits) {
|
|
136
|
+
keys += descriptors.length;
|
|
137
|
+
if (keys > limits.maximumKeys)
|
|
138
|
+
throw inputError(label, 'the graph exceeds the byte-budgeted key ceiling');
|
|
139
|
+
stringCodeUnits += descriptors.reduce((total, [key]) => total + key.length, 0);
|
|
140
|
+
if (stringCodeUnits > maximumStringCodeUnits) {
|
|
141
|
+
throw inputError(label, 'aggregate text exceeds the string ceiling');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
123
144
|
for (const [key, descriptor] of descriptors) {
|
|
124
145
|
pending.push({
|
|
125
146
|
value: descriptor.value,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { parse } from 'tldts';
|
|
3
3
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
4
|
export const INVESTIGATION_GUIDE_SCHEMA = 'whoisleuth.investigation-recipe';
|
|
@@ -13,7 +13,7 @@ export const MAX_INVESTIGATION_GUIDE_TIMESTAMP_LENGTH = 64;
|
|
|
13
13
|
export const MAX_INVESTIGATION_GUIDE_SERIALIZED_BYTES = 12_288;
|
|
14
14
|
export const MAX_INVESTIGATION_GUIDE_EXPORT_BYTES = 16_384;
|
|
15
15
|
const CONTROL_RE = /[\x00-\x1f\x7f]/u;
|
|
16
|
-
const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
16
|
+
export const SAFE_TEMPLATE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
17
17
|
const GUIDE_STATUSES = new Set(['active', 'paused']);
|
|
18
18
|
const GUIDE_OUTCOMES = new Set(['pending', 'complete', 'partial', 'skipped']);
|
|
19
19
|
function stage(id, workspace, label, detail, expectedEvidence, requestImpact, prerequisite, completionCriteria, instructions, requiresApproval) {
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, } from "./investigation-guide.mjs";
|
|
1
|
+
import { INVESTIGATION_RECIPES, normalizeInvestigationGuideTemplateSnapshot, SAFE_TEMPLATE_ID_RE, } from "./investigation-guide.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
3
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
4
|
+
import { exact, HEX_DIGEST_RE, text } from "../evidence/artifact-structure.mjs";
|
|
5
|
+
import { SORTED_JSON_V2 } from "../evidence/artifact-integrity.mjs";
|
|
4
6
|
import { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
5
7
|
export { INVESTIGATION_TEMPLATE_SCHEMA, INVESTIGATION_TEMPLATE_SUPPORTED_VERSIONS, INVESTIGATION_TEMPLATE_VERSION, MAX_INVESTIGATION_TEMPLATES, MAX_INVESTIGATION_TEMPLATE_IMPORT_BYTES, MAX_INVESTIGATION_TEMPLATE_STORE_BYTES, } from "../contracts/workspace-portability.mjs";
|
|
8
|
+
export function readTemplateLessonRevision(raw) {
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
assertWorkspaceInputGraph(raw, 'Template revision provenance');
|
|
12
|
+
const value = exact(raw, ['parentTemplateId', 'parentContentSha256', 'lessonContentSha256', 'canonicalization', 'applicability', 'rationale'], 'Template revision provenance');
|
|
13
|
+
if (typeof value.parentTemplateId !== 'string' || !SAFE_TEMPLATE_ID_RE.test(value.parentTemplateId)
|
|
14
|
+
|| value.canonicalization !== SORTED_JSON_V2
|
|
15
|
+
|| typeof value.parentContentSha256 !== 'string' || !HEX_DIGEST_RE.test(value.parentContentSha256)
|
|
16
|
+
|| typeof value.lessonContentSha256 !== 'string' || !HEX_DIGEST_RE.test(value.lessonContentSha256))
|
|
17
|
+
throw new TypeError('Template revision provenance is invalid.');
|
|
18
|
+
const applicability = text(value.applicability, 'Template applicability', 400).trim();
|
|
19
|
+
const rationale = text(value.rationale, 'Template revision reason', 400).trim();
|
|
20
|
+
if (!applicability || !rationale)
|
|
21
|
+
throw new TypeError('Template applicability and revision reason must not be empty.');
|
|
22
|
+
return { parentTemplateId: value.parentTemplateId, parentContentSha256: value.parentContentSha256, lessonContentSha256: value.lessonContentSha256,
|
|
23
|
+
canonicalization: SORTED_JSON_V2, applicability, rationale };
|
|
24
|
+
}
|
|
6
25
|
function record(value) {
|
|
7
26
|
return ordinaryWorkspaceRecord(value, 'Investigation-template input');
|
|
8
27
|
}
|
|
@@ -17,11 +36,13 @@ function template(raw) {
|
|
|
17
36
|
const snapshot = normalizeInvestigationGuideTemplateSnapshot(value);
|
|
18
37
|
const createdAt = timestamp(value?.createdAt);
|
|
19
38
|
const updatedAt = timestamp(value?.updatedAt);
|
|
39
|
+
const lessonRevision = readTemplateLessonRevision(value?.lessonRevision);
|
|
20
40
|
return snapshot && createdAt && updatedAt
|
|
21
|
-
? { ...snapshot, createdAt, updatedAt }
|
|
41
|
+
? { ...snapshot, createdAt, updatedAt, ...(lessonRevision ? { lessonRevision } : {}) }
|
|
22
42
|
: null;
|
|
23
43
|
}
|
|
24
44
|
export function normalizeInvestigationTemplate(raw) {
|
|
45
|
+
assertWorkspaceInputGraph(raw, 'Investigation template');
|
|
25
46
|
return template(raw);
|
|
26
47
|
}
|
|
27
48
|
export function normalizeInvestigationTemplateStore(raw) {
|
|
@@ -33,6 +54,9 @@ export function normalizeInvestigationTemplateStore(raw) {
|
|
|
33
54
|
throw new Error(`Investigation-template schema ${String(value.version)} is unsupported; no data was changed.`);
|
|
34
55
|
}
|
|
35
56
|
const source = Array.isArray(raw) ? raw : Array.isArray(value?.templates) ? value.templates : [];
|
|
57
|
+
if (value?.version === 2 && source.some(candidate => record(candidate)?.lessonRevision !== undefined)) {
|
|
58
|
+
throw new Error('Template revision provenance requires schema 3; no data was changed.');
|
|
59
|
+
}
|
|
36
60
|
const templates = [];
|
|
37
61
|
const seen = new Set();
|
|
38
62
|
for (const candidate of source.slice(0, MAX_INVESTIGATION_TEMPLATES * 2)) {
|
|
@@ -85,6 +109,9 @@ export function saveInvestigationTemplate(localRaw, candidateRaw) {
|
|
|
85
109
|
if (!candidate)
|
|
86
110
|
throw new Error('The investigation template is incomplete or invalid.');
|
|
87
111
|
const local = normalizeInvestigationTemplateStore(localRaw).templates;
|
|
112
|
+
if (!local.some((item) => item.id === candidate.id) && local.length >= MAX_INVESTIGATION_TEMPLATES) {
|
|
113
|
+
throw new Error(`Investigation-template storage is full (${MAX_INVESTIGATION_TEMPLATES} templates). Export and remove a template before saving another; no templates were changed.`);
|
|
114
|
+
}
|
|
88
115
|
return normalizeInvestigationTemplateStore([
|
|
89
116
|
candidate,
|
|
90
117
|
...local.filter((item) => item.id !== candidate.id),
|
|
@@ -126,11 +153,23 @@ export function mergeInvestigationTemplates(localRaw, incomingRaw) {
|
|
|
126
153
|
const byId = new Map(local.map((item) => [item.id, item]));
|
|
127
154
|
let added = 0;
|
|
128
155
|
let updated = 0;
|
|
156
|
+
let skipped = 0;
|
|
129
157
|
for (const item of incoming) {
|
|
130
|
-
|
|
158
|
+
const existing = byId.get(item.id);
|
|
159
|
+
if (existing) {
|
|
160
|
+
if (item.updatedAt <= existing.updatedAt) {
|
|
161
|
+
skipped += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
131
164
|
updated += 1;
|
|
132
|
-
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
if (byId.size >= MAX_INVESTIGATION_TEMPLATES) {
|
|
168
|
+
skipped += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
133
171
|
added += 1;
|
|
172
|
+
}
|
|
134
173
|
byId.set(item.id, item);
|
|
135
174
|
}
|
|
136
175
|
const templates = normalizeInvestigationTemplateStore([...byId.values()]).templates;
|
|
@@ -138,7 +177,7 @@ export function mergeInvestigationTemplates(localRaw, incomingRaw) {
|
|
|
138
177
|
templates,
|
|
139
178
|
added,
|
|
140
179
|
updated,
|
|
141
|
-
skipped
|
|
142
|
-
pruned:
|
|
180
|
+
skipped,
|
|
181
|
+
pruned: 0,
|
|
143
182
|
};
|
|
144
183
|
}
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// deliberately accepts only bounded, normalized page-identity fields. Raw
|
|
3
3
|
// HTML, URLs, headers, redirect inventories, and parser diagnostics never
|
|
4
4
|
// cross this storage boundary.
|
|
5
|
-
import { normalizeDomain } from "../
|
|
6
|
-
import { isInformativePerceptualHash as isInformativeFaviconHash } from "
|
|
5
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
6
|
+
import { isInformativePerceptualHash as isInformativeFaviconHash } from "../analysis/perceptual-hash-comparison.mjs";
|
|
7
7
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import { PAGE_FINGERPRINT_TOKEN_LIMITS } from "../contracts/page-fingerprints.mjs";
|
|
9
|
+
import { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_PARSERS, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
10
|
+
export { MAX_BASELINE_IDENTIFIERS, MAX_BASELINE_RESOURCE_HOSTS, MAX_BASELINE_TITLE_LENGTH, PAGE_BASELINE_VERSION, PAGE_FINGERPRINT_VERSION, PAGE_FINGERPRINT_PARSERS, PAGE_IDENTITY_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
10
11
|
const SHA256_RE = /^[a-f0-9]{64}$/i;
|
|
11
12
|
const SIMHASH_RE = /^[a-f0-9]{16}$/i;
|
|
12
13
|
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
13
|
-
const MAX_HTML_TOKENS = 4096;
|
|
14
14
|
const MAX_TEXT_TOKENS = 8192;
|
|
15
15
|
const MAX_FORMS = 50;
|
|
16
16
|
const MAX_FORM_CONTROLS = 500;
|
|
@@ -50,12 +50,12 @@ function shaComponent(raw, countKey, maximum) {
|
|
|
50
50
|
truncated: value.truncated === true,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
function visibleTextComponent(raw) {
|
|
53
|
+
function visibleTextComponent(raw, maximum = MAX_TEXT_TOKENS) {
|
|
54
54
|
const value = record(raw);
|
|
55
55
|
if (!value || value.algorithm !== 'simhash64-v1' || typeof value.value !== 'string' || !SIMHASH_RE.test(value.value))
|
|
56
56
|
return null;
|
|
57
|
-
const tokenCount = count(value.tokenCount,
|
|
58
|
-
const featureCount = count(value.featureCount,
|
|
57
|
+
const tokenCount = count(value.tokenCount, maximum);
|
|
58
|
+
const featureCount = count(value.featureCount, maximum);
|
|
59
59
|
if (tokenCount === null || featureCount === null)
|
|
60
60
|
return null;
|
|
61
61
|
return {
|
|
@@ -66,20 +66,21 @@ function visibleTextComponent(raw) {
|
|
|
66
66
|
truncated: value.truncated === true,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
-
function domComponent(raw) {
|
|
69
|
+
function domComponent(raw, fingerprintVersion) {
|
|
70
70
|
const value = record(raw);
|
|
71
|
-
if (!value || value.algorithm !== 'sha256' || value.parser !==
|
|
71
|
+
if (!value || value.algorithm !== 'sha256' || value.parser !== PAGE_FINGERPRINT_PARSERS[fingerprintVersion])
|
|
72
72
|
return null;
|
|
73
73
|
const digest = sha256(value.value);
|
|
74
|
-
const
|
|
74
|
+
const maximum = PAGE_FINGERPRINT_TOKEN_LIMITS[fingerprintVersion];
|
|
75
|
+
const nodeCount = count(value.nodeCount, maximum);
|
|
75
76
|
if (!digest || nodeCount === null)
|
|
76
77
|
return null;
|
|
77
|
-
const similarity = value.similarity == null ? null : visibleTextComponent(value.similarity);
|
|
78
|
+
const similarity = value.similarity == null ? null : visibleTextComponent(value.similarity, maximum);
|
|
78
79
|
return {
|
|
79
80
|
algorithm: 'sha256',
|
|
80
81
|
value: digest,
|
|
81
82
|
nodeCount,
|
|
82
|
-
parser:
|
|
83
|
+
parser: PAGE_FINGERPRINT_PARSERS[fingerprintVersion],
|
|
83
84
|
truncated: value.truncated === true,
|
|
84
85
|
...(value.similarity == null ? {} : { similarity }),
|
|
85
86
|
};
|
|
@@ -175,11 +176,13 @@ export function normalizePageBaseline(raw) {
|
|
|
175
176
|
const lookupDomain = normalizeDomain(value.lookupDomain);
|
|
176
177
|
const observedAt = timestamp(value.observedAt);
|
|
177
178
|
const pageIdentityVersion = count(value.pageIdentityVersion, PAGE_IDENTITY_VERSION);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
179
|
+
if (typeof value.fingerprintVersion !== 'number' || !Object.hasOwn(PAGE_FINGERPRINT_PARSERS, value.fingerprintVersion))
|
|
180
|
+
return null;
|
|
181
|
+
const fingerprintVersion = value.fingerprintVersion;
|
|
182
|
+
const normalizedHtml = shaComponent(value.normalizedHtml, 'tokenCount', PAGE_FINGERPRINT_TOKEN_LIMITS[fingerprintVersion]);
|
|
183
|
+
const domStructure = domComponent(value.domStructure, fingerprintVersion);
|
|
181
184
|
if (!domain || !lookupDomain || !observedAt || pageIdentityVersion !== PAGE_IDENTITY_VERSION
|
|
182
|
-
||
|
|
185
|
+
|| !normalizedHtml || !domStructure)
|
|
183
186
|
return null;
|
|
184
187
|
const visibleText = value.visibleText == null ? null : visibleTextComponent(value.visibleText);
|
|
185
188
|
const formStructure = value.formStructure == null ? null : formComponent(value.formStructure);
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { normalizeDomain } from "../
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
2
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
3
|
+
import { RELATIONSHIP_EVIDENCE_VERSION, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, RELATIONSHIP_PUBLIC_ADDRESS_VERSION } from "../contracts/offline-comparison.mjs";
|
|
4
|
+
import { RELATIONSHIP_TYPES, qualifyRelationshipSources, isNonPublicAddressRelationship, NON_PUBLIC_RELATIONSHIP_LIMITATION, FAVICON_RELATIONSHIP_DESCRIPTION } from "../comparison/relationship-provenance.mjs";
|
|
3
5
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
4
6
|
import { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
5
7
|
export { MAX_RELATIONSHIP_OBSERVATIONS, MAX_RELATIONSHIP_OBSERVATION_DOMAINS, MAX_RELATIONSHIP_OBSERVATION_INPUTS, MAX_RELATIONSHIP_OBSERVATION_LIMITATIONS, MAX_RELATIONSHIP_OBSERVATION_STORE_BYTES, MAX_RELATIONSHIP_OBSERVATION_VALUE_LENGTH, RELATIONSHIP_OBSERVATION_SCHEMA, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, } from "../contracts/workspace-portability.mjs";
|
|
6
|
-
export const RELATIONSHIP_OBSERVATION_TYPES =
|
|
7
|
-
'nameserver_set',
|
|
8
|
-
'ip_address',
|
|
9
|
-
'certificate',
|
|
10
|
-
'tracking_identifier',
|
|
11
|
-
'favicon',
|
|
12
|
-
'official_asset',
|
|
13
|
-
]);
|
|
8
|
+
export const RELATIONSHIP_OBSERVATION_TYPES = RELATIONSHIP_TYPES;
|
|
14
9
|
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
15
10
|
const SAFE_ID_RE = /^relationship-[a-z0-9]{1,16}-[a-z0-9]{1,16}$/u;
|
|
16
11
|
const SHA256_RE = /^[a-f0-9]{64}$/iu;
|
|
@@ -39,9 +34,9 @@ const TYPE_METADATA = Object.freeze({
|
|
|
39
34
|
description: 'These pages exposed the same recognised public tracking identifier in bounded static HTML.',
|
|
40
35
|
}),
|
|
41
36
|
favicon: Object.freeze({
|
|
42
|
-
label: '
|
|
37
|
+
label: 'Connected favicon matches',
|
|
43
38
|
method: 'Exact SHA-256 or perceptual dHash distance ≤ 6',
|
|
44
|
-
description:
|
|
39
|
+
description: FAVICON_RELATIONSHIP_DESCRIPTION,
|
|
45
40
|
}),
|
|
46
41
|
official_asset: Object.freeze({
|
|
47
42
|
label: 'Official asset host match',
|
|
@@ -205,7 +200,7 @@ export function relationshipObservationId(raw) {
|
|
|
205
200
|
}
|
|
206
201
|
export function relationshipObservationStoreVersion(raw) {
|
|
207
202
|
const value = record(raw);
|
|
208
|
-
return
|
|
203
|
+
return typeof value?.version === 'number' && Number.isSafeInteger(value.version) && value.version > 0 ? value.version : null;
|
|
209
204
|
}
|
|
210
205
|
function observationList(raw) {
|
|
211
206
|
if (Array.isArray(raw))
|
|
@@ -213,7 +208,7 @@ function observationList(raw) {
|
|
|
213
208
|
const value = record(raw);
|
|
214
209
|
return Array.isArray(value?.observations) ? value.observations : [];
|
|
215
210
|
}
|
|
216
|
-
export function normalizeRelationshipObservation(raw) {
|
|
211
|
+
export function normalizeRelationshipObservation(raw, sourceStoreVersion = RELATIONSHIP_OBSERVATION_SCHEMA_VERSION) {
|
|
217
212
|
const value = record(raw);
|
|
218
213
|
if (!value || typeof value.type !== 'string' || !TYPES.has(value.type))
|
|
219
214
|
return null;
|
|
@@ -230,34 +225,50 @@ export function normalizeRelationshipObservation(raw) {
|
|
|
230
225
|
const id = observationId(canonical);
|
|
231
226
|
const observedAt = timestamp(value.observedAt);
|
|
232
227
|
const retainedAt = timestamp(value.retainedAt);
|
|
233
|
-
if (!
|
|
228
|
+
if (!retainedAt || (value.observedAt !== null && !observedAt))
|
|
234
229
|
return null;
|
|
230
|
+
const sourceVersion = positiveInteger(value.sourceVersion) || 1;
|
|
231
|
+
if (value.sourceVersion !== undefined && !positiveInteger(value.sourceVersion))
|
|
232
|
+
throw new TypeError('This retained relationship uses an unsupported source-evidence version; no evidence was interpreted.');
|
|
233
|
+
if (sourceVersion > RELATIONSHIP_EVIDENCE_VERSION)
|
|
234
|
+
throw new TypeError('This retained relationship uses a newer source-evidence version; no evidence was interpreted.');
|
|
235
|
+
if (sourceStoreVersion === 1 && sourceVersion > 2)
|
|
236
|
+
throw new TypeError('Relationship schema 1 cannot contain newer source evidence; no evidence was interpreted.');
|
|
237
|
+
const qualified = qualifyRelationshipSources(sourceVersion >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? value.sourceEvidence : [], domains, { truncated: value.truncated === true, type });
|
|
238
|
+
const nonPublicAddress = isNonPublicAddressRelationship(type, normalizedValue);
|
|
235
239
|
return {
|
|
236
240
|
id,
|
|
237
241
|
type,
|
|
238
|
-
label: metadata.label,
|
|
242
|
+
label: nonPublicAddress ? 'Shared non-public DNS answer' : metadata.label,
|
|
239
243
|
method: metadata.method,
|
|
240
244
|
normalizedValue,
|
|
241
245
|
displayValue: type === 'favicon' ? '' : normalizedValue,
|
|
242
246
|
domains,
|
|
243
|
-
description: metadata.description,
|
|
247
|
+
description: nonPublicAddress ? NON_PUBLIC_RELATIONSHIP_LIMITATION : metadata.description,
|
|
244
248
|
classification: 'derived',
|
|
245
249
|
source: 'bulk_relationship_analysis',
|
|
246
|
-
sourceVersion
|
|
247
|
-
|
|
250
|
+
sourceVersion,
|
|
251
|
+
// Legacy scan timestamps remain available, but cannot qualify missing
|
|
252
|
+
// contributing-source timestamps or turn the old complete flag into proof.
|
|
253
|
+
observedAt: qualified.observedAt ?? (observedAt || null),
|
|
254
|
+
sourceEvidence: qualified.sourceEvidence,
|
|
248
255
|
retainedAt,
|
|
249
|
-
complete: value.complete === true,
|
|
250
|
-
truncated:
|
|
256
|
+
complete: value.complete === true && qualified.complete,
|
|
257
|
+
truncated: qualified.truncated
|
|
251
258
|
|| (Array.isArray(value.domains) && value.domains.length > MAX_RELATIONSHIP_OBSERVATION_DOMAINS),
|
|
252
|
-
limitations: observationLimitations(value.limitations),
|
|
259
|
+
limitations: observationLimitations([...(nonPublicAddress ? [NON_PUBLIC_RELATIONSHIP_LIMITATION] : []), ...(Array.isArray(value.limitations) ? value.limitations : [])]),
|
|
253
260
|
};
|
|
254
261
|
}
|
|
255
262
|
export function normalizeRelationshipObservationStore(raw) {
|
|
256
263
|
assertWorkspaceInputGraph(raw, 'Relationship-observation store');
|
|
257
264
|
assertWorkspaceDeclaredVersion(raw, 'Relationship-observation store');
|
|
265
|
+
const sourceStoreVersion = relationshipObservationStoreVersion(raw) ?? RELATIONSHIP_OBSERVATION_SCHEMA_VERSION;
|
|
266
|
+
if (sourceStoreVersion > RELATIONSHIP_OBSERVATION_SCHEMA_VERSION) {
|
|
267
|
+
throw new TypeError(`This relationship-observation store uses newer schema ${sourceStoreVersion}; no evidence was interpreted.`);
|
|
268
|
+
}
|
|
258
269
|
const byId = new Map();
|
|
259
270
|
for (const candidate of observationList(raw).slice(0, MAX_RELATIONSHIP_OBSERVATION_INPUTS)) {
|
|
260
|
-
const observation = normalizeRelationshipObservation(candidate);
|
|
271
|
+
const observation = normalizeRelationshipObservation(candidate, sourceStoreVersion);
|
|
261
272
|
if (!observation)
|
|
262
273
|
continue;
|
|
263
274
|
const existing = byId.get(observation.id);
|
|
@@ -283,31 +294,40 @@ export function createRelationshipObservation(raw, options = {}) {
|
|
|
283
294
|
throw new Error('That relationship does not contain a supported bounded value and at least one valid domain.');
|
|
284
295
|
}
|
|
285
296
|
const now = new Date().toISOString();
|
|
286
|
-
const
|
|
297
|
+
const sourceVersion = Object.hasOwn(options, 'sourceVersion') ? positiveInteger(options.sourceVersion) : RELATIONSHIP_EVIDENCE_VERSION;
|
|
298
|
+
if (!sourceVersion || sourceVersion > RELATIONSHIP_EVIDENCE_VERSION)
|
|
299
|
+
throw new TypeError('This relationship needs a supported source-evidence version.');
|
|
300
|
+
const nonPublicAddress = isNonPublicAddressRelationship(type, normalizedValue);
|
|
301
|
+
if (nonPublicAddress && sourceVersion >= RELATIONSHIP_PUBLIC_ADDRESS_VERSION)
|
|
302
|
+
throw new TypeError('Non-public DNS answers cannot be retained as new public-infrastructure relationships.');
|
|
303
|
+
const qualified = qualifyRelationshipSources(sourceVersion >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION ? raw.sourceEvidence : [], domains, { truncated: options.truncated === true, type });
|
|
304
|
+
// Older callers can supply their original scan timestamp. It does not
|
|
305
|
+
// qualify the missing source metadata, and an explicit current projection
|
|
306
|
+
// never substitutes that timestamp for unknown contributing-source times.
|
|
307
|
+
const observedAt = qualified.observedAt
|
|
308
|
+
?? (!Object.hasOwn(raw, 'sourceEvidence') ? timestamp(options.observedAt) || null : null);
|
|
287
309
|
const retainedAt = timestamp(options.retainedAt) || now;
|
|
288
|
-
if (!observedAt) {
|
|
289
|
-
throw new Error('A relationship observation needs the contributing scan observation time; an unknown time is not replaced with the retention time.');
|
|
290
|
-
}
|
|
291
310
|
const canonical = canonicalIdentity(type, normalizedValue, domains);
|
|
292
311
|
const metadata = TYPE_METADATA[type];
|
|
293
312
|
return {
|
|
294
313
|
id: observationId(canonical),
|
|
295
314
|
type,
|
|
296
|
-
label: metadata.label,
|
|
315
|
+
label: nonPublicAddress ? 'Shared non-public DNS answer' : metadata.label,
|
|
297
316
|
method: metadata.method,
|
|
298
317
|
normalizedValue,
|
|
299
318
|
displayValue: type === 'favicon' ? '' : normalizedValue,
|
|
300
319
|
domains,
|
|
301
|
-
description: metadata.description,
|
|
320
|
+
description: nonPublicAddress ? NON_PUBLIC_RELATIONSHIP_LIMITATION : metadata.description,
|
|
302
321
|
classification: 'derived',
|
|
303
322
|
source: 'bulk_relationship_analysis',
|
|
304
|
-
sourceVersion
|
|
323
|
+
sourceVersion,
|
|
305
324
|
observedAt,
|
|
325
|
+
sourceEvidence: qualified.sourceEvidence,
|
|
306
326
|
retainedAt,
|
|
307
|
-
complete: options.complete
|
|
308
|
-
truncated:
|
|
327
|
+
complete: options.complete !== false && qualified.complete,
|
|
328
|
+
truncated: qualified.truncated
|
|
309
329
|
|| (Array.isArray(raw.domains) && raw.domains.length > MAX_RELATIONSHIP_OBSERVATION_DOMAINS),
|
|
310
|
-
limitations: observationLimitations(options.limitations),
|
|
330
|
+
limitations: observationLimitations([...(nonPublicAddress ? [NON_PUBLIC_RELATIONSHIP_LIMITATION] : []), ...(Array.isArray(options.limitations) ? options.limitations : [])]),
|
|
311
331
|
};
|
|
312
332
|
}
|
|
313
333
|
function byteLength(value) {
|
|
@@ -359,12 +379,11 @@ export function deleteRelationshipObservation(rawStore, id) {
|
|
|
359
379
|
export function mergeRelationshipObservations(localRaw, importedRaw) {
|
|
360
380
|
assertWorkspaceInputGraph(localRaw, 'Local relationship-observation store');
|
|
361
381
|
assertWorkspaceInputGraph(importedRaw, 'Imported relationship-observation document');
|
|
362
|
-
assertWorkspacePortableVersion(importedRaw, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, 'Imported relationship-observation document');
|
|
382
|
+
const importedVersion = assertWorkspacePortableVersion(importedRaw, RELATIONSHIP_OBSERVATION_SCHEMA_VERSION, 'Imported relationship-observation document');
|
|
363
383
|
const importedRecord = record(importedRaw);
|
|
364
384
|
if (importedRecord?.schema !== undefined && importedRecord.schema !== RELATIONSHIP_OBSERVATION_SCHEMA) {
|
|
365
385
|
throw new Error('This JSON section is not a WHOISleuth relationship-observation export.');
|
|
366
386
|
}
|
|
367
|
-
const importedVersion = relationshipObservationStoreVersion(importedRaw);
|
|
368
387
|
if (importedVersion !== null && importedVersion > RELATIONSHIP_OBSERVATION_SCHEMA_VERSION) {
|
|
369
388
|
throw new Error(`This relationship-observation section uses newer schema ${importedVersion}. Update the app before importing it.`);
|
|
370
389
|
}
|
|
@@ -375,7 +394,7 @@ export function mergeRelationshipObservations(localRaw, importedRaw) {
|
|
|
375
394
|
let updated = 0;
|
|
376
395
|
let skipped = Math.max(0, importedInput.length - MAX_RELATIONSHIP_OBSERVATION_INPUTS);
|
|
377
396
|
for (const candidate of importedInput.slice(0, MAX_RELATIONSHIP_OBSERVATION_INPUTS)) {
|
|
378
|
-
const observation = normalizeRelationshipObservation(candidate);
|
|
397
|
+
const observation = normalizeRelationshipObservation(candidate, importedVersion ?? RELATIONSHIP_OBSERVATION_SCHEMA_VERSION);
|
|
379
398
|
if (!observation) {
|
|
380
399
|
skipped += 1;
|
|
381
400
|
continue;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// and downloads; this module owns schema migration, record normalization,
|
|
3
3
|
// import merging, and exact serialized-byte accounting.
|
|
4
4
|
import { compactWatchlistResults } from "./watchlist-history.mjs";
|
|
5
|
-
import { normalizeOpportunityModelVersion } from "
|
|
6
|
-
import { normalizeRiskModelVersion } from "
|
|
5
|
+
import { normalizeOpportunityModelVersion } from "../analysis/opportunity-scoring.mjs";
|
|
6
|
+
import { normalizeRiskModelVersion } from "../analysis/risk-scoring.mjs";
|
|
7
7
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
8
8
|
import { assertWorkspaceDeclaredVersion, assertWorkspaceInputGraph, assertWorkspacePortableVersion, ordinaryWorkspaceRecord } from "./hostile-input.mjs";
|
|
9
9
|
import { MAX_SHORTLIST_ENTRIES, MAX_SHORTLIST_FACTORS, MAX_SHORTLIST_INPUTS, MAX_SHORTLIST_STORE_BYTES, SHORTLIST_SCHEMA, SHORTLIST_SCHEMA_VERSION, SUPPORTED_SHORTLIST_SCHEMA_VERSIONS, } from "../contracts/workspace-portability.mjs";
|
|
@@ -188,14 +188,23 @@ export function mergeShortlistStores(localRaw, importedRaw) {
|
|
|
188
188
|
skipped++;
|
|
189
189
|
continue;
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
const prior = imported.get(record.domain);
|
|
192
|
+
if (prior)
|
|
192
193
|
skipped++;
|
|
193
|
-
|
|
194
|
+
if (!prior || Date.parse(record.savedAt) > Date.parse(prior.savedAt))
|
|
195
|
+
imported.set(record.domain, record);
|
|
194
196
|
}
|
|
195
197
|
let added = 0;
|
|
196
198
|
let updated = 0;
|
|
197
199
|
for (const record of imported.values()) {
|
|
198
|
-
|
|
200
|
+
const existing = byDomain.get(record.domain);
|
|
201
|
+
// A backup must not roll back an observation. Equal-time conflicts retain
|
|
202
|
+
// the local record rather than choosing whichever file was imported last.
|
|
203
|
+
if (existing && Date.parse(record.savedAt) <= Date.parse(existing.savedAt)) {
|
|
204
|
+
skipped++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (existing)
|
|
199
208
|
updated++;
|
|
200
209
|
else if (byDomain.size >= MAX_SHORTLIST_ENTRIES) {
|
|
201
210
|
skipped++;
|