@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/cli/lookup-diff.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { buildBulkDomainComparison, } from "../packages/investigation/bulk-domai
|
|
|
4
4
|
import { relationshipObservation } from "../packages/comparison/relationship-evidence.mjs";
|
|
5
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
6
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
7
|
+
import { lookupObservationHostname } from "../packages/evidence/lookup-target.mjs";
|
|
8
|
+
import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
|
|
7
9
|
export const CLI_LOOKUP_DIFF_SCHEMA = 'whoisleuth.cli.lookup-diff';
|
|
8
10
|
export const CLI_LOOKUP_DIFF_VERSION = 1;
|
|
9
11
|
function text(value, maximum = 300) {
|
|
@@ -129,19 +131,22 @@ function lookupComparisonInput(document) {
|
|
|
129
131
|
function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOString(), options = {}) {
|
|
130
132
|
const left = parseSavedLookupDocument(leftText, { label: 'Left lookup input' });
|
|
131
133
|
const right = parseSavedLookupDocument(rightText, { label: 'Right lookup input' });
|
|
132
|
-
const domainMode = options.domainMode
|
|
134
|
+
const domainMode = options.domainMode === 'auto'
|
|
135
|
+
? left.registrableDomain === right.registrableDomain ? 'same' : 'different'
|
|
136
|
+
: options.domainMode ?? 'different';
|
|
133
137
|
if (domainMode === 'different' && left.registrableDomain === right.registrableDomain) {
|
|
134
138
|
throw new CliUsageError('Lookup diff requires documents for two different domains.');
|
|
135
139
|
}
|
|
136
140
|
if (domainMode === 'same' && left.registrableDomain !== right.registrableDomain) {
|
|
137
141
|
throw new CliUsageError('Lookup history requires observations for the same domain.');
|
|
138
142
|
}
|
|
139
|
-
const
|
|
143
|
+
const measuredComparison = buildBulkDomainComparison(lookupComparisonInput(left), lookupComparisonInput(right), domainMode === 'same' ? right.generatedAt : null, {
|
|
140
144
|
allowSameDomain: domainMode === 'same',
|
|
141
145
|
...(domainMode === 'same' ? { now: Date.parse(right.generatedAt) } : {}),
|
|
142
146
|
});
|
|
143
|
-
if (!
|
|
147
|
+
if (!measuredComparison)
|
|
144
148
|
throw new CliUsageError('Lookup documents could not be compared.');
|
|
149
|
+
const comparison = qualifyLookupComparison(measuredComparison, left, right, domainMode === 'same');
|
|
145
150
|
return {
|
|
146
151
|
schema: CLI_LOOKUP_DIFF_SCHEMA,
|
|
147
152
|
version: CLI_LOOKUP_DIFF_VERSION,
|
|
@@ -160,14 +165,44 @@ function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOS
|
|
|
160
165
|
],
|
|
161
166
|
};
|
|
162
167
|
}
|
|
168
|
+
function qualifyLookupComparison(comparison, left, right, temporal) {
|
|
169
|
+
const leftScope = recordOrEmpty(left.availability);
|
|
170
|
+
const rightScope = recordOrEmpty(right.availability);
|
|
171
|
+
const hostnameChanged = temporal && lookupObservationHostname(leftScope) !== lookupObservationHostname(rightScope);
|
|
172
|
+
const selectedPage = leftScope.webObservationMode === 'selected_url' || rightScope.webObservationMode === 'selected_url';
|
|
173
|
+
if (!hostnameChanged && !selectedPage)
|
|
174
|
+
return comparison;
|
|
175
|
+
const limitation = 'The retained collection hostname differs or a selected URL has no retained complete path/query identity. Values remain visible, but those fields cannot establish change at the same target.';
|
|
176
|
+
const rows = comparison.rows.map((row) => {
|
|
177
|
+
const web = ['web', 'technology', 'identity'].includes(row.category);
|
|
178
|
+
const incomparable = hostnameChanged && !['registration', 'lifecycle', 'source'].includes(row.category)
|
|
179
|
+
|| temporal && selectedPage && web;
|
|
180
|
+
return {
|
|
181
|
+
...row,
|
|
182
|
+
...(selectedPage && web ? { method: 'Bounded website observation; one or both captures concern a selected URL rather than the homepage' } : {}),
|
|
183
|
+
...(incomparable ? {
|
|
184
|
+
state: 'not_recorded',
|
|
185
|
+
source: `${row.source}; comparable target identity not recorded`,
|
|
186
|
+
leftSourceState: 'not_recorded',
|
|
187
|
+
rightSourceState: 'not_recorded',
|
|
188
|
+
limitations: [...row.limitations, limitation],
|
|
189
|
+
} : {}),
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
...comparison, rows,
|
|
194
|
+
counts: Object.fromEntries(Object.keys(comparison.counts).map((state) => [state, rows.filter((row) => row.state === state).length])),
|
|
195
|
+
limitations: [...comparison.limitations, limitation],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
163
198
|
function formatCliLookupDiff(document) {
|
|
164
199
|
const rows = document.comparison.rows.filter((row) => row.state !== 'equal');
|
|
165
200
|
const output = [
|
|
166
201
|
'Domain evidence diff',
|
|
167
|
-
`Left ${document.left.domain}`,
|
|
168
|
-
`Left observed ${document.left.generatedAt}`,
|
|
169
|
-
`Right ${document.right.domain}`,
|
|
170
|
-
`Right observed ${document.right.generatedAt}`,
|
|
202
|
+
`Left ${safeTerminalValue(document.left.domain)}`,
|
|
203
|
+
`Left observed ${safeTerminalValue(document.left.generatedAt)}`,
|
|
204
|
+
`Right ${safeTerminalValue(document.right.domain)}`,
|
|
205
|
+
`Right observed ${safeTerminalValue(document.right.generatedAt)}`,
|
|
171
206
|
`Changed ${document.comparison.counts.different + document.comparison.counts.conflicting}`,
|
|
172
207
|
`One-sided ${document.comparison.counts.missing}`,
|
|
173
208
|
`Unavailable ${document.comparison.counts.unavailable + document.comparison.counts.not_recorded}`,
|
|
@@ -176,14 +211,14 @@ function formatCliLookupDiff(document) {
|
|
|
176
211
|
if (!rows.length)
|
|
177
212
|
output.push('No bounded differences were observed in the comparable fields.');
|
|
178
213
|
for (const row of rows) {
|
|
179
|
-
output.push(`${row.label} [${row.state.replaceAll('_', ' ')}]`);
|
|
180
|
-
output.push(` Left: ${row.left}`);
|
|
181
|
-
output.push(` Right: ${row.right}`);
|
|
182
|
-
output.push(` Source: ${row.source}`);
|
|
214
|
+
output.push(`${safeTerminalValue(row.label)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]`);
|
|
215
|
+
output.push(` Left: ${safeTerminalValue(row.left)}`);
|
|
216
|
+
output.push(` Right: ${safeTerminalValue(row.right)}`);
|
|
217
|
+
output.push(` Source: ${safeTerminalValue(row.source)}`);
|
|
183
218
|
}
|
|
184
219
|
output.push('', 'Limitations:');
|
|
185
220
|
for (const limitation of document.limitations)
|
|
186
|
-
output.push(` - ${limitation}`);
|
|
221
|
+
output.push(` - ${safeTerminalValue(limitation)}`);
|
|
187
222
|
return `${output.join('\n')}\n`;
|
|
188
223
|
}
|
|
189
224
|
export { buildCliLookupDiff, formatCliLookupDiff };
|
package/cli/lookup-plan.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { plannedLookupSources } from "../lib/lookup-progress.mjs";
|
|
1
2
|
export const CLI_LOOKUP_PLAN_SCHEMA = 'whoisleuth.cli.lookup-plan';
|
|
2
3
|
export const CLI_LOOKUP_PLAN_VERSION = 1;
|
|
3
4
|
const RDAP = Object.freeze({
|
|
@@ -21,7 +22,7 @@ const FAST_DOMAIN_EVIDENCE = Object.freeze({
|
|
|
21
22
|
const DEEP_DOMAIN_EVIDENCE = Object.freeze({
|
|
22
23
|
source: 'domain_evidence',
|
|
23
24
|
purpose: 'Collect bounded DNS, HTTP, TLS, page-identity, technology, and security-posture evidence.',
|
|
24
|
-
disclosure: 'DNS
|
|
25
|
+
disclosure: 'DNS, TLS and website probes use the submitted hostname. Registration-delegation queries use the registrable domain.',
|
|
25
26
|
conditional: false,
|
|
26
27
|
});
|
|
27
28
|
const REGISTRAR_RDAP = Object.freeze({
|
|
@@ -42,19 +43,18 @@ const REVERSE_DNS = Object.freeze({
|
|
|
42
43
|
disclosure: 'A DNS resolver receives the reverse lookup for the normalised IP address.',
|
|
43
44
|
conditional: false,
|
|
44
45
|
});
|
|
45
|
-
function plannedSources(classified, deep) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return Object.freeze([RDAP, WHOIS]);
|
|
46
|
+
function plannedSources(classified, deep, selectedUrl) {
|
|
47
|
+
const domainEvidence = selectedUrl && deep ? Object.freeze({
|
|
48
|
+
...DEEP_DOMAIN_EVIDENCE,
|
|
49
|
+
disclosure: 'The selected URL path and query are sent to the website and followed redirects. The fragment is not sent. DNS and TLS use its hostname; registration uses the registrable domain. Saved HTTP provenance omits queries, but page-derived text may contain sensitive information.',
|
|
50
|
+
}) : deep ? DEEP_DOMAIN_EVIDENCE : FAST_DOMAIN_EVIDENCE;
|
|
51
|
+
const sources = {
|
|
52
|
+
rdap: RDAP, whois: WHOIS, domain_evidence: domainEvidence,
|
|
53
|
+
registrar_rdap: REGISTRAR_RDAP, network_context: NETWORK_CONTEXT, reverse_dns: REVERSE_DNS,
|
|
54
|
+
};
|
|
55
|
+
return Object.freeze(plannedLookupSources(classified.type, deep ? 'deep' : 'fast').map(id => sources[id]));
|
|
56
56
|
}
|
|
57
|
-
function buildCliLookupPlan(query, classified, deep) {
|
|
57
|
+
function buildCliLookupPlan(query, classified, deep, selectedUrl = false) {
|
|
58
58
|
return Object.freeze({
|
|
59
59
|
schema: CLI_LOOKUP_PLAN_SCHEMA,
|
|
60
60
|
version: CLI_LOOKUP_PLAN_VERSION,
|
|
@@ -69,7 +69,7 @@ function buildCliLookupPlan(query, classified, deep) {
|
|
|
69
69
|
planning: Object.freeze({
|
|
70
70
|
networkRequestsMade: false,
|
|
71
71
|
collectionRequiresNetwork: true,
|
|
72
|
-
sources: plannedSources(classified, deep),
|
|
72
|
+
sources: plannedSources(classified, deep, selectedUrl),
|
|
73
73
|
}),
|
|
74
74
|
limitations: Object.freeze([
|
|
75
75
|
'This is a local preflight. It does not test source availability, feature configuration, cache state, redirects, referrals, or the exact number of requests a completed lookup may require.',
|
|
@@ -81,6 +81,8 @@ function formatCliLookupPlan(plan) {
|
|
|
81
81
|
const lines = [
|
|
82
82
|
'WHOISleuth lookup preflight',
|
|
83
83
|
`Target: ${plan.target.normalized}`,
|
|
84
|
+
...(plan.target.inputHostname && plan.target.inputHostname !== plan.target.normalized
|
|
85
|
+
? [`Submitted hostname: ${plan.target.inputHostname}`] : []),
|
|
84
86
|
`Type: ${plan.target.type}`,
|
|
85
87
|
`Mode: ${plan.mode}`,
|
|
86
88
|
'Network requests made: no',
|
package/cli/lookup-reconcile.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CliUsageError } from "./errors.mjs";
|
|
2
2
|
import { buildCliLookupDiff } from "./lookup-diff.mjs";
|
|
3
3
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
4
|
+
import { compareObservationContexts } from "../packages/comparison/capture-context.mjs";
|
|
5
|
+
import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
|
|
4
6
|
const CLI_LOOKUP_RECONCILIATION_SCHEMA = 'whoisleuth.cli.lookup-reconciliation';
|
|
5
7
|
const CLI_LOOKUP_RECONCILIATION_VERSION = 1;
|
|
6
8
|
const MAX_LOOKUP_RECONCILIATION_INPUT_BYTES = 32 * 1024 * 1024;
|
|
@@ -79,15 +81,11 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
|
|
|
79
81
|
};
|
|
80
82
|
});
|
|
81
83
|
const observations = documents.map(observationContext);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
: null));
|
|
85
|
-
const labelsComplete = identities.every((identity) => identity !== null);
|
|
86
|
-
const labelsDistinct = labelsComplete && new Set(identities).size === identities.length;
|
|
87
|
-
const independence = labelsDistinct
|
|
84
|
+
const context = compareObservationContexts(observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
|
|
85
|
+
const independence = context.labels === 'distinct'
|
|
88
86
|
? {
|
|
89
87
|
state: 'verified_distinct_labels',
|
|
90
|
-
reason: 'Every observation carries a
|
|
88
|
+
reason: 'Every observation carries a distinct analyst-supplied observer/vantage label pair. Actual collection independence is not verified.',
|
|
91
89
|
}
|
|
92
90
|
: {
|
|
93
91
|
state: 'unverified',
|
|
@@ -117,25 +115,30 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
|
|
|
117
115
|
};
|
|
118
116
|
}
|
|
119
117
|
function formatCliLookupReconciliation(document) {
|
|
118
|
+
const context = compareObservationContexts(document.observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
|
|
120
119
|
const output = [
|
|
121
120
|
'Lookup observation reconciliation',
|
|
122
|
-
`Domain ${document.domain}`,
|
|
121
|
+
`Domain ${safeTerminalValue(document.domain)}`,
|
|
123
122
|
`Observations ${document.summary.observationCount}`,
|
|
124
|
-
`
|
|
123
|
+
`Declared labels ${context.labels}; collection independence not verified`,
|
|
124
|
+
`Capture time span ${context.time.spanMilliseconds === null ? 'unknown' : `${context.time.spanMilliseconds / 1000} seconds`}`,
|
|
125
125
|
`Agreement ${document.summary.agreement}`,
|
|
126
126
|
`Disagreement ${document.summary.disagreement}`,
|
|
127
127
|
`Non-comparable ${document.summary.nonComparable}`,
|
|
128
128
|
'',
|
|
129
|
+
...document.observations.map((observation, index) => `Observation ${index + 1}: ${safeTerminalValue(observation.generatedAt)} · ${safeTerminalValue(observation.mode)} · observer ${safeTerminalValue(observation.observerLabel, 'not declared')} · vantage ${safeTerminalValue(observation.vantageLabel, 'not declared')}`),
|
|
130
|
+
'',
|
|
129
131
|
];
|
|
130
132
|
for (const field of document.fields.filter((candidate) => candidate.state !== 'agreement')) {
|
|
131
|
-
output.push(`${field.label} [${field.state.replaceAll('_', ' ')}]`);
|
|
133
|
+
output.push(`${safeTerminalValue(field.label)} [${safeTerminalValue(field.state).replaceAll('_', ' ')}]`);
|
|
132
134
|
for (const value of field.values) {
|
|
133
|
-
output.push(` Observation ${value.observation + 1}: ${value.value} (${value.sourceState.replaceAll('_', ' ')})`);
|
|
135
|
+
output.push(` Observation ${value.observation + 1}: ${safeTerminalValue(value.value)} (${safeTerminalValue(value.sourceState).replaceAll('_', ' ')})`);
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
output.push('', 'Limitations:');
|
|
137
139
|
for (const limitation of document.limitations)
|
|
138
|
-
output.push(` - ${limitation}`);
|
|
140
|
+
output.push(` - ${safeTerminalValue(limitation)}`);
|
|
141
|
+
output.push(' - These selected observations do not establish worldwide availability, removal or takedown.');
|
|
139
142
|
return `${output.join('\n')}\n`;
|
|
140
143
|
}
|
|
141
144
|
export { CLI_LOOKUP_RECONCILIATION_SCHEMA, CLI_LOOKUP_RECONCILIATION_VERSION, MAX_LOOKUP_RECONCILIATION_INPUT_BYTES, buildCliLookupReconciliation, formatCliLookupReconciliation, };
|
package/cli/lookup-timeline.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { CliUsageError } from "./errors.mjs";
|
|
3
3
|
import { buildCliLookupDiff } from "./lookup-diff.mjs";
|
|
4
4
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
5
|
+
import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
|
|
5
6
|
export const CLI_LOOKUP_TIMELINE_SCHEMA = 'whoisleuth.cli.lookup-timeline';
|
|
6
7
|
export const CLI_LOOKUP_TIMELINE_VERSION = 1;
|
|
7
8
|
export const MAX_LOOKUP_TIMELINE_OBSERVATIONS = 20;
|
|
@@ -95,10 +96,10 @@ function buildCliLookupTimeline(inputs, generatedAt = new Date().toISOString())
|
|
|
95
96
|
function formatCliLookupTimeline(document) {
|
|
96
97
|
const output = [
|
|
97
98
|
'Same-domain observation timeline',
|
|
98
|
-
`Domain ${document.domain}`,
|
|
99
|
+
`Domain ${safeTerminalValue(document.domain)}`,
|
|
99
100
|
`Observations ${document.summary.observationCount}`,
|
|
100
|
-
`First observed ${document.observations[0]?.generatedAt
|
|
101
|
-
`Last observed ${document.observations.at(-1)?.generatedAt
|
|
101
|
+
`First observed ${safeTerminalValue(document.observations[0]?.generatedAt, 'Unavailable')}`,
|
|
102
|
+
`Last observed ${safeTerminalValue(document.observations.at(-1)?.generatedAt, 'Unavailable')}`,
|
|
102
103
|
`Changed transitions ${document.summary.transitionsWithObservedChanges}`,
|
|
103
104
|
];
|
|
104
105
|
for (const transition of document.transitions) {
|
|
@@ -106,18 +107,18 @@ function formatCliLookupTimeline(document) {
|
|
|
106
107
|
const to = document.observations[transition.toObservation];
|
|
107
108
|
const counts = transition.comparison.counts;
|
|
108
109
|
const changedRows = transition.comparison.rows.filter((row) => (row.state === 'conflicting' || row.state === 'different' || row.state === 'missing'));
|
|
109
|
-
output.push('', `${from?.generatedAt
|
|
110
|
+
output.push('', `${safeTerminalValue(from?.generatedAt, 'Unavailable')} to ${safeTerminalValue(to?.generatedAt, 'Unavailable')}`, ` Observed changes ${counts.conflicting + counts.different}`, ` One-sided fields ${counts.missing}`, ` Unavailable ${counts.unavailable + counts.not_recorded}`);
|
|
110
111
|
if (!changedRows.length)
|
|
111
112
|
output.push(' No observed field changes in comparable evidence.');
|
|
112
113
|
for (const row of changedRows.slice(0, 10)) {
|
|
113
|
-
output.push(` ${row.label} [${row.state.replaceAll('_', ' ')}]: ${row.left} -> ${row.right}`);
|
|
114
|
+
output.push(` ${safeTerminalValue(row.label)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]: ${safeTerminalValue(row.left)} -> ${safeTerminalValue(row.right)}`);
|
|
114
115
|
}
|
|
115
116
|
if (changedRows.length > 10)
|
|
116
117
|
output.push(` ${changedRows.length - 10} additional changed fields omitted; use --json for the complete bounded comparison.`);
|
|
117
118
|
}
|
|
118
119
|
output.push('', 'Limitations:');
|
|
119
120
|
for (const limitation of document.limitations)
|
|
120
|
-
output.push(` - ${limitation}`);
|
|
121
|
+
output.push(` - ${safeTerminalValue(limitation)}`);
|
|
121
122
|
return `${output.join('\n')}\n`;
|
|
122
123
|
}
|
|
123
124
|
export { buildCliLookupTimeline, formatCliLookupTimeline };
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import {
|
|
3
|
+
import { MAX_MAIL_REVIEW_DOMAINS, addressDomains, messageIdDomains, authenticationServiceDomains, dkimSigningDomains, normalizeHeaderDomain } from "../packages/investigation/mail-header-identity.mjs";
|
|
4
|
+
export { MAX_MAIL_REVIEW_DOMAINS } from "../packages/investigation/mail-header-identity.mjs";
|
|
4
5
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
6
|
import { CliUsageError } from "./errors.mjs";
|
|
7
|
+
import { reviewMailAuthentication, selectReceiverTrust, authenticationHeaderLabel, aggregateMailAuthentication } from "../packages/investigation/mail-authentication-review.mjs";
|
|
6
8
|
export const CLI_MAIL_HEADER_REVIEW_SCHEMA = 'whoisleuth.cli.mail-header-review';
|
|
7
|
-
export const CLI_MAIL_HEADER_REVIEW_VERSION =
|
|
9
|
+
export const CLI_MAIL_HEADER_REVIEW_VERSION = 2;
|
|
8
10
|
export const MAX_MAIL_HEADER_INPUT_BYTES = 256 * 1024;
|
|
9
11
|
export const MAX_MAIL_HEADER_FIELDS = 512;
|
|
10
12
|
export const MAX_MAIL_HEADER_LINE_BYTES = 4 * 1024;
|
|
11
13
|
export const MAX_MAIL_HEADER_VALUE_LENGTH = 16 * 1024;
|
|
12
14
|
export const MAX_MAIL_RECEIVED_HOPS = 64;
|
|
13
|
-
export const MAX_MAIL_REVIEW_DOMAINS = 128;
|
|
14
15
|
const UNSAFE_HEADER_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u;
|
|
15
16
|
const HEADER_NAME_RE = /^[A-Za-z0-9-]{1,78}$/u;
|
|
16
|
-
const AUTHENTICATION_METHODS = Object.freeze(['spf', 'dkim', 'dmarc', 'arc']);
|
|
17
|
-
const AUTHENTICATION_STATES = new Set(['fail', 'neutral', 'none', 'pass', 'permerror', 'softfail', 'temperror']);
|
|
18
17
|
function boundedHeaderSection(value) {
|
|
19
18
|
if (typeof value !== 'string' || !value)
|
|
20
19
|
throw new CliUsageError('Mail-header review requires one UTF-8 message or header block.');
|
|
@@ -75,240 +74,6 @@ function parseHeaderFields(value) {
|
|
|
75
74
|
function fieldsNamed(fields, name) {
|
|
76
75
|
return fields.filter((field) => field.name === name).map((field) => field.value);
|
|
77
76
|
}
|
|
78
|
-
function normalizeHeaderDomain(value) {
|
|
79
|
-
const normalized = value.trim().toLowerCase().replace(/^\[|\]$/gu, '').replace(/\.$/u, '');
|
|
80
|
-
return isValidAsciiDomainName(normalized, { requireLowercase: true }) ? normalized : null;
|
|
81
|
-
}
|
|
82
|
-
function lexicalSegments(value, delimiter, trackAngles = false) {
|
|
83
|
-
const segments = [];
|
|
84
|
-
let start = 0;
|
|
85
|
-
let quoted = false;
|
|
86
|
-
let escaped = false;
|
|
87
|
-
let commentDepth = 0;
|
|
88
|
-
let angleDepth = 0;
|
|
89
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
90
|
-
const character = value[index] ?? '';
|
|
91
|
-
if (escaped) {
|
|
92
|
-
escaped = false;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
96
|
-
escaped = true;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (commentDepth > 0) {
|
|
100
|
-
if (character === '(')
|
|
101
|
-
commentDepth += 1;
|
|
102
|
-
else if (character === ')')
|
|
103
|
-
commentDepth -= 1;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (quoted) {
|
|
107
|
-
if (character === '"')
|
|
108
|
-
quoted = false;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (character === '"')
|
|
112
|
-
quoted = true;
|
|
113
|
-
else if (character === '(')
|
|
114
|
-
commentDepth = 1;
|
|
115
|
-
else if (character === ')')
|
|
116
|
-
return null;
|
|
117
|
-
else if (trackAngles && character === '<') {
|
|
118
|
-
if (angleDepth !== 0)
|
|
119
|
-
return null;
|
|
120
|
-
angleDepth = 1;
|
|
121
|
-
}
|
|
122
|
-
else if (trackAngles && character === '>') {
|
|
123
|
-
if (angleDepth !== 1)
|
|
124
|
-
return null;
|
|
125
|
-
angleDepth = 0;
|
|
126
|
-
}
|
|
127
|
-
else if (character === delimiter && angleDepth === 0) {
|
|
128
|
-
segments.push(value.slice(start, index));
|
|
129
|
-
start = index + 1;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (escaped || quoted || commentDepth !== 0 || angleDepth !== 0)
|
|
133
|
-
return null;
|
|
134
|
-
segments.push(value.slice(start));
|
|
135
|
-
return segments;
|
|
136
|
-
}
|
|
137
|
-
function angleAddress(value) {
|
|
138
|
-
let quoted = false;
|
|
139
|
-
let escaped = false;
|
|
140
|
-
let commentDepth = 0;
|
|
141
|
-
let start = -1;
|
|
142
|
-
let end = -1;
|
|
143
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
144
|
-
const character = value[index] ?? '';
|
|
145
|
-
if (escaped) {
|
|
146
|
-
escaped = false;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
150
|
-
escaped = true;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (commentDepth > 0) {
|
|
154
|
-
if (character === '(')
|
|
155
|
-
commentDepth += 1;
|
|
156
|
-
else if (character === ')')
|
|
157
|
-
commentDepth -= 1;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (quoted) {
|
|
161
|
-
if (character === '"')
|
|
162
|
-
quoted = false;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (character === '"')
|
|
166
|
-
quoted = true;
|
|
167
|
-
else if (character === '(')
|
|
168
|
-
commentDepth = 1;
|
|
169
|
-
else if (character === ')')
|
|
170
|
-
return null;
|
|
171
|
-
else if (character === '<') {
|
|
172
|
-
if (start >= 0 || end >= 0)
|
|
173
|
-
return null;
|
|
174
|
-
start = index;
|
|
175
|
-
}
|
|
176
|
-
else if (character === '>') {
|
|
177
|
-
if (start < 0 || end >= 0)
|
|
178
|
-
return null;
|
|
179
|
-
end = index;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (escaped || quoted || commentDepth !== 0 || (start >= 0) !== (end >= 0))
|
|
183
|
-
return null;
|
|
184
|
-
return start >= 0 ? value.slice(start + 1, end) : value;
|
|
185
|
-
}
|
|
186
|
-
function addressDomain(value) {
|
|
187
|
-
let candidate = '';
|
|
188
|
-
let quoted = false;
|
|
189
|
-
let escaped = false;
|
|
190
|
-
let commentDepth = 0;
|
|
191
|
-
const separators = [];
|
|
192
|
-
for (const character of value) {
|
|
193
|
-
if (escaped) {
|
|
194
|
-
candidate += character;
|
|
195
|
-
escaped = false;
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
199
|
-
if (quoted)
|
|
200
|
-
candidate += character;
|
|
201
|
-
escaped = true;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (commentDepth > 0) {
|
|
205
|
-
if (character === '(')
|
|
206
|
-
commentDepth += 1;
|
|
207
|
-
else if (character === ')')
|
|
208
|
-
commentDepth -= 1;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (quoted) {
|
|
212
|
-
candidate += character;
|
|
213
|
-
if (character === '"')
|
|
214
|
-
quoted = false;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (character === '"') {
|
|
218
|
-
quoted = true;
|
|
219
|
-
candidate += character;
|
|
220
|
-
}
|
|
221
|
-
else if (character === '(')
|
|
222
|
-
commentDepth = 1;
|
|
223
|
-
else if (character === ')')
|
|
224
|
-
return null;
|
|
225
|
-
else {
|
|
226
|
-
if (character === '@')
|
|
227
|
-
separators.push(candidate.length);
|
|
228
|
-
candidate += character;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (escaped || quoted || commentDepth !== 0 || separators.length !== 1)
|
|
232
|
-
return null;
|
|
233
|
-
const separator = separators[0] ?? -1;
|
|
234
|
-
const local = candidate.slice(0, separator).trim();
|
|
235
|
-
const domain = candidate.slice(separator + 1).trim();
|
|
236
|
-
const validLocal = /^(?:[^\s"<>(),:;@]+|"(?:[^"\\]|\\.)+")$/u.test(local);
|
|
237
|
-
if (!validLocal || !domain || /\s/u.test(domain))
|
|
238
|
-
return null;
|
|
239
|
-
return normalizeHeaderDomain(domain);
|
|
240
|
-
}
|
|
241
|
-
function addressDomains(values) {
|
|
242
|
-
const domains = new Set();
|
|
243
|
-
for (const value of values) {
|
|
244
|
-
const mailboxes = lexicalSegments(value, ',', true);
|
|
245
|
-
if (!mailboxes)
|
|
246
|
-
continue;
|
|
247
|
-
for (const mailbox of mailboxes) {
|
|
248
|
-
const selected = angleAddress(mailbox);
|
|
249
|
-
const domain = selected === null ? null : addressDomain(selected);
|
|
250
|
-
if (domain)
|
|
251
|
-
domains.add(domain);
|
|
252
|
-
if (domains.size >= MAX_MAIL_REVIEW_DOMAINS)
|
|
253
|
-
return [...domains].sort();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return [...domains].sort();
|
|
257
|
-
}
|
|
258
|
-
function messageIdDomains(values) {
|
|
259
|
-
return addressDomains(values.map((value) => value.replace(/[<>]/gu, '')));
|
|
260
|
-
}
|
|
261
|
-
function authenticationState(values, method, directValues = []) {
|
|
262
|
-
const states = new Set();
|
|
263
|
-
let observations = 0;
|
|
264
|
-
for (const value of values) {
|
|
265
|
-
const clauses = lexicalSegments(value, ';');
|
|
266
|
-
if (!clauses)
|
|
267
|
-
continue;
|
|
268
|
-
for (const clause of clauses) {
|
|
269
|
-
const match = /^\s*([a-z][a-z0-9_-]*)\s*=\s*([a-z0-9_-]+)(?=\s|$)/iu.exec(clause);
|
|
270
|
-
if (match?.[1]?.toLowerCase() === method) {
|
|
271
|
-
observations += 1;
|
|
272
|
-
const raw = (match[2] ?? '').toLowerCase();
|
|
273
|
-
states.add(AUTHENTICATION_STATES.has(raw) ? raw : 'unknown');
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
for (const value of directValues) {
|
|
278
|
-
const match = /^\s*([a-z0-9_-]+)(?=\s|\(|$)/iu.exec(value);
|
|
279
|
-
const raw = (match?.[1] ?? '').toLowerCase();
|
|
280
|
-
if (!raw)
|
|
281
|
-
continue;
|
|
282
|
-
observations += 1;
|
|
283
|
-
states.add(AUTHENTICATION_STATES.has(raw) ? raw : 'unknown');
|
|
284
|
-
}
|
|
285
|
-
return Object.freeze({
|
|
286
|
-
state: observations === 0 ? 'unknown' : states.size === 1 ? [...states][0] : 'mixed',
|
|
287
|
-
observations,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
function authenticationServiceDomains(values) {
|
|
291
|
-
const output = new Set();
|
|
292
|
-
for (const value of values) {
|
|
293
|
-
const clauses = lexicalSegments(value, ';');
|
|
294
|
-
const service = normalizeHeaderDomain((clauses?.[0] ?? '').trim().split(/\s/u, 1)[0] ?? '');
|
|
295
|
-
if (service)
|
|
296
|
-
output.add(service);
|
|
297
|
-
}
|
|
298
|
-
return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
|
|
299
|
-
}
|
|
300
|
-
function dkimSigningDomains(values) {
|
|
301
|
-
const output = new Set();
|
|
302
|
-
for (const value of values) {
|
|
303
|
-
for (const clause of lexicalSegments(value, ';') ?? []) {
|
|
304
|
-
const match = /^\s*d\s*=\s*([^\s;]+)/iu.exec(clause);
|
|
305
|
-
const domain = normalizeHeaderDomain(match?.[1] ?? '');
|
|
306
|
-
if (domain)
|
|
307
|
-
output.add(domain);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
|
|
311
|
-
}
|
|
312
77
|
function reportedDate(values) {
|
|
313
78
|
if (values.length !== 1 || values[0].length > 240)
|
|
314
79
|
return null;
|
|
@@ -348,21 +113,26 @@ function addDomainRoles(store, domains, role) {
|
|
|
348
113
|
store.set(domain, roles);
|
|
349
114
|
}
|
|
350
115
|
}
|
|
351
|
-
export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOString()) {
|
|
116
|
+
export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOString(), trustedHeaders = []) {
|
|
352
117
|
const normalizedGeneratedAt = normalizeExplicitIsoTimestamp(generatedAt);
|
|
353
118
|
if (!normalizedGeneratedAt)
|
|
354
119
|
throw new CliUsageError('Mail-header review generation time must be a valid ISO 8601 timestamp.');
|
|
355
120
|
const parsed = parseHeaderFields(value);
|
|
121
|
+
let authenticationReview;
|
|
122
|
+
try {
|
|
123
|
+
authenticationReview = selectReceiverTrust(reviewMailAuthentication(parsed.fields), trustedHeaders);
|
|
124
|
+
}
|
|
125
|
+
catch (cause) {
|
|
126
|
+
if (cause instanceof TypeError)
|
|
127
|
+
throw new CliUsageError(cause.message);
|
|
128
|
+
throw cause;
|
|
129
|
+
}
|
|
356
130
|
const fromDomains = addressDomains(fieldsNamed(parsed.fields, 'from'));
|
|
357
131
|
const replyToDomains = addressDomains(fieldsNamed(parsed.fields, 'reply-to'));
|
|
358
132
|
const returnPathDomains = addressDomains(fieldsNamed(parsed.fields, 'return-path'));
|
|
359
133
|
const messageIdentifierDomains = messageIdDomains(fieldsNamed(parsed.fields, 'message-id'));
|
|
360
134
|
const authenticationResults = fieldsNamed(parsed.fields, 'authentication-results');
|
|
361
|
-
const
|
|
362
|
-
const authentication = Object.fromEntries(AUTHENTICATION_METHODS.map((method) => [
|
|
363
|
-
method,
|
|
364
|
-
authenticationState(authenticationResults, method, method === 'spf' ? receivedSpf : []),
|
|
365
|
-
]));
|
|
135
|
+
const authentication = aggregateMailAuthentication(authenticationReview);
|
|
366
136
|
const authServiceDomains = authenticationServiceDomains(authenticationResults);
|
|
367
137
|
const signerDomains = dkimSigningDomains(fieldsNamed(parsed.fields, 'dkim-signature'));
|
|
368
138
|
const receivedValues = fieldsNamed(parsed.fields, 'received');
|
|
@@ -414,6 +184,7 @@ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOSt
|
|
|
414
184
|
messageIdentifierDomains: Object.freeze(messageIdentifierDomains),
|
|
415
185
|
}),
|
|
416
186
|
authentication: Object.freeze(authentication),
|
|
187
|
+
authenticationReview,
|
|
417
188
|
routing: Object.freeze({
|
|
418
189
|
receivedHops: Object.freeze(receivedHops),
|
|
419
190
|
omittedReceivedHops,
|
|
@@ -428,6 +199,7 @@ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOSt
|
|
|
428
199
|
limitations: Object.freeze([
|
|
429
200
|
'This review parses analyst-selected message headers offline. It makes no DNS, SMTP, HTTP, registry or provider request.',
|
|
430
201
|
'Authentication results are claims reported in the supplied headers; they are not independently validated here.',
|
|
202
|
+
'Receiver trust is an explicit analyst selection for an exact header, not a conclusion from a matching service name. Aggregate counts combine all reported sources, not a trusted verdict.',
|
|
431
203
|
'Exact-domain alignment is descriptive. Divergence can be legitimate and does not establish spoofing, abuse or maliciousness.',
|
|
432
204
|
'Display names, subjects, address local parts, message bodies, attachments and raw header values are not retained in the output.',
|
|
433
205
|
...(omittedReceivedHops ? [`${omittedReceivedHops} older Received header${omittedReceivedHops === 1 ? ' was' : 's were'} omitted by the ${MAX_MAIL_RECEIVED_HOPS}-hop bound.`] : []),
|
|
@@ -444,8 +216,13 @@ export function formatCliMailHeaderReview(document) {
|
|
|
444
216
|
`Reply-To domains ${document.identity.replyToDomains.join(', ') || 'Unavailable'}`,
|
|
445
217
|
`Return-Path ${document.identity.returnPathDomains.join(', ') || 'Unavailable'}`,
|
|
446
218
|
'',
|
|
447
|
-
'
|
|
448
|
-
...
|
|
219
|
+
'Authentication by source header',
|
|
220
|
+
...document.authenticationReview.headers.flatMap(header => [
|
|
221
|
+
` ${authenticationHeaderLabel(header)} · ${header.state} · receiver trust: ${header.receiverTrust.replaceAll('_', ' ')}`,
|
|
222
|
+
...header.claims.map(claim => ` ${claim.method.toUpperCase()}/${claim.methodVersion}: ${claim.result}${claim.state === 'unsupported' ? ' (unsupported)' : ''}${claim.domains.map(value => ` · ${value.property}=${value.domain}`).join('')}${claim.duplicateProperties.length ? ` · ambiguous: ${claim.duplicateProperties.join(', ')}` : ''}`),
|
|
223
|
+
...(header.duplicateOf ? [` Duplicate of header ${header.duplicateOf} in this part.`] : []),
|
|
224
|
+
...header.issues.map(issue => ` ${issue}`),
|
|
225
|
+
]),
|
|
449
226
|
'',
|
|
450
227
|
'Exact-domain alignment',
|
|
451
228
|
` From / Reply-To ${document.alignment.fromToReplyTo}`,
|