@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
|
@@ -21,6 +21,24 @@ export const LOOKUP_CLAIM_REQUIREMENT_IDS = [
|
|
|
21
21
|
'reviewed-case-recipient',
|
|
22
22
|
'authoritative-rdap',
|
|
23
23
|
];
|
|
24
|
+
const CLAIM_QUESTIONS = Object.freeze({
|
|
25
|
+
'registration-state': 'What does the registration authority establish?',
|
|
26
|
+
'current-web-observation': 'What was the website doing at the observed time?',
|
|
27
|
+
'brand-resemblance': 'Does the observed page resemble the reviewed Brand?',
|
|
28
|
+
'controlled-change': 'Which registration and DNS dependencies need review?',
|
|
29
|
+
'incident-response': 'What evidence and recipient route support a response?',
|
|
30
|
+
'network-context': 'What does the authoritative network record establish?',
|
|
31
|
+
});
|
|
32
|
+
/** A presentation of existing readiness, not a new verdict or collection plan. */
|
|
33
|
+
export function lookupQuestionsNeedingEvidence(readiness) {
|
|
34
|
+
return readiness.entries.filter((entry) => entry.state !== 'ready').map((entry) => ({
|
|
35
|
+
id: entry.id,
|
|
36
|
+
question: CLAIM_QUESTIONS[entry.id],
|
|
37
|
+
href: entry.href,
|
|
38
|
+
requirements: entry.requirements.filter((requirement) => requirement.state !== 'complete'),
|
|
39
|
+
limitations: entry.limitations,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
24
42
|
const MAX_TEXT = 320;
|
|
25
43
|
const MAX_DIAGNOSTICS = 12;
|
|
26
44
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/gu;
|
|
@@ -40,7 +40,7 @@ export function buildLookupInvestigationBrief(input) {
|
|
|
40
40
|
const limitedSources = input.quality.entries
|
|
41
41
|
.filter((entry) => entry.state !== 'complete')
|
|
42
42
|
.map((entry) => `${entry.label}: ${entry.statusLabel}`);
|
|
43
|
-
const graphKinds = uniqueText(input.graph.edges.map((edge) => edge.label),
|
|
43
|
+
const graphKinds = uniqueText(input.graph.edges.map((edge) => edge.label), 64);
|
|
44
44
|
const brief = Object.freeze({
|
|
45
45
|
schema: LOOKUP_INVESTIGATION_BRIEF_SCHEMA,
|
|
46
46
|
schemaVersion: LOOKUP_INVESTIGATION_BRIEF_VERSION,
|
|
@@ -66,10 +66,10 @@ export function buildLookupInvestigationBrief(input) {
|
|
|
66
66
|
kinds: Object.freeze(graphKinds),
|
|
67
67
|
}),
|
|
68
68
|
limitations: Object.freeze(uniqueText([
|
|
69
|
-
...limitedSources,
|
|
70
|
-
...input.graph.limitations,
|
|
71
69
|
'This brief is a deterministic organisation of collected and derived evidence, not an attribution, ownership, safety, availability, or maliciousness conclusion.',
|
|
72
70
|
'Analyst assertions, hypotheses, and decisions must remain separate from observed evidence.',
|
|
71
|
+
...input.graph.limitations,
|
|
72
|
+
...limitedSources,
|
|
73
73
|
])),
|
|
74
74
|
});
|
|
75
75
|
if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const LOOKUP_GUIDANCE_TASKS = Object.freeze([
|
|
2
|
+
'general',
|
|
3
|
+
'acquisition',
|
|
4
|
+
'brand',
|
|
5
|
+
'incident',
|
|
6
|
+
'owned',
|
|
7
|
+
]);
|
|
8
|
+
const GUIDANCE = Object.freeze({
|
|
9
|
+
general: Object.freeze({
|
|
10
|
+
task: 'general',
|
|
11
|
+
label: 'General investigation',
|
|
12
|
+
recommendation: 'fast',
|
|
13
|
+
reason: 'Fast starts with authority-aware registration and availability evidence before a broader collection is justified.',
|
|
14
|
+
requestExplanation: 'Fast requests the existing lower-request authority and RDAP registration path and omits WHOIS, DNS, HTTP, TLS, network-context, and optional intelligence branches.',
|
|
15
|
+
limitation: 'A Fast result can still be partial, unavailable, or inconclusive. Use Deep only when the question needs the additional source families.',
|
|
16
|
+
}),
|
|
17
|
+
brand: Object.freeze({
|
|
18
|
+
task: 'brand',
|
|
19
|
+
label: 'Brand review',
|
|
20
|
+
recommendation: 'deep',
|
|
21
|
+
reason: 'Deep provides the existing source-qualified web, DNS, TLS, registration, and relationship context used by brand review.',
|
|
22
|
+
requestExplanation: 'Deep requests the existing registry RDAP, WHOIS, domain evidence, registrar RDAP, DNS, HTTP, TLS, and network-context branches. Optional intelligence sources receive data only when separately selected.',
|
|
23
|
+
limitation: 'Deep is broader, not complete or authoritative for every question. Missing and conflicting sources remain explicit.',
|
|
24
|
+
}),
|
|
25
|
+
acquisition: Object.freeze({
|
|
26
|
+
task: 'acquisition',
|
|
27
|
+
label: 'Acquisition review',
|
|
28
|
+
recommendation: 'deep',
|
|
29
|
+
reason: 'Deep provides the existing bounded registration, web, DNS, TLS, and uncertainty context expected by acquisition review.',
|
|
30
|
+
requestExplanation: 'Deep uses the existing request budget and source branches. It does not appraise a domain, contact a seller, or establish clean title or legal sufficiency.',
|
|
31
|
+
limitation: 'The recommendation organises due diligence; it does not make an acquisition recommendation or guarantee completeness.',
|
|
32
|
+
}),
|
|
33
|
+
incident: Object.freeze({
|
|
34
|
+
task: 'incident',
|
|
35
|
+
label: 'Incident response',
|
|
36
|
+
recommendation: 'deep',
|
|
37
|
+
reason: 'Deep collects the web, DNS, TLS, registration, and network evidence needed to review an incident and choose a response route.',
|
|
38
|
+
requestExplanation: 'Deep requests the existing source branches. It does not submit a report, test a published contact, or establish responsibility for the observed content.',
|
|
39
|
+
limitation: 'Collection supports review; it does not determine maliciousness or authorise a response.',
|
|
40
|
+
}),
|
|
41
|
+
owned: Object.freeze({
|
|
42
|
+
task: 'owned',
|
|
43
|
+
label: 'Owned-domain posture',
|
|
44
|
+
recommendation: 'deep',
|
|
45
|
+
reason: 'Deep collects the current registration, DNS, TLS, mail, and web observations needed for a source-by-source posture review.',
|
|
46
|
+
requestExplanation: 'Deep uses the existing bounded source branches. Retained baselines remain separate and must be compared using their observation times and source states.',
|
|
47
|
+
limitation: 'A changed or missing observation does not by itself establish compromise, remediation, or control.',
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
export const LOOKUP_TASK_VIEWS = Object.freeze(LOOKUP_GUIDANCE_TASKS.map((task) => Object.freeze({
|
|
51
|
+
id: task,
|
|
52
|
+
label: GUIDANCE[task].label,
|
|
53
|
+
})));
|
|
54
|
+
export function lookupTaskGuidance(task) {
|
|
55
|
+
return typeof task === 'string' && LOOKUP_GUIDANCE_TASKS.includes(task)
|
|
56
|
+
? GUIDANCE[task]
|
|
57
|
+
: GUIDANCE.general;
|
|
58
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { lexicalSegments, normalizeHeaderDomain, addressDomains } from "./mail-header-identity.mjs";
|
|
2
|
+
import { MAX_AUTHENTICATION_HEADERS, MAX_AUTHENTICATION_CLAUSES, MAX_AUTHENTICATION_VALUE_LENGTH, MAIL_AUTHENTICATION_METHODS, MAIL_AUTHENTICATION_PROPERTIES } from "../contracts/mail-authentication.mjs";
|
|
3
|
+
const KEYWORD = /^[a-z][a-z0-9_-]{0,31}$/iu;
|
|
4
|
+
const RESULTS = {
|
|
5
|
+
spf: ['pass', 'fail', 'softfail', 'neutral', 'none', 'temperror', 'permerror'],
|
|
6
|
+
dkim: ['pass', 'fail', 'policy', 'neutral', 'none', 'temperror', 'permerror'],
|
|
7
|
+
dmarc: ['pass', 'fail', 'none', 'temperror', 'permerror'],
|
|
8
|
+
arc: ['pass', 'fail', 'none'],
|
|
9
|
+
};
|
|
10
|
+
/** Remove comments, never quoted content. Keep separators so tokens cannot join. */
|
|
11
|
+
function withoutComments(value) {
|
|
12
|
+
let depth = 0, quoted = false, escaped = false, output = '';
|
|
13
|
+
for (const character of value) {
|
|
14
|
+
if (escaped) {
|
|
15
|
+
if (!depth)
|
|
16
|
+
output += character;
|
|
17
|
+
escaped = false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (character === '\\' && (depth || quoted)) {
|
|
21
|
+
if (!depth)
|
|
22
|
+
output += character;
|
|
23
|
+
escaped = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (depth) {
|
|
27
|
+
if (character === '(')
|
|
28
|
+
depth++;
|
|
29
|
+
else if (character === ')')
|
|
30
|
+
depth--;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (character === '"')
|
|
34
|
+
quoted = !quoted;
|
|
35
|
+
if (!quoted && character === '(') {
|
|
36
|
+
depth = 1;
|
|
37
|
+
output += ' ';
|
|
38
|
+
}
|
|
39
|
+
else if (!quoted && character === ')')
|
|
40
|
+
return null;
|
|
41
|
+
else
|
|
42
|
+
output += character;
|
|
43
|
+
}
|
|
44
|
+
return depth || quoted || escaped ? null : output;
|
|
45
|
+
}
|
|
46
|
+
// Only the restricted, non-personal identifier projection enters a report.
|
|
47
|
+
function serviceId(value) {
|
|
48
|
+
return /^[a-z0-9_.-]{1,253}$/iu.test(value) ? value : null;
|
|
49
|
+
}
|
|
50
|
+
function unquote(value) {
|
|
51
|
+
return value.startsWith('"') ? value.slice(1, -1).replace(/\\(.)/gu, '$1') : value;
|
|
52
|
+
}
|
|
53
|
+
function domainProperty(value) {
|
|
54
|
+
const direct = normalizeHeaderDomain(value) ?? normalizeHeaderDomain(value.startsWith('@') ? value.slice(1) : '');
|
|
55
|
+
if (direct)
|
|
56
|
+
return direct;
|
|
57
|
+
const domains = addressDomains([value]);
|
|
58
|
+
return domains.length === 1 ? domains[0] : null;
|
|
59
|
+
}
|
|
60
|
+
function properties(value) {
|
|
61
|
+
const result = new Map();
|
|
62
|
+
let rest = value.trim(), count = 0;
|
|
63
|
+
while (rest) {
|
|
64
|
+
if (++count > MAX_AUTHENTICATION_CLAUSES)
|
|
65
|
+
return { result, complete: false };
|
|
66
|
+
const match = /^([a-z][a-z0-9_-]*(?:\s*\.\s*[a-z][a-z0-9_-]*)?)\s*=\s*("(?:[^"\\]|\\.)*"|[^\s";]+)(?=\s|$)/iu.exec(rest);
|
|
67
|
+
if (!match)
|
|
68
|
+
return { result, complete: false };
|
|
69
|
+
const key = match[1].replaceAll(/\s/gu, '').toLowerCase();
|
|
70
|
+
const values = result.get(key) ?? [];
|
|
71
|
+
values.push(unquote(match[2]));
|
|
72
|
+
result.set(key, values);
|
|
73
|
+
rest = rest.slice(match[0].length).trim();
|
|
74
|
+
}
|
|
75
|
+
return { result, complete: true };
|
|
76
|
+
}
|
|
77
|
+
function domainsFromProperties(values) {
|
|
78
|
+
const domains = [];
|
|
79
|
+
const duplicateProperties = [];
|
|
80
|
+
let omitted = false;
|
|
81
|
+
for (const property of MAIL_AUTHENTICATION_PROPERTIES) {
|
|
82
|
+
const supplied = values.get(property);
|
|
83
|
+
if (!supplied)
|
|
84
|
+
continue;
|
|
85
|
+
if (supplied.length !== 1) {
|
|
86
|
+
duplicateProperties.push(property);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const domain = domainProperty(supplied[0]);
|
|
90
|
+
if (domain)
|
|
91
|
+
domains.push({ property, domain });
|
|
92
|
+
else
|
|
93
|
+
omitted = true;
|
|
94
|
+
}
|
|
95
|
+
return { domains, duplicateProperties, omitted };
|
|
96
|
+
}
|
|
97
|
+
function readHeader(input, part, headerIndex, duplicateOf) {
|
|
98
|
+
const headerName = input.name;
|
|
99
|
+
const base = { part, headerIndex, headerName, duplicateOf, receiverTrust: 'not_established' };
|
|
100
|
+
const issues = new Set();
|
|
101
|
+
const invalid = (issue) => ({ ...base, authservId: null, state: 'malformed', claims: [], issues: [issue] });
|
|
102
|
+
if (input.value.length > MAX_AUTHENTICATION_VALUE_LENGTH)
|
|
103
|
+
return invalid('Header value exceeds the authentication review bound.');
|
|
104
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]|\p{Default_Ignorable_Code_Point}/u.test(input.value))
|
|
105
|
+
return invalid('Header value contains unsupported control characters.');
|
|
106
|
+
const segments = lexicalSegments(input.value, ';');
|
|
107
|
+
if (!segments)
|
|
108
|
+
return invalid('Header has unbalanced quoting or comments.');
|
|
109
|
+
const clauses = segments.slice(0, MAX_AUTHENTICATION_CLAUSES + 1).map(withoutComments);
|
|
110
|
+
if (clauses.some(value => value === null))
|
|
111
|
+
return invalid('Header comments could not be parsed.');
|
|
112
|
+
if (segments.length > MAX_AUTHENTICATION_CLAUSES + 1)
|
|
113
|
+
issues.add('Some clauses exceed the authentication review bound.');
|
|
114
|
+
if (headerName === 'received-spf') {
|
|
115
|
+
const first = /^\s*([a-z][a-z0-9_-]*)\b(.*)$/iu.exec(clauses.shift() ?? '');
|
|
116
|
+
if (!first || !KEYWORD.test(first[1]))
|
|
117
|
+
return invalid('The reported SPF result is malformed.');
|
|
118
|
+
const supplied = properties([first[2], ...clauses].join(' '));
|
|
119
|
+
const receiver = supplied.result.get('receiver');
|
|
120
|
+
const authservId = receiver?.length === 1 ? serviceId(receiver[0]) : null;
|
|
121
|
+
if (!supplied.complete)
|
|
122
|
+
issues.add('Some SPF properties could not be parsed.');
|
|
123
|
+
if (receiver && (receiver.length !== 1 || !authservId))
|
|
124
|
+
issues.add('The receiver identifier is ambiguous or outside the retained identifier format.');
|
|
125
|
+
const renamed = new Map(supplied.result);
|
|
126
|
+
for (const [key, property] of [['envelope-from', 'smtp.mailfrom'], ['helo', 'smtp.helo']]) {
|
|
127
|
+
if (supplied.result.has(key))
|
|
128
|
+
renamed.set(property, supplied.result.get(key));
|
|
129
|
+
}
|
|
130
|
+
const projected = domainsFromProperties(renamed);
|
|
131
|
+
if (projected.omitted)
|
|
132
|
+
issues.add('Some identity properties cannot be represented as domains.');
|
|
133
|
+
if (projected.duplicateProperties.length)
|
|
134
|
+
issues.add('Repeated identity properties were retained as ambiguous, without selecting a value.');
|
|
135
|
+
const result = first[1].toLowerCase();
|
|
136
|
+
if (!RESULTS.spf.includes(result))
|
|
137
|
+
issues.add('The reported SPF result is unsupported.');
|
|
138
|
+
return { ...base, authservId, state: issues.size ? 'partial' : 'parsed', claims: [{ method: 'spf', methodVersion: 1, result,
|
|
139
|
+
state: RESULTS.spf.includes(result) ? 'reported' : 'unsupported', domains: projected.domains, duplicateProperties: projected.duplicateProperties }], issues: [...issues] };
|
|
140
|
+
}
|
|
141
|
+
const first = /^\s*("(?:[^"\\]|\\.)*"|[^\s";]+)(?:\s+(\d{1,9}))?\s*$/u.exec(clauses.shift() ?? '');
|
|
142
|
+
if (!first)
|
|
143
|
+
return invalid('The authentication service identifier or header version is malformed.');
|
|
144
|
+
const authservId = serviceId(unquote(first[1]));
|
|
145
|
+
if (!authservId)
|
|
146
|
+
issues.add('The service identifier is outside the retained non-personal identifier format.');
|
|
147
|
+
if (first[2] && first[2] !== '1')
|
|
148
|
+
return { ...base, authservId, state: 'unsupported', claims: [], issues: ['The header declares an unsupported authentication-results version.'] };
|
|
149
|
+
if (clauses.length === 1 && clauses[0].trim().toLowerCase() === 'none')
|
|
150
|
+
return { ...base, authservId, state: 'none', claims: [], issues: [...issues] };
|
|
151
|
+
const claims = [];
|
|
152
|
+
if (!clauses.length)
|
|
153
|
+
issues.add('The header does not contain a result clause.');
|
|
154
|
+
for (const clause of clauses) {
|
|
155
|
+
const match = /^\s*([a-z][a-z0-9_-]*)(?:\s*\/\s*(\d{1,9}))?\s*=\s*([a-z][a-z0-9_-]*)(?=\s|$)(.*)$/iu.exec(clause);
|
|
156
|
+
if (!match || !KEYWORD.test(match[1]) || !KEYWORD.test(match[3])) {
|
|
157
|
+
issues.add('A result clause is malformed.');
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const method = match[1].toLowerCase();
|
|
161
|
+
if (!MAIL_AUTHENTICATION_METHODS.includes(method)) {
|
|
162
|
+
issues.add('A reported authentication method is outside this review.');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const supplied = properties(match[4]);
|
|
166
|
+
const projected = domainsFromProperties(supplied.result);
|
|
167
|
+
if (!supplied.complete)
|
|
168
|
+
issues.add('Some result properties could not be parsed.');
|
|
169
|
+
if (projected.omitted)
|
|
170
|
+
issues.add('Some identity properties cannot be represented as domains.');
|
|
171
|
+
if (projected.duplicateProperties.length)
|
|
172
|
+
issues.add('Repeated identity properties were retained as ambiguous, without selecting a value.');
|
|
173
|
+
const known = method, result = match[3].toLowerCase(), methodVersion = Number(match[2] ?? 1);
|
|
174
|
+
const state = methodVersion === 1 && RESULTS[known].includes(result) ? 'reported' : 'unsupported';
|
|
175
|
+
if (state === 'unsupported')
|
|
176
|
+
issues.add('A method version or result is unsupported.');
|
|
177
|
+
claims.push({ method: known, methodVersion, result, state, domains: projected.domains, duplicateProperties: projected.duplicateProperties });
|
|
178
|
+
}
|
|
179
|
+
return { ...base, authservId, state: issues.size ? claims.length ? 'partial' : 'malformed' : 'parsed', claims, issues: [...issues] };
|
|
180
|
+
}
|
|
181
|
+
/** Physical header order is preserved. Equal service names never imply trust. */
|
|
182
|
+
export function reviewMailAuthentication(fields, part = 1, maximumHeaders = MAX_AUTHENTICATION_HEADERS) {
|
|
183
|
+
if (!Number.isSafeInteger(part) || part < 1 || !Number.isSafeInteger(maximumHeaders) || maximumHeaders < 0 || maximumHeaders > MAX_AUTHENTICATION_HEADERS)
|
|
184
|
+
throw new TypeError('Invalid authentication review bounds.');
|
|
185
|
+
const headers = [], previous = new Map();
|
|
186
|
+
let omittedHeaders = 0;
|
|
187
|
+
for (const [index, field] of fields.entries()) {
|
|
188
|
+
if (!['authentication-results', 'received-spf'].includes(field.name))
|
|
189
|
+
continue;
|
|
190
|
+
if (headers.length >= maximumHeaders) {
|
|
191
|
+
omittedHeaders++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// Do not retain an oversized raw value in the temporary duplicate index.
|
|
195
|
+
const key = field.value.length <= MAX_AUTHENTICATION_VALUE_LENGTH ? `${field.name}:${field.value}` : null;
|
|
196
|
+
const duplicateOf = key === null ? null : previous.get(key) ?? null;
|
|
197
|
+
headers.push(readHeader(field, part, index + 1, duplicateOf));
|
|
198
|
+
if (key !== null && duplicateOf === null)
|
|
199
|
+
previous.set(key, index + 1);
|
|
200
|
+
}
|
|
201
|
+
return { headers, omittedHeaders };
|
|
202
|
+
}
|
|
203
|
+
export function selectReceiverTrust(review, selected) {
|
|
204
|
+
if (!Array.isArray(selected) || selected.length > MAX_AUTHENTICATION_HEADERS || new Set(selected).size !== selected.length
|
|
205
|
+
|| selected.some(value => !/^[1-9]\d{0,5}:[1-9]\d{0,5}$/u.test(value)
|
|
206
|
+
|| !review.headers.some(header => `${header.part}:${header.headerIndex}` === value && header.authservId !== null && ['parsed', 'partial', 'none'].includes(header.state)))) {
|
|
207
|
+
throw new TypeError('Select each recognised receiver header by part:header-index; unidentified, malformed and unsupported headers cannot be trusted.');
|
|
208
|
+
}
|
|
209
|
+
return { ...review, headers: review.headers.map(header => ({ ...header, receiverTrust: selected.includes(`${header.part}:${header.headerIndex}`) ? 'analyst_selected' : 'not_established' })) };
|
|
210
|
+
}
|
|
211
|
+
export function authenticationHeaderLabel(header) {
|
|
212
|
+
return `Part ${header.part}, header ${header.headerIndex} · ${header.headerName} · ${header.authservId ?? 'service unidentified'}`;
|
|
213
|
+
}
|
|
214
|
+
/** Compatibility summary only: all parsed sources, with no receiver-trust verdict. */
|
|
215
|
+
export function aggregateMailAuthentication(review) {
|
|
216
|
+
const known = new Set(['fail', 'neutral', 'none', 'pass', 'permerror', 'softfail', 'temperror']);
|
|
217
|
+
return Object.fromEntries(MAIL_AUTHENTICATION_METHODS.map(method => {
|
|
218
|
+
const claims = review.headers.flatMap(header => header.claims.filter(claim => claim.method === method));
|
|
219
|
+
const states = new Set(claims.map(claim => claim.state === 'reported' && known.has(claim.result) ? claim.result : 'unknown'));
|
|
220
|
+
return [method, { state: states.size > 1 ? 'mixed' : [...states][0] ?? 'unknown', observations: claims.length }];
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { isValidAsciiDomainName } from "../contracts/domain-name.mjs";
|
|
2
|
+
import { MAIL_AUTHENTICATION_METHODS } from "../contracts/mail-authentication.mjs";
|
|
3
|
+
export const MAX_MAIL_REVIEW_DOMAINS = 128;
|
|
4
|
+
export const AUTHENTICATION_METHODS = MAIL_AUTHENTICATION_METHODS;
|
|
5
|
+
export function normalizeHeaderDomain(value) {
|
|
6
|
+
const normalized = value.trim().toLowerCase().replace(/^\[|\]$/gu, '').replace(/\.$/u, '');
|
|
7
|
+
return isValidAsciiDomainName(normalized, { requireLowercase: true }) ? normalized : null;
|
|
8
|
+
}
|
|
9
|
+
export function lexicalSegments(value, delimiter, trackAngles = false) {
|
|
10
|
+
const segments = [];
|
|
11
|
+
let start = 0;
|
|
12
|
+
let quoted = false;
|
|
13
|
+
let escaped = false;
|
|
14
|
+
let commentDepth = 0;
|
|
15
|
+
let angleDepth = 0;
|
|
16
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
17
|
+
const character = value[index] ?? '';
|
|
18
|
+
if (escaped) {
|
|
19
|
+
escaped = false;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
23
|
+
escaped = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (commentDepth > 0) {
|
|
27
|
+
if (character === '(')
|
|
28
|
+
commentDepth += 1;
|
|
29
|
+
else if (character === ')')
|
|
30
|
+
commentDepth -= 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (quoted) {
|
|
34
|
+
if (character === '"')
|
|
35
|
+
quoted = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (character === '"')
|
|
39
|
+
quoted = true;
|
|
40
|
+
else if (character === '(')
|
|
41
|
+
commentDepth = 1;
|
|
42
|
+
else if (character === ')')
|
|
43
|
+
return null;
|
|
44
|
+
else if (trackAngles && character === '<') {
|
|
45
|
+
if (angleDepth !== 0)
|
|
46
|
+
return null;
|
|
47
|
+
angleDepth = 1;
|
|
48
|
+
}
|
|
49
|
+
else if (trackAngles && character === '>') {
|
|
50
|
+
if (angleDepth !== 1)
|
|
51
|
+
return null;
|
|
52
|
+
angleDepth = 0;
|
|
53
|
+
}
|
|
54
|
+
else if (character === delimiter && angleDepth === 0) {
|
|
55
|
+
segments.push(value.slice(start, index));
|
|
56
|
+
start = index + 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (escaped || quoted || commentDepth !== 0 || angleDepth !== 0)
|
|
60
|
+
return null;
|
|
61
|
+
segments.push(value.slice(start));
|
|
62
|
+
return segments;
|
|
63
|
+
}
|
|
64
|
+
function angleAddress(value) {
|
|
65
|
+
let quoted = false;
|
|
66
|
+
let escaped = false;
|
|
67
|
+
let commentDepth = 0;
|
|
68
|
+
let start = -1;
|
|
69
|
+
let end = -1;
|
|
70
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
71
|
+
const character = value[index] ?? '';
|
|
72
|
+
if (escaped) {
|
|
73
|
+
escaped = false;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
77
|
+
escaped = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (commentDepth > 0) {
|
|
81
|
+
if (character === '(')
|
|
82
|
+
commentDepth += 1;
|
|
83
|
+
else if (character === ')')
|
|
84
|
+
commentDepth -= 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (quoted) {
|
|
88
|
+
if (character === '"')
|
|
89
|
+
quoted = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (character === '"')
|
|
93
|
+
quoted = true;
|
|
94
|
+
else if (character === '(')
|
|
95
|
+
commentDepth = 1;
|
|
96
|
+
else if (character === ')')
|
|
97
|
+
return null;
|
|
98
|
+
else if (character === '<') {
|
|
99
|
+
if (start >= 0 || end >= 0)
|
|
100
|
+
return null;
|
|
101
|
+
start = index;
|
|
102
|
+
}
|
|
103
|
+
else if (character === '>') {
|
|
104
|
+
if (start < 0 || end >= 0)
|
|
105
|
+
return null;
|
|
106
|
+
end = index;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (escaped || quoted || commentDepth !== 0 || (start >= 0) !== (end >= 0))
|
|
110
|
+
return null;
|
|
111
|
+
return start >= 0 ? value.slice(start + 1, end) : value;
|
|
112
|
+
}
|
|
113
|
+
function addressDomain(value) {
|
|
114
|
+
let candidate = '';
|
|
115
|
+
let quoted = false;
|
|
116
|
+
let escaped = false;
|
|
117
|
+
let commentDepth = 0;
|
|
118
|
+
const separators = [];
|
|
119
|
+
for (const character of value) {
|
|
120
|
+
if (escaped) {
|
|
121
|
+
candidate += character;
|
|
122
|
+
escaped = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (character === '\\' && (quoted || commentDepth > 0)) {
|
|
126
|
+
if (quoted)
|
|
127
|
+
candidate += character;
|
|
128
|
+
escaped = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (commentDepth > 0) {
|
|
132
|
+
if (character === '(')
|
|
133
|
+
commentDepth += 1;
|
|
134
|
+
else if (character === ')')
|
|
135
|
+
commentDepth -= 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (quoted) {
|
|
139
|
+
candidate += character;
|
|
140
|
+
if (character === '"')
|
|
141
|
+
quoted = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (character === '"') {
|
|
145
|
+
quoted = true;
|
|
146
|
+
candidate += character;
|
|
147
|
+
}
|
|
148
|
+
else if (character === '(')
|
|
149
|
+
commentDepth = 1;
|
|
150
|
+
else if (character === ')')
|
|
151
|
+
return null;
|
|
152
|
+
else {
|
|
153
|
+
if (character === '@')
|
|
154
|
+
separators.push(candidate.length);
|
|
155
|
+
candidate += character;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (escaped || quoted || commentDepth !== 0 || separators.length !== 1)
|
|
159
|
+
return null;
|
|
160
|
+
const separator = separators[0] ?? -1;
|
|
161
|
+
const local = candidate.slice(0, separator).trim();
|
|
162
|
+
const domain = candidate.slice(separator + 1).trim();
|
|
163
|
+
const validLocal = /^(?:[^\s"<>(),:;@]+|"(?:[^"\\]|\\.)+")$/u.test(local);
|
|
164
|
+
if (!validLocal || !domain || /\s/u.test(domain))
|
|
165
|
+
return null;
|
|
166
|
+
return normalizeHeaderDomain(domain);
|
|
167
|
+
}
|
|
168
|
+
export function addressDomains(values) {
|
|
169
|
+
const domains = new Set();
|
|
170
|
+
for (const value of values) {
|
|
171
|
+
const mailboxes = lexicalSegments(value, ',', true);
|
|
172
|
+
if (!mailboxes)
|
|
173
|
+
continue;
|
|
174
|
+
for (const mailbox of mailboxes) {
|
|
175
|
+
const selected = angleAddress(mailbox);
|
|
176
|
+
const domain = selected === null ? null : addressDomain(selected);
|
|
177
|
+
if (domain)
|
|
178
|
+
domains.add(domain);
|
|
179
|
+
if (domains.size >= MAX_MAIL_REVIEW_DOMAINS)
|
|
180
|
+
return [...domains].sort();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return [...domains].sort();
|
|
184
|
+
}
|
|
185
|
+
export function messageIdDomains(values) {
|
|
186
|
+
return addressDomains(values.map((value) => value.replace(/[<>]/gu, '')));
|
|
187
|
+
}
|
|
188
|
+
export function authenticationServiceDomains(values) {
|
|
189
|
+
const output = new Set();
|
|
190
|
+
for (const value of values) {
|
|
191
|
+
const clauses = lexicalSegments(value, ';');
|
|
192
|
+
const service = normalizeHeaderDomain((clauses?.[0] ?? '').trim().split(/\s/u, 1)[0] ?? '');
|
|
193
|
+
if (service)
|
|
194
|
+
output.add(service);
|
|
195
|
+
}
|
|
196
|
+
return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
|
|
197
|
+
}
|
|
198
|
+
export function dkimSigningDomains(values) {
|
|
199
|
+
const output = new Set();
|
|
200
|
+
for (const value of values) {
|
|
201
|
+
for (const clause of lexicalSegments(value, ';') ?? []) {
|
|
202
|
+
const match = /^\s*d\s*=\s*([^\s;]+)/iu.exec(clause);
|
|
203
|
+
const domain = normalizeHeaderDomain(match?.[1] ?? '');
|
|
204
|
+
if (domain)
|
|
205
|
+
output.add(domain);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
|
|
209
|
+
}
|