@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,279 @@
|
|
|
1
|
+
import { CASE_SCHEMA_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES } from "../packages/contracts/case-portability.mjs";
|
|
2
|
+
import { MESSAGE_INTAKE_KINDS, MESSAGE_INTAKE_SCHEMA } from "../packages/contracts/message-intake.mjs";
|
|
3
|
+
import { IDENTITY_EVENTS_INPUT_SCHEMA } from "../packages/contracts/identity-events.mjs";
|
|
4
|
+
import { CONTEXT_INPUT_SCHEMAS, CONTEXT_REVIEW_SCHEMA } from "../packages/contracts/context-review.mjs";
|
|
5
|
+
import { CASE_CONTAINMENT_INPUT_SCHEMA, CASE_CONTAINMENT_HANDOFF_SCHEMA } from "../packages/cases/case-containment-handoff.mjs";
|
|
6
|
+
import { INTAKE_CONTEXT_SCHEMA } from "../packages/investigation/intake-context.mjs";
|
|
7
|
+
import { MANAGED_INDICATOR_SET_SCHEMA, MAX_MANAGED_INDICATOR_SET_BYTES, MAX_MANAGED_INDICATOR_PLAN_BYTES } from "../packages/contracts/analyst-interchange.mjs";
|
|
8
|
+
import { CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS, positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
|
|
9
|
+
export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
|
|
10
|
+
"verify-artifact": commandSeed({
|
|
11
|
+
reference: {
|
|
12
|
+
description: 'Validate a supported archive, ordinary Case export, claim passport, packet, manifest, saved Lookup or Lookup-evidence export without printing evidence contents. Use --package for an evidence ZIP or encrypted package, with --passphrase-file to unlock it; use --folder ./evidence for an unencrypted evidence folder. Add --bagit with --package or --folder to verify BagIt 1.0.',
|
|
13
|
+
example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
|
|
14
|
+
boundary: 'Verification is offline and redacted. ZIP and folder entries are reported separately without importing them. Case exports are checked without repairing content; ordinary package review also counts original references with matching bytes. Ordinary folders allow only the declared layout; BagIt allows bounded nested payloads and checks SHA-256/SHA-512 manifests without interpreting payloads. Symbolic links are refused. BagIt fetch.txt is never fetched; missing files, mismatches and unsupported algorithms remain explicit. Package digests describe bytes, not filesystem metadata or authenticity. In scripts, use --strict-exit: incomplete verification returns 4. Unencrypted workspace archives always return 4 under --strict-exit because section digests do not cover mutable root metadata; structure-only formats can pass their narrower contract. Default exit 0 means the report was produced, not that its checks passed.',
|
|
15
|
+
},
|
|
16
|
+
collection: { scope: 'Reads one selected bounded artefact, ZIP or explicit evidence folder and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
|
|
17
|
+
summary: 'Validate saved evidence offline',
|
|
18
|
+
options: ['--passphrase-file', '--manifest', '--manifest-entry', '--package', '--bagit', '--folder', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
19
|
+
positionals: Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin', ['--package'])]),
|
|
20
|
+
constraints: Object.freeze([
|
|
21
|
+
constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
|
|
22
|
+
constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file', '--manifest', '--manifest-entry'] }),
|
|
23
|
+
constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
|
|
24
|
+
constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
|
|
25
|
+
constraint({ kind: 'excludes_all', option: '--package', excludedOptions: ['--manifest', '--manifest-entry'] }),
|
|
26
|
+
constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--manifest', '--manifest-entry', '--passphrase-file'] }),
|
|
27
|
+
]),
|
|
28
|
+
handlerOwner: 'review',
|
|
29
|
+
networkEffect: 'offline',
|
|
30
|
+
common: true,
|
|
31
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.offline-artifact-verification']),
|
|
32
|
+
primaryArtefacts: Object.freeze(['Offline verification report']),
|
|
33
|
+
planSupport: false,
|
|
34
|
+
additionalOutputFormats: Object.freeze([]),
|
|
35
|
+
bootstrapProfile: 'allowed',
|
|
36
|
+
}),
|
|
37
|
+
"interchange-report": commandSeed({
|
|
38
|
+
reference: {
|
|
39
|
+
description: 'Report what one recognised portable artefact preserves, excludes, and supports across browser and CLI workflows.',
|
|
40
|
+
example: 'whoisleuth interchange-report workspace.json --json',
|
|
41
|
+
boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
|
|
42
|
+
},
|
|
43
|
+
collection: { scope: 'Reads one selected bounded portable artefact and emits fixed compatibility metadata only.' },
|
|
44
|
+
summary: 'Report portable artefact fidelity offline',
|
|
45
|
+
options: ['--passphrase-file', '--json', '--quiet', '--no-color'],
|
|
46
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
47
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
48
|
+
handlerOwner: 'review',
|
|
49
|
+
networkEffect: 'offline',
|
|
50
|
+
common: false,
|
|
51
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.interchange-fidelity-report']),
|
|
52
|
+
primaryArtefacts: Object.freeze([]),
|
|
53
|
+
planSupport: false,
|
|
54
|
+
additionalOutputFormats: Object.freeze([]),
|
|
55
|
+
bootstrapProfile: 'allowed',
|
|
56
|
+
}),
|
|
57
|
+
"source-report": commandSeed({
|
|
58
|
+
reference: {
|
|
59
|
+
description: 'Create a target-free reliability summary from a saved lookup.',
|
|
60
|
+
example: 'whoisleuth source-report lookup.json --json',
|
|
61
|
+
boundary: 'The report retains source states and timings but excludes targets, queries, endpoints, and raw evidence.',
|
|
62
|
+
},
|
|
63
|
+
collection: { scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
64
|
+
summary: 'Build a target-free source report',
|
|
65
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
66
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
67
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
68
|
+
handlerOwner: 'review',
|
|
69
|
+
networkEffect: 'offline',
|
|
70
|
+
common: false,
|
|
71
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.source-reliability-report']),
|
|
72
|
+
primaryArtefacts: Object.freeze([]),
|
|
73
|
+
planSupport: false,
|
|
74
|
+
additionalOutputFormats: Object.freeze([]),
|
|
75
|
+
bootstrapProfile: 'allowed',
|
|
76
|
+
}),
|
|
77
|
+
compare: commandSeed({
|
|
78
|
+
reference: {
|
|
79
|
+
description: 'Compare separately attributed registry publications in a saved lookup.',
|
|
80
|
+
example: 'whoisleuth compare lookup.json --json',
|
|
81
|
+
boundary: 'Comparison is offline. Differences are review context and do not by themselves prove which publication is current.',
|
|
82
|
+
},
|
|
83
|
+
collection: { scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
|
|
84
|
+
summary: 'Compare registry publications in one lookup',
|
|
85
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
86
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
87
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
88
|
+
handlerOwner: 'review',
|
|
89
|
+
networkEffect: 'offline',
|
|
90
|
+
common: false,
|
|
91
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.compare']),
|
|
92
|
+
primaryArtefacts: Object.freeze([]),
|
|
93
|
+
planSupport: false,
|
|
94
|
+
additionalOutputFormats: Object.freeze([]),
|
|
95
|
+
bootstrapProfile: 'allowed',
|
|
96
|
+
}),
|
|
97
|
+
"page-compare": commandSeed({
|
|
98
|
+
reference: {
|
|
99
|
+
description: 'Compare static page identity, favicon, technology, and TLS evidence in two saved deep lookups.',
|
|
100
|
+
example: 'whoisleuth page-compare official.json candidate.json --json',
|
|
101
|
+
boundary: 'Comparison is offline and component-based. It executes no page code and produces no aggregate similarity or maliciousness score.',
|
|
102
|
+
},
|
|
103
|
+
collection: { scope: 'Reads two saved Lookup documents and executes no page code.' },
|
|
104
|
+
summary: 'Compare saved static page evidence',
|
|
105
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
106
|
+
positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
|
|
107
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
108
|
+
handlerOwner: 'review',
|
|
109
|
+
networkEffect: 'offline',
|
|
110
|
+
common: false,
|
|
111
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.page-compare']),
|
|
112
|
+
primaryArtefacts: Object.freeze([]),
|
|
113
|
+
planSupport: false,
|
|
114
|
+
additionalOutputFormats: Object.freeze([]),
|
|
115
|
+
bootstrapProfile: 'allowed',
|
|
116
|
+
}),
|
|
117
|
+
"mail-review": commandSeed({
|
|
118
|
+
reference: {
|
|
119
|
+
description: 'Review passive MX, null MX, SPF, DMARC, and shared mail-provider evidence from saved Bulk results.',
|
|
120
|
+
example: 'whoisleuth mail-review candidates.json --json',
|
|
121
|
+
boundary: 'Review is offline and sends no SMTP traffic. Missing or partial DNS evidence remains inconclusive.',
|
|
122
|
+
},
|
|
123
|
+
collection: { scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
|
|
124
|
+
summary: 'Review saved passive mail evidence',
|
|
125
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
126
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
127
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
128
|
+
handlerOwner: 'review',
|
|
129
|
+
networkEffect: 'offline',
|
|
130
|
+
common: false,
|
|
131
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-review']),
|
|
132
|
+
primaryArtefacts: Object.freeze([]),
|
|
133
|
+
planSupport: false,
|
|
134
|
+
additionalOutputFormats: Object.freeze([]),
|
|
135
|
+
bootstrapProfile: 'allowed',
|
|
136
|
+
}),
|
|
137
|
+
"mail-headers": commandSeed({
|
|
138
|
+
reference: {
|
|
139
|
+
description: 'Review identity, reported authentication, domain alignment, and Received routing from selected message headers.',
|
|
140
|
+
example: 'whoisleuth mail-headers message.eml --json',
|
|
141
|
+
boundary: 'Review is offline. It makes no DNS, SMTP, HTTP, registry, or provider request, and does not retain address local parts, display names, subjects, message bodies, attachments, or raw header values. Reported authentication is not independently validated.',
|
|
142
|
+
},
|
|
143
|
+
collection: { scope: 'Parses only the bounded header block from one selected message or standard input.' },
|
|
144
|
+
summary: 'Review message headers offline',
|
|
145
|
+
options: ['--trusted-auth-header', '--json', '--quiet', '--no-color'],
|
|
146
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
147
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
148
|
+
handlerOwner: 'review',
|
|
149
|
+
networkEffect: 'offline',
|
|
150
|
+
common: false,
|
|
151
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-header-review']),
|
|
152
|
+
primaryArtefacts: Object.freeze([]),
|
|
153
|
+
planSupport: false,
|
|
154
|
+
additionalOutputFormats: Object.freeze([]),
|
|
155
|
+
bootstrapProfile: 'allowed',
|
|
156
|
+
}),
|
|
157
|
+
intake: commandSeed({
|
|
158
|
+
reference: {
|
|
159
|
+
description: 'Review selected messages, indicators, document links, QR destinations, HTTP archives or identity events locally. Plain text offers phone candidates; --intake-context adds explicit selections and source declarations.',
|
|
160
|
+
example: 'whoisleuth intake email message.eml --json',
|
|
161
|
+
boundary: 'Offline only: no destination, embedded command, phone number or QR payload is opened, contacted or executed. Output omits original bodies, subjects, address local parts, URL paths, queries and fragments. Literal IPs and labelled hashes are bounded text observations, not lookup targets or verdicts; headers, URL contents, QR, HAR and identity-event fields are not scanned for them. Plain-text phone discovery writes only counts, source digest, ranges and status to stderr; --quiet suppresses it. Optional --intake-context reads one separate file up to 8 KiB, not stdin. Version 1 carries distribution declarations; version 2 can also bind explicit phone selections and a supplied destination pair to the exact input digest. Selected phone strings and source declarations enter output; unselected candidates and exact private URLs do not. Authentication headers remain attributed claims. Selected PDF and DOCX review retains part identities and extraction coverage; embedded raster QR support does not imply complete rendered-page review. HAR retains minimised request order and timings, never replay authority, headers, cookies or bodies. Use --strict-exit to return 4 for partial coverage.',
|
|
162
|
+
},
|
|
163
|
+
collection: { scope: 'Reads one selected text, MIME email, calendar, PNG, PDF, DOCX, HAR or identity-event JSON file and, only with --intake-context, one bounded declaration/selection file. No collection or automatic Case write.' },
|
|
164
|
+
summary: 'Review selected files and identity events offline',
|
|
165
|
+
options: ['--json', '--reported-action', '--trusted-auth-header', '--intake-context', '--strict-exit', '--quiet', '--no-color'],
|
|
166
|
+
positionals: Object.freeze([positional('kind', 'enum', 1, 1, MESSAGE_INTAKE_KINDS), positional('source', 'file', 0, 1, [], 'argv_or_stdin')]),
|
|
167
|
+
constraints: EMPTY_CONSTRAINTS, handlerOwner: 'review', networkEffect: 'offline', common: true,
|
|
168
|
+
schemaIdentifiers: Object.freeze([MESSAGE_INTAKE_SCHEMA, INTAKE_CONTEXT_SCHEMA, IDENTITY_EVENTS_INPUT_SCHEMA]), primaryArtefacts: Object.freeze([]),
|
|
169
|
+
planSupport: false, additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
|
|
170
|
+
}),
|
|
171
|
+
"review-evidence": commandSeed({
|
|
172
|
+
reference: {
|
|
173
|
+
description: 'Review versioned protocol evidence, source-qualified infrastructure snapshots/comparisons, incident sequences, domain history, platform objects, storefronts, connector configuration or an explicitly selected internal containment handoff offline.',
|
|
174
|
+
example: 'whoisleuth review-evidence domain-change.json --json --strict-exit',
|
|
175
|
+
boundary: 'The command reads only the supplied document and, with --mmdb, one explicitly selected local database up to 512 MiB. MMDB parsing and lookup run in one disposable worker with a ten-second processing deadline and an 8 KiB result limit. Version-2 MMDB queries require a justified age policy; unavailable or partial current reviews return 4 under --strict-exit. Historical version-1 MMDB queries preserve their output without freshness admission. Domain-history v2 accepts explicit source-linked registration boundaries; v1 retains its original semantics. Internal containment selects retained next-step assertions and supporting pins after explicit audience-disclosure review; it does not change their state or execute controls. No database is bundled, downloaded, updated or transmitted. It performs no DNS, RDAP, BGP, GeoIP-provider, TLS, HTTP, certificate-authority, or SMTP request.',
|
|
176
|
+
},
|
|
177
|
+
collection: { scope: 'Reads one bounded versioned evidence or request-planning document and, only with --mmdb, one selected bounded local database. Performs no collection.' },
|
|
178
|
+
summary: 'Review supplied evidence offline',
|
|
179
|
+
options: ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
180
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
181
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
182
|
+
handlerOwner: 'review',
|
|
183
|
+
networkEffect: 'offline',
|
|
184
|
+
common: true,
|
|
185
|
+
schemaIdentifiers: Object.freeze([
|
|
186
|
+
CONTEXT_REVIEW_SCHEMA,
|
|
187
|
+
CASE_CONTAINMENT_INPUT_SCHEMA,
|
|
188
|
+
CASE_CONTAINMENT_HANDOFF_SCHEMA,
|
|
189
|
+
...CONTEXT_INPUT_SCHEMAS,
|
|
190
|
+
'whoisleuth.cli.offline-evidence-review',
|
|
191
|
+
'whoisleuth.rdap-search-input',
|
|
192
|
+
'whoisleuth.dnssec-evidence-input',
|
|
193
|
+
'whoisleuth.tlsa-evidence-input',
|
|
194
|
+
'whoisleuth.rpki-route-input',
|
|
195
|
+
'whoisleuth.local-geoip-query',
|
|
196
|
+
'whoisleuth.local-mmdb-query',
|
|
197
|
+
'whoisleuth.local-mmdb-review',
|
|
198
|
+
'whoisleuth.encrypted-dns-plan-input',
|
|
199
|
+
'whoisleuth.infrastructure-observation',
|
|
200
|
+
'whoisleuth.infrastructure-comparison.input',
|
|
201
|
+
]),
|
|
202
|
+
primaryArtefacts: Object.freeze([]),
|
|
203
|
+
planSupport: false,
|
|
204
|
+
additionalOutputFormats: Object.freeze([]),
|
|
205
|
+
bootstrapProfile: 'allowed',
|
|
206
|
+
}),
|
|
207
|
+
brief: commandSeed({
|
|
208
|
+
reference: {
|
|
209
|
+
description: 'Turn one saved Lookup into a compact decision brief with facts, unknowns, contradictions, and next actions.',
|
|
210
|
+
example: 'whoisleuth brief lookup.json --json',
|
|
211
|
+
boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
|
|
212
|
+
},
|
|
213
|
+
collection: { scope: 'Reads one bounded saved Lookup and emits a compact source-attributed decision brief.' },
|
|
214
|
+
summary: 'Build a decision brief from a saved lookup',
|
|
215
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
216
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
217
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
218
|
+
handlerOwner: 'review',
|
|
219
|
+
networkEffect: 'offline',
|
|
220
|
+
common: false,
|
|
221
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-brief']),
|
|
222
|
+
primaryArtefacts: Object.freeze([]),
|
|
223
|
+
planSupport: false,
|
|
224
|
+
additionalOutputFormats: Object.freeze([]),
|
|
225
|
+
bootstrapProfile: 'allowed',
|
|
226
|
+
}),
|
|
227
|
+
case: commandSeed({
|
|
228
|
+
reference: {
|
|
229
|
+
description: 'Show or open a local Case, append a note or evidence pin, link evidence, record an assessment or offline recheck, retain an incident-link, create an action, append an action-event, save a recheck-question, or close-object. JSON mutations use --input; notes use --text or --note-file. Link input contains fromPinId, toPinId, kind and basis. Action-event input contains id and transition. Explicit responseObjects bind an action; a typed objectOutcome requires an explicit affected subset on its event. Object closure never closes other objects or the Case. Mutations require --output and write the complete current Case export.',
|
|
230
|
+
example: 'whoisleuth case open --domain example.test --output cases.json\n whoisleuth case show cases.json\n whoisleuth case note cases.json --text "Review the retained observation" --output cases.json --force',
|
|
231
|
+
boundary: 'No database, browser launch, request or external report is created. Select --case-id when a file contains multiple Cases. Existing files require --force; --expect-file-digest sha256:<digest> additionally checks the exact file reviewed earlier. Source and output leases reject concurrent changes. Interrupted .workflow.lock files require deliberate inspection. Recheck records supplied observations; it does not collect them. Not reproduced requires an existing saved question, a complete observation and comparable conditions. Working exports include private analyst content and file references, not attached file bytes.',
|
|
232
|
+
},
|
|
233
|
+
collection: { scope: `Reads exact Case schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}. Input is bounded to ${MAX_EDITABLE_CASE_INPUT_BYTES / 1024 / 1024} MiB including formatting; the complete canonical Case store must fit ${MAX_CASE_STORE_BYTES / 1024 / 1024} MiB without pruning. Writes current schema ${CASE_SCHEMA_VERSION}.` },
|
|
234
|
+
summary: 'Review and update ordinary local Case files',
|
|
235
|
+
options: ['--case-id', '--domain', '--title', '--new-incident', '--text', '--note-file', '--input', '--expect-file-digest', '--json', '--no-color'],
|
|
236
|
+
positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_CASE_OPERATIONS), positional('source', 'file', 0, 1)]),
|
|
237
|
+
constraints: Object.freeze([constraint({ kind: 'mutually_exclusive', options: ['--text', '--note-file'] })]),
|
|
238
|
+
handlerOwner: 'review', networkEffect: 'offline', common: true,
|
|
239
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.case-export']),
|
|
240
|
+
primaryArtefacts: Object.freeze(['Case export']), planSupport: false,
|
|
241
|
+
additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
|
|
242
|
+
}),
|
|
243
|
+
'indicator-set': commandSeed({
|
|
244
|
+
reference: {
|
|
245
|
+
description: 'Create or revise a file-based indicator set, inspect its identities and review windows, or export the exact revision as STIX or MISP JSON. Revise takes a plan containing basis, name, expiresAt, reviewed rows and selectedDomains; later plans embed previous and explicitly select renewIds, withdrawIds or reintroduceDomains. Use --json with revise to retain the manifest. STIX and MISP operations always output JSON.',
|
|
246
|
+
example: 'whoisleuth indicator-set revise indicator-plan.json --json --output indicators.json\n whoisleuth indicator-set inspect indicators.json\n whoisleuth indicator-set stix indicators.json --output indicators.stix.json',
|
|
247
|
+
boundary: 'Offline files only; no submission, feed synchronisation or automatic deletion. Missing candidates are unchanged. Renewal changes the review window, not the original observation. Withdrawn identities stay withdrawn; reintroduction requires a new identity. Keep prior manifests: a digest verifies content, not authorship or the complete revision chain. MISP expiry is a review deadline, not automatic enforcement.',
|
|
248
|
+
},
|
|
249
|
+
collection: { scope: `Reads a selected ${MAX_MANAGED_INDICATOR_PLAN_BYTES / 1024 / 1024} MiB revision plan or ${MAX_MANAGED_INDICATOR_SET_BYTES / 1024 / 1024} MiB manifest. Makes no requests and does not alter the source file.` },
|
|
250
|
+
summary: 'Manage reviewed indicator revisions offline',
|
|
251
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
252
|
+
positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_INDICATOR_OPERATIONS), positional('source', 'file', 0, 1)]),
|
|
253
|
+
constraints: EMPTY_CONSTRAINTS, handlerOwner: 'review', networkEffect: 'offline', common: false,
|
|
254
|
+
schemaIdentifiers: Object.freeze([MANAGED_INDICATOR_SET_SCHEMA]), primaryArtefacts: Object.freeze(['Managed indicator revision']),
|
|
255
|
+
planSupport: false, additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
|
|
256
|
+
}),
|
|
257
|
+
"case-pack": commandSeed({
|
|
258
|
+
reference: {
|
|
259
|
+
description: `Package browser-created Case records from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} as a reviewed, audience-specific Case-pack v2 with current schema ${CASE_SCHEMA_VERSION}.`,
|
|
260
|
+
example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
|
|
261
|
+
boundary: 'The command creates a separate offline package without changing the source or durable Cases and requires an explicit review acknowledgement. Every audience, including public, retains domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review that analyst-authored content as well as the exclusions before sharing.',
|
|
262
|
+
},
|
|
263
|
+
collection: { scope: `Reads one bounded Case export from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} and writes a separate audience-specific Case-pack v2.` },
|
|
264
|
+
summary: 'Build a reviewed case package',
|
|
265
|
+
options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
|
|
266
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
267
|
+
constraints: Object.freeze([
|
|
268
|
+
constraint({ kind: 'required', options: ['--audience', '--reviewed'] }),
|
|
269
|
+
]),
|
|
270
|
+
handlerOwner: 'review',
|
|
271
|
+
networkEffect: 'offline',
|
|
272
|
+
common: true,
|
|
273
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.case-pack', 'whoisleuth.case-report']),
|
|
274
|
+
primaryArtefacts: Object.freeze(['Reviewed Case-pack v2']),
|
|
275
|
+
planSupport: false,
|
|
276
|
+
additionalOutputFormats: Object.freeze([]),
|
|
277
|
+
bootstrapProfile: 'allowed',
|
|
278
|
+
}),
|
|
279
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { runIntakeCommand } from "./intake-command.mjs";
|
|
2
|
+
import { runIndicatorSetCommand } from "./indicator-set-command.mjs";
|
|
3
|
+
import { CONTEXT_REVIEW_KINDS } from "../packages/contracts/context-review.mjs";
|
|
1
4
|
import { readBoundedRegularFile } from "../lib/bounded-file.mjs";
|
|
2
5
|
import { MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES } from "../packages/contracts/investigation-package-limits.mjs";
|
|
3
6
|
import { verifyOfflineInvestigationFolder, verifyOfflineInvestigationPackage } from "./investigation-package-review.mjs";
|
|
@@ -9,6 +12,7 @@ import { MAX_OFFLINE_ARTIFACT_BYTES, formatOfflineArtifactVerification, isComple
|
|
|
9
12
|
import { MAX_CASE_PACK_INPUT_BYTES, buildCliCasePack, formatCliCasePack } from "./case-pack.mjs";
|
|
10
13
|
import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, } from "./compare.mjs";
|
|
11
14
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
15
|
+
import { LOCAL_MMDB_REVIEW_SCHEMA, LOCAL_MMDB_REVIEW_VERSION } from "./local-mmdb-review.mjs";
|
|
12
16
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
13
17
|
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
|
|
14
18
|
import { buildCliLookupBrief, formatCliLookupBrief } from "./lookup-brief.mjs";
|
|
@@ -23,6 +27,28 @@ import { formatTerminalCompare } from "./formatters/terminal.mjs";
|
|
|
23
27
|
import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
|
|
24
28
|
import { REVIEW_INLINE_COMMANDS } from "./inline-command-families.mjs";
|
|
25
29
|
import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
|
|
30
|
+
/** Readers retain their command-specific bounds and injection points. Only
|
|
31
|
+
* usage-error presentation and empty-input handling are shared. */
|
|
32
|
+
async function readReviewInput(read, label, emptyMessage) {
|
|
33
|
+
let input;
|
|
34
|
+
try {
|
|
35
|
+
input = await read();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof CliUsageError)
|
|
39
|
+
throw error;
|
|
40
|
+
throw new CliUsageError(`Could not read ${label}: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
41
|
+
}
|
|
42
|
+
if (emptyMessage && !input.trim())
|
|
43
|
+
throw new CliUsageError(emptyMessage);
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
function writeReviewReport(context, args, report, format) {
|
|
47
|
+
if (!args.quiet)
|
|
48
|
+
context.writeStdout(args.output === 'json'
|
|
49
|
+
? formatJsonDocument(report)
|
|
50
|
+
: context.terminal(format(report), args.color));
|
|
51
|
+
}
|
|
26
52
|
async function runVerifyArtifactCommand(args, dependencies, context) {
|
|
27
53
|
context.setFailureLabel('Artefact verification');
|
|
28
54
|
if (args.folder) {
|
|
@@ -37,8 +63,7 @@ async function runVerifyArtifactCommand(args, dependencies, context) {
|
|
|
37
63
|
}
|
|
38
64
|
const report = await (args.bagit ? verifyOfflineBagIt : verifyOfflineInvestigationFolder)(files);
|
|
39
65
|
dependencies.signal?.throwIfAborted();
|
|
40
|
-
|
|
41
|
-
context.writeStdout(args.output === 'json' ? formatJsonDocument(report) : context.terminal(formatOfflineArtifactVerification(report), args.color));
|
|
66
|
+
writeReviewReport(context, args, report, formatOfflineArtifactVerification);
|
|
42
67
|
return args.strictExit && !isCompleteOfflineArtifactVerification(report) ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
43
68
|
}
|
|
44
69
|
if (args.package) {
|
|
@@ -57,23 +82,12 @@ async function runVerifyArtifactCommand(args, dependencies, context) {
|
|
|
57
82
|
const passphrase = args.passphraseSource ? await context.readPassphraseSource(args.passphraseSource) : undefined;
|
|
58
83
|
const report = args.bagit ? await verifyOfflineBagIt(bytes) : await verifyOfflineInvestigationPackage(bytes, passphrase);
|
|
59
84
|
dependencies.signal?.throwIfAborted();
|
|
60
|
-
|
|
61
|
-
context.writeStdout(args.output === 'json' ? formatJsonDocument(report) : context.terminal(formatOfflineArtifactVerification(report), args.color));
|
|
85
|
+
writeReviewReport(context, args, report, formatOfflineArtifactVerification);
|
|
62
86
|
return args.strictExit && !isCompleteOfflineArtifactVerification(report) ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
63
87
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
input
|
|
67
|
-
? await dependencies.readArtifactInput(args.source)
|
|
68
|
-
: await context.readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input');
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
if (error instanceof CliUsageError)
|
|
72
|
-
throw error;
|
|
73
|
-
throw new CliUsageError(`Could not read artifact input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
74
|
-
}
|
|
75
|
-
if (!input.trim())
|
|
76
|
-
throw new CliUsageError('verify-artifact requires one JSON file or an artefact on stdin.');
|
|
88
|
+
const input = await readReviewInput(async () => dependencies.readArtifactInput
|
|
89
|
+
? await dependencies.readArtifactInput(args.source)
|
|
90
|
+
: await context.readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input'), 'artifact input', 'verify-artifact requires one JSON file or an artefact on stdin.');
|
|
77
91
|
const passphrase = args.passphraseSource ? await context.readPassphraseSource(args.passphraseSource) : null;
|
|
78
92
|
let manifest = null;
|
|
79
93
|
if (args.manifestSource && args.manifestEntryId) {
|
|
@@ -93,90 +107,44 @@ async function runVerifyArtifactCommand(args, dependencies, context) {
|
|
|
93
107
|
manifest = Object.freeze({ raw, entryId: args.manifestEntryId });
|
|
94
108
|
}
|
|
95
109
|
const report = await verifyOfflineArtifact(input, { passphrase, manifest });
|
|
96
|
-
|
|
97
|
-
context.writeStdout(args.output === 'json'
|
|
98
|
-
? formatJsonDocument(report)
|
|
99
|
-
: context.terminal(formatOfflineArtifactVerification(report), args.color));
|
|
100
|
-
}
|
|
110
|
+
writeReviewReport(context, args, report, formatOfflineArtifactVerification);
|
|
101
111
|
return args.strictExit && !isCompleteOfflineArtifactVerification(report)
|
|
102
112
|
? EXIT_CODES.PARTIAL_FAILURE
|
|
103
113
|
: EXIT_CODES.SUCCESS;
|
|
104
114
|
}
|
|
105
115
|
async function runInterchangeReportCommand(args, dependencies, context) {
|
|
106
116
|
context.setFailureLabel('Interchange fidelity report');
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
input
|
|
110
|
-
? await dependencies.readArtifactInput(args.source)
|
|
111
|
-
: await context.readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Interchange input');
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
if (error instanceof CliUsageError)
|
|
115
|
-
throw error;
|
|
116
|
-
throw new CliUsageError(`Could not read interchange input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
117
|
-
}
|
|
118
|
-
if (!input.trim())
|
|
119
|
-
throw new CliUsageError('interchange-report requires one JSON file or an artefact on stdin.');
|
|
117
|
+
const input = await readReviewInput(async () => dependencies.readArtifactInput
|
|
118
|
+
? await dependencies.readArtifactInput(args.source)
|
|
119
|
+
: await context.readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Interchange input'), 'interchange input', 'interchange-report requires one JSON file or an artefact on stdin.');
|
|
120
120
|
const passphrase = args.passphraseSource ? await context.readPassphraseSource(args.passphraseSource) : null;
|
|
121
121
|
const report = await buildInterchangeFidelityReport(input, {
|
|
122
122
|
generatedAt: context.now(),
|
|
123
123
|
passphrase,
|
|
124
124
|
});
|
|
125
|
-
|
|
126
|
-
context.writeStdout(args.output === 'json'
|
|
127
|
-
? formatJsonDocument(report)
|
|
128
|
-
: context.terminal(formatInterchangeFidelityReport(report), args.color));
|
|
129
|
-
}
|
|
125
|
+
writeReviewReport(context, args, report, formatInterchangeFidelityReport);
|
|
130
126
|
return EXIT_CODES.SUCCESS;
|
|
131
127
|
}
|
|
132
128
|
async function runSourceReportCommand(args, dependencies, context) {
|
|
133
129
|
context.setFailureLabel('Source reliability report');
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
input
|
|
137
|
-
? await dependencies.readSourceReliabilityInput(args.source)
|
|
138
|
-
: await context.readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input');
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
if (error instanceof CliUsageError)
|
|
142
|
-
throw error;
|
|
143
|
-
throw new CliUsageError(`Could not read source reliability input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
144
|
-
}
|
|
145
|
-
if (!input.trim())
|
|
146
|
-
throw new CliUsageError('source-report requires one JSON file or lookup documents on stdin.');
|
|
130
|
+
const input = await readReviewInput(async () => dependencies.readSourceReliabilityInput
|
|
131
|
+
? await dependencies.readSourceReliabilityInput(args.source)
|
|
132
|
+
: await context.readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input'), 'source reliability input', 'source-report requires one JSON file or lookup documents on stdin.');
|
|
147
133
|
const report = buildSourceReliabilityReport(input, context.now());
|
|
148
|
-
|
|
149
|
-
context.writeStdout(args.output === 'json'
|
|
150
|
-
? formatJsonDocument(report)
|
|
151
|
-
: context.terminal(formatSourceReliabilityReport(report), args.color));
|
|
152
|
-
}
|
|
134
|
+
writeReviewReport(context, args, report, formatSourceReliabilityReport);
|
|
153
135
|
return EXIT_CODES.SUCCESS;
|
|
154
136
|
}
|
|
155
137
|
async function runCompareCommand(args, dependencies, context) {
|
|
156
138
|
context.setFailureLabel('Registry comparison');
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
input
|
|
160
|
-
? await dependencies.readCompareInput(args.source)
|
|
161
|
-
: await context.readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input');
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
if (error instanceof CliUsageError)
|
|
165
|
-
throw error;
|
|
166
|
-
throw new CliUsageError(`Could not read comparison input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
167
|
-
}
|
|
168
|
-
if (!input.trim())
|
|
169
|
-
throw new CliUsageError('compare requires one lookup JSON file or a lookup document on stdin.');
|
|
139
|
+
const input = await readReviewInput(async () => dependencies.readCompareInput
|
|
140
|
+
? await dependencies.readCompareInput(args.source)
|
|
141
|
+
: await context.readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input'), 'comparison input', 'compare requires one lookup JSON file or a lookup document on stdin.');
|
|
170
142
|
const parsed = parseCliLookupDocument(input);
|
|
171
143
|
const loadComparison = dependencies.loadRegistryComparison || (() => import("../lib/registry-comparison.mjs"));
|
|
172
144
|
const comparisonModule = await loadComparison();
|
|
173
145
|
const result = compareLookupDocument(parsed, comparisonModule.compareRegistrySources, comparisonModule.compareRdapPublications);
|
|
174
146
|
const document = buildCliCompareDocument(result, context.now());
|
|
175
|
-
|
|
176
|
-
context.writeStdout(args.output === 'json'
|
|
177
|
-
? formatJsonDocument(document)
|
|
178
|
-
: context.terminal(formatTerminalCompare(document), args.color));
|
|
179
|
-
}
|
|
147
|
+
writeReviewReport(context, args, document, formatTerminalCompare);
|
|
180
148
|
return EXIT_CODES.SUCCESS;
|
|
181
149
|
}
|
|
182
150
|
async function runPageCompareCommand(args, dependencies, context) {
|
|
@@ -197,80 +165,37 @@ async function runPageCompareCommand(args, dependencies, context) {
|
|
|
197
165
|
throw new CliUsageError(`Could not read page comparison input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
198
166
|
}
|
|
199
167
|
const document = buildCliPageComparison(leftInput, rightInput, context.now());
|
|
200
|
-
|
|
201
|
-
context.writeStdout(args.output === 'json'
|
|
202
|
-
? formatJsonDocument(document)
|
|
203
|
-
: context.terminal(formatCliPageComparison(document), args.color));
|
|
204
|
-
}
|
|
168
|
+
writeReviewReport(context, args, document, formatCliPageComparison);
|
|
205
169
|
return EXIT_CODES.SUCCESS;
|
|
206
170
|
}
|
|
207
171
|
async function runMailReviewCommand(args, dependencies, context) {
|
|
208
172
|
context.setFailureLabel('Passive mail review');
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
input
|
|
212
|
-
? await dependencies.readMailReviewInput(args.source)
|
|
213
|
-
: await context.readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input');
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
if (error instanceof CliUsageError)
|
|
217
|
-
throw error;
|
|
218
|
-
throw new CliUsageError(`Could not read mail review input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
219
|
-
}
|
|
173
|
+
const input = await readReviewInput(async () => dependencies.readMailReviewInput
|
|
174
|
+
? await dependencies.readMailReviewInput(args.source)
|
|
175
|
+
: await context.readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input'), 'mail review input');
|
|
220
176
|
const document = buildCliMailReview(input, context.now());
|
|
221
|
-
|
|
222
|
-
context.writeStdout(args.output === 'json'
|
|
223
|
-
? formatJsonDocument(document)
|
|
224
|
-
: context.terminal(formatCliMailReview(document), args.color));
|
|
225
|
-
}
|
|
177
|
+
writeReviewReport(context, args, document, formatCliMailReview);
|
|
226
178
|
return EXIT_CODES.SUCCESS;
|
|
227
179
|
}
|
|
228
180
|
async function runMailHeadersCommand(args, dependencies, context) {
|
|
229
181
|
context.setFailureLabel('Mail-header review');
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
input
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
if (error instanceof CliUsageError)
|
|
238
|
-
throw error;
|
|
239
|
-
throw new CliUsageError(`Could not read mail-header input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
240
|
-
}
|
|
241
|
-
if (!input.trim())
|
|
242
|
-
throw new CliUsageError('mail-headers requires one message or header file, or headers on stdin.');
|
|
243
|
-
const document = buildCliMailHeaderReview(input, context.now());
|
|
244
|
-
if (!args.quiet) {
|
|
245
|
-
context.writeStdout(args.output === 'json'
|
|
246
|
-
? formatJsonDocument(document)
|
|
247
|
-
: context.terminal(formatCliMailHeaderReview(document), args.color));
|
|
248
|
-
}
|
|
182
|
+
const input = await readReviewInput(async () => dependencies.readMailHeaderInput
|
|
183
|
+
? await dependencies.readMailHeaderInput(args.source)
|
|
184
|
+
: await context.readHeaderInput(args.source, MAX_MAIL_HEADER_INPUT_BYTES, 'Mail-header input'), 'mail-header input', 'mail-headers requires one message or header file, or headers on stdin.');
|
|
185
|
+
const document = buildCliMailHeaderReview(input, context.now(), args.trustedAuthHeaders);
|
|
186
|
+
writeReviewReport(context, args, document, formatCliMailHeaderReview);
|
|
249
187
|
return EXIT_CODES.SUCCESS;
|
|
250
188
|
}
|
|
251
189
|
async function runOfflineEvidenceReviewCommand(args, dependencies, context) {
|
|
252
190
|
context.setFailureLabel('Offline evidence review');
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
input
|
|
256
|
-
? await dependencies.readArtifactInput(args.source)
|
|
257
|
-
: await context.readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input');
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
if (error instanceof CliUsageError)
|
|
261
|
-
throw error;
|
|
262
|
-
throw new CliUsageError(`Could not read offline evidence input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
263
|
-
}
|
|
264
|
-
if (!input.trim())
|
|
265
|
-
throw new CliUsageError('review-evidence requires one JSON file or a document on stdin.');
|
|
191
|
+
const input = await readReviewInput(async () => dependencies.readArtifactInput
|
|
192
|
+
? await dependencies.readArtifactInput(args.source)
|
|
193
|
+
: await context.readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input'), 'offline evidence input', 'review-evidence requires one JSON file or a document on stdin.');
|
|
266
194
|
const document = args.mmdbSource
|
|
267
|
-
? await buildOfflineEvidenceReviewWithLocalResources(input, context.now(), { mmdbPath: args.mmdbSource })
|
|
195
|
+
? await buildOfflineEvidenceReviewWithLocalResources(input, context.now(), { mmdbPath: args.mmdbSource, ...(dependencies.signal ? { signal: dependencies.signal } : {}) })
|
|
268
196
|
: buildOfflineEvidenceReview(input, context.now());
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
? formatJsonDocument(document)
|
|
272
|
-
: context.terminal(formatOfflineEvidenceReview(document), args.color));
|
|
273
|
-
}
|
|
197
|
+
dependencies.signal?.throwIfAborted();
|
|
198
|
+
writeReviewReport(context, args, document, formatOfflineEvidenceReview);
|
|
274
199
|
if (args.strictExit) {
|
|
275
200
|
const result = document.result && typeof document.result === 'object' && !Array.isArray(document.result)
|
|
276
201
|
? document.result
|
|
@@ -281,7 +206,11 @@ async function runOfflineEvidenceReviewCommand(args, dependencies, context) {
|
|
|
281
206
|
const zoneMismatch = document.kind === 'zone_intent' && (result.complete !== true
|
|
282
207
|
|| (result.counts && typeof result.counts === 'object' && !Array.isArray(result.counts)
|
|
283
208
|
&& ['different', 'missing', 'unexpected', 'incomplete'].some((key) => Number(result.counts[key]) > 0)));
|
|
284
|
-
|
|
209
|
+
const contextPartial = (CONTEXT_REVIEW_KINDS.includes(document.kind) || document.kind === 'internal_containment') && result.state === 'partial';
|
|
210
|
+
const mmdbIncomplete = result.schema === LOCAL_MMDB_REVIEW_SCHEMA && result.version === LOCAL_MMDB_REVIEW_VERSION && result.completeness !== 'complete';
|
|
211
|
+
const infrastructureIncomplete = document.kind === 'infrastructure' ? result.state !== 'complete'
|
|
212
|
+
: document.kind === 'infrastructure_comparison' && result.state !== 'compared';
|
|
213
|
+
if (gate?.pass === false || zoneMismatch || contextPartial || mmdbIncomplete || infrastructureIncomplete)
|
|
285
214
|
return EXIT_CODES.PARTIAL_FAILURE;
|
|
286
215
|
}
|
|
287
216
|
return EXIT_CODES.SUCCESS;
|
|
@@ -289,34 +218,16 @@ async function runOfflineEvidenceReviewCommand(args, dependencies, context) {
|
|
|
289
218
|
async function runBriefOrCasePackCommand(args, dependencies, context) {
|
|
290
219
|
const isBrief = args.action === 'brief';
|
|
291
220
|
context.setFailureLabel(isBrief ? 'Lookup brief' : 'Case pack');
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
input
|
|
295
|
-
? await dependencies.readArtifactInput(args.source)
|
|
296
|
-
: await context.readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : MAX_CASE_PACK_INPUT_BYTES, isBrief ? 'Lookup brief input' : 'Case-pack input');
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
if (error instanceof CliUsageError)
|
|
300
|
-
throw error;
|
|
301
|
-
throw new CliUsageError(`Could not read ${isBrief ? 'Lookup brief' : 'case-pack'} input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
302
|
-
}
|
|
303
|
-
if (!input.trim())
|
|
304
|
-
throw new CliUsageError(`${args.action} requires one JSON file or a document on stdin.`);
|
|
221
|
+
const input = await readReviewInput(async () => dependencies.readArtifactInput
|
|
222
|
+
? await dependencies.readArtifactInput(args.source)
|
|
223
|
+
: await context.readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : MAX_CASE_PACK_INPUT_BYTES, isBrief ? 'Lookup brief input' : 'Case-pack input'), `${isBrief ? 'Lookup brief' : 'case-pack'} input`, `${args.action} requires one JSON file or a document on stdin.`);
|
|
305
224
|
if (args.action === 'brief') {
|
|
306
225
|
const document = buildCliLookupBrief(input, context.now());
|
|
307
|
-
|
|
308
|
-
context.writeStdout(args.output === 'json'
|
|
309
|
-
? formatJsonDocument(document)
|
|
310
|
-
: context.terminal(formatCliLookupBrief(document), args.color));
|
|
311
|
-
}
|
|
226
|
+
writeReviewReport(context, args, document, formatCliLookupBrief);
|
|
312
227
|
}
|
|
313
228
|
else {
|
|
314
229
|
const document = buildCliCasePack(input, { audience: args.audience, reviewed: args.reviewed }, context.now());
|
|
315
|
-
|
|
316
|
-
context.writeStdout(args.output === 'json'
|
|
317
|
-
? formatJsonDocument(document)
|
|
318
|
-
: context.terminal(formatCliCasePack(document), args.color));
|
|
319
|
-
}
|
|
230
|
+
writeReviewReport(context, args, document, formatCliCasePack);
|
|
320
231
|
}
|
|
321
232
|
return EXIT_CODES.SUCCESS;
|
|
322
233
|
}
|
|
@@ -328,10 +239,12 @@ const REVIEW_COMMAND_HANDLERS = Object.freeze({
|
|
|
328
239
|
'page-compare': runPageCompareCommand,
|
|
329
240
|
'mail-review': runMailReviewCommand,
|
|
330
241
|
'mail-headers': runMailHeadersCommand,
|
|
242
|
+
intake: runIntakeCommand,
|
|
331
243
|
'review-evidence': runOfflineEvidenceReviewCommand,
|
|
332
244
|
'brief': runBriefOrCasePackCommand,
|
|
333
245
|
'case': runCaseCommand,
|
|
334
246
|
'case-pack': runBriefOrCasePackCommand,
|
|
247
|
+
'indicator-set': runIndicatorSetCommand,
|
|
335
248
|
});
|
|
336
249
|
function runReviewCommand(args, dependencies, context) {
|
|
337
250
|
return runDiscriminatedCommandHandler(REVIEW_COMMAND_HANDLERS, args, dependencies, context);
|