@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
package/lib/lookup-response.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
// Lookup result shaping after the source plan has settled. This module starts no
|
|
2
2
|
// collection and preserves compact, diagnostics and enrichment response shapes.
|
|
3
|
+
import { withoutHttpDeliveryMetadata } from "./http-intelligence.mjs";
|
|
3
4
|
import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
|
|
4
5
|
import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
|
|
5
|
-
import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail,
|
|
6
|
+
import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, } from "./lookup-diagnostics.mjs";
|
|
6
7
|
import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
|
|
7
8
|
import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
|
|
8
|
-
import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "
|
|
9
|
+
import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "../packages/analysis/threat-intelligence-types.mjs";
|
|
9
10
|
import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
|
|
10
11
|
import { buildRegistryInsights } from "./registry-insights.mjs";
|
|
11
12
|
import { buildRegistrarStanding } from "./registrar-standing.mjs";
|
|
12
13
|
import { resolveRegistrarIanaId } from "./registrar-standing-contract.mjs";
|
|
13
14
|
import { securityTxtUnavailable } from "./security-txt.mjs";
|
|
14
15
|
import { parseWhoisChain } from "./whois.mjs";
|
|
16
|
+
import { whoisCollectionStatus } from "./whois-authority.mjs";
|
|
15
17
|
function withoutNestedPublicationMetadata(value) {
|
|
16
18
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
19
|
return value;
|
|
@@ -19,17 +21,6 @@ function withoutNestedPublicationMetadata(value) {
|
|
|
19
21
|
delete output.publicationMetadata;
|
|
20
22
|
return output;
|
|
21
23
|
}
|
|
22
|
-
function withoutNestedDeliveryMetadata(value) {
|
|
23
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
24
|
-
return value;
|
|
25
|
-
const output = { ...value };
|
|
26
|
-
if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
|
|
27
|
-
const response = { ...output.response };
|
|
28
|
-
delete response.deliveryMetadata;
|
|
29
|
-
output.response = response;
|
|
30
|
-
}
|
|
31
|
-
return output;
|
|
32
|
-
}
|
|
33
24
|
async function buildUnifiedLookupResponse(context) {
|
|
34
25
|
const { availabilityEnabled, availabilityPromise, classified, compact, dnsIntelligenceEnabled, fast, inspectCertificateWarningData, networkContextPromise, options, rdapEnabled, rdapPromise, registrarRdapPromise, reverseDnsEligible, reverseDnsPromise, securityTxtPromise, skipWhois, threatfoxIntelligencePromise, timing, urlhausIntelligencePromise, urlscanIntelligencePromise, whoisEnabled, whoisPromise, } = context;
|
|
35
26
|
const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
|
|
@@ -99,7 +90,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
99
90
|
whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
|
|
100
91
|
}
|
|
101
92
|
else if (Array.isArray(whoisChain)) {
|
|
102
|
-
whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
|
|
93
|
+
whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined) };
|
|
103
94
|
}
|
|
104
95
|
else {
|
|
105
96
|
whois = {
|
|
@@ -125,10 +116,8 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
125
116
|
availability = {
|
|
126
117
|
applicable: true,
|
|
127
118
|
...result,
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// top-level response; exact-host sources such as security.txt retain
|
|
131
|
-
// their own requested/final URL attribution.
|
|
119
|
+
// Registration authority retains its registrable target. Deep source
|
|
120
|
+
// observations declare their independently collected hostname.
|
|
132
121
|
domain: classified.value,
|
|
133
122
|
};
|
|
134
123
|
}
|
|
@@ -138,7 +127,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
138
127
|
domain: classified.value,
|
|
139
128
|
state: 'unknown',
|
|
140
129
|
confidence: 'low',
|
|
141
|
-
detail:
|
|
130
|
+
detail: boundedSourceDetail(availabilityResult.reason, 'Availability lookup failed'),
|
|
142
131
|
};
|
|
143
132
|
}
|
|
144
133
|
}
|
|
@@ -153,11 +142,9 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
153
142
|
? 'disabled'
|
|
154
143
|
: skipWhois
|
|
155
144
|
? 'skipped'
|
|
156
|
-
: whoisResult.status === 'rejected'
|
|
145
|
+
: whoisResult.status === 'rejected'
|
|
157
146
|
? 'error'
|
|
158
|
-
: whoisChain.
|
|
159
|
-
? 'unsupported'
|
|
160
|
-
: whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
|
|
147
|
+
: whoisCollectionStatus(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
|
|
161
148
|
const availabilityStatus = classified.type !== 'domain'
|
|
162
149
|
? 'not_applicable'
|
|
163
150
|
: !availabilityEnabled ? 'disabled'
|
|
@@ -301,7 +288,7 @@ async function buildUnifiedLookupResponse(context) {
|
|
|
301
288
|
availability: {
|
|
302
289
|
...compactAvailability,
|
|
303
290
|
...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
|
|
304
|
-
...(richHttp !== undefined ? { http:
|
|
291
|
+
...(richHttp !== undefined ? { http: withoutHttpDeliveryMetadata(richHttp) } : {}),
|
|
305
292
|
...(bulkComparison ? { bulkComparison } : {}),
|
|
306
293
|
},
|
|
307
294
|
diagnostics,
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Lookup. These deliberately exclude raw RDAP/WHOIS bodies, contacts, URLs,
|
|
3
3
|
// provider payloads, and request errors. The ordinary final Lookup response
|
|
4
4
|
// remains the only authoritative and persistable result.
|
|
5
|
-
import {
|
|
5
|
+
import { whoisCollectionStatus } from "./whois-authority.mjs";
|
|
6
|
+
import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "../packages/analysis/threat-intelligence-types.mjs";
|
|
7
|
+
import { plannedLookupSources } from "./lookup-progress.mjs";
|
|
6
8
|
const RESULT_STATES = new Set([
|
|
7
9
|
'available',
|
|
8
10
|
'expiring',
|
|
@@ -31,44 +33,33 @@ function record(value) {
|
|
|
31
33
|
? value
|
|
32
34
|
: {};
|
|
33
35
|
}
|
|
36
|
+
function sourceTruncated(value) {
|
|
37
|
+
// RDAP's normalised *Truncated fields describe loss independently of HTTP success.
|
|
38
|
+
return [record(value), record(record(value).parsed)].some(part => Object.entries(part)
|
|
39
|
+
.some(([key, field]) => field === true && (key === 'truncated' || key.endsWith('Truncated'))));
|
|
40
|
+
}
|
|
34
41
|
function plannedLookupProgressSources(classified, options = {}) {
|
|
35
|
-
|
|
36
|
-
if (classified.type === 'domain') {
|
|
37
|
-
sources.push('domain_evidence', 'registrar_rdap', 'network_context');
|
|
38
|
-
if (options.securityTxt)
|
|
39
|
-
sources.push('security_txt');
|
|
40
|
-
if (options.externalIntelligence)
|
|
41
|
-
sources.push('external_intelligence');
|
|
42
|
-
if (options.malwareHostIntelligence)
|
|
43
|
-
sources.push('malware_host_intelligence');
|
|
44
|
-
if (options.malwareIocIntelligence)
|
|
45
|
-
sources.push('malware_ioc_intelligence');
|
|
46
|
-
}
|
|
47
|
-
else if (classified.type === 'ipv4' || classified.type === 'ipv6') {
|
|
48
|
-
sources.push('reverse_dns');
|
|
49
|
-
}
|
|
50
|
-
return Object.freeze(sources);
|
|
42
|
+
return plannedLookupSources(classified.type, 'deep', options);
|
|
51
43
|
}
|
|
52
|
-
function normalizedState(source, outcome, value) {
|
|
44
|
+
function normalizedState(source, outcome, value, requestedDomain, requested = true) {
|
|
53
45
|
if (outcome === 'rejected')
|
|
54
46
|
return 'error';
|
|
55
47
|
if (value === null || value === undefined)
|
|
56
|
-
return 'skipped';
|
|
48
|
+
return source === 'rdap' && requested ? 'unsupported' : 'skipped';
|
|
57
49
|
if (source === 'rdap') {
|
|
58
50
|
const rdap = record(value);
|
|
59
51
|
if (rdap.upstreamStatus === 404)
|
|
60
52
|
return 'not_found';
|
|
61
|
-
|
|
53
|
+
if (rdap.upstreamStatus !== 200 || !Object.keys(record(rdap.parsed)).length)
|
|
54
|
+
return 'error';
|
|
55
|
+
return sourceTruncated(rdap) ? 'partial' : 'success';
|
|
62
56
|
}
|
|
63
57
|
if (source === 'whois') {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (value.length <= 1)
|
|
67
|
-
return 'unsupported';
|
|
68
|
-
return 'success';
|
|
58
|
+
const status = whoisCollectionStatus(value, requestedDomain);
|
|
59
|
+
return status === 'complete' ? 'success' : status;
|
|
69
60
|
}
|
|
70
61
|
if (source === 'domain_evidence') {
|
|
71
|
-
return record(value).deepScanComplete ===
|
|
62
|
+
return record(value).deepScanComplete === true ? 'success' : 'partial';
|
|
72
63
|
}
|
|
73
64
|
if (THREAT_INTELLIGENCE_SOURCES.has(source)) {
|
|
74
65
|
const intelligence = record(value);
|
|
@@ -81,16 +72,16 @@ function normalizedState(source, outcome, value) {
|
|
|
81
72
|
}
|
|
82
73
|
const status = record(value).status;
|
|
83
74
|
if (typeof status === 'string' && DIRECT_STATES.has(status)) {
|
|
75
|
+
if (status === 'success' && (sourceTruncated(value) || record(value).complete === false))
|
|
76
|
+
return 'partial';
|
|
84
77
|
return status;
|
|
85
78
|
}
|
|
86
|
-
if (status === 'complete' || status === 'observed' || status === 'found')
|
|
87
|
-
return 'success';
|
|
88
79
|
if (status === 'not_applicable' || status === 'disabled')
|
|
89
80
|
return 'skipped';
|
|
90
|
-
return '
|
|
81
|
+
return 'error';
|
|
91
82
|
}
|
|
92
|
-
function normalizeLookupSourceSettlement(source, outcome, value) {
|
|
93
|
-
const state = normalizedState(source, outcome, value);
|
|
83
|
+
function normalizeLookupSourceSettlement(source, outcome, value, requestedDomain, requested = true) {
|
|
84
|
+
const state = normalizedState(source, outcome, value, requestedDomain, requested);
|
|
94
85
|
const sourceRecord = record(value);
|
|
95
86
|
const threatObservation = THREAT_INTELLIGENCE_SOURCES.has(source)
|
|
96
87
|
? record(sourceRecord.observation)
|
|
@@ -102,12 +93,10 @@ function normalizeLookupSourceSettlement(source, outcome, value) {
|
|
|
102
93
|
: null;
|
|
103
94
|
const complete = threatObservation
|
|
104
95
|
? state !== 'error' && threatObservation.complete === true
|
|
105
|
-
:
|
|
96
|
+
: ['success', 'not_found'].includes(state) && sourceRecord.complete !== false;
|
|
106
97
|
const truncated = threatObservation
|
|
107
98
|
? threatObservation.truncated === true
|
|
108
|
-
:
|
|
109
|
-
|| sourceRecord.recordsTruncated === true
|
|
110
|
-
|| sourceRecord.bodyTruncated === true;
|
|
99
|
+
: sourceTruncated(value);
|
|
111
100
|
const limitation = outcome === 'rejected'
|
|
112
101
|
? 'This source did not complete. No absence or safety conclusion was inferred.'
|
|
113
102
|
: state === 'skipped'
|
package/lib/lookup.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
|
|
|
21
21
|
import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
|
|
22
22
|
import { buildUnifiedLookupResponse } from "./lookup-response.mjs";
|
|
23
23
|
import { createLookupTimingTracker } from "./lookup-diagnostics.mjs";
|
|
24
|
+
import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
|
|
24
25
|
async function runUnifiedLookup(classified, options = {}) {
|
|
25
26
|
const fetchRdap = options.fetchRdapRecord || fetchRdapRecord;
|
|
26
27
|
const fetchRegistrarRdap = options.fetchRegistrarRdapRecord || fetchRegistrarRdapRecord;
|
|
@@ -44,19 +45,30 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
44
45
|
const securityTxtRequested = options.securityTxt === true;
|
|
45
46
|
const featurePolicy = options.featurePolicy || networkFeaturePolicy();
|
|
46
47
|
const timing = createLookupTimingTracker(!fast && !compact, options.now || Date.now);
|
|
47
|
-
const
|
|
48
|
+
const startedCollectors = [];
|
|
49
|
+
const measure = (source, operation) => (timing.measure(source, () => abortable(() => {
|
|
50
|
+
const running = Promise.resolve(operation());
|
|
51
|
+
if (options.waitForStartedCollectors)
|
|
52
|
+
startedCollectors.push(running.catch(() => { }));
|
|
53
|
+
return running;
|
|
54
|
+
}, options.signal)));
|
|
48
55
|
const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
|
|
49
56
|
const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
|
|
50
57
|
const availabilityEnabled = featureDecision('availability', featurePolicy).enabled;
|
|
51
58
|
const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
|
|
52
59
|
const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
|
|
53
60
|
const skipWhois = fast || !whoisEnabled;
|
|
61
|
+
const selectedUrl = options.selectedUrl === undefined ? undefined
|
|
62
|
+
: prepareSelectedLookupUrl(options.selectedUrl, classified.inputHostname ?? classified.value);
|
|
63
|
+
if (selectedUrl && (classified.type !== 'domain' || fast || compact || !availabilityEnabled || !websiteProbeEnabled)) {
|
|
64
|
+
throw new TypeError('Selected URL collection requires an enabled full Deep domain lookup.');
|
|
65
|
+
}
|
|
54
66
|
const rdapPromise = rdapEnabled
|
|
55
|
-
? measure('rdap', () => fetchRdap(classified.type, classified.value))
|
|
67
|
+
? measure('rdap', () => fetchRdap(classified.type, classified.value, options.signal ? { signal: options.signal } : {}))
|
|
56
68
|
: Promise.resolve(null);
|
|
57
69
|
const whoisPromise = skipWhois
|
|
58
70
|
? Promise.resolve(null)
|
|
59
|
-
: measure('whois', () => fetchWhois(classified.value));
|
|
71
|
+
: measure('whois', () => fetchWhois(classified.value, options.signal ? { signal: options.signal } : {}));
|
|
60
72
|
// Registrar RDAP is a separately attributed deep-lookup enrichment. It may
|
|
61
73
|
// overlap the WHOIS chain, but it never joins the promises used to decide
|
|
62
74
|
// availability and can add up to its own bounded timeout to a deep lookup.
|
|
@@ -76,7 +88,10 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
76
88
|
includeStructuredDataIdentity: !fast && !compact,
|
|
77
89
|
includeTechnologyProfile: !fast,
|
|
78
90
|
includeSecurityPosture: !compact,
|
|
91
|
+
...(!fast && !compact ? { observationHostname: classified.inputHostname } : {}),
|
|
92
|
+
...(selectedUrl ? { selectedUrl } : {}),
|
|
79
93
|
featurePolicy,
|
|
94
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
80
95
|
rdapRecordPromise: rdapPromise,
|
|
81
96
|
whoisChainPromise: whoisPromise,
|
|
82
97
|
...(selectedDnsResolvers ? {
|
|
@@ -155,7 +170,7 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
155
170
|
]);
|
|
156
171
|
const notify = (source, outcome, value) => {
|
|
157
172
|
try {
|
|
158
|
-
options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value));
|
|
173
|
+
options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined, source !== 'rdap' || rdapEnabled));
|
|
159
174
|
}
|
|
160
175
|
catch {
|
|
161
176
|
// Presentation callbacks must never change evidence collection.
|
|
@@ -171,30 +186,36 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
171
186
|
void Promise.resolve(sourcePromise).then((value) => notify(source, 'fulfilled', value), (error) => notify(source, 'rejected', error));
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
189
|
+
try {
|
|
190
|
+
return await buildUnifiedLookupResponse({
|
|
191
|
+
availabilityEnabled,
|
|
192
|
+
availabilityPromise,
|
|
193
|
+
classified,
|
|
194
|
+
compact,
|
|
195
|
+
dnsIntelligenceEnabled,
|
|
196
|
+
fast,
|
|
197
|
+
inspectCertificateWarningData,
|
|
198
|
+
networkContextPromise,
|
|
199
|
+
options,
|
|
200
|
+
rdapEnabled,
|
|
201
|
+
rdapPromise,
|
|
202
|
+
registrarRdapPromise,
|
|
203
|
+
reverseDnsEligible,
|
|
204
|
+
reverseDnsPromise,
|
|
205
|
+
securityTxtPromise,
|
|
206
|
+
skipWhois,
|
|
207
|
+
threatfoxIntelligencePromise,
|
|
208
|
+
timing,
|
|
209
|
+
urlhausIntelligencePromise,
|
|
210
|
+
urlscanIntelligencePromise,
|
|
211
|
+
whoisEnabled,
|
|
212
|
+
whoisPromise,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (options.waitForStartedCollectors)
|
|
217
|
+
await Promise.all(startedCollectors);
|
|
218
|
+
}
|
|
198
219
|
}
|
|
199
220
|
export { runUnifiedLookup };
|
|
200
221
|
export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, } from "./lookup-diagnostics.mjs";
|
|
@@ -11,6 +11,7 @@ const OBSERVED_NETWORK_CONTEXT_VERSION = 1;
|
|
|
11
11
|
const MAX_NETWORK_CIDRS = 16;
|
|
12
12
|
const MAX_NETWORK_ATTEMPTS = 3;
|
|
13
13
|
const MAX_NETWORK_ABUSE_ROUTES = 6;
|
|
14
|
+
const NETWORK_ABUSE_ROUTE_LIMITATION = 'The network abuse-route projection reached its local retention limit; additional published routes may exist.';
|
|
14
15
|
const MAX_ENDPOINT_LENGTH = 2048;
|
|
15
16
|
const MAX_DETAIL_LENGTH = 240;
|
|
16
17
|
const MAX_NAME_LENGTH = 300;
|
|
@@ -144,20 +145,25 @@ function networkAbuseEntities(parsed) {
|
|
|
144
145
|
const fallback = parsed.abuse && typeof parsed.abuse === 'object' && !Array.isArray(parsed.abuse)
|
|
145
146
|
? [parsed.abuse]
|
|
146
147
|
: [];
|
|
147
|
-
return
|
|
148
|
-
.
|
|
149
|
-
|
|
148
|
+
return {
|
|
149
|
+
entities: [...roleEntities.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1), ...fallback]
|
|
150
|
+
.filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item)),
|
|
151
|
+
truncated: roleEntities.length > MAX_NETWORK_ABUSE_ROUTES + 1,
|
|
152
|
+
};
|
|
150
153
|
}
|
|
151
154
|
function projectNetworkAbuseRoutes(parsed, provenance) {
|
|
152
155
|
const routes = new Map();
|
|
153
|
-
|
|
156
|
+
const inventory = networkAbuseEntities(parsed);
|
|
157
|
+
let truncated = inventory.truncated;
|
|
158
|
+
for (const entity of inventory.entities) {
|
|
154
159
|
const emails = Array.isArray(entity.emails) ? entity.emails : [];
|
|
155
160
|
const phones = Array.isArray(entity.phones) ? entity.phones : [];
|
|
161
|
+
truncated ||= emails.length > MAX_NETWORK_ABUSE_ROUTES + 1 || phones.length > MAX_NETWORK_ABUSE_ROUTES + 1;
|
|
156
162
|
const candidates = [
|
|
157
163
|
{ channel: 'email', value: entity.email },
|
|
158
|
-
...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'email', value })),
|
|
164
|
+
...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'email', value })),
|
|
159
165
|
{ channel: 'phone', value: entity.phone },
|
|
160
|
-
...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'phone', value })),
|
|
166
|
+
...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'phone', value })),
|
|
161
167
|
];
|
|
162
168
|
for (const candidate of candidates) {
|
|
163
169
|
const contact = normalizedAbuseContact(candidate.value, candidate.channel);
|
|
@@ -165,6 +171,10 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
|
|
|
165
171
|
continue;
|
|
166
172
|
const key = `${candidate.channel}:${contact.toLowerCase()}`;
|
|
167
173
|
if (!routes.has(key)) {
|
|
174
|
+
if (routes.size >= MAX_NETWORK_ABUSE_ROUTES) {
|
|
175
|
+
truncated = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
168
178
|
routes.set(key, {
|
|
169
179
|
kind: 'network_hosting',
|
|
170
180
|
channel: candidate.channel,
|
|
@@ -186,11 +196,14 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
|
|
|
186
196
|
],
|
|
187
197
|
});
|
|
188
198
|
}
|
|
189
|
-
if (routes.size >= MAX_NETWORK_ABUSE_ROUTES)
|
|
190
|
-
return [...routes.values()];
|
|
191
199
|
}
|
|
192
200
|
}
|
|
193
|
-
return
|
|
201
|
+
return {
|
|
202
|
+
routes: [...routes.values()].map((route) => truncated
|
|
203
|
+
? { ...route, complete: false, truncated: true, limitations: [...route.limitations, NETWORK_ABUSE_ROUTE_LIMITATION] }
|
|
204
|
+
: route),
|
|
205
|
+
truncated,
|
|
206
|
+
};
|
|
194
207
|
}
|
|
195
208
|
function networkSummary(parsedValue, family) {
|
|
196
209
|
const parsed = record(parsedValue);
|
|
@@ -204,10 +217,12 @@ function networkSummary(parsedValue, family) {
|
|
|
204
217
|
const startAddress = publicAddress(parsed.startAddress, family);
|
|
205
218
|
const endAddress = publicAddress(parsed.endAddress, family);
|
|
206
219
|
const country = boundedString(parsed.country, 2);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
const limitations = [
|
|
221
|
+
...(parsed.serverTruncated === true ? ['The RDAP server declared that part of its response was truncated.'] : []),
|
|
222
|
+
...(parsed.entitiesTruncated === true ? ['Some IP RDAP contact records or fields were omitted during bounded normalisation.'] : []),
|
|
223
|
+
...(parsed.cidrsTruncated === true ? ['Some IP RDAP CIDR entries were invalid or exceeded the normalisation limit.'] : []),
|
|
224
|
+
...(sourceCidrs.length > MAX_NETWORK_CIDRS ? ['The network CIDR summary reached its local retention limit.'] : []),
|
|
225
|
+
];
|
|
211
226
|
return {
|
|
212
227
|
value: {
|
|
213
228
|
handle: boundedString(parsed.handle, MAX_NAME_LENGTH),
|
|
@@ -220,8 +235,8 @@ function networkSummary(parsedValue, family) {
|
|
|
220
235
|
networkType: boundedString(parsed.networkType, 160),
|
|
221
236
|
databaseUpdatedAt: isoTimestamp(lifecycle.databaseUpdatedDateIso || lifecycle.databaseUpdatedDate),
|
|
222
237
|
},
|
|
223
|
-
truncated,
|
|
224
|
-
|
|
238
|
+
truncated: limitations.length > 0,
|
|
239
|
+
limitations,
|
|
225
240
|
};
|
|
226
241
|
}
|
|
227
242
|
function baseContext(selection, input) {
|
|
@@ -299,28 +314,33 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
|
|
|
299
314
|
if (rdap.httpStatus !== 200 || !Object.keys(parsed).length)
|
|
300
315
|
throw new Error('IP RDAP returned no usable normalised object');
|
|
301
316
|
const summary = networkSummary(parsed, selection.family);
|
|
302
|
-
const partial = summary.truncated;
|
|
303
317
|
const sourceObservedAt = rdap.fetchedAt || observedAt();
|
|
304
|
-
const
|
|
318
|
+
const routeProjection = projectNetworkAbuseRoutes(parsed, {
|
|
305
319
|
endpoint: rdap.endpoint,
|
|
306
320
|
observedAt: sourceObservedAt,
|
|
307
321
|
selection,
|
|
308
|
-
complete: !
|
|
309
|
-
truncated:
|
|
322
|
+
complete: !summary.truncated,
|
|
323
|
+
truncated: summary.truncated,
|
|
310
324
|
});
|
|
325
|
+
const incompleteReasons = [
|
|
326
|
+
...summary.limitations,
|
|
327
|
+
...(routeProjection.truncated ? [NETWORK_ABUSE_ROUTE_LIMITATION] : []),
|
|
328
|
+
];
|
|
329
|
+
const partial = incompleteReasons.length > 0;
|
|
311
330
|
return baseContext(selection, {
|
|
312
331
|
status: partial ? 'partial' : 'success',
|
|
313
332
|
observedAt: sourceObservedAt,
|
|
314
333
|
durationMs,
|
|
315
334
|
complete: !partial,
|
|
316
335
|
truncated: partial,
|
|
317
|
-
detail:
|
|
336
|
+
detail: partial
|
|
337
|
+
? `Network registration was retained, but the source record is incomplete. ${incompleteReasons[0]}`
|
|
338
|
+
: 'The selected public endpoint address was mapped to its separately attributed IP RDAP registration.',
|
|
318
339
|
limitations: [
|
|
340
|
+
...incompleteReasons,
|
|
319
341
|
'This identifies the registered network for one point-in-time endpoint address, not a definitive origin host or hosting provider.',
|
|
320
342
|
'CDNs, reverse proxies, load balancers, shared hosting, and location-dependent DNS can present different networks.',
|
|
321
343
|
'Network registration is an investigative lead and does not prove control, ownership, intent, or maliciousness.',
|
|
322
|
-
...(summary.serverTruncated ? ['The RDAP server declared that part of its response was truncated.'] : []),
|
|
323
|
-
...(sourceCidrsWereCapped(parsed) ? ['The network CIDR summary reached its local retention limit.'] : []),
|
|
324
344
|
],
|
|
325
345
|
diagnostics: {
|
|
326
346
|
requestCount: 1,
|
|
@@ -330,7 +350,7 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
|
|
|
330
350
|
},
|
|
331
351
|
rdap,
|
|
332
352
|
network: summary.value,
|
|
333
|
-
abuseRouting,
|
|
353
|
+
abuseRouting: routeProjection.routes,
|
|
334
354
|
});
|
|
335
355
|
}
|
|
336
356
|
catch (error) {
|
|
@@ -348,8 +368,4 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
|
|
|
348
368
|
});
|
|
349
369
|
}
|
|
350
370
|
}
|
|
351
|
-
function sourceCidrsWereCapped(parsed) {
|
|
352
|
-
return parsed.cidrsTruncated === true
|
|
353
|
-
|| (Array.isArray(parsed.cidrs) && parsed.cidrs.length > MAX_NETWORK_CIDRS);
|
|
354
|
-
}
|
|
355
371
|
export { MAX_NETWORK_ATTEMPTS, MAX_NETWORK_ABUSE_ROUTES, MAX_NETWORK_CIDRS, OBSERVED_NETWORK_CONTEXT_VERSION, collectObservedNetworkContext, selectObservedNetworkAddress, };
|
|
@@ -1,107 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
export const OPPORTUNITY_REVIEW_THRESHOLD = 70;
|
|
4
|
-
const STATE_BASE = Object.freeze({
|
|
5
|
-
available: 82,
|
|
6
|
-
for_sale: 70,
|
|
7
|
-
expiring: 52,
|
|
8
|
-
registered: 22,
|
|
9
|
-
});
|
|
10
|
-
const STATE_LABELS = Object.freeze({
|
|
11
|
-
available: 'available',
|
|
12
|
-
for_sale: 'registered with a sale signal',
|
|
13
|
-
expiring: 'registered in a lifecycle transition',
|
|
14
|
-
registered: 'registered',
|
|
15
|
-
});
|
|
16
|
-
function finite(value) {
|
|
17
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
18
|
-
}
|
|
19
|
-
function formatAge(days) {
|
|
20
|
-
if (days < 60)
|
|
21
|
-
return `${Math.max(0, Math.round(days))}d old`;
|
|
22
|
-
const years = days / 365.25;
|
|
23
|
-
return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
|
|
24
|
-
}
|
|
25
|
-
function addFactor(factors, dimensions, family, label, delta) {
|
|
26
|
-
factors.push({ family, label, delta });
|
|
27
|
-
dimensions.set(family, (dimensions.get(family) ?? 0) + delta);
|
|
28
|
-
}
|
|
29
|
-
export function normalizeOpportunityModelVersion(value) {
|
|
30
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
|
|
31
|
-
}
|
|
32
|
-
// Opportunity is acquisition readiness, not price, value, eligibility, or a
|
|
33
|
-
// prediction that a registration will lapse. It uses only bounded evidence
|
|
34
|
-
// already collected for the current lookup.
|
|
35
|
-
export function explainOpportunityScore(value) {
|
|
36
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
-
return null;
|
|
38
|
-
const input = value;
|
|
39
|
-
const state = input.availability ?? input.state;
|
|
40
|
-
if (typeof state !== 'string' || !Object.hasOwn(STATE_BASE, state))
|
|
41
|
-
return null;
|
|
42
|
-
const factors = [];
|
|
43
|
-
const dimensions = new Map();
|
|
44
|
-
addFactor(factors, dimensions, 'registration', `Base readiness for “${STATE_LABELS[state] ?? state}”`, STATE_BASE[state] ?? 0);
|
|
45
|
-
if (input.confidence === 'high')
|
|
46
|
-
addFactor(factors, dimensions, 'registration', 'High-confidence registration decision', 8);
|
|
47
|
-
else if (input.confidence === 'medium')
|
|
48
|
-
addFactor(factors, dimensions, 'registration', 'Medium-confidence registration decision', 3);
|
|
49
|
-
else if (input.confidence === 'low')
|
|
50
|
-
addFactor(factors, dimensions, 'registration', 'Low-confidence registration decision', -8);
|
|
51
|
-
if (state === 'for_sale')
|
|
52
|
-
addFactor(factors, dimensions, 'listing', 'Bounded sale signal observed; listing remains unverified', 6);
|
|
53
|
-
else if (input.activityStatus === 'parked')
|
|
54
|
-
addFactor(factors, dimensions, 'listing', 'Parked page observed without a verified sale offer', 3);
|
|
55
|
-
else if (state === 'registered' && input.activityStatus === 'active') {
|
|
56
|
-
addFactor(factors, dimensions, 'listing', 'Active website suggests an in-use registration', -12);
|
|
57
|
-
}
|
|
58
|
-
if (input.hasPublicRegistrantContact === true) {
|
|
59
|
-
addFactor(factors, dimensions, 'contactability', 'Public registrant contact route observed; acquisition suitability is not established', 10);
|
|
60
|
-
}
|
|
61
|
-
const expiresInDays = finite(input.expiresInDays);
|
|
62
|
-
if (state === 'registered' && expiresInDays !== null && expiresInDays >= 0 && expiresInDays < 30) {
|
|
63
|
-
addFactor(factors, dimensions, 'lifecycle', 'Published expiry is within 30 days; release is not implied', 4);
|
|
64
|
-
}
|
|
65
|
-
if (typeof input.privacyProtected === 'boolean') {
|
|
66
|
-
addFactor(factors, dimensions, 'contactability', 'Registration privacy is neutral; it does not establish contactability', 0);
|
|
67
|
-
}
|
|
68
|
-
const ageDays = finite(input.domainAgeDays);
|
|
69
|
-
if (ageDays !== null && ageDays >= 0) {
|
|
70
|
-
addFactor(factors, dimensions, 'registration', `Domain age (${formatAge(ageDays)}) is context only`, 0);
|
|
71
|
-
}
|
|
72
|
-
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
73
|
-
const score = Math.max(0, Math.min(100, rawScore));
|
|
74
|
-
const dimensionLabels = {
|
|
75
|
-
registration: 'Registration certainty',
|
|
76
|
-
listing: 'Listing readiness',
|
|
77
|
-
contactability: 'Contactability',
|
|
78
|
-
lifecycle: 'Lifecycle timing',
|
|
79
|
-
};
|
|
80
|
-
const observedFamilies = [...dimensions.keys()];
|
|
81
|
-
const evidenceQuality = buildScoreEvidenceQuality(input, {
|
|
82
|
-
expectedDepth: 'fast',
|
|
83
|
-
observedFamilies,
|
|
84
|
-
expectedFamilies: ['registration'],
|
|
85
|
-
});
|
|
86
|
-
return {
|
|
87
|
-
modelVersion: OPPORTUNITY_MODEL_VERSION,
|
|
88
|
-
score,
|
|
89
|
-
rawScore,
|
|
90
|
-
capped: score !== rawScore,
|
|
91
|
-
factors,
|
|
92
|
-
dimensions: [...dimensions.entries()].map(([id, contribution]) => ({ id, label: dimensionLabels[id], contribution })),
|
|
93
|
-
evidenceQuality,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
export function computeOpportunityScore(value) {
|
|
97
|
-
return explainOpportunityScore(value)?.score ?? null;
|
|
98
|
-
}
|
|
99
|
-
export function opportunityTone(score) {
|
|
100
|
-
if (score === null)
|
|
101
|
-
return 'neutral';
|
|
102
|
-
if (score >= OPPORTUNITY_REVIEW_THRESHOLD)
|
|
103
|
-
return 'good';
|
|
104
|
-
if (score >= 40)
|
|
105
|
-
return 'neutral';
|
|
106
|
-
return 'warn';
|
|
107
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/opportunity-scoring.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
|
|
2
|
-
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "
|
|
2
|
+
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "../packages/analysis/project-metadata.mjs";
|
|
3
3
|
export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
|
|
4
4
|
export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
|
|
5
5
|
export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// disclosures cannot drift from the enforced request boundaries.
|
|
4
4
|
export const MAX_OUTBOUND_REDIRECTS = 5;
|
|
5
5
|
export const HOMEPAGE_FETCH_TIMEOUT_MS = 6_000;
|
|
6
|
-
|
|
6
|
+
// One MiB admits large static application pages while bounding bytes retained
|
|
7
|
+
// from each response. Parsing, derived outputs and request concurrency retain
|
|
8
|
+
// separate work limits; this is not a claim that larger pages lack evidence.
|
|
9
|
+
export const MAX_HOMEPAGE_BYTES = 1024 * 1024;
|
|
7
10
|
export const FAVICON_FETCH_TIMEOUT_MS = 5_000;
|
|
8
11
|
export const MAX_FAVICON_BYTES = 200_000;
|
|
9
12
|
export const MAX_FAVICON_CANDIDATES = 4;
|