@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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Pure defensive-domain export formatting for filtered Bulk findings. These
|
|
2
|
+
// files are generated locally and never submitted or applied automatically.
|
|
3
|
+
import { caseDispositionSupportsDefensiveResponse } from "../cases/case-record-decisions.mjs";
|
|
4
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
5
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
+
import { DEFENSIVE_INDICATOR_EXPORT_VERSION, DEFENSIVE_INDICATOR_MANIFEST_SCHEMA, DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA, } from "../contracts/analyst-interchange.mjs";
|
|
7
|
+
export { DEFENSIVE_INDICATOR_EXPORT_VERSION, DEFENSIVE_INDICATOR_MANIFEST_SCHEMA, DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA, } from "../contracts/analyst-interchange.mjs";
|
|
8
|
+
export const MAX_DEFENSIVE_INDICATORS = 2000;
|
|
9
|
+
export const MAX_DEFENSIVE_INDICATOR_INPUTS = MAX_DEFENSIVE_INDICATORS * 4;
|
|
10
|
+
export const DEFENSIVE_INDICATOR_FORMATS = Object.freeze(['domains', 'hosts', 'dnsmasq', 'rpz']);
|
|
11
|
+
const REGISTERED_STATES = new Set(['registered', 'for_sale', 'expiring']);
|
|
12
|
+
const MINIMUM_RISK_SCORE = 70;
|
|
13
|
+
const CONTROL_RE = /[\x00-\x1f\x7f]/;
|
|
14
|
+
function plainRecord(value) {
|
|
15
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function riskScore(record) {
|
|
20
|
+
const value = record.risk ?? record.riskScore;
|
|
21
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
22
|
+
}
|
|
23
|
+
/** Source metadata shared by interchange writers; export time is not evidence time. */
|
|
24
|
+
export function defensiveIndicatorProvenance(value) {
|
|
25
|
+
const source = plainRecord(value) ?? {};
|
|
26
|
+
const saved = plainRecord(source.saved) ?? {};
|
|
27
|
+
const score = riskScore(source);
|
|
28
|
+
const version = source.riskModelVersion ?? saved.riskModelVersion;
|
|
29
|
+
const depth = source.scanDepth ?? saved.scanDepth;
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
riskScore: score === null ? null : Math.max(0, Math.min(100, Math.round(score))),
|
|
32
|
+
riskModelVersion: typeof version === 'number' && Number.isSafeInteger(version) && version > 0 && version <= 1_000 ? version : null,
|
|
33
|
+
scanDepth: depth === 'fast' || depth === 'deep' ? depth : 'unknown',
|
|
34
|
+
observedAt: normalizeExplicitIsoTimestamp(source.observedAt) || normalizeExplicitIsoTimestamp(saved.observedAt),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function normalizedDomainSet(value) {
|
|
38
|
+
if (!Array.isArray(value))
|
|
39
|
+
return new Set();
|
|
40
|
+
return new Set(value.slice(0, MAX_DEFENSIVE_INDICATOR_INPUTS)
|
|
41
|
+
.map((item) => normalizeDomain(item))
|
|
42
|
+
.filter((item) => Boolean(item)));
|
|
43
|
+
}
|
|
44
|
+
function analystDisposition(record) {
|
|
45
|
+
return typeof record.analystDisposition === 'string'
|
|
46
|
+
? record.analystDisposition
|
|
47
|
+
: typeof record.disposition === 'string'
|
|
48
|
+
? record.disposition
|
|
49
|
+
: '';
|
|
50
|
+
}
|
|
51
|
+
function hasReadyProfileContext(record) {
|
|
52
|
+
const saved = plainRecord(record.saved);
|
|
53
|
+
const profileContext = plainRecord(saved?.profileContext ?? record.profileContext);
|
|
54
|
+
return profileContext?.sourceState === 'ready';
|
|
55
|
+
}
|
|
56
|
+
export function isDefensiveIndicatorCandidate(value) {
|
|
57
|
+
const record = plainRecord(value);
|
|
58
|
+
if (!record
|
|
59
|
+
|| !hasReadyProfileContext(record)
|
|
60
|
+
|| record.trusted
|
|
61
|
+
|| record.status === 'error'
|
|
62
|
+
|| typeof record.availability !== 'string'
|
|
63
|
+
|| !REGISTERED_STATES.has(record.availability))
|
|
64
|
+
return false;
|
|
65
|
+
return Boolean(normalizeDomain(record.domain)) && (riskScore(record) ?? -1) >= MINIMUM_RISK_SCORE;
|
|
66
|
+
}
|
|
67
|
+
export function collectDefensiveIndicatorCandidates(records, limit = MAX_DEFENSIVE_INDICATORS) {
|
|
68
|
+
if (!Array.isArray(records))
|
|
69
|
+
throw new TypeError('Defensive indicator export requires an array of Bulk results.');
|
|
70
|
+
const retainedLimit = Number.isSafeInteger(limit) && limit > 0
|
|
71
|
+
? Math.min(limit, MAX_DEFENSIVE_INDICATORS)
|
|
72
|
+
: MAX_DEFENSIVE_INDICATORS;
|
|
73
|
+
const byDomain = new Map();
|
|
74
|
+
for (const item of records.slice(0, MAX_DEFENSIVE_INDICATOR_INPUTS)) {
|
|
75
|
+
if (!isDefensiveIndicatorCandidate(item))
|
|
76
|
+
continue;
|
|
77
|
+
const record = plainRecord(item);
|
|
78
|
+
const domain = normalizeDomain(record?.domain);
|
|
79
|
+
if (record && domain && !byDomain.has(domain))
|
|
80
|
+
byDomain.set(domain, record);
|
|
81
|
+
}
|
|
82
|
+
const candidates = [...byDomain.entries()]
|
|
83
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
84
|
+
.map(([domain, source]) => ({ domain, source }));
|
|
85
|
+
const entries = candidates.slice(0, retainedLimit);
|
|
86
|
+
return {
|
|
87
|
+
entries,
|
|
88
|
+
domains: entries.map((entry) => entry.domain),
|
|
89
|
+
truncated: records.length > MAX_DEFENSIVE_INDICATOR_INPUTS || candidates.length > retainedLimit,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function prepareDefensiveIndicatorExport(records, options = {}, limit = MAX_DEFENSIVE_INDICATORS) {
|
|
93
|
+
if (!Array.isArray(records))
|
|
94
|
+
throw new TypeError('Defensive indicator export requires an array of Bulk results.');
|
|
95
|
+
const explicitSelection = Array.isArray(options.selectedDomains);
|
|
96
|
+
const selected = normalizedDomainSet(options.selectedDomains);
|
|
97
|
+
const official = normalizedDomainSet(options.officialDomains);
|
|
98
|
+
const allowlisted = normalizedDomainSet(options.allowlistedDomains);
|
|
99
|
+
const common = normalizedDomainSet(options.commonInfrastructureDomains);
|
|
100
|
+
const retainedLimit = Number.isSafeInteger(limit) && limit > 0
|
|
101
|
+
? Math.min(limit, MAX_DEFENSIVE_INDICATORS)
|
|
102
|
+
: MAX_DEFENSIVE_INDICATORS;
|
|
103
|
+
const exclusions = [];
|
|
104
|
+
const seenExclusions = new Set();
|
|
105
|
+
const accepted = new Map();
|
|
106
|
+
const exclude = (domain, reason) => {
|
|
107
|
+
const key = `${domain}\u0000${reason}`;
|
|
108
|
+
if (!seenExclusions.has(key)) {
|
|
109
|
+
exclusions.push({ domain, reason });
|
|
110
|
+
seenExclusions.add(key);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
for (const item of records.slice(0, MAX_DEFENSIVE_INDICATOR_INPUTS)) {
|
|
114
|
+
const source = plainRecord(item);
|
|
115
|
+
const domain = normalizeDomain(source?.domain);
|
|
116
|
+
if (!source || !domain)
|
|
117
|
+
continue;
|
|
118
|
+
if (explicitSelection && !selected.has(domain)) {
|
|
119
|
+
exclude(domain, 'not_selected');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!hasReadyProfileContext(source)) {
|
|
123
|
+
exclude(domain, 'profile_context_unavailable');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (official.has(domain)) {
|
|
127
|
+
exclude(domain, 'official_domain');
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (allowlisted.has(domain)) {
|
|
131
|
+
exclude(domain, 'allowlisted_domain');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (common.has(domain) || source.commonInfrastructure === true) {
|
|
135
|
+
exclude(domain, 'common_infrastructure');
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!isDefensiveIndicatorCandidate(source)) {
|
|
139
|
+
exclude(domain, 'not_eligible');
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const disposition = analystDisposition(source);
|
|
143
|
+
if (explicitSelection && !caseDispositionSupportsDefensiveResponse(disposition)) {
|
|
144
|
+
exclude(domain, 'unreviewed_disposition');
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!accepted.has(domain))
|
|
148
|
+
accepted.set(domain, source);
|
|
149
|
+
}
|
|
150
|
+
const candidates = [...accepted.entries()]
|
|
151
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
152
|
+
.map(([domain, source]) => ({ domain, source }));
|
|
153
|
+
const entries = candidates.slice(0, retainedLimit);
|
|
154
|
+
return {
|
|
155
|
+
entries,
|
|
156
|
+
domains: entries.map((entry) => entry.domain),
|
|
157
|
+
exclusions: exclusions.sort((left, right) => left.domain.localeCompare(right.domain) || left.reason.localeCompare(right.reason)),
|
|
158
|
+
truncated: records.length > MAX_DEFENSIVE_INDICATOR_INPUTS || candidates.length > retainedLimit,
|
|
159
|
+
explicitSelection,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function timestamp(value) {
|
|
163
|
+
if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
|
|
164
|
+
return null;
|
|
165
|
+
const parsed = Date.parse(value);
|
|
166
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
167
|
+
}
|
|
168
|
+
function header(prefix, generatedAt, count) {
|
|
169
|
+
return [
|
|
170
|
+
`${prefix} WHOISleuth defensive domain indicators v${DEFENSIVE_INDICATOR_EXPORT_VERSION}`,
|
|
171
|
+
`${prefix} Generated ${generatedAt}`,
|
|
172
|
+
`${prefix} ${count} high-risk registered domain${count === 1 ? '' : 's'}`,
|
|
173
|
+
`${prefix} Review before use. Heuristic findings can include false positives.`,
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
function formatDomains(domains, generatedAt) {
|
|
177
|
+
return [...header('#', generatedAt, domains.length), ...domains].join('\n');
|
|
178
|
+
}
|
|
179
|
+
function formatHosts(domains, generatedAt) {
|
|
180
|
+
return [...header('#', generatedAt, domains.length), ...domains.map((domain) => `0.0.0.0 ${domain}`)].join('\n');
|
|
181
|
+
}
|
|
182
|
+
function formatDnsmasq(domains, generatedAt) {
|
|
183
|
+
return [...header('#', generatedAt, domains.length), ...domains.map((domain) => `address=/${domain}/0.0.0.0`)].join('\n');
|
|
184
|
+
}
|
|
185
|
+
function formatRpz(domains, generatedAt, includeWildcards) {
|
|
186
|
+
const serial = Math.floor(Date.parse(generatedAt) / 1000) >>> 0;
|
|
187
|
+
return [
|
|
188
|
+
...header(';', generatedAt, domains.length),
|
|
189
|
+
'$TTL 60',
|
|
190
|
+
`@ IN SOA localhost. root.localhost. (${serial} 60 60 60 60)`,
|
|
191
|
+
'@ IN NS localhost.',
|
|
192
|
+
...domains.flatMap((domain) => [
|
|
193
|
+
`${domain} CNAME .`,
|
|
194
|
+
...(includeWildcards ? [`*.${domain} CNAME .`] : []),
|
|
195
|
+
]),
|
|
196
|
+
].join('\n');
|
|
197
|
+
}
|
|
198
|
+
function contentFor(format, domains, generatedAt, includeWildcards) {
|
|
199
|
+
if (format === 'hosts')
|
|
200
|
+
return formatHosts(domains, generatedAt);
|
|
201
|
+
if (format === 'dnsmasq')
|
|
202
|
+
return formatDnsmasq(domains, generatedAt);
|
|
203
|
+
if (format === 'rpz')
|
|
204
|
+
return formatRpz(domains, generatedAt, includeWildcards);
|
|
205
|
+
return formatDomains(domains, generatedAt);
|
|
206
|
+
}
|
|
207
|
+
export function buildDefensiveIndicatorExport(records, options = {}) {
|
|
208
|
+
const format = (typeof options.format === 'string'
|
|
209
|
+
&& DEFENSIVE_INDICATOR_FORMATS.includes(options.format)) ? options.format : 'domains';
|
|
210
|
+
const generatedAt = timestamp(options.generatedAt) || new Date().toISOString();
|
|
211
|
+
const includeWildcards = options.includeWildcards === true;
|
|
212
|
+
const expiryDays = typeof options.expiryDays === 'number' && Number.isSafeInteger(options.expiryDays)
|
|
213
|
+
? Math.max(1, Math.min(365, options.expiryDays))
|
|
214
|
+
: 30;
|
|
215
|
+
const collected = prepareDefensiveIndicatorExport(records, options);
|
|
216
|
+
const expiresAt = new Date(Date.parse(generatedAt) + expiryDays * 86_400_000).toISOString();
|
|
217
|
+
const manifest = {
|
|
218
|
+
schema: DEFENSIVE_INDICATOR_MANIFEST_SCHEMA,
|
|
219
|
+
version: DEFENSIVE_INDICATOR_EXPORT_VERSION,
|
|
220
|
+
generatedAt,
|
|
221
|
+
expiresAt,
|
|
222
|
+
reviewRequired: true,
|
|
223
|
+
explicitSelection: collected.explicitSelection,
|
|
224
|
+
includeWildcards,
|
|
225
|
+
entries: collected.entries.map(({ domain, source }) => ({
|
|
226
|
+
domain,
|
|
227
|
+
disposition: analystDisposition(source) || 'not_recorded',
|
|
228
|
+
reason: `High-risk registered Bulk finding (risk ${riskScore(source) ?? 'unknown'}).`,
|
|
229
|
+
source: 'bulk',
|
|
230
|
+
generatedAt,
|
|
231
|
+
expiresAt,
|
|
232
|
+
})),
|
|
233
|
+
exclusions: collected.exclusions,
|
|
234
|
+
};
|
|
235
|
+
const rollback = {
|
|
236
|
+
schema: DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA,
|
|
237
|
+
version: DEFENSIVE_INDICATOR_EXPORT_VERSION,
|
|
238
|
+
generatedAt,
|
|
239
|
+
removes: collected.domains.map((domain) => ({
|
|
240
|
+
domain,
|
|
241
|
+
includeWildcard: includeWildcards,
|
|
242
|
+
reason: 'Remove the indicator generated by the paired reviewed manifest.',
|
|
243
|
+
})),
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
version: DEFENSIVE_INDICATOR_EXPORT_VERSION,
|
|
247
|
+
format,
|
|
248
|
+
generatedAt,
|
|
249
|
+
expiresAt,
|
|
250
|
+
domains: collected.domains,
|
|
251
|
+
entries: collected.entries,
|
|
252
|
+
exclusions: collected.exclusions,
|
|
253
|
+
explicitSelection: collected.explicitSelection,
|
|
254
|
+
includeWildcards,
|
|
255
|
+
truncated: collected.truncated,
|
|
256
|
+
filename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.${format === 'rpz' ? 'zone' : 'txt'}`,
|
|
257
|
+
manifestFilename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.manifest.json`,
|
|
258
|
+
rollbackFilename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.rollback.json`,
|
|
259
|
+
mimeType: 'text/plain;charset=utf-8',
|
|
260
|
+
content: `${contentFor(format, collected.domains, generatedAt, includeWildcards)}\n`,
|
|
261
|
+
manifestContent: `${JSON.stringify(manifest, null, 2)}\n`,
|
|
262
|
+
rollbackContent: `${JSON.stringify(rollback, null, 2)}\n`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { normalizeDomain
|
|
2
|
-
import {
|
|
1
|
+
import { normalizeDomain } from "../evidence/domain-name.mjs";
|
|
2
|
+
import { openOrCreateCase, updateCase } from "../cases/case-record-operations.mjs";
|
|
3
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
3
4
|
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
4
|
-
import {
|
|
5
|
+
import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
|
|
6
|
+
import { sha256IdentityHex } from "../evidence/record-identity.mjs";
|
|
7
|
+
import { MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_VALUE_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, normalizeCaseEvidencePins, normalizeCaseSightings, } from "../cases/case-response-model.mjs";
|
|
8
|
+
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
|
|
5
9
|
import { hasUnsafeRetainedText } from "./retained-text.mjs";
|
|
6
|
-
export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
|
|
10
|
+
export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
|
|
7
11
|
export const EXTERNAL_FINDING_CATEGORIES = [
|
|
8
12
|
'certificate',
|
|
9
13
|
'dns',
|
|
@@ -83,14 +87,31 @@ function iso(value, label, optional = false) {
|
|
|
83
87
|
function limitations(value, index) {
|
|
84
88
|
if (value === undefined)
|
|
85
89
|
return [];
|
|
86
|
-
if (!Array.isArray(value) || value.length >
|
|
87
|
-
throw new Error(`Finding ${index + 1} limitations must be an array with no more than
|
|
90
|
+
if (!Array.isArray(value) || value.length > MAX_EXTERNAL_FINDING_LIMITATIONS) {
|
|
91
|
+
throw new Error(`Finding ${index + 1} limitations must be an array with no more than ${MAX_EXTERNAL_FINDING_LIMITATIONS} entries.`);
|
|
88
92
|
}
|
|
89
93
|
const unique = new Set();
|
|
90
94
|
for (const item of value)
|
|
91
|
-
unique.add(requiredText(item,
|
|
95
|
+
unique.add(requiredText(item, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, `Finding ${index + 1} limitation`));
|
|
92
96
|
return [...unique];
|
|
93
97
|
}
|
|
98
|
+
/** Keep provenance and omissions visible within the existing import bound. */
|
|
99
|
+
export function retainExternalFindingLimitations(supplied, mandatory) {
|
|
100
|
+
const required = limitations(mandatory, 0);
|
|
101
|
+
const optional = limitations(supplied, 0).filter((entry) => !required.includes(entry));
|
|
102
|
+
const available = MAX_EXTERNAL_FINDING_LIMITATIONS - required.length;
|
|
103
|
+
if (optional.length <= available)
|
|
104
|
+
return Object.freeze([...required, ...optional]);
|
|
105
|
+
if (available < 1)
|
|
106
|
+
throw new TypeError('Mandatory finding limitations leave no room to disclose omitted supplied limitations.');
|
|
107
|
+
const retained = optional.slice(0, available - 1);
|
|
108
|
+
const omitted = optional.length - retained.length;
|
|
109
|
+
return Object.freeze([
|
|
110
|
+
...required,
|
|
111
|
+
...retained,
|
|
112
|
+
`${omitted} supplied limitation${omitted === 1 ? ' was' : 's were'} omitted to retain mandatory provenance and omission notices; review the supplied input for the remaining qualifications.`,
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
94
115
|
function structuredObservation(value, index) {
|
|
95
116
|
if (value === undefined || value === null)
|
|
96
117
|
return null;
|
|
@@ -162,16 +183,7 @@ function structuredObservation(value, index) {
|
|
|
162
183
|
};
|
|
163
184
|
}
|
|
164
185
|
function findingKey(finding, sourceName) {
|
|
165
|
-
return
|
|
166
|
-
finding.domain,
|
|
167
|
-
finding.category,
|
|
168
|
-
finding.evidenceClass,
|
|
169
|
-
finding.summary,
|
|
170
|
-
finding.observedAt,
|
|
171
|
-
finding.completeness,
|
|
172
|
-
sourceName,
|
|
173
|
-
finding.structuredObservation?.eventId ?? '',
|
|
174
|
-
].join('\u0000');
|
|
186
|
+
return canonicalArtifactJsonV2({ sourceName, finding });
|
|
175
187
|
}
|
|
176
188
|
export function parseExternalFindingsDocument(value) {
|
|
177
189
|
const root = record(value);
|
|
@@ -267,21 +279,19 @@ function importedSourceLabel(finding, sourceName) {
|
|
|
267
279
|
? `Deployment observation: ${sourceName}`
|
|
268
280
|
: `Provider report: ${sourceName}`;
|
|
269
281
|
}
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
&&
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
&& pin.sourceSchema.version === observation.sourceVersion
|
|
284
|
-
&& (observation.eventId === null || pin.certificateObservation?.eventId === observation.eventId))))) ? findingKey(finding, sourceName) : '';
|
|
282
|
+
function alreadyRetained(recordValue, projection) {
|
|
283
|
+
const { importContentSha256: expectedIdentity, ...expected } = projection.evidencePin;
|
|
284
|
+
return recordValue.evidencePins.some((pin) => {
|
|
285
|
+
const { id: _id, createdAt: _createdAt, importContentSha256, ...material } = pin;
|
|
286
|
+
if (importContentSha256 && importContentSha256 !== expectedIdentity)
|
|
287
|
+
return false;
|
|
288
|
+
// Older pins have no content digest. Only a complete, exact retained
|
|
289
|
+
// projection can establish duplication; a shortened prefix cannot.
|
|
290
|
+
if (!importContentSha256 && (projection.shortenedFields.length || projection.omittedLimitations))
|
|
291
|
+
return false;
|
|
292
|
+
// An imported digest alone cannot substitute for matching retained fields.
|
|
293
|
+
return canonicalArtifactJsonV2(material) === canonicalArtifactJsonV2(expected);
|
|
294
|
+
});
|
|
285
295
|
}
|
|
286
296
|
function structuredPinFields(finding) {
|
|
287
297
|
const observation = finding.structuredObservation;
|
|
@@ -313,37 +323,41 @@ function structuredPinFields(finding) {
|
|
|
313
323
|
certificateObservation,
|
|
314
324
|
};
|
|
315
325
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
326
|
+
/** Exact material retained by both the import preview and Case mutation. */
|
|
327
|
+
export function externalFindingCaseProjection(finding, source) {
|
|
328
|
+
const identitySha256 = sha256IdentityHex(new TextEncoder().encode(canonicalArtifactJsonV2({
|
|
329
|
+
sourceName: source.name, sourceReference: source.reference, finding,
|
|
330
|
+
})));
|
|
331
|
+
const value = finding.structuredObservation?.value ?? pinValue(finding);
|
|
332
|
+
const sourceLabel = importedSourceLabel(finding, source.name);
|
|
333
|
+
const sourceReference = source.reference ? `Source reference: ${source.reference}` : null;
|
|
334
|
+
const shortenedFields = [
|
|
335
|
+
...(value.length > MAX_RESPONSE_VALUE_LENGTH ? ['finding value'] : []),
|
|
336
|
+
...(sourceLabel.length > MAX_RESPONSE_LABEL_LENGTH ? ['source label'] : []),
|
|
337
|
+
...(sourceReference && sourceReference.length > MAX_RESPONSE_LIMITATION_LENGTH ? ['source reference'] : []),
|
|
338
|
+
];
|
|
339
|
+
const mandatory = [
|
|
324
340
|
finding.evidenceClass === 'deployment_observation'
|
|
325
341
|
? `Imported as an observation made by ${source.name}; this browser session did not collect or independently verify it.`
|
|
326
342
|
: `Reported by ${source.name}; WHOISleuth did not collect or independently verify this provider finding.`,
|
|
327
|
-
...(
|
|
328
|
-
|
|
343
|
+
...(sourceReference ? [sourceReference.length > MAX_RESPONSE_LIMITATION_LENGTH
|
|
344
|
+
? `${sourceReference.slice(0, MAX_RESPONSE_LIMITATION_LENGTH - 13)}… [shortened]` : sourceReference] : []),
|
|
345
|
+
...(shortenedFields.length ? [`Case storage shortened ${shortenedFields.join(', ')}. The content digest covers the complete accepted finding.`] : []),
|
|
329
346
|
];
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
347
|
+
const sourceLimitations = retainExternalFindingLimitations(finding.limitations, mandatory);
|
|
348
|
+
const omittedLimitations = finding.limitations.filter((item) => !sourceLimitations.includes(item)).length;
|
|
349
|
+
const pin = normalizeCaseEvidencePins([{
|
|
333
350
|
...structuredPinFields(finding),
|
|
351
|
+
importContentSha256: identitySha256,
|
|
334
352
|
label: `External ${finding.category} finding`,
|
|
335
|
-
value
|
|
336
|
-
source:
|
|
353
|
+
value,
|
|
354
|
+
source: sourceLabel,
|
|
337
355
|
observedAt: finding.observedAt,
|
|
338
356
|
completeness: finding.completeness,
|
|
357
|
+
truncated: shortenedFields.length || omittedLimitations ? true : null,
|
|
339
358
|
limitations: sourceLimitations,
|
|
340
|
-
},
|
|
341
|
-
|
|
342
|
-
const addedPin = updated.record.evidencePins.find((pin) => !existingPinIds.has(pin.id)) ?? null;
|
|
343
|
-
if (!addedPin)
|
|
344
|
-
throw new Error('The imported finding could not be retained as Case evidence.');
|
|
345
|
-
updated = updateCase(updated.cases, target.id, {
|
|
346
|
-
sighting: {
|
|
359
|
+
}], finding.observedAt)[0];
|
|
360
|
+
const observed = normalizeCaseSightings([{
|
|
347
361
|
state: finding.evidenceClass === 'deployment_observation'
|
|
348
362
|
? 'observed_by_deployment'
|
|
349
363
|
: 'reported_by_provider',
|
|
@@ -359,9 +373,28 @@ function mergeExternalFindingIntoCase(current, caseId, finding, source, now) {
|
|
|
359
373
|
source: source.name,
|
|
360
374
|
observedAt: finding.observedAt,
|
|
361
375
|
completeness: finding.completeness,
|
|
362
|
-
evidencePinId: addedPin.id,
|
|
363
376
|
limitations: sourceLimitations,
|
|
364
|
-
},
|
|
377
|
+
}], finding.observedAt)[0];
|
|
378
|
+
if (!pin || !observed)
|
|
379
|
+
throw new TypeError('The finding has no valid retained Case projection.');
|
|
380
|
+
const { id: _pinId, createdAt: _pinCreatedAt, ...evidencePin } = pin;
|
|
381
|
+
const { id: _sightingId, createdAt: _sightingCreatedAt, evidencePinId: _pinReference, ...sighting } = observed;
|
|
382
|
+
return { evidencePin, sighting, identitySha256, shortenedFields, omittedLimitations };
|
|
383
|
+
}
|
|
384
|
+
function mergeExternalFindingIntoCase(current, caseId, finding, source, now) {
|
|
385
|
+
const target = current.find((candidate) => candidate.id === caseId) ?? null;
|
|
386
|
+
if (!target)
|
|
387
|
+
throw new Error('The selected Case is unavailable for this finding.');
|
|
388
|
+
const projection = externalFindingCaseProjection(finding, source);
|
|
389
|
+
if (alreadyRetained(target, projection))
|
|
390
|
+
return { cases: [...current], record: target, added: false };
|
|
391
|
+
const existingPinIds = new Set(target.evidencePins.map((pin) => pin.id));
|
|
392
|
+
let updated = updateCase([...current], target.id, { evidencePin: projection.evidencePin }, now);
|
|
393
|
+
const addedPin = updated.record.evidencePins.find((pin) => !existingPinIds.has(pin.id)) ?? null;
|
|
394
|
+
if (!addedPin)
|
|
395
|
+
throw new Error('The imported finding could not be retained as Case evidence.');
|
|
396
|
+
updated = updateCase(updated.cases, target.id, {
|
|
397
|
+
sighting: { ...projection.sighting, evidencePinId: addedPin.id },
|
|
365
398
|
}, now);
|
|
366
399
|
return { cases: updated.cases, record: updated.record, added: true };
|
|
367
400
|
}
|
|
@@ -372,7 +405,6 @@ export function mergeExternalFindingsIntoCases(current, document, now = new Date
|
|
|
372
405
|
let findingsAdded = 0;
|
|
373
406
|
let duplicatesSkipped = 0;
|
|
374
407
|
for (const finding of document.findings) {
|
|
375
|
-
const existing = cases.find((candidate) => candidate.domain === finding.domain) ?? null;
|
|
376
408
|
const opened = openOrCreateCase(cases, { domain: finding.domain, source: 'import' }, now);
|
|
377
409
|
cases = opened.cases;
|
|
378
410
|
const target = cases.find((candidate) => candidate.id === opened.record.id) ?? opened.record;
|
|
@@ -383,10 +415,10 @@ export function mergeExternalFindingsIntoCases(current, document, now = new Date
|
|
|
383
415
|
continue;
|
|
384
416
|
}
|
|
385
417
|
findingsAdded += 1;
|
|
386
|
-
if (
|
|
387
|
-
updatedDomains.add(finding.domain);
|
|
388
|
-
else
|
|
418
|
+
if (opened.created)
|
|
389
419
|
createdDomains.add(finding.domain);
|
|
420
|
+
else
|
|
421
|
+
updatedDomains.add(finding.domain);
|
|
390
422
|
}
|
|
391
423
|
return {
|
|
392
424
|
cases,
|
|
@@ -422,7 +454,7 @@ export function externalFindingsCaseTargets(document, caseDomain) {
|
|
|
422
454
|
}
|
|
423
455
|
/**
|
|
424
456
|
* Merges already-validated external findings into one selected Case. A Case is
|
|
425
|
-
*
|
|
457
|
+
* identified by its immutable ID with a registrable-domain pivot, while rendered captures may retain an exact
|
|
426
458
|
* hostname. Only same-domain findings are accepted, the exact hostname remains
|
|
427
459
|
* explicit in the retained evidence, and this operation can never open another
|
|
428
460
|
* Case as a side effect.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readManagedIndicatorSet } from "./managed-indicator-set.mjs";
|
|
2
|
+
import { stixIndicatorObjects, stixIndicatorProducer } from "./stix-indicator-export.mjs";
|
|
3
|
+
import { mispIndicatorAttribute, mispIndicatorEvent } from "./misp-indicator-export.mjs";
|
|
4
|
+
/** Re-exporting a revision is deterministic; revoked object versions never advance. */
|
|
5
|
+
export async function exportManagedIndicators(input, format) {
|
|
6
|
+
const manifest = await readManagedIndicatorSet(input);
|
|
7
|
+
let document = manifest;
|
|
8
|
+
if (format === 'stix') {
|
|
9
|
+
const producer = `identity--${manifest.producerId}`;
|
|
10
|
+
document = { type: 'bundle', id: `bundle--${manifest.revisionId}`, objects: [
|
|
11
|
+
stixIndicatorProducer(producer, manifest.createdAt),
|
|
12
|
+
...manifest.entries.flatMap(entry => stixIndicatorObjects(entry.domain, entry.observation.availability, entry.observation, entry.createdAt, producer, type => `${type}--${entry.id}`)
|
|
13
|
+
.map(object => object.type !== 'indicator' ? object : { ...object,
|
|
14
|
+
modified: entry.modifiedAt, valid_until: entry.expiresAt, revoked: entry.withdrawal !== null,
|
|
15
|
+
x_whoisleuth_managed_set: manifest.id, x_whoisleuth_original_review_basis: entry.basis,
|
|
16
|
+
x_whoisleuth_review_basis: entry.reviewBasis,
|
|
17
|
+
...(entry.withdrawal ? { x_whoisleuth_withdrawal_reason: entry.withdrawal.reason } : {}),
|
|
18
|
+
})),
|
|
19
|
+
] };
|
|
20
|
+
}
|
|
21
|
+
else if (format === 'misp') {
|
|
22
|
+
const attributes = manifest.entries.map(entry => {
|
|
23
|
+
const attribute = mispIndicatorAttribute(entry.domain, entry.observation.availability, entry.observation, entry.id, entry.modifiedAt);
|
|
24
|
+
return { ...attribute, deleted: entry.withdrawal !== null,
|
|
25
|
+
comment: `${attribute.comment}; review-expires=${entry.expiresAt}; expiry requires recipient review, not automatic deletion; original-basis=${entry.basis}; latest-review=${entry.reviewBasis}${entry.withdrawal ? `; withdrawn=${entry.withdrawal.reason}` : ''}` };
|
|
26
|
+
});
|
|
27
|
+
document = { Event: { ...mispIndicatorEvent(manifest.id, manifest.createdAt, attributes),
|
|
28
|
+
info: `${manifest.name} · reviewed domain indicators`, timestamp: String(Math.floor(Date.parse(manifest.modifiedAt) / 1000)) } };
|
|
29
|
+
}
|
|
30
|
+
else if (format !== 'manifest')
|
|
31
|
+
throw new TypeError('Unsupported managed indicator export format.');
|
|
32
|
+
return { document, content: `${JSON.stringify(document, null, 2)}\n`,
|
|
33
|
+
filename: `whoisleuth-indicators-${manifest.id}-r${manifest.revision}.${format}.json`,
|
|
34
|
+
mimeType: format === 'stix' ? 'application/stix+json;charset=utf-8' : 'application/json;charset=utf-8' };
|
|
35
|
+
}
|