@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0
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/DISCLOSURE +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- 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 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- 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-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -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 +348 -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-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -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 +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- 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 +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -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/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -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/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SUPPORT_COMMAND_DEFINITIONS } from "./support-command-definitions.mjs";
|
|
2
|
+
import { ASSURANCE_COMMAND_DEFINITIONS } from "./assurance-command-definitions.mjs";
|
|
3
|
+
import { COLLECTION_COMMAND_DEFINITIONS } from "./collection-command-definitions.mjs";
|
|
4
|
+
import { NETWORK_COMMAND_DEFINITIONS } from "./network-command-definitions.mjs";
|
|
5
|
+
import { REVIEW_COMMAND_DEFINITIONS } from "./review-command-definitions.mjs";
|
|
6
|
+
import { EVIDENCE_COMMAND_DEFINITIONS } from "./evidence-command-definitions.mjs";
|
|
7
|
+
import { WORKFLOW_COMMAND_DEFINITIONS } from "./workflow-command-definitions.mjs";
|
|
8
|
+
import { HISTORY_COMMAND_DEFINITIONS } from "./history-command-definitions.mjs";
|
|
9
|
+
export const COMMAND_SEEDS = Object.freeze({
|
|
10
|
+
...SUPPORT_COMMAND_DEFINITIONS,
|
|
11
|
+
...ASSURANCE_COMMAND_DEFINITIONS,
|
|
12
|
+
...COLLECTION_COMMAND_DEFINITIONS,
|
|
13
|
+
...NETWORK_COMMAND_DEFINITIONS,
|
|
14
|
+
...REVIEW_COMMAND_DEFINITIONS,
|
|
15
|
+
...EVIDENCE_COMMAND_DEFINITIONS,
|
|
16
|
+
...WORKFLOW_COMMAND_DEFINITIONS,
|
|
17
|
+
...HISTORY_COMMAND_DEFINITIONS,
|
|
18
|
+
});
|
package/cli/compare.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliUsageError } from "./
|
|
1
|
+
import { CliUsageError } from "./errors.mjs";
|
|
2
2
|
import { isRdapRegistryAccessProfile, isWhoisRegistryAccessProfile, } from "./registry-access.mjs";
|
|
3
3
|
import { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, readSavedLookupInputBounded, } from "./saved-lookup.mjs";
|
|
4
4
|
const MAX_COMPARE_INPUT_BYTES = MAX_SAVED_LOOKUP_INPUT_BYTES;
|
package/cli/config-profile.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
import { CliUsageError } from "./errors.mjs";
|
|
5
5
|
import { cliInvocationOptionIndices, cliMetaActionForInvocation } from "./command-reference.mjs";
|
|
6
6
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
7
|
-
import { scanBoundedJson } from "../
|
|
7
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
8
8
|
export const CLI_CONFIG_SCHEMA = 'whoisleuth.cli.config';
|
|
9
9
|
export const CLI_CONFIG_VERSION = 1;
|
|
10
10
|
export const MAX_CLI_CONFIG_BYTES = 64 * 1024;
|
package/cli/ct-event-intake.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { domainToASCII } from 'node:url';
|
|
3
|
-
import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
|
+
import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
4
|
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
|
|
5
5
|
export const CT_EVENT_BATCH_SCHEMA = 'whoisleuth.ct-event-batch';
|
|
6
6
|
export const CT_EVENT_BATCH_VERSION = 1;
|
|
@@ -118,9 +118,9 @@ export function buildCtEventFindings(inputRaw) {
|
|
|
118
118
|
});
|
|
119
119
|
const seen = new Set();
|
|
120
120
|
const unique = candidates
|
|
121
|
-
.sort((left, right) => left.domain
|
|
122
|
-
|| left.observedAt
|
|
123
|
-
|| left.structuredObservation.value
|
|
121
|
+
.sort((left, right) => compareCodeUnits(left.domain, right.domain)
|
|
122
|
+
|| compareCodeUnits(left.observedAt, right.observedAt)
|
|
123
|
+
|| compareCodeUnits(left.structuredObservation.value, right.structuredObservation.value))
|
|
124
124
|
.filter((item) => {
|
|
125
125
|
const key = `${item.domain}\u0000${item.observedAt}\u0000${item.structuredObservation.value}\u0000${item.structuredObservation.eventId}`;
|
|
126
126
|
if (seen.has(key))
|
package/cli/discover.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
3
|
-
import { CliUsageError } from "./
|
|
4
|
-
import { normalizeDiscoverySuffix } from "../
|
|
3
|
+
import { CliUsageError } from "./errors.mjs";
|
|
4
|
+
import { normalizeDiscoverySuffix } from "../packages/analysis/registrable-domain.mjs";
|
|
5
5
|
const DEFAULT_DISCOVERY_TLDS = Object.freeze(['com', 'net', 'org']);
|
|
6
6
|
const MAX_DISCOVERY_TLD_TEXT_LENGTH = 1024;
|
|
7
7
|
const MAX_DISCOVERY_TLD_TOKENS_INSPECTED = 80;
|
|
@@ -2,9 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { availabilityState, bulkDnsSummary } from "./bulk-output.mjs";
|
|
3
3
|
import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
4
4
|
import { writePrivateFile } from "./output-file.mjs";
|
|
5
|
-
import { isValidAsciiDomainName } from "../
|
|
5
|
+
import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
|
|
6
6
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
7
|
-
import { scanBoundedJson } from "../
|
|
7
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
8
8
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
9
|
export const CLI_DISCOVERY_OBSERVATION_SCHEMA = 'whoisleuth.cli.discovery-observation-snapshot';
|
|
10
10
|
export const CLI_DISCOVERY_OBSERVATION_VERSION = 2;
|
|
@@ -23,7 +23,7 @@ async function readAllowlist(source, dependencies, context, classify) {
|
|
|
23
23
|
catch (error) {
|
|
24
24
|
if (error instanceof CliUsageError)
|
|
25
25
|
throw error;
|
|
26
|
-
throw new CliUsageError(`Could not read discovery scan allowlist: ${boundedCliErrorMessage(error, 'Input could not be read')}
|
|
26
|
+
throw new CliUsageError(`Could not read discovery scan allowlist: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
@@ -52,7 +52,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
52
52
|
const queries = candidates.map((candidate) => String(candidate.domain));
|
|
53
53
|
if (args.plan) {
|
|
54
54
|
const document = buildCollectionPreflight({
|
|
55
|
-
command: 'discover-scan', targetCount: queries.length,
|
|
55
|
+
command: 'discover-scan', targetCount: queries.length,
|
|
56
56
|
deep: args.deep, concurrency: args.concurrency, output: args.output, checkpoint: false,
|
|
57
57
|
customResolvers: Boolean(args.resolverText), allowlist: Boolean(args.allowlistSource),
|
|
58
58
|
});
|
|
@@ -86,7 +86,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
86
86
|
throw new CliUsageError('Bulk checkpoint collection context does not match the current DNS resolver selection.');
|
|
87
87
|
}
|
|
88
88
|
if (item.observedAt === null || item.observedAt === undefined) {
|
|
89
|
-
throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.');
|
|
89
|
+
throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.', 'missing_input');
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
const indicator = context.beginProgress(`Collecting 0 of ${queries.length} generated candidates`);
|
|
@@ -135,6 +135,10 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
135
135
|
}
|
|
136
136
|
catch (error) {
|
|
137
137
|
checkpointFailure = error;
|
|
138
|
+
eventProgress.emit({ event: 'warning', state: 'checkpoint_unavailable' });
|
|
139
|
+
if (!eventProgress.enabled) {
|
|
140
|
+
context.writeStderr(`Checkpoint warning: ${boundedCliErrorMessage(error, 'Checkpoint could not be written')}. The latest progress may not be saved.\n`);
|
|
141
|
+
}
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
const snapshot = args.observationSnapshot
|
|
@@ -170,12 +174,6 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
|
|
|
170
174
|
else
|
|
171
175
|
context.writeStdout(context.terminal(formatTerminalDiscoveryScan(document), args.color));
|
|
172
176
|
}
|
|
173
|
-
if (checkpointFailure) {
|
|
174
|
-
eventProgress.emit({ event: 'warning', state: 'checkpoint_unavailable' });
|
|
175
|
-
if (!eventProgress.enabled) {
|
|
176
|
-
context.writeStderr(`Checkpoint warning: ${boundedCliErrorMessage(checkpointFailure, 'Checkpoint could not be written')}. Completed output is still available.\n`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
177
|
const policyFindings = evaluateCliFailPolicies(document, args.failOn || []);
|
|
180
178
|
if (policyFindings.length && !eventProgress.enabled)
|
|
181
179
|
context.writeStderr(formatFailPolicyNotice(policyFindings));
|
package/cli/discovery-scan.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
3
|
import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
|
|
4
|
-
import { MAX_DISCOVERY_SUFFIX_LENGTH } from "../
|
|
4
|
+
import { MAX_DISCOVERY_SUFFIX_LENGTH } from "../packages/analysis/registrable-domain.mjs";
|
|
5
5
|
import { runBulkLookups } from "./bulk.mjs";
|
|
6
6
|
import { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkCsvMetadataValues, bulkDnsSummary } from "./bulk-output.mjs";
|
|
7
7
|
import { cliCsvCell } from "./csv.mjs";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { boundedCliInputError, CliUsageError } from "./errors.mjs";
|
|
3
3
|
import { writePrivateFile } from "./output-file.mjs";
|
|
4
|
-
import { isValidAsciiDomainName } from "../
|
|
4
|
+
import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
|
|
5
5
|
import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
|
|
6
|
-
import { scanBoundedJson } from "../
|
|
6
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
7
7
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
8
8
|
export const CLI_DISCOVERY_SNAPSHOT_SCHEMA = 'whoisleuth.cli.discovery-snapshot';
|
|
9
9
|
export const CLI_DISCOVERY_SNAPSHOT_VERSION = 1;
|
|
@@ -4,7 +4,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
|
4
4
|
async function generateDiscoveryCandidates(args, dependencies, context) {
|
|
5
5
|
const seed = args.seed || await context.readSingleInput();
|
|
6
6
|
if (!seed)
|
|
7
|
-
throw new CliUsageError('discover requires one brand label or domain as an argument or on stdin.');
|
|
7
|
+
throw new CliUsageError('discover requires one brand label or domain as an argument or on stdin.', 'missing_input');
|
|
8
8
|
const loadGenerator = dependencies.loadTyposquatGenerator || (() => import("../lib/typosquat-generator.mjs"));
|
|
9
9
|
const generator = await loadGenerator();
|
|
10
10
|
const tlds = normalizeDiscoveryTlds(args.tldText || DEFAULT_DISCOVERY_TLDS.join(','), generator.MAX_GENERATION_TLDS);
|
|
@@ -23,7 +23,7 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
|
|
|
23
23
|
if (args.preset === 'custom'
|
|
24
24
|
&& !mutationFamilies.includes('dictionary')
|
|
25
25
|
&& !mutationFamilies.includes('dictionary_token_replacement')) {
|
|
26
|
-
throw new CliUsageError('--dictionary requires a dictionary mutation family.');
|
|
26
|
+
throw new CliUsageError('--dictionary requires a dictionary mutation family.', 'conflicting_options');
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
29
29
|
dictionaryText = dependencies.readDiscoveryDictionary
|
|
@@ -33,11 +33,11 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
|
|
|
33
33
|
catch (error) {
|
|
34
34
|
if (error instanceof CliUsageError)
|
|
35
35
|
throw error;
|
|
36
|
-
throw new CliUsageError(`Could not read discovery dictionary: ${boundedCliErrorMessage(error, 'Input could not be read')}
|
|
36
|
+
throw new CliUsageError(`Could not read discovery dictionary: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
|
|
37
37
|
}
|
|
38
38
|
const normalizedDictionary = generator.normalizeCustomDictionaryTerms(dictionaryText);
|
|
39
39
|
if (!normalizedDictionary.values.length) {
|
|
40
|
-
throw new CliUsageError('The discovery dictionary did not contain any valid terms.');
|
|
40
|
+
throw new CliUsageError('The discovery dictionary did not contain any valid terms.', 'missing_input');
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
const result = generator.generateTyposquatCandidateSet(seed, tlds, {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { classifyQuery } from "../lib/classify.mjs";
|
|
2
|
-
import { scanBoundedJson } from "../
|
|
2
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
3
3
|
import { buildDomainControlFlightRecorder, validateDomainControlFlightRecorderDocument, normalizeDomainControlFlightRecorderObservations, } from "../lib/domain-control-flight-recorder.mjs";
|
|
4
|
-
import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION,
|
|
5
|
-
import { DOMAIN_CONTROL_REVIEW_SCHEMA,
|
|
6
|
-
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
|
|
4
|
+
import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, } from "../packages/contracts/domain-control-flight-recorder.mjs";
|
|
5
|
+
import { DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, } from "../packages/contracts/domain-control-review.mjs";
|
|
6
|
+
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
|
|
7
7
|
import { validateDomainControlReviewDocument, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
8
8
|
import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
|
|
9
9
|
import { buildCliLookupDocument } from "./formatters/json.mjs";
|
|
@@ -62,8 +62,7 @@ function previousSnapshot(input, currentGeneratedAt) {
|
|
|
62
62
|
throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} supported version ${SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS.join(' or ')}.`);
|
|
63
63
|
}
|
|
64
64
|
const legacy = root.version === PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION;
|
|
65
|
-
const reviewVersion
|
|
66
|
-
const flightVersion = legacy ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION : DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION;
|
|
65
|
+
const { review: reviewVersion, flightRecorder: flightVersion } = DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS[root.version];
|
|
67
66
|
let previousGeneratedAt;
|
|
68
67
|
try {
|
|
69
68
|
previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
|
|
@@ -307,6 +306,7 @@ export function formatDomainControlMonitor(document) {
|
|
|
307
306
|
`Succeeded ${document.collection.succeeded}`,
|
|
308
307
|
`Failed ${document.collection.failed}`,
|
|
309
308
|
`Review ${document.review.state}`,
|
|
309
|
+
`Due ${document.review.counts.due ?? 0}`,
|
|
310
310
|
`Unexpected ${document.flightRecorder.summary.unexpectedChanges}`,
|
|
311
311
|
'',
|
|
312
312
|
].join('\n');
|
|
@@ -4,7 +4,7 @@ import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDoma
|
|
|
4
4
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
5
5
|
import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
|
|
6
6
|
import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
|
|
7
|
-
import { scanBoundedJson, } from "../
|
|
7
|
+
import { scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
|
|
8
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
9
|
import { MAX_FLIGHT_RECORDER_VALUES, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
|
|
10
10
|
import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
@@ -289,6 +289,7 @@ export function formatCliDomainControlReview(document) {
|
|
|
289
289
|
`Domains ${review.domains.length}`,
|
|
290
290
|
`Lookups ${document.input.lookupsReceived}`,
|
|
291
291
|
`Drift ${review.counts.drift ?? 0}`,
|
|
292
|
+
`Due ${review.counts.due ?? 0}`,
|
|
292
293
|
`Incomplete ${(review.counts.partial ?? 0) + (review.counts.unavailable ?? 0) + (review.counts.unsupported ?? 0)}`,
|
|
293
294
|
'',
|
|
294
295
|
...review.domains.map((item) => `${item.domain} ${item.state}`),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { DOMAIN_FEED_LIMITS, normalizeDomainFeedSelection } from "../packages/monitoring/domain-feed.mjs";
|
|
4
|
+
import { CliUsageError } from "./errors.mjs";
|
|
5
|
+
export function parseDomainFeedSelectors(values) {
|
|
6
|
+
const hosts = [], terms = [], negativeTerms = [];
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
if (value.startsWith('host:'))
|
|
9
|
+
hosts.push(value.slice(5));
|
|
10
|
+
else if (value.startsWith('term:'))
|
|
11
|
+
terms.push(value.slice(5));
|
|
12
|
+
else if (value.startsWith('exclude:'))
|
|
13
|
+
negativeTerms.push(value.slice(8));
|
|
14
|
+
else
|
|
15
|
+
throw new CliUsageError('--select requires host:<plain-hostname>, term:<literal-text> or exclude:<literal-text>.');
|
|
16
|
+
}
|
|
17
|
+
const selection = normalizeDomainFeedSelection({ hosts, terms, ...(negativeTerms.length ? { negativeTerms } : {}) });
|
|
18
|
+
if (!selection.hosts.length && !selection.terms.length)
|
|
19
|
+
throw new CliUsageError('Select at least one exact host or literal term.');
|
|
20
|
+
return selection;
|
|
21
|
+
}
|
|
22
|
+
/** Stream one descriptor-bound local regular file; URLs, pipes and symlinks are not inputs. */
|
|
23
|
+
export async function* readDomainFeedFile(source, signal) {
|
|
24
|
+
if (!source || /^[a-z][a-z0-9+.-]*:\/\//iu.test(source))
|
|
25
|
+
throw new CliUsageError('Domain feed review requires an explicit local regular file, not a URL.');
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
throw new DOMException('Feed reading was cancelled.', 'AbortError');
|
|
28
|
+
const handle = await open(source, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
29
|
+
let stream;
|
|
30
|
+
const abort = () => stream?.destroy(new DOMException('Feed reading was cancelled.', 'AbortError'));
|
|
31
|
+
try {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
throw new DOMException('Feed reading was cancelled.', 'AbortError');
|
|
34
|
+
const before = await handle.stat();
|
|
35
|
+
if (!before.isFile())
|
|
36
|
+
throw new CliUsageError('The domain feed must be a regular file.');
|
|
37
|
+
if (before.size > DOMAIN_FEED_LIMITS.bytes)
|
|
38
|
+
throw new CliUsageError('The domain feed exceeds its 256 MiB raw-byte ceiling.');
|
|
39
|
+
stream = handle.createReadStream({ autoClose: false, highWaterMark: 64 * 1024 });
|
|
40
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
41
|
+
if (signal?.aborted)
|
|
42
|
+
abort();
|
|
43
|
+
let bytes = 0;
|
|
44
|
+
for await (const chunk of stream) {
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
throw new DOMException('Feed reading was cancelled.', 'AbortError');
|
|
47
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength > DOMAIN_FEED_LIMITS.bytes - bytes)
|
|
48
|
+
throw new CliUsageError('The domain feed exceeds its raw-byte ceiling.');
|
|
49
|
+
bytes += chunk.byteLength;
|
|
50
|
+
yield chunk;
|
|
51
|
+
}
|
|
52
|
+
const after = await handle.stat();
|
|
53
|
+
if (bytes !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
|
|
54
|
+
throw new CliUsageError('The domain feed changed while it was being read; no review was produced.');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
signal?.removeEventListener('abort', abort);
|
|
58
|
+
stream?.destroy();
|
|
59
|
+
await handle.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
package/cli/errors.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { hasUnsafeCliText } from "../packages/contracts/cli-grammar.mjs";
|
|
2
3
|
const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
|
|
3
4
|
const MAX_CLI_DIAGNOSTIC_BYTES = 4_096;
|
|
4
|
-
const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
|
|
5
5
|
const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
6
6
|
class CliUsageError extends Error {
|
|
7
|
-
|
|
7
|
+
reason;
|
|
8
|
+
constructor(message, reason = 'invalid_input') {
|
|
8
9
|
super(message);
|
|
9
10
|
this.name = 'CliUsageError';
|
|
11
|
+
this.reason = reason;
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
function errorMessage(error) {
|
|
@@ -22,9 +24,6 @@ function boundedCliErrorMessage(error, fallback = 'Unexpected command failure')
|
|
|
22
24
|
.trim()
|
|
23
25
|
.slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
|
|
24
26
|
}
|
|
25
|
-
function hasUnsafeCliText(value) {
|
|
26
|
-
return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
|
|
27
|
-
}
|
|
28
27
|
// A transient stderr preview, never a checkpoint field. Bound before collecting
|
|
29
28
|
// and redact request URLs and credential-shaped fields before shortening text.
|
|
30
29
|
function createCliDiagnosticOutput() {
|
|
@@ -59,6 +58,6 @@ function boundedCliInputError(error, label) {
|
|
|
59
58
|
if (error instanceof CliUsageError)
|
|
60
59
|
return error;
|
|
61
60
|
const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
|
|
62
|
-
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');
|
|
63
62
|
}
|
|
64
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,6 +1,7 @@
|
|
|
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";
|
|
@@ -8,9 +9,7 @@ import { readCliTextInput } from "./input.mjs";
|
|
|
8
9
|
import { assessEvidenceSignerTrust, formatSignerTrustReport, MAX_SIGNER_TRUST_STORE_BYTES } from "./signer-trust.mjs";
|
|
9
10
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
10
11
|
export function isEvidenceCommand(args) {
|
|
11
|
-
return args.action
|
|
12
|
-
|| args.action === 'sign-artifact'
|
|
13
|
-
|| args.action === 'verify-signature';
|
|
12
|
+
return commandHasOwner(args.action, 'evidence');
|
|
14
13
|
}
|
|
15
14
|
export function evidenceCommandFailureLabel(action) {
|
|
16
15
|
if (action === 'inspect-archive')
|
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,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { domainToASCII } from 'node:url';
|
|
2
|
-
import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
2
|
+
import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
|
|
3
3
|
import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
|
|
4
4
|
export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
|
|
5
5
|
export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
|
|
@@ -112,7 +112,7 @@ export function mapExternalObservations(inputRaw) {
|
|
|
112
112
|
: null,
|
|
113
113
|
structuredObservation: null,
|
|
114
114
|
});
|
|
115
|
-
}).sort((left, right) => left.domain
|
|
115
|
+
}).sort((left, right) => compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.observedAt, right.observedAt) || compareCodeUnits(left.summary, right.summary));
|
|
116
116
|
const selected = [];
|
|
117
117
|
const seen = new Set();
|
|
118
118
|
const domainCounts = new Map();
|
package/cli/formatters/json.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { bulkDnsSummary } from "../bulk-output.mjs";
|
|
2
2
|
import { SAVED_LOOKUP_SCHEMA_VERSION, buildCliLookupDocument, } from "../saved-lookup.mjs";
|
|
3
3
|
import { CLI_LOOKUP_SCHEMA } from "../../packages/contracts/cli-lookup.mjs";
|
|
4
|
+
import { terminalSafeJson } from "../../packages/interchange/json-output.mjs";
|
|
4
5
|
const CLI_BULK_SCHEMA = 'whoisleuth.cli.bulk';
|
|
5
6
|
const CLI_BULK_ITEM_SCHEMA = 'whoisleuth.cli.bulk.item';
|
|
6
7
|
const CLI_CT_SEARCH_SCHEMA = 'whoisleuth.cli.ct-search';
|
|
@@ -18,19 +19,6 @@ const CLI_POSTURE_SCHEMA_VERSION = 1;
|
|
|
18
19
|
const CLI_HTTP_SCHEMA_VERSION = 3;
|
|
19
20
|
const CLI_TLS_SCHEMA_VERSION = 1;
|
|
20
21
|
const CLI_COMPARE_SCHEMA_VERSION = 3;
|
|
21
|
-
const TERMINAL_UNSAFE_JSON_RE = /[\u007f-\u009f\u2028\u2029]|\p{Default_Ignorable_Code_Point}/gu;
|
|
22
|
-
function jsonUnicodeEscape(value) {
|
|
23
|
-
const codePoint = value.codePointAt(0);
|
|
24
|
-
if (codePoint <= 0xffff)
|
|
25
|
-
return `\\u${codePoint.toString(16).padStart(4, '0')}`;
|
|
26
|
-
const offset = codePoint - 0x10000;
|
|
27
|
-
const high = 0xd800 + (offset >> 10);
|
|
28
|
-
const low = 0xdc00 + (offset & 0x3ff);
|
|
29
|
-
return `\\u${high.toString(16).padStart(4, '0')}\\u${low.toString(16).padStart(4, '0')}`;
|
|
30
|
-
}
|
|
31
|
-
function terminalSafeJson(value, space) {
|
|
32
|
-
return JSON.stringify(value, null, space).replace(TERMINAL_UNSAFE_JSON_RE, jsonUnicodeEscape);
|
|
33
|
-
}
|
|
34
22
|
function formatJsonDocument(document) {
|
|
35
23
|
return `${terminalSafeJson(document, 2)}\n`;
|
|
36
24
|
}
|
|
@@ -168,6 +168,12 @@ function formatTerminalPosture(document) {
|
|
|
168
168
|
`Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
|
|
169
169
|
'',
|
|
170
170
|
];
|
|
171
|
+
for (const value of dmarcAuthorizations.slice(0, 10)) {
|
|
172
|
+
const item = terminalRecord(value);
|
|
173
|
+
lines.push(`Reporting ${safeTerminalValue(item.destination)} · ${safeTerminalValue(item.reportType)} · ${item.state === 'self' ? 'same organisational scope' : safeTerminalValue(item.state).replaceAll('_', ' ')}`);
|
|
174
|
+
if (item.error)
|
|
175
|
+
lines.push(` Detail ${safeTerminalValue(item.error)}`);
|
|
176
|
+
}
|
|
171
177
|
for (const value of checks) {
|
|
172
178
|
const item = terminalRecord(value);
|
|
173
179
|
lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.summary)}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registryAccessProfileLabel } from "../registry-access.mjs";
|
|
2
|
+
import { dnsAddressFamilyEvidence } from "../../packages/evidence/dns-query-outcome.mjs";
|
|
2
3
|
import { appendTechnologyLines } from "./terminal-technology.mjs";
|
|
3
4
|
import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
|
|
4
5
|
import { securityPostureReview, summarizeSecurityPostureReview } from "../../lib/website-security-posture.mjs";
|
|
@@ -319,6 +320,11 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
|
|
|
319
320
|
if (Object.keys(dns).length) {
|
|
320
321
|
dnsLines.push(`Evidence ${titleCase(dns.status)}`);
|
|
321
322
|
dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
|
|
323
|
+
for (const family of ['a', 'aaaa']) {
|
|
324
|
+
const evidence = dnsAddressFamilyEvidence(dns, family);
|
|
325
|
+
if (evidence.value)
|
|
326
|
+
dnsLines.push(`${family.toUpperCase().padEnd(14)} ${safeTerminalValue(evidence.value)}`);
|
|
327
|
+
}
|
|
322
328
|
const recordTypes = [
|
|
323
329
|
['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
|
|
324
330
|
['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {} from "./command-argument-grammar.mjs";
|
|
2
|
+
import { CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { terminalOptions, jsonOutput, parseTwoFileComparisonArguments, uniqueSources, } from "./argument-values.mjs";
|
|
4
|
+
function parseDiffArguments(parsed) {
|
|
5
|
+
const compared = parseTwoFileComparisonArguments(parsed, 'diff');
|
|
6
|
+
const leftSessionId = parsed.optionValue('--left-session');
|
|
7
|
+
const rightSessionId = parsed.optionValue('--right-session');
|
|
8
|
+
for (const [option, value] of [
|
|
9
|
+
['--left-session', leftSessionId],
|
|
10
|
+
['--right-session', rightSessionId],
|
|
11
|
+
]) {
|
|
12
|
+
if (value !== null && !/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
|
|
13
|
+
throw new CliUsageError(`${option} requires one bounded saved-session ID.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { action: 'diff', ...compared, leftSessionId, rightSessionId };
|
|
17
|
+
}
|
|
18
|
+
export const HISTORY_ARGUMENT_PARSERS = Object.freeze({
|
|
19
|
+
diff: parseDiffArguments,
|
|
20
|
+
reconcile: (parsed) => ({
|
|
21
|
+
action: 'reconcile',
|
|
22
|
+
sources: uniqueSources(parsed, 'reconcile'),
|
|
23
|
+
output: jsonOutput(parsed),
|
|
24
|
+
...terminalOptions(parsed),
|
|
25
|
+
}),
|
|
26
|
+
timeline: (parsed) => ({
|
|
27
|
+
action: 'timeline',
|
|
28
|
+
sources: uniqueSources(parsed, 'timeline'),
|
|
29
|
+
output: jsonOutput(parsed),
|
|
30
|
+
...terminalOptions(parsed),
|
|
31
|
+
}),
|
|
32
|
+
export: (parsed) => ({
|
|
33
|
+
action: 'export',
|
|
34
|
+
source: parsed.positionalValue('source'),
|
|
35
|
+
format: parsed.hasOption('--markdown') ? 'markdown' : parsed.hasOption('--html') ? 'html' : 'json',
|
|
36
|
+
compact: parsed.hasOption('--compact'),
|
|
37
|
+
includeAttribution: !parsed.hasOption('--no-attribution'),
|
|
38
|
+
}),
|
|
39
|
+
});
|