@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { zipSync } from 'fflate';
|
|
2
|
+
import { MAX_INVESTIGATION_PACKAGE_ENTRIES, MAX_INVESTIGATION_PACKAGE_BYTES } from "../contracts/investigation-package-limits.mjs";
|
|
3
|
+
export { MAX_INVESTIGATION_PACKAGE_ENTRIES, MAX_INVESTIGATION_PACKAGE_BYTES } from "../contracts/investigation-package-limits.mjs";
|
|
4
|
+
import { extractBoundedZipEntries } from "../interchange/bounded-zip-extraction.mjs";
|
|
5
|
+
import { canonicalArtifactJsonV2, sha256ArtifactBytes, sha256ArtifactDigestV2 } from "../evidence/artifact-integrity.mjs";
|
|
6
|
+
import { INVESTIGATION_CAPSULE_SCHEMA, investigationCapsuleSourceIdentity } from "./investigation-capsule.mjs";
|
|
7
|
+
import { matchCaptureArtifacts, readWebCaptureManifest } from "../interchange/web-capture-import.mjs";
|
|
8
|
+
import { MAX_WEB_CAPTURE_MANIFEST_BYTES, WEB_CAPTURE_MANIFEST_SCHEMA } from "../contracts/web-capture.mjs";
|
|
9
|
+
import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, investigationJsonMetadata, prepareInvestigationManifest, readInvestigationManifest, } from "./investigation-manifest.mjs";
|
|
10
|
+
export const INVESTIGATION_PACKAGE_MANIFEST_PATH = 'manifest.json';
|
|
11
|
+
const MANIFEST_PATH = INVESTIGATION_PACKAGE_MANIFEST_PATH;
|
|
12
|
+
const MAX_PACKAGE_ENTRIES = MAX_INVESTIGATION_PACKAGE_ENTRIES;
|
|
13
|
+
export function investigationPackageEntryPath(id) {
|
|
14
|
+
const match = /^artifact-([1-9]\d{0,2})$/u.exec(id);
|
|
15
|
+
if (!match || Number(match[1]) > MAX_INVESTIGATION_MANIFEST_ARTIFACTS)
|
|
16
|
+
throw new TypeError('Invalid investigation package entry identity.');
|
|
17
|
+
return `artifacts/${id}`;
|
|
18
|
+
}
|
|
19
|
+
export function investigationPackagePath(name) {
|
|
20
|
+
if (typeof name !== 'string')
|
|
21
|
+
throw new TypeError('Investigation package entry paths must be strings.');
|
|
22
|
+
if (name === MANIFEST_PATH)
|
|
23
|
+
return name;
|
|
24
|
+
if (!name.startsWith('artifacts/') || investigationPackageEntryPath(name.slice('artifacts/'.length)) !== name)
|
|
25
|
+
throw new TypeError('Investigation package contains an unexpected entry path.');
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
export async function prepareInvestigationPackageEntries(input, generatedAt, applicationVersion) {
|
|
29
|
+
const prepared = await prepareInvestigationManifest(input, generatedAt, applicationVersion);
|
|
30
|
+
const files = new Map();
|
|
31
|
+
files.set(MANIFEST_PATH, new TextEncoder().encode(canonicalArtifactJsonV2(prepared.manifest)));
|
|
32
|
+
for (const [index, item] of prepared.manifest.artifacts.entries())
|
|
33
|
+
files.set(investigationPackageEntryPath(item.id), prepared.contents[index]);
|
|
34
|
+
return Object.freeze({ manifest: prepared.manifest, files });
|
|
35
|
+
}
|
|
36
|
+
/** Capture a directory selection before hashing; no caller-owned bytes survive. */
|
|
37
|
+
function capturePackageEntries(input) {
|
|
38
|
+
if (!(input instanceof Map) || input.size < 2 || input.size > MAX_PACKAGE_ENTRIES)
|
|
39
|
+
throw new TypeError('Investigation package has an invalid entry count.');
|
|
40
|
+
const files = new Map();
|
|
41
|
+
let total = 0;
|
|
42
|
+
for (const [name, bytes] of input) {
|
|
43
|
+
investigationPackagePath(name);
|
|
44
|
+
const maximum = name === MANIFEST_PATH ? MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES : MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES;
|
|
45
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 1 || bytes.byteLength > maximum)
|
|
46
|
+
throw new TypeError('Investigation package entry exceeds its byte limit.');
|
|
47
|
+
if (name !== MANIFEST_PATH) {
|
|
48
|
+
if (bytes.byteLength > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES - total)
|
|
49
|
+
throw new TypeError('Investigation package exceeds its combined byte limit.');
|
|
50
|
+
total += bytes.byteLength;
|
|
51
|
+
}
|
|
52
|
+
files.set(name, new Uint8Array(bytes));
|
|
53
|
+
}
|
|
54
|
+
if (!files.has(MANIFEST_PATH))
|
|
55
|
+
throw new TypeError('Investigation package is missing its manifest.');
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
/** A stable container representation, independent of directory listing order. */
|
|
59
|
+
export function encodeInvestigationPackageEntries(input) {
|
|
60
|
+
const captured = capturePackageEntries(input);
|
|
61
|
+
const files = Object.create(null);
|
|
62
|
+
for (const name of [...captured.keys()].sort())
|
|
63
|
+
files[name] = captured.get(name);
|
|
64
|
+
// Stored entries avoid re-compressing images and large JSON while retaining
|
|
65
|
+
// exact source bytes. The reader also admits independently bounded deflate.
|
|
66
|
+
const bytes = zipSync(files, { level: 0, mtime: new Date(1980, 0, 1) });
|
|
67
|
+
if (bytes.byteLength > MAX_INVESTIGATION_PACKAGE_BYTES)
|
|
68
|
+
throw new TypeError('Investigation package exceeds its byte limit.');
|
|
69
|
+
return bytes;
|
|
70
|
+
}
|
|
71
|
+
export async function buildInvestigationPackage(input, generatedAt, applicationVersion) {
|
|
72
|
+
const prepared = await prepareInvestigationPackageEntries(input, generatedAt, applicationVersion);
|
|
73
|
+
return Object.freeze({ manifest: prepared.manifest, bytes: encodeInvestigationPackageEntries(prepared.files) });
|
|
74
|
+
}
|
|
75
|
+
export async function inspectInvestigationPackage(input) {
|
|
76
|
+
if (!(input instanceof Uint8Array) || input.byteLength < 22 || input.byteLength > MAX_INVESTIGATION_PACKAGE_BYTES)
|
|
77
|
+
throw new TypeError(`Investigation package input must fit within ${MAX_INVESTIGATION_PACKAGE_BYTES} bytes.`);
|
|
78
|
+
// The bounded reader verifies local/central identity and every selected CRC.
|
|
79
|
+
// All entries are selected: an unverified trailing file cannot hide in a pack.
|
|
80
|
+
const extracted = extractBoundedZipEntries(input, {
|
|
81
|
+
maximumEntries: MAX_PACKAGE_ENTRIES,
|
|
82
|
+
maximumSelectedBytes: MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES + MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES,
|
|
83
|
+
selectedBytesExceededMessage: 'Investigation package exceeds its inflated byte limit.',
|
|
84
|
+
metadataMismatchMessage: 'Investigation package ZIP metadata or CRC is invalid.',
|
|
85
|
+
keyForName: investigationPackagePath,
|
|
86
|
+
inspect(info) {
|
|
87
|
+
const key = investigationPackagePath(info.name);
|
|
88
|
+
const maximumBytes = key === MANIFEST_PATH ? MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES : MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES;
|
|
89
|
+
if (info.originalSize < 1 || info.originalSize > maximumBytes)
|
|
90
|
+
throw new TypeError('Investigation package entry exceeds its byte limit.');
|
|
91
|
+
return { key, selected: true, maximumBytes, exceededMessage: 'Investigation package entry exceeds its inflated byte limit.' };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return inspectOwnedPackageEntries(extracted.files);
|
|
95
|
+
}
|
|
96
|
+
export async function inspectInvestigationPackageEntries(input) {
|
|
97
|
+
return inspectOwnedPackageEntries(capturePackageEntries(input));
|
|
98
|
+
}
|
|
99
|
+
async function inspectOwnedPackageEntries(files) {
|
|
100
|
+
const manifestBytes = files.get(MANIFEST_PATH);
|
|
101
|
+
if (!manifestBytes)
|
|
102
|
+
throw new TypeError('Investigation package is missing its manifest.');
|
|
103
|
+
const manifest = await readInvestigationManifest(new TextDecoder('utf-8', { fatal: true }).decode(manifestBytes));
|
|
104
|
+
if (files.size !== manifest.artifacts.length + 1)
|
|
105
|
+
throw new TypeError('Investigation package has missing or unlisted files.');
|
|
106
|
+
const entries = [];
|
|
107
|
+
const contents = new Map();
|
|
108
|
+
const captureDeclarations = new Map();
|
|
109
|
+
const jsonSources = new Map();
|
|
110
|
+
for (const entry of manifest.artifacts) {
|
|
111
|
+
const content = files.get(investigationPackageEntryPath(entry.id));
|
|
112
|
+
if (!content)
|
|
113
|
+
throw new TypeError('Investigation package is missing a declared file.');
|
|
114
|
+
const isJson = !('mediaType' in entry) || entry.mediaType === 'application/json';
|
|
115
|
+
const byteLength = content.byteLength === entry.byteLength;
|
|
116
|
+
const rawDigest = await sha256ArtifactBytes(content) === entry.contentDigestSha256;
|
|
117
|
+
let canonicalDigest = isJson ? false : null;
|
|
118
|
+
let schema = isJson ? false : null;
|
|
119
|
+
let version = isJson ? false : null;
|
|
120
|
+
let issue = null;
|
|
121
|
+
let interpretation = isJson ? 'not_checked' : 'opaque';
|
|
122
|
+
if (isJson) {
|
|
123
|
+
try {
|
|
124
|
+
const metadata = investigationJsonMetadata(new TextDecoder('utf-8', { fatal: true }).decode(content));
|
|
125
|
+
canonicalDigest = await sha256ArtifactDigestV2(metadata.value) === entry.canonicalDigestSha256;
|
|
126
|
+
schema = metadata.schema === entry.schema;
|
|
127
|
+
version = metadata.version === entry.version;
|
|
128
|
+
if (!metadata.object)
|
|
129
|
+
interpretation = 'unsupported_json_value';
|
|
130
|
+
if (metadata.object && byteLength && rawDigest && canonicalDigest && schema && version) {
|
|
131
|
+
if (metadata.schema === WEB_CAPTURE_MANIFEST_SCHEMA) {
|
|
132
|
+
try {
|
|
133
|
+
if (content.byteLength > MAX_WEB_CAPTURE_MANIFEST_BYTES)
|
|
134
|
+
throw new TypeError('Capture manifest is oversized.');
|
|
135
|
+
captureDeclarations.set(entry.id, readWebCaptureManifest(metadata.object).artifacts);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
captureDeclarations.set(entry.id, null);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const query = metadata.object.query;
|
|
142
|
+
const queryObject = query && typeof query === 'object' && !Array.isArray(query) ? query : null;
|
|
143
|
+
jsonSources.set(entry.id, {
|
|
144
|
+
schema: metadata.schema, version: metadata.version, digest: entry.canonicalDigestSha256,
|
|
145
|
+
query: queryObject ? { submitted: queryObject.submitted, type: queryObject.type } : null,
|
|
146
|
+
target: metadata.schema === INVESTIGATION_CAPSULE_SCHEMA ? metadata.object.target : null,
|
|
147
|
+
references: metadata.schema === INVESTIGATION_CAPSULE_SCHEMA ? metadata.object.sourceContracts : null,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
issue = 'The declared JSON file is not valid bounded UTF-8 JSON.';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const verified = byteLength && rawDigest && canonicalDigest !== false && schema !== false && version !== false;
|
|
156
|
+
entries.push(Object.freeze({ entry, state: verified ? 'identity_verified' : 'rejected',
|
|
157
|
+
checks: Object.freeze({ byteLength, rawDigest, canonicalDigest, schema, version }),
|
|
158
|
+
interpretation,
|
|
159
|
+
issue: issue ?? (verified ? null : 'File content does not match its manifest identity.'),
|
|
160
|
+
}));
|
|
161
|
+
// Rejected bytes remain in the selected archive but cannot be used by an
|
|
162
|
+
// importing caller as if they had passed identity verification.
|
|
163
|
+
if (verified)
|
|
164
|
+
contents.set(entry.id, content);
|
|
165
|
+
}
|
|
166
|
+
const links = [];
|
|
167
|
+
const captureManifests = [...captureDeclarations].map(([entryId, declarations]) => {
|
|
168
|
+
if (!declarations)
|
|
169
|
+
return { entryId, state: 'rejected', artifacts: [] };
|
|
170
|
+
const artifacts = matchCaptureArtifacts(declarations, entries
|
|
171
|
+
.filter(candidate => candidate.entry.id !== entryId && candidate.state === 'identity_verified')
|
|
172
|
+
.map(candidate => ({ id: candidate.entry.id, bytes: candidate.entry.byteLength, sha256: candidate.entry.contentDigestSha256 })));
|
|
173
|
+
return { entryId, state: artifacts.every(artifact => artifact.state === 'matched') ? 'matched' : 'missing_attachments', artifacts };
|
|
174
|
+
});
|
|
175
|
+
for (const [id, capsule] of jsonSources) {
|
|
176
|
+
if (capsule.schema !== INVESTIGATION_CAPSULE_SCHEMA)
|
|
177
|
+
continue;
|
|
178
|
+
const references = Array.isArray(capsule.references) && capsule.references.length <= 4
|
|
179
|
+
? capsule.references.filter((item) => item && typeof item === 'object' && item.id === 'lookup-evidence') : [];
|
|
180
|
+
if (references.length !== 1) {
|
|
181
|
+
links.push({ capsuleEntryId: id, sourceEntryId: null, state: 'mismatch' });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const reference = references[0];
|
|
185
|
+
const candidates = [...jsonSources].filter(([otherId, source]) => otherId !== id && source.schema === reference.schema && source.digest === reference.digest);
|
|
186
|
+
if (candidates.length !== 1) {
|
|
187
|
+
const state = candidates.length > 1 ? 'ambiguous' : [...jsonSources.values()].some((source) => source.schema === reference.schema) ? 'mismatch' : 'missing';
|
|
188
|
+
links.push({ capsuleEntryId: id, sourceEntryId: null, state });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const [sourceEntryId, source] = candidates[0];
|
|
192
|
+
const identity = investigationCapsuleSourceIdentity(capsule.target, reference, source);
|
|
193
|
+
links.push({ capsuleEntryId: id, sourceEntryId, state: identity.linked ? 'linked' : 'mismatch' });
|
|
194
|
+
}
|
|
195
|
+
return Object.freeze({ manifest, entries: Object.freeze(entries), contents, links: Object.freeze(links), captureManifests: Object.freeze(captureManifests),
|
|
196
|
+
identityVerified: entries.every((entry) => entry.state === 'identity_verified'),
|
|
197
|
+
storageEffect: 'none',
|
|
198
|
+
signatureTrust: 'not_checked', timestampAssurance: 'not_checked', factualAccuracy: 'not_established',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
|
|
2
|
+
import { parseCredentialFreeHttpUrl } from "../evidence/lookup-target.mjs";
|
|
3
|
+
import { MAX_INTAKE_LINKS, MAX_INTAKE_URL_LENGTH, MAX_EMBEDDED_LINK_DEPTH, MAX_AUTH_SCOPES } from "../contracts/message-intake.mjs";
|
|
4
|
+
const EXCLUDED_INPUTS = {
|
|
5
|
+
malformed: 'Malformed or overlong links were not reviewed',
|
|
6
|
+
credentials: 'Links containing credentials were not reviewed',
|
|
7
|
+
protocol: 'Non-HTTP(S) links were not reviewed',
|
|
8
|
+
port: 'Links using non-default ports were not reviewed',
|
|
9
|
+
host: 'IP-address or non-registrable-host links were not reviewed',
|
|
10
|
+
qrNetwork: 'QR network settings were not interpreted',
|
|
11
|
+
qrContact: 'QR contact details were not interpreted',
|
|
12
|
+
qrProtocol: 'QR non-HTTP(S) payloads were not interpreted',
|
|
13
|
+
qrHost: 'QR hostnames without an HTTP(S) scheme were not offered for collection',
|
|
14
|
+
qrText: 'QR text without an HTTP(S) link was not interpreted',
|
|
15
|
+
};
|
|
16
|
+
const AUTH_KEYS = ['client_id', 'scope', 'redirect_uri', 'response_type', 'prompt'];
|
|
17
|
+
const EMBEDDED_KEYS = new Set(['url', 'u', 'target', 'redirect', 'redirect_uri', 'redirect_url', 'return', 'returnurl', 'continue', 'next', 'dest', 'destination']);
|
|
18
|
+
const safeIdentifier = (value) => value && /^[a-zA-Z0-9._~:-]{1,160}$/u.test(value) ? value : null;
|
|
19
|
+
const tokens = (value) => (value ?? '').split(/\s+/u).filter(Boolean);
|
|
20
|
+
export function reviewAuthorisationLink(url) {
|
|
21
|
+
const params = url.searchParams;
|
|
22
|
+
const authorisation = params.has('client_id') && (params.has('scope') || params.has('response_type') || params.has('redirect_uri'));
|
|
23
|
+
const device = /\/(?:device|devicecode|devicelogin)(?:\/|$)/iu.test(url.pathname);
|
|
24
|
+
if (!authorisation && !device)
|
|
25
|
+
return null;
|
|
26
|
+
const duplicateParameters = AUTH_KEYS.filter(key => params.getAll(key).length > 1);
|
|
27
|
+
const single = (key) => duplicateParameters.includes(key) ? null : params.get(key);
|
|
28
|
+
const scopes = tokens(single('scope'));
|
|
29
|
+
const retainedScopes = scopes.filter(scope => /^[a-zA-Z0-9._:/-]{1,160}$/u.test(scope)).slice(0, MAX_AUTH_SCOPES);
|
|
30
|
+
const redirect = parseCredentialFreeHttpUrl(single('redirect_uri'), MAX_INTAKE_URL_LENGTH);
|
|
31
|
+
const responseTypes = tokens(single('response_type')).filter(value => ['code', 'token', 'id_token', 'none'].includes(value));
|
|
32
|
+
const prompt = tokens(single('prompt')).filter(value => ['none', 'login', 'consent', 'select_account', 'create'].includes(value));
|
|
33
|
+
return {
|
|
34
|
+
kind: authorisation ? 'authorisation_parameters' : 'device_code_reference',
|
|
35
|
+
clientId: safeIdentifier(single('client_id')), scopes: retainedScopes,
|
|
36
|
+
redirectOrigin: redirect?.origin ?? null, responseTypes, prompt, duplicateParameters,
|
|
37
|
+
omittedParameters: scopes.length !== retainedScopes.length || (params.has('client_id') && !safeIdentifier(single('client_id')))
|
|
38
|
+
|| (params.has('redirect_uri') && !redirect) || tokens(single('response_type')).length !== responseTypes.length
|
|
39
|
+
|| tokens(single('prompt')).length !== prompt.length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function refangIntakeUrl(raw) {
|
|
43
|
+
// Only the scheme and authority are defanged. Path/query/fragment bytes may be
|
|
44
|
+
// significant to an exact supplied target, even when they resemble defanging.
|
|
45
|
+
return raw.replace(/^(https?|hxxps?)(?::|\[:\])\/\/([^/?#]*)/iu, (_match, scheme, authority) => `${scheme.toLowerCase().replace('hxxp', 'http')}://${authority.replace(/\[\.\]|\(\.\)/gu, '.')}`);
|
|
46
|
+
}
|
|
47
|
+
const HTTP_PREFIX = /^(?:https?|hxxps?)(?::|\[:\])\/\//iu;
|
|
48
|
+
function proseUrl(value) {
|
|
49
|
+
// Prose delimiters are not part of a URL; balanced path brackets are. Direct
|
|
50
|
+
// supplied links bypass this text-scanning cleanup entirely.
|
|
51
|
+
let result = value.replace(/[.,;!?]+$/u, '');
|
|
52
|
+
let round = 0, square = 0;
|
|
53
|
+
for (const char of result) {
|
|
54
|
+
if (char === '(')
|
|
55
|
+
round++;
|
|
56
|
+
else if (char === ')')
|
|
57
|
+
round--;
|
|
58
|
+
else if (char === '[')
|
|
59
|
+
square++;
|
|
60
|
+
else if (char === ']')
|
|
61
|
+
square--;
|
|
62
|
+
}
|
|
63
|
+
for (;;) {
|
|
64
|
+
const end = result.at(-1);
|
|
65
|
+
if (end === ')' && round < 0)
|
|
66
|
+
round++;
|
|
67
|
+
else if (end === ']' && square < 0)
|
|
68
|
+
square++;
|
|
69
|
+
else
|
|
70
|
+
return result;
|
|
71
|
+
result = result.slice(0, -1).replace(/[.,;!?]+$/u, '');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function displayedHost(value) {
|
|
75
|
+
const bounded = value.trim();
|
|
76
|
+
if (bounded.length > MAX_INTAKE_URL_LENGTH || /\s/u.test(bounded))
|
|
77
|
+
return null;
|
|
78
|
+
const parsed = parseCredentialFreeHttpUrl(refangIntakeUrl(HTTP_PREFIX.test(bounded) ? bounded : `https://${bounded}`), MAX_INTAKE_URL_LENGTH);
|
|
79
|
+
return parsed && canonicalRegistrableDomain(parsed.hostname) ? parsed.hostname : null;
|
|
80
|
+
}
|
|
81
|
+
/** No resolution or request occurs here; embedded targets remain supplied text. */
|
|
82
|
+
export function createLinkIntake() {
|
|
83
|
+
const links = [], targets = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const exclusions = new Map();
|
|
86
|
+
let rejected = 0, bounded = false;
|
|
87
|
+
function exclude(reason, isLink = true) {
|
|
88
|
+
if (isLink)
|
|
89
|
+
rejected++;
|
|
90
|
+
exclusions.set(reason, (exclusions.get(reason) ?? 0) + 1);
|
|
91
|
+
}
|
|
92
|
+
function add(raw, source, displayed = '', parentId = null, depth = 0, location) {
|
|
93
|
+
if (links.length >= MAX_INTAKE_LINKS) {
|
|
94
|
+
bounded = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const value = refangIntakeUrl(raw.trim());
|
|
98
|
+
const url = parseCredentialFreeHttpUrl(value, MAX_INTAKE_URL_LENGTH);
|
|
99
|
+
if (!url) {
|
|
100
|
+
let reason = 'malformed';
|
|
101
|
+
if (value.length <= MAX_INTAKE_URL_LENGTH && !/[\x00-\x20\x7f]/u.test(value)) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = new URL(value);
|
|
104
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
105
|
+
reason = 'protocol';
|
|
106
|
+
else if (parsed.username || parsed.password)
|
|
107
|
+
reason = 'credentials';
|
|
108
|
+
}
|
|
109
|
+
catch { /* Retain only the fixed malformed-input category. */ }
|
|
110
|
+
}
|
|
111
|
+
exclude(reason);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (url.port) {
|
|
115
|
+
exclude('port');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!canonicalRegistrableDomain(url.hostname)) {
|
|
119
|
+
exclude('host');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const shown = displayedHost(displayed);
|
|
123
|
+
const key = JSON.stringify([url.href, source, parentId, shown, location ?? null]);
|
|
124
|
+
if (seen.has(key))
|
|
125
|
+
return;
|
|
126
|
+
seen.add(key);
|
|
127
|
+
const id = `link-${links.length + 1}`;
|
|
128
|
+
links.push({ id, origin: url.origin, hostname: url.hostname, registrationDomain: canonicalRegistrableDomain(url.hostname),
|
|
129
|
+
source, ...(location ? { location } : {}), parentId, displayedHostname: shown, displayedDestination: shown ? shown === url.hostname ? 'same_host' : 'different_host' : 'not_a_hostname',
|
|
130
|
+
hasPrivateLocation: url.pathname !== '/' || Boolean(url.search || url.hash), authorisation: reviewAuthorisationLink(url) });
|
|
131
|
+
targets.push({ id, exactUrl: url.href });
|
|
132
|
+
for (const [name, value] of url.searchParams) {
|
|
133
|
+
if (!EMBEDDED_KEYS.has(name.toLowerCase()) || !HTTP_PREFIX.test(value))
|
|
134
|
+
continue;
|
|
135
|
+
if (depth >= MAX_EMBEDDED_LINK_DEPTH) {
|
|
136
|
+
bounded = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
add(value, 'embedded_parameter', '', id, depth + 1, location);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function addText(text, source = 'text', location) {
|
|
143
|
+
// The caller admits bytes before this scan; iteration stops at the work bound.
|
|
144
|
+
for (const match of text.matchAll(/(?:https?|hxxps?)(?::|\[:\])\/\/[^\s<>"'`]+/giu)) {
|
|
145
|
+
add(match[0].length > MAX_INTAKE_URL_LENGTH ? match[0] : proseUrl(match[0]), source, '', null, 0, location);
|
|
146
|
+
if (links.length >= MAX_INTAKE_LINKS) {
|
|
147
|
+
bounded = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function addQr(text, source = 'qr', location) {
|
|
153
|
+
const value = refangIntakeUrl(text.trim());
|
|
154
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/iu.exec(value)?.[1]?.toLowerCase();
|
|
155
|
+
if (/^WIFI:/iu.test(value)) {
|
|
156
|
+
exclude('qrNetwork', false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (/^(?:BEGIN:VCARD|MECARD:)/iu.test(value)) {
|
|
160
|
+
exclude('qrContact', false);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (scheme && !['http', 'https'].includes(scheme)) {
|
|
164
|
+
exclude('qrProtocol', false);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!/(?:https?|hxxps?):\/\//iu.test(value)) {
|
|
168
|
+
exclude(displayedHost(value) ? 'qrHost' : 'qrText', false);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
addText(value, source, location);
|
|
172
|
+
}
|
|
173
|
+
return { add, addText, addQr, result: () => ({ links: [...links], targets: [...targets], rejected, bounded,
|
|
174
|
+
limitations: Object.keys(EXCLUDED_INPUTS).flatMap(reason => {
|
|
175
|
+
const count = exclusions.get(reason);
|
|
176
|
+
return count ? [`${EXCLUDED_INPUTS[reason]} (${count}).`] : [];
|
|
177
|
+
}),
|
|
178
|
+
}) };
|
|
179
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Counts describe the projection input, not the collector's upstream universe. */
|
|
2
|
+
export function createGraphInputReader() {
|
|
3
|
+
const coverage = new Map();
|
|
4
|
+
function values(id, value, maximum, normalise, identity) {
|
|
5
|
+
const input = Array.isArray(value) ? value : [];
|
|
6
|
+
const inspected = Math.min(input.length, maximum * 4);
|
|
7
|
+
const result = [];
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
let invalid = 0;
|
|
10
|
+
let duplicates = 0;
|
|
11
|
+
let omitted = 0;
|
|
12
|
+
for (let index = 0; index < inspected; index += 1) {
|
|
13
|
+
const item = normalise(input[index]);
|
|
14
|
+
if (item === null) {
|
|
15
|
+
invalid += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const key = identity?.(item);
|
|
19
|
+
if (key !== undefined && seen.has(key)) {
|
|
20
|
+
duplicates += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (key !== undefined)
|
|
24
|
+
seen.add(key);
|
|
25
|
+
if (result.length === maximum) {
|
|
26
|
+
omitted += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
result.push(item);
|
|
30
|
+
}
|
|
31
|
+
coverage.set(id, { id, supplied: input.length, inspected, admitted: result.length, invalid, duplicates, omitted });
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return { values, coverage };
|
|
35
|
+
}
|
|
36
|
+
export function graphInputIsIncomplete(row) {
|
|
37
|
+
return row.supplied > row.inspected || row.invalid > 0 || row.omitted > 0;
|
|
38
|
+
}
|