@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
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { latestObservationCohort } from "../packages/evidence/latest-observations.mjs";
|
|
3
|
+
import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDomainControlRecords, canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
|
|
4
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
|
+
import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
|
|
6
|
+
import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
|
|
7
|
+
import { scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
|
|
8
|
+
import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
|
|
9
|
+
import { MAX_FLIGHT_RECORDER_VALUES, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
|
|
5
10
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
6
11
|
import { CliUsageError } from "./errors.mjs";
|
|
7
12
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
8
13
|
export { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, };
|
|
9
14
|
const CLI_REVIEW_INPUT_KEY_SET = new Set(CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS);
|
|
15
|
+
export const DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS = Object.freeze({
|
|
16
|
+
registry_nameservers: 'nameservers', delegation_ds: 'ds', mail_exchangers: 'mx', caa_policy: 'caa',
|
|
17
|
+
tls_certificate: 'tlsIssuer', tls_public_key: 'tlsSpkiSha256', registrar_lock: 'registrarLock',
|
|
18
|
+
});
|
|
10
19
|
function record(value) {
|
|
11
20
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
12
21
|
}
|
|
@@ -21,116 +30,147 @@ function state(value) {
|
|
|
21
30
|
return 'unavailable';
|
|
22
31
|
}
|
|
23
32
|
function text(value, maximum = 500) {
|
|
24
|
-
if (typeof value !== 'string')
|
|
33
|
+
if (typeof value !== 'string' || value.length > maximum || /[\u0000-\u001f\u007f]/u.test(value))
|
|
25
34
|
return null;
|
|
26
|
-
const normalized = value.
|
|
35
|
+
const normalized = value.trim();
|
|
27
36
|
return normalized || null;
|
|
28
37
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
38
|
+
function sourceClock(...values) {
|
|
39
|
+
const declared = values.filter((value) => value !== undefined && value !== null);
|
|
40
|
+
const times = declared.map(normalizeExplicitIsoTimestamp);
|
|
41
|
+
return times.length && times.every((time) => time !== null && time === times[0]) ? times[0] : null;
|
|
42
|
+
}
|
|
43
|
+
function sourceState(value) {
|
|
44
|
+
const result = state(value.status);
|
|
45
|
+
return result === 'observed' && (value.complete === false || value.truncated === true) ? 'partial' : result;
|
|
46
|
+
}
|
|
47
|
+
function dnsRecordState(dns, name) {
|
|
48
|
+
const query = record(record(dns.diagnostics)[name]);
|
|
49
|
+
if (!Object.keys(query).length)
|
|
50
|
+
return sourceState(dns);
|
|
51
|
+
const values = record(dns.records)[name];
|
|
52
|
+
const contradictoryAbsence = query.status === 'not_found' && Array.isArray(values) && values.length > 0;
|
|
53
|
+
if ((query.status === 'success' || query.status === 'not_found') && query.truncated === false && query.discarded === 0
|
|
54
|
+
&& !contradictoryAbsence && (query.error === undefined || query.error === null))
|
|
55
|
+
return 'observed';
|
|
56
|
+
return state(query.status) === 'observed' || query.status === 'not_found' ? 'partial' : state(query.status);
|
|
57
|
+
}
|
|
58
|
+
function field(id, source, sourceState, values, observedAt) {
|
|
59
|
+
return Object.freeze({ id, source, state: sourceState, values: Object.freeze([...values].sort()), observedAt });
|
|
60
|
+
}
|
|
61
|
+
function recordField(id, source, sourceState, input, normalize, observedAt) {
|
|
62
|
+
const rows = Array.isArray(input) ? input.slice(0, MAX_DOMAIN_CONTROL_INPUT_RECORDS) : [];
|
|
63
|
+
const values = new Set();
|
|
64
|
+
let invalid = Array.isArray(input) || (input === undefined && sourceState !== 'observed') ? 0 : 1;
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
let value = '';
|
|
67
|
+
try {
|
|
68
|
+
value = normalize(row);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof TypeError))
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (value)
|
|
75
|
+
values.add(value);
|
|
76
|
+
else
|
|
77
|
+
invalid += 1;
|
|
78
|
+
}
|
|
79
|
+
const sorted = [...values].sort();
|
|
80
|
+
const omitted = Math.max(0, (Array.isArray(input) ? input.length : 0) - rows.length)
|
|
81
|
+
+ Math.max(0, sorted.length - MAX_FLIGHT_RECORDER_VALUES);
|
|
82
|
+
return field(id, invalid || omitted ? `${source} (${invalid} invalid; ${omitted} omitted records)` : source, sourceState === 'observed' && (invalid || omitted) ? 'partial' : sourceState, sorted.slice(0, MAX_FLIGHT_RECORDER_VALUES), observedAt);
|
|
69
83
|
}
|
|
70
84
|
function diagnostic(document, key) {
|
|
71
|
-
return
|
|
85
|
+
return sourceState(record(record(document.diagnostics)[key]));
|
|
86
|
+
}
|
|
87
|
+
function registryClock(document, key) {
|
|
88
|
+
const detail = record(record(document.diagnostics)[key]);
|
|
89
|
+
const publication = record(document[key]);
|
|
90
|
+
const named = key === 'rdap' ? 'fetchedAt' : 'queriedAt';
|
|
91
|
+
return sourceClock(detail.observedAt, detail[named], publication.observedAt, publication[named]);
|
|
72
92
|
}
|
|
73
93
|
function registryParsed(document) {
|
|
74
94
|
const rdapState = diagnostic(document, 'rdap');
|
|
75
95
|
const whoisState = diagnostic(document, 'whois');
|
|
76
96
|
const rdap = record(record(document.rdap).parsed);
|
|
77
97
|
if (rdapState === 'observed' && Object.keys(rdap).length)
|
|
78
|
-
return { parsed: rdap, source: 'Registry RDAP', sourceState: rdapState };
|
|
98
|
+
return { parsed: rdap, source: 'Registry RDAP', sourceState: rdapState, observedAt: registryClock(document, 'rdap') };
|
|
79
99
|
const whois = record(record(document.whois).parsed);
|
|
80
|
-
return { parsed: whois, source: 'WHOIS', sourceState: whoisState };
|
|
100
|
+
return { parsed: whois, source: 'WHOIS', sourceState: whoisState, observedAt: registryClock(document, 'whois') };
|
|
81
101
|
}
|
|
82
102
|
function lockValue(parsed) {
|
|
83
|
-
const
|
|
84
|
-
|
|
103
|
+
const input = parsed.statuses ?? parsed.status;
|
|
104
|
+
const statuses = Array.isArray(input) ? canonicalPostureRecords('registration_lock', input) : null;
|
|
105
|
+
return statuses ? postureTransferRestriction(statuses) : null;
|
|
85
106
|
}
|
|
86
107
|
function pageIdentityValue(availability) {
|
|
87
108
|
const identity = record(availability.pageIdentity);
|
|
88
109
|
const components = [
|
|
89
|
-
text(identity.bodySha256
|
|
90
|
-
text(identity.faviconHash
|
|
91
|
-
text(identity.title
|
|
110
|
+
text(identity.bodySha256, 64),
|
|
111
|
+
text(identity.faviconHash, 128),
|
|
112
|
+
text(identity.title, 240),
|
|
92
113
|
].filter((item) => Boolean(item));
|
|
93
|
-
return components.length ? [components.join(' | ')
|
|
114
|
+
return components.length ? [components.join(' | ')] : [];
|
|
94
115
|
}
|
|
95
116
|
export function domainControlObservationFromSavedLookup(document) {
|
|
96
117
|
const availability = record(document.availability);
|
|
97
118
|
const dns = record(availability.dns);
|
|
98
119
|
const dnsRecords = record(dns.records);
|
|
120
|
+
const dnsAt = sourceClock(dns.observedAt, dns.checkedAt);
|
|
99
121
|
const delegation = record(dns.delegation);
|
|
100
122
|
const delegationRecords = record(delegation.records);
|
|
101
123
|
const tls = record(availability.tls);
|
|
102
|
-
const tlsState =
|
|
124
|
+
const tlsState = sourceState(tls);
|
|
125
|
+
const tlsAt = sourceClock(tls.observedAt, tls.checkedAt);
|
|
103
126
|
const certificate = record(tls.certificate);
|
|
104
127
|
const publicKey = record(certificate.publicKey);
|
|
105
128
|
const http = record(availability.http);
|
|
106
|
-
const httpState = state(http.status);
|
|
107
129
|
const registry = registryParsed(document);
|
|
108
130
|
const rdapParsed = record(record(document.rdap).parsed);
|
|
109
131
|
const rdapState = diagnostic(document, 'rdap');
|
|
110
|
-
const
|
|
132
|
+
const rdapAt = registryClock(document, 'rdap');
|
|
133
|
+
const scalar = (id, source, status, value, observedAt, maximum = 500) => recordField(id, source, status, [value], (item) => text(item, maximum) ?? '', observedAt);
|
|
134
|
+
const hostname = (value) => canonicalDomainControlRecords([value], 'nameservers')[0] ?? '';
|
|
135
|
+
const identity = record(availability.pageIdentity);
|
|
136
|
+
const identityValues = pageIdentityValue(availability);
|
|
111
137
|
const fields = [
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
field('page_identity', 'Static page identity',
|
|
138
|
+
scalar('registrar', registry.source, registry.sourceState, record(registry.parsed.registrar).name ?? registry.parsed.registrar, registry.observedAt, 300),
|
|
139
|
+
scalar('registrar_lock', registry.source, registry.sourceState, lockValue(registry.parsed), registry.observedAt, 20),
|
|
140
|
+
scalar('registry_dnssec', 'Registry RDAP', rdapState, rdapParsed.dnssec, rdapAt, 80),
|
|
141
|
+
recordField('registry_nameservers', 'Registry RDAP', rdapState, rdapParsed.nameservers, hostname, rdapAt),
|
|
142
|
+
recordField('whois_nameservers', 'WHOIS', diagnostic(document, 'whois'), record(record(document.whois).parsed).nameservers, hostname, registryClock(document, 'whois')),
|
|
143
|
+
recordField('delegated_nameservers', 'DNS NS', dnsRecordState(dns, 'ns'), dnsRecords.ns, hostname, dnsAt),
|
|
144
|
+
recordField('delegation_ds', 'DNS delegation', Object.hasOwn(delegationRecords, 'ds') ? sourceState(delegation) : 'unsupported', delegationRecords.ds, canonicalDsRecord, sourceClock(delegation.observedAt)),
|
|
145
|
+
recordField('mail_exchangers', 'DNS MX', dnsRecordState(dns, 'mx'), dnsRecords.mx, canonicalMxRecord, dnsAt),
|
|
146
|
+
recordField('caa_policy', 'DNS CAA', dnsRecordState(dns, 'caa'), dnsRecords.caa, canonicalCaaRecord, dnsAt),
|
|
147
|
+
scalar('tls_certificate', 'TLS', tlsState, certificate.fingerprintSha256 ?? tls.fingerprintSha256, tlsAt, 128),
|
|
148
|
+
scalar('tls_public_key', 'TLS', tlsState, publicKey.fingerprintSha256 ?? tls.spkiSha256, tlsAt, 128),
|
|
149
|
+
scalar('http_origin', 'HTTP', sourceState(http), http.finalOrigin, sourceClock(http.observedAt, http.checkedAt)),
|
|
150
|
+
field('page_identity', 'Static page identity', sourceState(identity) === 'observed' && !identityValues.length ? 'partial' : sourceState(identity), identityValues, sourceClock(identity.observedAt)),
|
|
125
151
|
];
|
|
126
152
|
return Object.freeze({
|
|
127
|
-
domain: document.registrableDomain,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
domain: document.registrableDomain, capturedAt: document.generatedAt, collectionDepth: document.mode,
|
|
154
|
+
fields: Object.freeze(fields.map((item) => item.state === 'observed' && item.observedAt !== null
|
|
155
|
+
&& Date.parse(item.observedAt) > Date.parse(document.generatedAt) ? Object.freeze({ ...item, state: 'partial' }) : item)),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function reviewFields(document, observation) {
|
|
159
|
+
return observation.fields.map((candidate) => {
|
|
160
|
+
if (candidate.id !== 'tls_certificate')
|
|
161
|
+
return candidate;
|
|
162
|
+
const issuer = record(record(record(document.availability).tls).certificate).issuer;
|
|
163
|
+
const value = record(issuer);
|
|
164
|
+
const commonNames = Array.isArray(value.commonNames) ? value.commonNames : [];
|
|
165
|
+
const organisationNames = Array.isArray(value.organizationNames) ? value.organizationNames : [];
|
|
166
|
+
return recordField(candidate.id, candidate.source, candidate.state, [commonNames[0] ?? organisationNames[0] ?? issuer], (item) => text(item, 300) ?? '', candidate.observedAt);
|
|
131
167
|
});
|
|
132
168
|
}
|
|
133
169
|
export function buildCliDomainControlReview(inputText, generatedAt = new Date().toISOString()) {
|
|
170
|
+
const reviewTime = normalizeExplicitIsoTimestamp(generatedAt);
|
|
171
|
+
if (!reviewTime)
|
|
172
|
+
throw new CliUsageError('Domain-control review time must include a valid explicit timezone.');
|
|
173
|
+
generatedAt = reviewTime;
|
|
134
174
|
if (Buffer.byteLength(inputText, 'utf8') > MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES) {
|
|
135
175
|
throw new CliUsageError(`Domain-control review input is limited to ${MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES} bytes.`);
|
|
136
176
|
}
|
|
@@ -161,7 +201,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
161
201
|
}
|
|
162
202
|
const input = record(parsed);
|
|
163
203
|
if (input.schema !== CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
164
|
-
||
|
|
204
|
+
|| !SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS.some((version) => version === input.version)
|
|
165
205
|
|| !Array.isArray(input.lookups)) {
|
|
166
206
|
throw new CliUsageError(`Domain-control review input must use ${CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA} version ${CLI_DOMAIN_CONTROL_REVIEW_VERSION}.`);
|
|
167
207
|
}
|
|
@@ -173,42 +213,49 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
173
213
|
throw new CliUsageError(`Domain-control review requires from ${MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS} to ${MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS} saved Lookup documents.`);
|
|
174
214
|
}
|
|
175
215
|
const manifest = verifyDomainControlManifest(input.manifest);
|
|
216
|
+
if (input.version === PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION && manifest.version !== PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION) {
|
|
217
|
+
throw new CliUsageError(`Public domain-control review input version ${PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION} requires manifest version ${PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION}.`);
|
|
218
|
+
}
|
|
176
219
|
const lookups = input.lookups.map((item, index) => parseSavedLookupDocument(JSON.stringify(item), { label: `Lookup ${index + 1}` }));
|
|
177
|
-
const
|
|
220
|
+
const byDomain = new Map();
|
|
178
221
|
for (const lookup of lookups) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
222
|
+
const group = byDomain.get(lookup.registrableDomain) ?? [];
|
|
223
|
+
group.push(lookup);
|
|
224
|
+
byDomain.set(lookup.registrableDomain, group);
|
|
182
225
|
}
|
|
183
|
-
|
|
226
|
+
let ignoredHistoricalLookups = 0;
|
|
227
|
+
const selected = [...byDomain.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([domain, documents]) => {
|
|
228
|
+
const cohort = latestObservationCohort(documents, (document) => document.generatedAt);
|
|
229
|
+
ignoredHistoricalLookups += cohort.superseded;
|
|
230
|
+
const captures = cohort.latest.map((document) => ({ document, observation: domainControlObservationFromSavedLookup(document) }));
|
|
231
|
+
const first = captures[0];
|
|
232
|
+
if (!first || !cohort.observedAt || cohort.undated.length)
|
|
233
|
+
throw new CliUsageError('Saved Lookup observation times could not be ordered.');
|
|
234
|
+
const mergedFields = mergeConcurrentDomainControlFields(captures.map((capture) => capture.observation.fields));
|
|
235
|
+
const futureCapture = Date.parse(cohort.observedAt) > Date.parse(generatedAt);
|
|
236
|
+
const observation = Object.freeze({
|
|
237
|
+
domain,
|
|
238
|
+
capturedAt: cohort.observedAt,
|
|
239
|
+
collectionDepth: captures.every((capture) => capture.observation.collectionDepth === first.observation.collectionDepth)
|
|
240
|
+
? first.observation.collectionDepth : 'unknown',
|
|
241
|
+
fields: futureCapture ? mergedFields.map((field) => ({ ...field, state: 'partial' })) : mergedFields,
|
|
242
|
+
});
|
|
243
|
+
const fields = mergeConcurrentDomainControlFields(captures.map((capture) => reviewFields(capture.document, capture.observation)));
|
|
244
|
+
return { observation, reviewFields: futureCapture || observation.collectionDepth === 'unknown'
|
|
245
|
+
? fields.map((field) => ({ ...field, state: 'partial' })) : fields };
|
|
246
|
+
});
|
|
247
|
+
const observations = selected.map((item) => item.observation);
|
|
184
248
|
const review = reviewDomainControlManifest({
|
|
185
249
|
schema: DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA,
|
|
186
250
|
version: DOMAIN_CONTROL_REVIEW_VERSION,
|
|
187
251
|
manifest,
|
|
188
|
-
observations:
|
|
189
|
-
domain:
|
|
190
|
-
fields: Object.fromEntries(
|
|
191
|
-
const
|
|
192
|
-
registry_nameservers: 'nameservers',
|
|
193
|
-
delegation_ds: 'ds',
|
|
194
|
-
mail_exchangers: 'mx',
|
|
195
|
-
caa_policy: 'caa',
|
|
196
|
-
tls_certificate: 'tlsIssuer',
|
|
197
|
-
tls_public_key: 'tlsSpkiSha256',
|
|
198
|
-
registrar_lock: 'registrarLock',
|
|
199
|
-
};
|
|
200
|
-
const target = mapping[candidate.id];
|
|
252
|
+
observations: selected.map(({ observation, reviewFields: fields }) => ({
|
|
253
|
+
domain: observation.domain,
|
|
254
|
+
fields: Object.fromEntries(fields.flatMap((candidate) => {
|
|
255
|
+
const target = DOMAIN_CONTROL_REVIEW_SOURCE_FIELDS[candidate.id];
|
|
201
256
|
if (!target)
|
|
202
257
|
return [];
|
|
203
|
-
|
|
204
|
-
? list([record(record(record(latest.get(item.domain)?.availability).tls).certificate).issuer], (value) => {
|
|
205
|
-
const issuer = record(value);
|
|
206
|
-
const commonNames = Array.isArray(issuer.commonNames) ? issuer.commonNames : [];
|
|
207
|
-
const organisationNames = Array.isArray(issuer.organizationNames) ? issuer.organizationNames : [];
|
|
208
|
-
return text(commonNames[0] ?? organisationNames[0] ?? value, 300);
|
|
209
|
-
})
|
|
210
|
-
: candidate.values;
|
|
211
|
-
return [[target, { state: candidate.state, values: candidateValues, source: candidate.source, observedAt: item.observedAt }]];
|
|
258
|
+
return [[target, { state: candidate.state, values: candidate.values, source: candidate.source, observedAt: candidate.observedAt }]];
|
|
212
259
|
})),
|
|
213
260
|
})),
|
|
214
261
|
}, generatedAt);
|
|
@@ -221,7 +268,7 @@ export function buildCliDomainControlReview(inputText, generatedAt = new Date().
|
|
|
221
268
|
input: Object.freeze({
|
|
222
269
|
lookupsReceived: lookups.length,
|
|
223
270
|
latestDomainObservations: observations.length,
|
|
224
|
-
ignoredHistoricalLookups
|
|
271
|
+
ignoredHistoricalLookups,
|
|
225
272
|
}),
|
|
226
273
|
limitations: CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS,
|
|
227
274
|
});
|
|
@@ -242,6 +289,7 @@ export function formatCliDomainControlReview(document) {
|
|
|
242
289
|
`Domains ${review.domains.length}`,
|
|
243
290
|
`Lookups ${document.input.lookupsReceived}`,
|
|
244
291
|
`Drift ${review.counts.drift ?? 0}`,
|
|
292
|
+
`Due ${review.counts.due ?? 0}`,
|
|
245
293
|
`Incomplete ${(review.counts.partial ?? 0) + (review.counts.unavailable ?? 0) + (review.counts.unsupported ?? 0)}`,
|
|
246
294
|
'',
|
|
247
295
|
...review.domains.map((item) => `${item.domain} ${item.state}`),
|
package/cli/errors.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { hasUnsafeCliText } from "../packages/contracts/cli-grammar.mjs";
|
|
1
3
|
const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
|
|
2
|
-
const
|
|
4
|
+
const MAX_CLI_DIAGNOSTIC_BYTES = 4_096;
|
|
3
5
|
const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
4
6
|
class CliUsageError extends Error {
|
|
5
|
-
|
|
7
|
+
reason;
|
|
8
|
+
constructor(message, reason = 'invalid_input') {
|
|
6
9
|
super(message);
|
|
7
10
|
this.name = 'CliUsageError';
|
|
11
|
+
this.reason = reason;
|
|
8
12
|
}
|
|
9
13
|
}
|
|
10
14
|
function errorMessage(error) {
|
|
@@ -20,13 +24,40 @@ function boundedCliErrorMessage(error, fallback = 'Unexpected command failure')
|
|
|
20
24
|
.trim()
|
|
21
25
|
.slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
|
|
22
26
|
}
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
// A transient stderr preview, never a checkpoint field. Bound before collecting
|
|
28
|
+
// and redact request URLs and credential-shaped fields before shortening text.
|
|
29
|
+
function createCliDiagnosticOutput() {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let remaining = MAX_CLI_DIAGNOSTIC_BYTES;
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
stream: {
|
|
34
|
+
isTTY: false,
|
|
35
|
+
columns: 80,
|
|
36
|
+
write(chunk) {
|
|
37
|
+
if (remaining > 0 && chunk.length > 0) {
|
|
38
|
+
const bytes = Buffer.from(chunk.slice(0, remaining), 'utf8').subarray(0, remaining);
|
|
39
|
+
chunks.push(bytes.toString('utf8'));
|
|
40
|
+
remaining -= bytes.byteLength;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
value() {
|
|
46
|
+
const source = stripVTControlCharacters(chunks.join('')).replace(CLI_DEFAULT_IGNORABLE_GLOBAL_RE, '');
|
|
47
|
+
if (!source.trim())
|
|
48
|
+
return '';
|
|
49
|
+
const redacted = source
|
|
50
|
+
.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']*/giu, '[URL omitted]')
|
|
51
|
+
.replace(/\b(authorization|cookie|password|passphrase|token|secret|api[-_ ]?key)\s*[:=]\s*(?:"[^"]*(?:"|$)|'[^']*(?:'|$)|[^\r\n,;]*)/giu, '$1=[omitted]')
|
|
52
|
+
.replace(/\b(?:bearer|basic)\s+[^\s,;]+/giu, '[credential omitted]');
|
|
53
|
+
return boundedCliErrorMessage(redacted);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
25
56
|
}
|
|
26
57
|
function boundedCliInputError(error, label) {
|
|
27
58
|
if (error instanceof CliUsageError)
|
|
28
59
|
return error;
|
|
29
60
|
const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
|
|
30
|
-
return new CliUsageError(`${safeLabel} could not be read as a bounded regular file
|
|
61
|
+
return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`, 'input_unavailable');
|
|
31
62
|
}
|
|
32
|
-
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, hasUnsafeCliText, };
|
|
63
|
+
export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, createCliDiagnosticOutput, hasUnsafeCliText, };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {} from "./command-argument-grammar.mjs";
|
|
2
|
+
import { CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
|
|
4
|
+
import { terminalOptions } from "./argument-values.mjs";
|
|
5
|
+
function parseInspectArchiveArguments(parsed) {
|
|
6
|
+
const expectedContentDigest = parsed.optionValue('--expect-content-digest');
|
|
7
|
+
try {
|
|
8
|
+
parseArchiveContentDigest(expectedContentDigest);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new CliUsageError('--expect-content-digest requires sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
action: 'inspect-archive',
|
|
15
|
+
source: parsed.positionalValue('source'),
|
|
16
|
+
passphraseSource: parsed.optionValue('--passphrase-file'),
|
|
17
|
+
search: parsed.optionValue('--search'),
|
|
18
|
+
reveal: parsed.hasOption('--reveal'),
|
|
19
|
+
requireMatch: parsed.hasOption('--require-match'),
|
|
20
|
+
expectedContentDigest,
|
|
21
|
+
output: parsed.hasOption('--json') ? 'json' : 'terminal',
|
|
22
|
+
...terminalOptions(parsed),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseSignArtifactArguments(parsed) {
|
|
26
|
+
return {
|
|
27
|
+
action: 'sign-artifact',
|
|
28
|
+
source: parsed.positionalValue('source'),
|
|
29
|
+
privateKeySource: parsed.optionValue('--private-key-file'),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseVerifySignatureArguments(parsed) {
|
|
33
|
+
return {
|
|
34
|
+
action: 'verify-signature',
|
|
35
|
+
source: parsed.positionalValue('source'),
|
|
36
|
+
publicKeySource: parsed.optionValue('--public-key-file'),
|
|
37
|
+
trustStoreSource: parsed.optionValue('--trust-store-file'),
|
|
38
|
+
output: parsed.hasOption('--json') ? 'json' : 'terminal',
|
|
39
|
+
...terminalOptions(parsed),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export const EVIDENCE_ARGUMENT_PARSERS = Object.freeze({
|
|
43
|
+
'inspect-archive': parseInspectArchiveArguments,
|
|
44
|
+
'sign-artifact': parseSignArtifactArguments,
|
|
45
|
+
'verify-signature': parseVerifySignatureArguments,
|
|
46
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { WORKSPACE_ARCHIVE_VERSION } from "../packages/contracts/case-portability.mjs";
|
|
2
|
+
import { LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION, LEGACY_WORKSPACE_ARCHIVE_SCOPE, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
|
|
3
|
+
export const EVIDENCE_COMMAND_DEFINITIONS = Object.freeze({
|
|
4
|
+
"inspect-archive": commandSeed({
|
|
5
|
+
reference: {
|
|
6
|
+
description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
|
|
7
|
+
example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
|
|
8
|
+
boundary: 'Exact values require --reveal. New content comparisons use the reported sorted-json-v2:sha256 identity with --expect-content-digest; bare sha256 hashes retain their legacy locale-sensitive meaning. Retired and future archives are rejected. The archive is read locally and is never uploaded.',
|
|
9
|
+
},
|
|
10
|
+
collection: { scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
|
|
11
|
+
summary: 'Inspect an archive locally',
|
|
12
|
+
options: ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
|
|
13
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
14
|
+
constraints: Object.freeze([
|
|
15
|
+
constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
|
|
16
|
+
constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
|
|
17
|
+
]),
|
|
18
|
+
handlerOwner: 'evidence',
|
|
19
|
+
networkEffect: 'offline',
|
|
20
|
+
common: false,
|
|
21
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.workspace-archive-inspection']),
|
|
22
|
+
primaryArtefacts: Object.freeze([]),
|
|
23
|
+
planSupport: false,
|
|
24
|
+
additionalOutputFormats: Object.freeze([]),
|
|
25
|
+
bootstrapProfile: 'allowed',
|
|
26
|
+
}),
|
|
27
|
+
"sign-artifact": commandSeed({
|
|
28
|
+
reference: {
|
|
29
|
+
description: 'Sign one reviewed response packet or supported manifest with a local private key.',
|
|
30
|
+
example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
|
|
31
|
+
boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
|
|
32
|
+
},
|
|
33
|
+
collection: { scope: 'Reads one selected artefact and one local private key without transmitting either.' },
|
|
34
|
+
summary: 'Sign a reviewed artefact locally',
|
|
35
|
+
options: ['--private-key-file'],
|
|
36
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
37
|
+
constraints: Object.freeze([
|
|
38
|
+
constraint({ kind: 'required', options: ['--private-key-file'] }),
|
|
39
|
+
]),
|
|
40
|
+
handlerOwner: 'evidence',
|
|
41
|
+
networkEffect: 'offline',
|
|
42
|
+
common: false,
|
|
43
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.signed-evidence-package']),
|
|
44
|
+
primaryArtefacts: Object.freeze([]),
|
|
45
|
+
planSupport: false,
|
|
46
|
+
additionalOutputFormats: Object.freeze([]),
|
|
47
|
+
bootstrapProfile: 'allowed',
|
|
48
|
+
}),
|
|
49
|
+
"verify-signature": commandSeed({
|
|
50
|
+
reference: {
|
|
51
|
+
description: 'Verify one signed package and report embedded-artefact assurance separately. --trust-store-file also checks an explicit local fingerprint policy and emits a signer-trust report; unknown, retired, revoked or future-reviewed entries exit 4.',
|
|
52
|
+
example: 'whoisleuth verify-signature packet.signed.json --trust-store-file trust.json --json',
|
|
53
|
+
boundary: 'A valid signature proves package consistency for the embedded key, not identity, authority or evidence accuracy. With --trust-store-file, unknown, retired, revoked or future-reviewed entries exit 4 even if --public-key-file matches. Replacement fingerprints need their own trusted entry; no signing date overrides current revocation.',
|
|
54
|
+
},
|
|
55
|
+
collection: { scope: 'Reads one selected signed package, optional local public key and explicit fingerprint trust file. No automatic trust discovery or network requests.' },
|
|
56
|
+
summary: 'Verify a signed evidence package',
|
|
57
|
+
options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
|
|
58
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
59
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
60
|
+
handlerOwner: 'evidence',
|
|
61
|
+
networkEffect: 'offline',
|
|
62
|
+
common: false,
|
|
63
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification', 'whoisleuth.evidence-signer-trust-store', 'whoisleuth.evidence-signer-trust-report']),
|
|
64
|
+
primaryArtefacts: Object.freeze([]),
|
|
65
|
+
planSupport: false,
|
|
66
|
+
additionalOutputFormats: Object.freeze([]),
|
|
67
|
+
bootstrapProfile: 'allowed',
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, } from "./artifact-verify.mjs";
|
|
2
2
|
import { formatArchiveInspection, inspectWorkspaceArchive, } from "./archive-inspect.mjs";
|
|
3
3
|
import { MAX_SIGNING_KEY_FILE_BYTES, formatEvidenceSignatureVerification, signEvidencePackage, verifyEvidencePackageSignature, } from "./evidence-signing.mjs";
|
|
4
|
+
import { commandHasOwner } from "./command-reference.mjs";
|
|
4
5
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
5
6
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
7
|
import { formatJsonDocument } from "./formatters/json.mjs";
|
|
7
8
|
import { readCliTextInput } from "./input.mjs";
|
|
9
|
+
import { assessEvidenceSignerTrust, formatSignerTrustReport, MAX_SIGNER_TRUST_STORE_BYTES } from "./signer-trust.mjs";
|
|
8
10
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
9
11
|
export function isEvidenceCommand(args) {
|
|
10
|
-
return args.action
|
|
11
|
-
|| args.action === 'sign-artifact'
|
|
12
|
-
|| args.action === 'verify-signature';
|
|
12
|
+
return commandHasOwner(args.action, 'evidence');
|
|
13
13
|
}
|
|
14
14
|
export function evidenceCommandFailureLabel(action) {
|
|
15
15
|
if (action === 'inspect-archive')
|
|
@@ -18,12 +18,12 @@ export function evidenceCommandFailureLabel(action) {
|
|
|
18
18
|
return 'Evidence signing';
|
|
19
19
|
return 'Evidence signature verification';
|
|
20
20
|
}
|
|
21
|
-
async function readArtifact(source, label, dependencies) {
|
|
21
|
+
async function readArtifact(source, label, dependencies, maximumBytes = MAX_OFFLINE_ARTIFACT_BYTES) {
|
|
22
22
|
try {
|
|
23
23
|
return dependencies.readArtifactInput
|
|
24
24
|
? await dependencies.readArtifactInput(source)
|
|
25
25
|
: await readCliTextInput(source, dependencies.stdin, {
|
|
26
|
-
maximumBytes
|
|
26
|
+
maximumBytes,
|
|
27
27
|
label,
|
|
28
28
|
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
29
29
|
});
|
|
@@ -105,12 +105,22 @@ async function runVerifySignatureCommand(args, dependencies) {
|
|
|
105
105
|
? await readKey(args.publicKeySource, 'Public key file', dependencies.readPublicKeyFile, dependencies)
|
|
106
106
|
: null;
|
|
107
107
|
const report = await verifyEvidencePackageSignature(input, publicKey);
|
|
108
|
+
let trustReport = null;
|
|
109
|
+
if (args.trustStoreSource) {
|
|
110
|
+
const trustRaw = await readArtifact(args.trustStoreSource, 'Signer trust store', dependencies, MAX_SIGNER_TRUST_STORE_BYTES);
|
|
111
|
+
try {
|
|
112
|
+
trustReport = assessEvidenceSignerTrust(report, trustRaw, dependencies.now ? dependencies.now() : new Date().toISOString());
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid signer trust store.'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
108
118
|
if (!args.quiet) {
|
|
109
119
|
dependencies.stdout.write(args.output === 'json'
|
|
110
|
-
? formatJsonDocument(report)
|
|
111
|
-
: formatEvidenceSignatureVerification(report));
|
|
120
|
+
? formatJsonDocument(trustReport ?? report)
|
|
121
|
+
: trustReport ? formatSignerTrustReport(trustReport) : formatEvidenceSignatureVerification(report));
|
|
112
122
|
}
|
|
113
|
-
return EXIT_CODES.SUCCESS;
|
|
123
|
+
return trustReport && trustReport.state !== 'trusted' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
114
124
|
}
|
|
115
125
|
const EVIDENCE_COMMAND_HANDLERS = Object.freeze({
|
|
116
126
|
'inspect-archive': runInspectArchiveCommand,
|
package/cli/evidence-signing.mjs
CHANGED
|
@@ -169,7 +169,8 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
169
169
|
}
|
|
170
170
|
catch { /* Signature validity remains independently reportable. */ }
|
|
171
171
|
let publicKeyMatched = null;
|
|
172
|
-
|
|
172
|
+
const trustedKeySupplied = trustedPublicKeyPem !== undefined && trustedPublicKeyPem !== null;
|
|
173
|
+
if (trustedKeySupplied) {
|
|
173
174
|
const trustedDer = spkiDer(importPublicKey(trustedPublicKeyPem, 'Trusted public key file'));
|
|
174
175
|
publicKeyMatched = trustedDer.equals(publicDer);
|
|
175
176
|
if (!publicKeyMatched)
|
|
@@ -181,7 +182,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
181
182
|
state: 'signature_valid',
|
|
182
183
|
signature: Object.freeze({
|
|
183
184
|
state: 'valid',
|
|
184
|
-
signerTrust:
|
|
185
|
+
signerTrust: trustedKeySupplied ? 'trusted_key' : 'embedded_key_only',
|
|
185
186
|
signedAt,
|
|
186
187
|
keyIdSha256: signature.keyIdSha256,
|
|
187
188
|
publicKeyMatched,
|
|
@@ -216,11 +217,11 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
|
|
|
216
217
|
]),
|
|
217
218
|
});
|
|
218
219
|
}
|
|
219
|
-
export function formatEvidenceSignatureVerification(report) {
|
|
220
|
+
export function formatEvidenceSignatureVerification(report, includeStandaloneTrust = true) {
|
|
220
221
|
const lines = [
|
|
221
222
|
'WHOISleuth evidence signature verification',
|
|
222
223
|
`State: ${report.state}`,
|
|
223
|
-
`Signer trust: ${report.signature.signerTrust}
|
|
224
|
+
...(includeStandaloneTrust ? [`Signer trust: ${report.signature.signerTrust}`] : []),
|
|
224
225
|
`Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'unrecognised'} v${report.artifact.version ?? 'unrecognised'}`,
|
|
225
226
|
`Artifact assurance: ${report.artifact.assurance.state}`,
|
|
226
227
|
`Signed: ${report.signature.signedAt}`,
|