@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/perceptual-hash.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Pure JS, no external dependency and no native bindings, so it runs the same
|
|
10
10
|
// on the serverless (Netlify) and self-hosted (Express) paths. That means
|
|
11
|
-
// decoding the image ourselves:
|
|
11
|
+
// decoding the image ourselves: the runtime has no built-in image decoder, only
|
|
12
12
|
// zlib (used here for PNG inflate). We support the favicon formats that
|
|
13
13
|
// actually occur in practice - PNG, and PNG- or BMP-encoded ICO - and return
|
|
14
14
|
// null for anything else (GIF/JPEG/SVG), which simply falls back to
|
|
@@ -21,11 +21,23 @@
|
|
|
21
21
|
// case, while staying cheap (no DCT). Output is 16 lowercase hex chars;
|
|
22
22
|
// similarity is the Hamming distance between two such strings.
|
|
23
23
|
import * as zlib from 'node:zlib';
|
|
24
|
-
import { hammingDistanceHex, isInformativePerceptualHash, } from "
|
|
24
|
+
import { hammingDistanceHex, isInformativePerceptualHash, } from "../packages/analysis/perceptual-hash-comparison.mjs";
|
|
25
25
|
// Bound the decode work on attacker-controlled image bytes. Real favicons top
|
|
26
|
-
// out around 256x256;
|
|
27
|
-
//
|
|
26
|
+
// out around 256x256; larger inputs above this ceiling remain exact-only
|
|
27
|
+
// evidence rather than causing unbounded pixel allocation.
|
|
28
28
|
const MAX_DIM = 1024;
|
|
29
|
+
// Permit two maximum-sized decode attempts (more than a normal resolution
|
|
30
|
+
// pyramid), while bounding aggregate inflation/filtering across a container.
|
|
31
|
+
// Encoded payloads may consume at most the container's byte length in total;
|
|
32
|
+
// ordinary non-overlapping entries never exceed that bound.
|
|
33
|
+
const MAX_DECODE_PIXELS = 2 * MAX_DIM * MAX_DIM;
|
|
34
|
+
function admitPixels(work, width, height) {
|
|
35
|
+
const count = width * height;
|
|
36
|
+
if (count > work.pixels)
|
|
37
|
+
return false;
|
|
38
|
+
work.pixels -= count;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
29
41
|
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
30
42
|
function byteAt(bytes, index) {
|
|
31
43
|
return bytes[index] ?? 0;
|
|
@@ -53,7 +65,7 @@ function paethPredictor(a, b, c) {
|
|
|
53
65
|
// { width, height, pixels } with pixels as RGBA bytes, or null for anything
|
|
54
66
|
// outside that subset (or malformed input). CRCs are intentionally not
|
|
55
67
|
// verified - we're computing a fuzzy hash, not validating integrity.
|
|
56
|
-
function decodePng(buf) {
|
|
68
|
+
function decodePng(buf, work) {
|
|
57
69
|
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
58
70
|
let width = 0;
|
|
59
71
|
let height = 0;
|
|
@@ -70,14 +82,18 @@ function decodePng(buf) {
|
|
|
70
82
|
const dataStart = offset + 8;
|
|
71
83
|
const dataEnd = dataStart + length;
|
|
72
84
|
if (dataEnd + 4 > buf.length)
|
|
73
|
-
|
|
85
|
+
return null;
|
|
74
86
|
const data = buf.subarray(dataStart, dataEnd);
|
|
75
87
|
if (type === 'IHDR') {
|
|
88
|
+
if (length !== 13 || width !== 0 || offset !== 8)
|
|
89
|
+
return null;
|
|
76
90
|
width = buf.readUInt32BE(dataStart);
|
|
77
91
|
height = buf.readUInt32BE(dataStart + 4);
|
|
78
92
|
bitDepth = byteAt(data, 8);
|
|
79
93
|
colorType = byteAt(data, 9);
|
|
80
94
|
interlace = byteAt(data, 12);
|
|
95
|
+
if (byteAt(data, 10) !== 0 || byteAt(data, 11) !== 0)
|
|
96
|
+
return null;
|
|
81
97
|
}
|
|
82
98
|
else if (type === 'PLTE') {
|
|
83
99
|
palette = data;
|
|
@@ -86,7 +102,7 @@ function decodePng(buf) {
|
|
|
86
102
|
transparency = data;
|
|
87
103
|
}
|
|
88
104
|
else if (type === 'IDAT') {
|
|
89
|
-
idatParts.push(
|
|
105
|
+
idatParts.push(data);
|
|
90
106
|
}
|
|
91
107
|
else if (type === 'IEND') {
|
|
92
108
|
break;
|
|
@@ -100,8 +116,17 @@ function decodePng(buf) {
|
|
|
100
116
|
return null;
|
|
101
117
|
if (colorType === 3 && !palette)
|
|
102
118
|
return null;
|
|
119
|
+
if (palette && (palette.length < 3 || palette.length > 768 || palette.length % 3 !== 0))
|
|
120
|
+
return null;
|
|
121
|
+
if (transparency && ((colorType === 0 && transparency.length !== 2)
|
|
122
|
+
|| (colorType === 2 && transparency.length !== 6)
|
|
123
|
+
|| (colorType === 3 && (!palette || transparency.length > palette.length / 3))
|
|
124
|
+
|| colorType === 4 || colorType === 6))
|
|
125
|
+
return null;
|
|
103
126
|
if (idatParts.length === 0)
|
|
104
127
|
return null;
|
|
128
|
+
if (!admitPixels(work, width, height))
|
|
129
|
+
return null;
|
|
105
130
|
// A valid non-interlaced 8-bit PNG decompresses to exactly one filter byte
|
|
106
131
|
// plus one scanline per row. Cap the inflate at that size so a malicious
|
|
107
132
|
// IDAT can't expand a sub-200KB favicon into hundreds of MB (a zip bomb)
|
|
@@ -162,6 +187,8 @@ function decodePng(buf) {
|
|
|
162
187
|
let alpha = 255;
|
|
163
188
|
if (colorType === 0) {
|
|
164
189
|
r = g = b = byteAt(recon, s);
|
|
190
|
+
if (transparency && r === transparency.readUInt16BE(0))
|
|
191
|
+
alpha = 0;
|
|
165
192
|
}
|
|
166
193
|
else if (colorType === 4) {
|
|
167
194
|
r = g = b = byteAt(recon, s);
|
|
@@ -171,6 +198,9 @@ function decodePng(buf) {
|
|
|
171
198
|
r = byteAt(recon, s);
|
|
172
199
|
g = byteAt(recon, s + 1);
|
|
173
200
|
b = byteAt(recon, s + 2);
|
|
201
|
+
if (transparency && r === transparency.readUInt16BE(0)
|
|
202
|
+
&& g === transparency.readUInt16BE(2) && b === transparency.readUInt16BE(4))
|
|
203
|
+
alpha = 0;
|
|
174
204
|
}
|
|
175
205
|
else if (colorType === 6) {
|
|
176
206
|
r = byteAt(recon, s);
|
|
@@ -200,13 +230,13 @@ function decodePng(buf) {
|
|
|
200
230
|
// BITMAPINFOHEADER, an optional palette (for <=8-bit depths), then bottom-up
|
|
201
231
|
// pixel rows. Supports the depths favicons actually ship: 1/4/8-bit
|
|
202
232
|
// palettized and 24/32-bit truecolour. The stored height is doubled (it
|
|
203
|
-
// includes the 1-bpp AND transparency mask)
|
|
204
|
-
//
|
|
205
|
-
function decodeDib(buf) {
|
|
233
|
+
// includes the 1-bpp AND transparency mask). Non-alpha and legacy all-zero
|
|
234
|
+
// alpha images use that mask; meaningful 32-bit alpha remains authoritative.
|
|
235
|
+
function decodeDib(buf, work) {
|
|
206
236
|
if (buf.length < 40)
|
|
207
237
|
return null;
|
|
208
238
|
const headerSize = buf.readUInt32LE(0);
|
|
209
|
-
if (headerSize < 40)
|
|
239
|
+
if (headerSize < 40 || headerSize > buf.length)
|
|
210
240
|
return null;
|
|
211
241
|
const width = buf.readInt32LE(4);
|
|
212
242
|
const rawHeight = buf.readInt32LE(8);
|
|
@@ -216,7 +246,9 @@ function decodeDib(buf) {
|
|
|
216
246
|
return null; // BI_RGB only
|
|
217
247
|
if (![1, 4, 8, 24, 32].includes(bitCount))
|
|
218
248
|
return null;
|
|
219
|
-
|
|
249
|
+
if (rawHeight <= 0 || rawHeight % 2 !== 0 || buf.readUInt16LE(12) !== 1)
|
|
250
|
+
return null;
|
|
251
|
+
const height = rawHeight / 2;
|
|
220
252
|
if (width < 1 || height < 1 || width > MAX_DIM || height > MAX_DIM)
|
|
221
253
|
return null;
|
|
222
254
|
// Palette (BGRA quads) for indexed depths, sitting between the header and
|
|
@@ -235,6 +267,13 @@ function decodeDib(buf) {
|
|
|
235
267
|
const rowSize = Math.floor((bitCount * width + 31) / 32) * 4; // padded to 4 bytes
|
|
236
268
|
if (pixelStart + rowSize * height > buf.length)
|
|
237
269
|
return null;
|
|
270
|
+
const maskStart = pixelStart + rowSize * height;
|
|
271
|
+
const maskRowSize = Math.ceil(width / 32) * 4;
|
|
272
|
+
const maskPresent = maskStart + maskRowSize * height <= buf.length;
|
|
273
|
+
if (bitCount !== 32 && !maskPresent)
|
|
274
|
+
return null;
|
|
275
|
+
if (!admitPixels(work, width, height))
|
|
276
|
+
return null;
|
|
238
277
|
const paletteColor = (index) => {
|
|
239
278
|
if (!palette || index >= palette.count)
|
|
240
279
|
return [0, 0, 0];
|
|
@@ -282,11 +321,16 @@ function decodeDib(buf) {
|
|
|
282
321
|
}
|
|
283
322
|
// Many legacy 32-bit BMP-in-ICO payloads leave the alpha plane all-zero and
|
|
284
323
|
// rely on the separate 1-bit AND mask for transparency. Trusting those alpha
|
|
285
|
-
// bytes literally would composite the whole icon to white
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
324
|
+
// bytes literally would composite the whole icon to white. Restore opacity
|
|
325
|
+
// only where the legacy mask permits it. No mask plus meaningful 32-bit
|
|
326
|
+
// alpha is valid; an omitted legacy mask retains the opaque fallback.
|
|
327
|
+
if (bitCount !== 32 || !sawNonzeroAlpha) {
|
|
328
|
+
for (let y = 0; y < height; y += 1) {
|
|
329
|
+
for (let x = 0; x < width; x += 1) {
|
|
330
|
+
const mask = maskPresent ? byteAt(buf, maskStart + (height - 1 - y) * maskRowSize + (x >> 3)) : 0;
|
|
331
|
+
pixels[(y * width + x) * 4 + 3] = mask & (0x80 >> (x & 7)) ? 0 : 255;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
290
334
|
}
|
|
291
335
|
return { width, height, pixels };
|
|
292
336
|
}
|
|
@@ -295,9 +339,10 @@ function decodeDib(buf) {
|
|
|
295
339
|
// one is in a variant we don't decode (e.g. a 4-bit-palette PNG sub-entry
|
|
296
340
|
// alongside a plain BMP one). A multi-size favicon.ico only needs one usable
|
|
297
341
|
// entry, and any size hashes to nearly the same dHash since it downsamples.
|
|
298
|
-
function decodeIco(buf) {
|
|
342
|
+
function decodeIco(buf, work) {
|
|
299
343
|
const count = buf.readUInt16LE(4);
|
|
300
|
-
|
|
344
|
+
const directoryEnd = 6 + count * 16;
|
|
345
|
+
if (count < 1 || directoryEnd > buf.length)
|
|
301
346
|
return null;
|
|
302
347
|
const entries = [];
|
|
303
348
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -306,14 +351,22 @@ function decodeIco(buf) {
|
|
|
306
351
|
const h = buf[entry + 1] || 256;
|
|
307
352
|
const size = buf.readUInt32LE(entry + 8);
|
|
308
353
|
const dataOffset = buf.readUInt32LE(entry + 12);
|
|
309
|
-
if (dataOffset + size > buf.length || size < 1)
|
|
354
|
+
if (dataOffset < directoryEnd || dataOffset + size > buf.length || size < 1)
|
|
310
355
|
continue;
|
|
311
356
|
entries.push({ area: w * h, dataOffset, size });
|
|
312
357
|
}
|
|
313
358
|
entries.sort((a, b) => b.area - a.area);
|
|
359
|
+
const visited = new Set();
|
|
314
360
|
for (const { dataOffset, size } of entries) {
|
|
361
|
+
const identity = `${dataOffset}:${size}`;
|
|
362
|
+
if (visited.has(identity))
|
|
363
|
+
continue;
|
|
364
|
+
visited.add(identity);
|
|
365
|
+
if (size > work.encodedBytes)
|
|
366
|
+
return null;
|
|
367
|
+
work.encodedBytes -= size;
|
|
315
368
|
const payload = buf.subarray(dataOffset, dataOffset + size);
|
|
316
|
-
const image = isPng(payload) ? decodePng(payload) : decodeDib(payload);
|
|
369
|
+
const image = isPng(payload) ? decodePng(payload, work) : decodeDib(payload, work);
|
|
317
370
|
if (image)
|
|
318
371
|
return image;
|
|
319
372
|
}
|
|
@@ -322,10 +375,11 @@ function decodeIco(buf) {
|
|
|
322
375
|
function decodeImage(buf) {
|
|
323
376
|
if (!Buffer.isBuffer(buf))
|
|
324
377
|
return null;
|
|
378
|
+
const work = { encodedBytes: buf.length, pixels: MAX_DECODE_PIXELS };
|
|
325
379
|
if (isPng(buf))
|
|
326
|
-
return decodePng(buf);
|
|
380
|
+
return decodePng(buf, work);
|
|
327
381
|
if (isIco(buf))
|
|
328
|
-
return decodeIco(buf);
|
|
382
|
+
return decodeIco(buf, work);
|
|
329
383
|
return null;
|
|
330
384
|
}
|
|
331
385
|
// Downscales to cols x rows of grayscale luma. Transparency is composited
|
|
@@ -428,4 +482,5 @@ function inspectDecodedImage(buf) {
|
|
|
428
482
|
// produced by the optional local capture package. Keep the generic name as an
|
|
429
483
|
// alias so the evidence contract does not imply that a screenshot is a favicon.
|
|
430
484
|
const imagePerceptualHash = faviconPerceptualHash;
|
|
431
|
-
|
|
485
|
+
// Pixel consumers share the same strict byte/dimension admission as hashing.
|
|
486
|
+
export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash, inspectDecodedImage, decodeImage as decodeBoundedImagePixels, dHash as imagePixelsPerceptualHash };
|
|
@@ -1,29 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
4
|
-
const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
|
|
5
|
-
export function isUriShapedLabel(value) {
|
|
6
|
-
if (typeof value !== 'string')
|
|
7
|
-
return false;
|
|
8
|
-
const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
|
|
9
|
-
return URI_LABEL_SCHEME_RE.test(candidate);
|
|
10
|
-
}
|
|
11
|
-
export function buildPortableGeneratorMetadata(applicationVersion) {
|
|
12
|
-
let version = null;
|
|
13
|
-
if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
|
|
14
|
-
try {
|
|
15
|
-
version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
version = null;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return Object.freeze({
|
|
22
|
-
name: 'WHOISleuth',
|
|
23
|
-
version,
|
|
24
|
-
projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
export function portableGeneratorAttribution(generator) {
|
|
28
|
-
return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
|
|
29
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/portable-generator.mjs";
|
package/lib/project-metadata.mjs
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
export const WHOISLEUTH_SITE_ORIGIN = 'https://www.whoisleuth.com';
|
|
4
|
-
export const WHOISLEUTH_PROJECT_URL = `${WHOISLEUTH_SITE_ORIGIN}/`;
|
|
5
|
-
export const WHOISLEUTH_REQUEST_POLICY_URL = `${WHOISLEUTH_SITE_ORIGIN}/request-policy`;
|
|
6
|
-
export const WHOISLEUTH_SOURCE_REPOSITORY_URL = 'https://github.com/slicedearth/whoisleuth';
|
|
7
|
-
export const WHOISLEUTH_SOURCE_REPOSITORY_GIT_URL = `git+${WHOISLEUTH_SOURCE_REPOSITORY_URL}.git`;
|
|
8
|
-
export const WHOISLEUTH_SOURCE_ISSUES_URL = `${WHOISLEUTH_SOURCE_REPOSITORY_URL}/issues`;
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/project-metadata.mjs";
|
package/lib/rdap-bootstrap.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// IANA RDAP bootstrap retrieval, bounded stale caching, and authority
|
|
2
2
|
// selection for domain, IP, and ASN queries.
|
|
3
3
|
import net from 'node:net';
|
|
4
|
+
import { abortable } from "./abort.mjs";
|
|
4
5
|
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
5
6
|
const BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
6
7
|
const BOOTSTRAP_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -116,21 +117,17 @@ function validBootstrap(data) {
|
|
|
116
117
|
&& Array.isArray(service[1]) && service[1].length > 0
|
|
117
118
|
&& service[1].some((url) => typeof url === 'string' && /^https?:\/\//i.test(url))));
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
if (cached && now() - cached.fetchedAt < BOOTSTRAP_TTL_MS)
|
|
126
|
-
return cached.data;
|
|
127
|
-
const inflight = bootstrapInflight.get(kind);
|
|
128
|
-
if (inflight)
|
|
129
|
-
return inflight;
|
|
130
|
-
const request = (async () => {
|
|
120
|
+
function startBootstrap(kind, options) {
|
|
121
|
+
const now = options.now ?? Date.now;
|
|
122
|
+
const fetchUpstream = options.fetchUpstream ?? fetchRdapDetailedWithTimeout;
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const { signal } = controller;
|
|
125
|
+
const request = abortable(async () => {
|
|
131
126
|
try {
|
|
127
|
+
signal.throwIfAborted();
|
|
132
128
|
const requestedEndpoint = `https://data.iana.org/rdap/${kind}.json`;
|
|
133
|
-
const response = await fetchUpstream(requestedEndpoint, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
|
|
129
|
+
const response = await fetchUpstream(requestedEndpoint, { signal }, BOOTSTRAP_FETCH_TIMEOUT_MS);
|
|
130
|
+
signal.throwIfAborted();
|
|
134
131
|
const finalEndpoint = admitRdapEndpoint(response.finalUrl ?? requestedEndpoint);
|
|
135
132
|
if (finalEndpoint !== requestedEndpoint) {
|
|
136
133
|
throw new Error(`IANA bootstrap redirected outside its fixed source endpoint for ${kind}`);
|
|
@@ -152,21 +149,74 @@ async function fetchBootstrap(kind, options = {}) {
|
|
|
152
149
|
return data;
|
|
153
150
|
}
|
|
154
151
|
catch (cause) {
|
|
152
|
+
signal.throwIfAborted();
|
|
155
153
|
const fallback = bootstrapCache.get(kind);
|
|
156
154
|
if (fallback && now() - fallback.fetchedAt <= BOOTSTRAP_STALE_TTL_MS) {
|
|
157
155
|
return fallback.data;
|
|
158
156
|
}
|
|
159
157
|
throw cause;
|
|
160
158
|
}
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
}, signal);
|
|
160
|
+
const flight = {
|
|
161
|
+
controller, readers: 0, settled: false,
|
|
162
|
+
request: request.finally(() => {
|
|
163
|
+
flight.settled = true;
|
|
164
|
+
if (bootstrapInflight.get(kind) === flight)
|
|
165
|
+
bootstrapInflight.delete(kind);
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
bootstrapInflight.set(kind, flight);
|
|
169
|
+
return flight;
|
|
170
|
+
}
|
|
171
|
+
async function fetchBootstrap(kind, options = {}) {
|
|
172
|
+
if (!BOOTSTRAP_KINDS.has(kind))
|
|
173
|
+
throw new Error(`Unsupported RDAP bootstrap kind: ${kind}`);
|
|
174
|
+
options.signal?.throwIfAborted();
|
|
175
|
+
const now = options.now ?? Date.now;
|
|
176
|
+
const cached = bootstrapCache.get(kind);
|
|
177
|
+
if (cached && now() - cached.fetchedAt < BOOTSTRAP_TTL_MS)
|
|
178
|
+
return cached.data;
|
|
179
|
+
const flight = bootstrapInflight.get(kind) ?? startBootstrap(kind, options);
|
|
180
|
+
flight.readers++;
|
|
181
|
+
let released = false;
|
|
182
|
+
const release = () => {
|
|
183
|
+
if (released)
|
|
184
|
+
return;
|
|
185
|
+
released = true;
|
|
186
|
+
// One deadline releases only its reader. The fixed-source transport lives
|
|
187
|
+
// until the last reader leaves, and never caches a cancelled late response.
|
|
188
|
+
flight.readers--;
|
|
189
|
+
if (!flight.readers && !flight.settled) {
|
|
190
|
+
flight.controller.abort();
|
|
191
|
+
if (bootstrapInflight.get(kind) === flight)
|
|
192
|
+
bootstrapInflight.delete(kind);
|
|
163
193
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
};
|
|
195
|
+
const { signal } = options;
|
|
196
|
+
let cancel = () => { };
|
|
197
|
+
const result = new Promise((resolve, reject) => {
|
|
198
|
+
// Attach before observing cancellation so even an abandoned queued flight
|
|
199
|
+
// retains a rejection handler. Release synchronously to avoid starting it.
|
|
200
|
+
flight.request.then(resolve, reject);
|
|
201
|
+
cancel = () => { release(); reject(signal?.reason); };
|
|
202
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
203
|
+
if (signal?.aborted)
|
|
204
|
+
cancel();
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
return await result;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
signal?.removeEventListener('abort', cancel);
|
|
211
|
+
release();
|
|
212
|
+
if (flight.controller.signal.aborted)
|
|
213
|
+
await flight.request.catch(() => { });
|
|
214
|
+
}
|
|
167
215
|
}
|
|
168
216
|
function clearRdapBootstrapCache() {
|
|
169
217
|
bootstrapCache.clear();
|
|
218
|
+
for (const flight of bootstrapInflight.values())
|
|
219
|
+
flight.controller.abort();
|
|
170
220
|
bootstrapInflight.clear();
|
|
171
221
|
}
|
|
172
222
|
function uniqueRdapBases(urls) {
|
|
@@ -185,9 +235,9 @@ function uniqueRdapBases(urls) {
|
|
|
185
235
|
return true;
|
|
186
236
|
});
|
|
187
237
|
}
|
|
188
|
-
async function findRdapBases(type, value) {
|
|
238
|
+
async function findRdapBases(type, value, options = {}) {
|
|
189
239
|
if (type === 'domain') {
|
|
190
|
-
const bootstrap = await fetchBootstrap('dns');
|
|
240
|
+
const bootstrap = await fetchBootstrap('dns', options);
|
|
191
241
|
const tld = value.split('.').pop()?.toLowerCase() || '';
|
|
192
242
|
for (const [tlds, urls] of bootstrap.services) {
|
|
193
243
|
if (tlds.some((entry) => entry.toLowerCase() === tld))
|
|
@@ -196,7 +246,7 @@ async function findRdapBases(type, value) {
|
|
|
196
246
|
return [];
|
|
197
247
|
}
|
|
198
248
|
if (type === 'ipv4' || type === 'ipv6') {
|
|
199
|
-
const bootstrap = await fetchBootstrap(type);
|
|
249
|
+
const bootstrap = await fetchBootstrap(type, options);
|
|
200
250
|
const matcher = type === 'ipv4' ? ipInCidrV4 : ipInCidrV6;
|
|
201
251
|
let best = null;
|
|
202
252
|
let bestPrefix = -1;
|
|
@@ -215,7 +265,7 @@ async function findRdapBases(type, value) {
|
|
|
215
265
|
return best || [];
|
|
216
266
|
}
|
|
217
267
|
if (type === 'asn') {
|
|
218
|
-
const bootstrap = await fetchBootstrap('asn');
|
|
268
|
+
const bootstrap = await fetchBootstrap('asn', options);
|
|
219
269
|
const number = parseInt(value.replace(/^AS/i, ''), 10);
|
|
220
270
|
for (const [ranges, urls] of bootstrap.services) {
|
|
221
271
|
for (const range of ranges) {
|
package/lib/rdap-client.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cached } from "./lookup-cache.mjs";
|
|
2
2
|
import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
|
|
3
3
|
import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
|
|
4
|
-
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
4
|
+
import { fetchRdapDetailedWithTimeout, RdapBodyAdmissionError, } from "./rdap-transport.mjs";
|
|
5
5
|
import { admitRdapObjectEndpoint, validateRdapResponse, } from "./rdap-validation.mjs";
|
|
6
6
|
const UPSTREAM_TIMEOUT_MS = 7_000;
|
|
7
7
|
const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
|
|
@@ -24,20 +24,23 @@ function rdapPathFor(type, value) {
|
|
|
24
24
|
* Query the bounded bootstrap candidate set while retaining every attempted
|
|
25
25
|
* endpoint as separately attributed transport evidence.
|
|
26
26
|
*/
|
|
27
|
-
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout) {
|
|
27
|
+
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout, signal) {
|
|
28
|
+
signal?.throwIfAborted();
|
|
28
29
|
const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
|
|
29
30
|
if (candidates.length === 0)
|
|
30
31
|
return null;
|
|
31
32
|
const startedAt = Date.now();
|
|
32
33
|
const attempts = [];
|
|
33
34
|
for (const base of candidates) {
|
|
35
|
+
signal?.throwIfAborted();
|
|
34
36
|
const elapsed = Date.now() - startedAt;
|
|
35
37
|
const remaining = UPSTREAM_TOTAL_DEADLINE_MS - elapsed;
|
|
36
38
|
if (remaining <= 0)
|
|
37
39
|
break;
|
|
38
40
|
const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
|
|
39
41
|
try {
|
|
40
|
-
const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
|
|
42
|
+
const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' }, ...(signal ? { signal } : {}) }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
|
|
43
|
+
signal?.throwIfAborted();
|
|
41
44
|
const selectedEndpoint = admitRdapObjectEndpoint(type, value, upstream.finalUrl ?? url);
|
|
42
45
|
if (!selectedEndpoint) {
|
|
43
46
|
attempts.push(rdapAttempt(url, 'invalid_response', {
|
|
@@ -58,21 +61,25 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
58
61
|
}));
|
|
59
62
|
continue;
|
|
60
63
|
}
|
|
61
|
-
let data;
|
|
64
|
+
let data = null;
|
|
62
65
|
try {
|
|
63
66
|
data = JSON.parse(upstream.text);
|
|
64
67
|
}
|
|
65
68
|
catch {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// RFC 7480 section 5.3 makes the explanatory body optional for a
|
|
70
|
+
// negative answer. URL binding and transport/body bounds still apply.
|
|
71
|
+
if (upstream.status !== 404) {
|
|
72
|
+
attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
|
|
73
|
+
status: upstream.status,
|
|
74
|
+
detail: 'The endpoint returned invalid JSON.',
|
|
75
|
+
}));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
71
78
|
}
|
|
72
79
|
if (upstream.status === 404) {
|
|
73
80
|
attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
|
|
74
81
|
status: upstream.status,
|
|
75
|
-
detail:
|
|
82
|
+
detail: `The authoritative endpoint reported no matching object.${data === null ? ' No usable JSON explanation was supplied.' : ''}`,
|
|
76
83
|
selected: true,
|
|
77
84
|
}));
|
|
78
85
|
return {
|
|
@@ -110,6 +117,11 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
catch (cause) {
|
|
120
|
+
signal?.throwIfAborted();
|
|
121
|
+
if (cause instanceof RdapBodyAdmissionError) {
|
|
122
|
+
attempts.push(rdapAttempt(url, 'invalid_response', { status: cause.status, detail: cause.message }));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
113
125
|
const detail = String(errorProperty(cause, 'message') || 'request failed');
|
|
114
126
|
const outcome = errorProperty(cause, 'name') === 'AbortError' ||
|
|
115
127
|
/timed? out|time limit/i.test(detail)
|
|
@@ -125,9 +137,9 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
125
137
|
: 'the total upstream deadline expired';
|
|
126
138
|
throw Object.assign(new Error(`RDAP lookup failed across ${candidates.length} endpoint(s): ${detail}`), { attempts });
|
|
127
139
|
}
|
|
128
|
-
export async function fetchRdapRecordWithParser(type, value, parseRdap) {
|
|
140
|
+
export async function fetchRdapRecordWithParser(type, value, parseRdap, options = {}) {
|
|
129
141
|
return cached(`rdap:${type}:${value}`, async () => {
|
|
130
|
-
const bases = await findRdapBases(type, value);
|
|
131
|
-
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap);
|
|
132
|
-
});
|
|
142
|
+
const bases = await findRdapBases(type, value, options);
|
|
143
|
+
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, options.fetchUpstream, options.signal);
|
|
144
|
+
}, options.signal);
|
|
133
145
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Bounded normalization of untrusted RDAP response objects.
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import { registryDateIso } from "./registry-dates.mjs";
|
|
4
|
-
import {} from "./rdap-types.mjs";
|
|
4
|
+
import { MAX_RDAP_STATUSES, MAX_RDAP_REDACTIONS, } from "./rdap-types.mjs";
|
|
5
5
|
const MAX_RDAP_ENTITIES = 100;
|
|
6
|
+
const MAX_RDAP_EVENTS = 100;
|
|
6
7
|
const MAX_RDAP_ENTITY_DEPTH = 6;
|
|
7
8
|
const MAX_ENTITIES_PER_ROLE = 5;
|
|
8
9
|
const MAX_VCARD_ENTRIES = 100;
|
|
@@ -10,7 +11,6 @@ const MAX_ENTITY_ROLES = 12;
|
|
|
10
11
|
const MAX_CONTACT_VALUES = 8;
|
|
11
12
|
const MAX_ENTITY_LINKS = 10;
|
|
12
13
|
const MAX_RDAP_LINKS = 20;
|
|
13
|
-
const MAX_RDAP_REDACTIONS = 100;
|
|
14
14
|
const MAX_RDAP_VARIANT_GROUPS = 20;
|
|
15
15
|
const MAX_RDAP_VARIANT_NAMES = 50;
|
|
16
16
|
const MAX_RDAP_SERVER_TRUNCATION_REASONS = 8;
|
|
@@ -166,8 +166,10 @@ function contactValuesTruncated(vcardArray) {
|
|
|
166
166
|
return rawPart.length > 300 || /[\u0000-\u001f\u007f]/u.test(rawPart) || rawPart.trim() !== '';
|
|
167
167
|
});
|
|
168
168
|
const retainedParts = parts.filter((part) => part !== null);
|
|
169
|
+
// Empty structured components publish no address; they are not omitted
|
|
170
|
+
// evidence. Supplied non-blank values still pass every validation bound.
|
|
169
171
|
if (rejectedNonBlankPart
|
|
170
|
-
|| boundedString(retainedParts.join(', '), 1000) === null)
|
|
172
|
+
|| (retainedParts.length > 0 && boundedString(retainedParts.join(', '), 1000) === null))
|
|
171
173
|
return true;
|
|
172
174
|
}
|
|
173
175
|
return false;
|
|
@@ -506,18 +508,33 @@ function boundedEventString(value, maxLength) {
|
|
|
506
508
|
return null;
|
|
507
509
|
return value.trim() || null;
|
|
508
510
|
}
|
|
509
|
-
function
|
|
511
|
+
function normalizedEventInventory(value) {
|
|
510
512
|
if (!Array.isArray(value))
|
|
511
|
-
return [];
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
return { items: [], truncated: value !== undefined };
|
|
514
|
+
let truncated = value.length > MAX_RDAP_EVENTS;
|
|
515
|
+
const items = [];
|
|
516
|
+
for (const event of value.slice(0, MAX_RDAP_EVENTS)) {
|
|
517
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
518
|
+
truncated = true;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
515
521
|
const record = event;
|
|
516
522
|
const action = boundedEventString(record.eventAction, 100)?.toLowerCase().replace(/\s+/g, ' ') || null;
|
|
517
523
|
const date = boundedEventString(record.eventDate, 64);
|
|
518
524
|
const actor = boundedEventString(record.eventActor, 160);
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
if (record.eventAction !== undefined && action === null
|
|
526
|
+
|| record.eventDate !== undefined && date === null
|
|
527
|
+
|| record.eventActor !== undefined && actor === null)
|
|
528
|
+
truncated = true;
|
|
529
|
+
if (action || date)
|
|
530
|
+
items.push({ action, date, actor });
|
|
531
|
+
else
|
|
532
|
+
truncated = true;
|
|
533
|
+
}
|
|
534
|
+
return { items, truncated };
|
|
535
|
+
}
|
|
536
|
+
function normalizeRdapEvents(value) {
|
|
537
|
+
return normalizedEventInventory(value).items;
|
|
521
538
|
}
|
|
522
539
|
function lifecycleDate(events, action, newest) {
|
|
523
540
|
let selected = null;
|
|
@@ -697,7 +714,8 @@ function entityInventory(entities) {
|
|
|
697
714
|
function parseRdapObject(type, data) {
|
|
698
715
|
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
699
716
|
return null;
|
|
700
|
-
const
|
|
717
|
+
const eventInfo = normalizedEventInventory(data.events);
|
|
718
|
+
const events = eventInfo.items;
|
|
701
719
|
const redactionInfo = normalizeRedactions(data.redacted);
|
|
702
720
|
const noticesInfo = summarizeTextBlocks(data.notices);
|
|
703
721
|
const remarksInfo = summarizeTextBlocks(data.remarks);
|
|
@@ -708,12 +726,11 @@ function parseRdapObject(type, data) {
|
|
|
708
726
|
// to every RDAP object type. Unlike set-like fields, status order and
|
|
709
727
|
// repetition remain as published for backwards compatibility.
|
|
710
728
|
const statuses = Array.isArray(data.status)
|
|
711
|
-
? data.status.slice(0,
|
|
729
|
+
? data.status.slice(0, MAX_RDAP_STATUSES)
|
|
712
730
|
.map((status) => boundedString(status, 160))
|
|
713
731
|
.filter((status) => status !== null)
|
|
714
732
|
: [];
|
|
715
|
-
const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length,
|
|
716
|
-
const inspectedEventCount = Array.isArray(data.events) ? Math.min(data.events.length, 100) : 0;
|
|
733
|
+
const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length, MAX_RDAP_STATUSES) : 0;
|
|
717
734
|
const common = {
|
|
718
735
|
objectClassName: boundedString(data.objectClassName, 80, { lower: true }),
|
|
719
736
|
language: boundedString(data.lang, 35, { lower: true }),
|
|
@@ -732,12 +749,10 @@ function parseRdapObject(type, data) {
|
|
|
732
749
|
serverTruncationReasons,
|
|
733
750
|
statuses,
|
|
734
751
|
statusesTruncated: data.status !== undefined && (!Array.isArray(data.status)
|
|
735
|
-
|| data.status.length >
|
|
752
|
+
|| data.status.length > MAX_RDAP_STATUSES
|
|
736
753
|
|| statuses.length < inspectedStatusCount),
|
|
737
754
|
events,
|
|
738
|
-
eventsTruncated:
|
|
739
|
-
|| data.events.length > 100
|
|
740
|
-
|| events.length < inspectedEventCount),
|
|
755
|
+
eventsTruncated: eventInfo.truncated,
|
|
741
756
|
lifecycle: summarizeLifecycle(events),
|
|
742
757
|
};
|
|
743
758
|
if (type === 'domain') {
|
|
@@ -213,7 +213,7 @@ function inspectRdapReverseSearchResponse(value, requestedProperties = []) {
|
|
|
213
213
|
mappings.push(Object.freeze({
|
|
214
214
|
property,
|
|
215
215
|
propertyPath,
|
|
216
|
-
state: REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
|
|
216
|
+
state: Object.hasOwn(REGISTERED_PROPERTY_PATHS, property) && REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
|
|
217
217
|
? 'registered'
|
|
218
218
|
: 'unrecognized',
|
|
219
219
|
}));
|