@slicedearth/whoisleuth-cli 1.34.0 → 1.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +19 -2
  2. package/bin/whoisleuth.mjs +41 -24
  3. package/cli/arguments.mjs +326 -3
  4. package/cli/artifact-verify.mjs +60 -14
  5. package/cli/bulk.mjs +1 -2
  6. package/cli/compare.mjs +1 -1
  7. package/cli/completion.mjs +36 -2
  8. package/cli/doctor.mjs +11 -5
  9. package/cli/evidence-command-arguments.mjs +1 -1
  10. package/cli/evidence-command-runner.mjs +2 -2
  11. package/cli/evidence-signing.mjs +4 -4
  12. package/cli/export-evidence.mjs +9 -2
  13. package/cli/formatters/html.mjs +1 -1
  14. package/cli/formatters/json.mjs +13 -1
  15. package/cli/formatters/terminal.mjs +14 -4
  16. package/cli/investigation-plan.mjs +112 -0
  17. package/cli/local-mmdb-review.mjs +76 -0
  18. package/cli/lookalike-calibration.mjs +121 -0
  19. package/cli/lookup-command-runner.mjs +4 -1
  20. package/cli/lookup-plan.mjs +3 -3
  21. package/cli/lookup-reconcile.mjs +141 -0
  22. package/cli/lookup-timeline.mjs +1 -1
  23. package/cli/mail-review.mjs +48 -3
  24. package/cli/offline-evidence-review.mjs +159 -0
  25. package/cli/registry-doctor.mjs +125 -0
  26. package/cli/registry-support.mjs +1 -4
  27. package/cli/risk-calibration.mjs +117 -9
  28. package/cli/runner.mjs +342 -15
  29. package/cli/saved-lookup.mjs +2 -2
  30. package/cli/sharing-review.mjs +202 -0
  31. package/cli/source-reliability.mjs +1 -1
  32. package/docs/cli-reference.md +1110 -0
  33. package/docs/cli.md +224 -847
  34. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  35. package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
  36. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  37. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  38. package/frontend/src/lib/analysis/bulk-retry-plan.js +3 -10
  39. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  40. package/frontend/src/lib/analysis/bulk-source-coverage.js +76 -0
  41. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  42. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  43. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  44. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  45. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  46. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  47. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  48. package/frontend/src/lib/analysis/http-summary.js +2 -1
  49. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  50. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  51. package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
  52. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  53. package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
  54. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  55. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  56. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  57. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  58. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  59. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  60. package/frontend/src/lib/analysis/scoring.js +8 -88
  61. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  62. package/frontend/src/lib/analysis/utils.js +7 -40
  63. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  64. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  65. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  66. package/frontend/src/lib/analysis/visualization-models.js +370 -0
  67. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  68. package/frontend/src/lib/analysis/website-snapshot-model.js +57 -3
  69. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  70. package/frontend/src/lib/analysis/workspace-archive.js +2 -2
  71. package/lib/analyst-taxonomy.mjs +40 -0
  72. package/lib/availability.mjs +30 -10
  73. package/lib/browser-library-profile.mjs +15 -2
  74. package/lib/bulk-limits.mjs +13 -0
  75. package/lib/certificate-extension-profile.mjs +71 -0
  76. package/lib/credential-surface-profile.mjs +1 -1
  77. package/lib/ct-response-bounds.mjs +9 -0
  78. package/lib/ct-search.mjs +66 -12
  79. package/lib/dns-delegation-health.mjs +56 -5
  80. package/lib/dnssec-evidence-validation.mjs +235 -0
  81. package/lib/domain-assurance.mjs +368 -0
  82. package/lib/domain-control-manifest.mjs +382 -0
  83. package/lib/domain-posture.mjs +11 -10
  84. package/lib/encrypted-dns-contract.mjs +81 -0
  85. package/lib/evidence-export.mjs +17 -3
  86. package/lib/evidence-report-markdown.mjs +1 -1
  87. package/lib/evidence-report.mjs +4 -1
  88. package/lib/favicon.mjs +3 -8
  89. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  90. package/lib/html-signals.mjs +4 -0
  91. package/lib/http-evidence-bounds.mjs +4 -0
  92. package/lib/http-intelligence.mjs +2 -1
  93. package/lib/ip-prefix.mjs +87 -0
  94. package/lib/local-geoip-evidence.mjs +89 -0
  95. package/lib/lookup-response-contract.mjs +346 -0
  96. package/lib/observed-network-context.mjs +1 -1
  97. package/lib/opportunity-scoring.mjs +107 -0
  98. package/lib/outbound-identity.mjs +9 -0
  99. package/lib/outbound-request-bounds.mjs +9 -0
  100. package/lib/page-fingerprints.mjs +1 -1
  101. package/lib/perceptual-hash-comparison.mjs +34 -0
  102. package/lib/perceptual-hash.mjs +3 -31
  103. package/lib/project-metadata.mjs +8 -0
  104. package/lib/provider-policy-admission.mjs +49 -0
  105. package/lib/rdap-capabilities.mjs +6 -6
  106. package/lib/rdap-search-workbench.mjs +247 -0
  107. package/lib/rdap.mjs +7 -2
  108. package/lib/registry-access.mjs +1 -1
  109. package/lib/registry-capabilities.mjs +31 -1
  110. package/lib/registry-capability-catalogue.mjs +15 -14
  111. package/lib/registry-insights.mjs +6 -6
  112. package/lib/response-policy.mjs +81 -6
  113. package/lib/risk-scoring.mjs +206 -83
  114. package/lib/rpki-evidence.mjs +105 -0
  115. package/lib/safe-fetch.mjs +2 -1
  116. package/lib/scoring-evidence-quality.mjs +107 -0
  117. package/lib/security-txt.mjs +3 -3
  118. package/lib/semantic-version.mjs +54 -0
  119. package/lib/static-html-analysis.mjs +27 -0
  120. package/lib/threat-intelligence-manifests.mjs +1 -1
  121. package/lib/threatfox-intelligence.mjs +11 -6
  122. package/lib/tls-intelligence.mjs +13 -3
  123. package/lib/tlsa-evidence.mjs +194 -0
  124. package/lib/urlhaus-intelligence.mjs +11 -6
  125. package/lib/urlscan-intelligence.mjs +10 -5
  126. package/lib/website-security-posture.mjs +16 -9
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-chain.mjs +14 -152
  129. package/lib/whois.mjs +2 -2
  130. package/package.json +6 -2
  131. package/third-party-notices.txt +911 -19
package/README.md CHANGED
@@ -50,6 +50,7 @@ node bin/whoisleuth.mts --help
50
50
  node bin/whoisleuth.mts lookup example.test
51
51
  node bin/whoisleuth.mts lookup example.test --deep --summary
52
52
  node bin/whoisleuth.mts lookup example.test --deep --plan --json
53
+ node bin/whoisleuth.mts lookup example.test --deep --observer workstation-a --vantage office-egress --json
53
54
  node bin/whoisleuth.mts lookup example.test --deep --markdown --output lookup.md
54
55
  node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json
55
56
  node bin/whoisleuth.mts bulk domains.txt --csv --registered-only
@@ -58,9 +59,16 @@ node bin/whoisleuth.mts discover example.test --dictionary private-terms.txt --s
58
59
  node bin/whoisleuth.mts discover-scan example.test --scan-limit 50 --checkpoint candidate-scan.json --json
59
60
  node bin/whoisleuth.mts page-compare official.json candidate.json --json
60
61
  node bin/whoisleuth.mts mail-review bulk.json --json
62
+ node bin/whoisleuth.mts domain-control domain-control-input.json --json
63
+ node bin/whoisleuth.mts assurance domain-assurance-input.json --json
64
+ node bin/whoisleuth.mts sharing-review response-packet.json --marking amber --recipient-scope organization --purpose 'Reviewed handoff' --human-reviewed --personal-data-reviewed --redactions-confirmed --json
65
+ node bin/whoisleuth.mts workflow-plan domain-triage example.test --json
61
66
  node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
67
+ node bin/whoisleuth.mts reconcile office.json mobile.json external.json --json
62
68
  node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
63
69
  node bin/whoisleuth.mts registry-support example.test --json
70
+ node bin/whoisleuth.mts registry-doctor lookup.json --json
71
+ node bin/whoisleuth.mts lookalike-calibrate reviewed-candidates.json --json
64
72
  node bin/whoisleuth.mts doctor
65
73
  node bin/whoisleuth.mts commands --json
66
74
  node bin/whoisleuth.mts completion zsh
@@ -95,10 +103,18 @@ cleanup before exiting immediately.
95
103
 
96
104
  The offline `timeline` command orders 2 to 20 saved Lookup documents for one
97
105
  domain and compares each adjacent observation. It retains observation times and
98
- normalized field states but no input filenames or raw registry payloads.
106
+ normalised field states but no input filenames or raw registry payloads.
99
107
  Observed differences remain separate from missing, unavailable, and changed
100
108
  collection conditions.
101
109
 
110
+ Optional bounded observer and vantage labels let analysts distinguish saved
111
+ collection contexts without changing collection. `reconcile` compares 2 to 5
112
+ same-domain observations while keeping agreement, disagreement, and
113
+ non-comparable evidence separate; the labels are context, not proof of
114
+ independence. `registry-doctor`, `assurance`, `sharing-review`, and
115
+ `lookalike-calibrate` add offline compatibility, change/recovery, sharing, and
116
+ review-yield workflows without contacting the hosted application.
117
+
102
118
  Bulk also supports fixed-column CSV, domain-only or exact-query output,
103
119
  registered, inconclusive, and hard-failure output filters, and bounded A, AAAA,
104
120
  NS, and MX summaries. Discover
@@ -113,7 +129,8 @@ lookups can be compared across static page, parse5-tokenized DOM structure,
113
129
  favicon, technology, and TLS evidence, while saved Bulk output can be reviewed
114
130
  for passive MX, null MX, SPF, DMARC, and shared-provider context.
115
131
 
116
- See `docs/cli.md` in the package for the complete command, privacy, output, and
132
+ See `docs/cli.md` in the package for installation and first use, and
133
+ `docs/cli-reference.md` for the complete command, privacy, output, and
117
134
  evidence-contract reference. Report suspected vulnerabilities through the
118
135
  private channel described in `SECURITY.md`.
119
136
 
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { boundedCliErrorMessage } from "../cli/errors.mjs";
3
- import { cleanupPendingOutputFilesSync } from "../cli/output-file.mjs";
4
- import { runCli } from "../cli/runner.mjs";
2
+ import { createRequire } from 'node:module';
5
3
  for (const stream of [process.stdout, process.stderr]) {
6
4
  stream.on('error', (error) => {
7
5
  if (error.code === 'EPIPE')
@@ -9,27 +7,46 @@ for (const stream of [process.stdout, process.stderr]) {
9
7
  throw error;
10
8
  });
11
9
  }
12
- const cancellation = new AbortController();
13
- let interruptionCount = 0;
14
- const interrupt = () => {
15
- interruptionCount += 1;
16
- if (interruptionCount === 1)
17
- cancellation.abort(new DOMException('Aborted', 'AbortError'));
10
+ const argv = process.argv.slice(2);
11
+ if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) {
12
+ const require = createRequire(import.meta.url);
13
+ const metadata = require('../package.json');
14
+ if (typeof metadata.version !== 'string' || !metadata.version) {
15
+ process.stderr.write('Internal CLI error: Package version is unavailable.\n');
16
+ process.exitCode = 70;
17
+ }
18
18
  else {
19
- cleanupPendingOutputFilesSync();
20
- process.exit(130);
19
+ process.stdout.write(`${metadata.version}\n`);
21
20
  }
22
- };
23
- process.on('SIGINT', interrupt);
24
- runCli(process.argv.slice(2), { signal: cancellation.signal }).then((code) => {
25
- if (code === 130) {
21
+ }
22
+ else {
23
+ const [errorsModule, outputModule, runnerModule] = await Promise.all([
24
+ import("../cli/errors.mjs"),
25
+ import("../cli/output-file.mjs"),
26
+ import("../cli/runner.mjs"),
27
+ ]);
28
+ const cancellation = new AbortController();
29
+ let interruptionCount = 0;
30
+ const interrupt = () => {
31
+ interruptionCount += 1;
32
+ if (interruptionCount === 1)
33
+ cancellation.abort(new DOMException('Aborted', 'AbortError'));
34
+ else {
35
+ outputModule.cleanupPendingOutputFilesSync();
36
+ process.exit(130);
37
+ }
38
+ };
39
+ process.on('SIGINT', interrupt);
40
+ runnerModule.runCli(argv, { signal: cancellation.signal }).then((code) => {
41
+ if (code === 130) {
42
+ process.removeListener('SIGINT', interrupt);
43
+ process.exit(130);
44
+ }
45
+ process.exitCode = code;
46
+ }).catch((error) => {
47
+ process.stderr.write(`Internal CLI error: ${errorsModule.boundedCliErrorMessage(error)}\n`);
48
+ process.exitCode = 70;
49
+ }).finally(() => {
26
50
  process.removeListener('SIGINT', interrupt);
27
- process.exit(130);
28
- }
29
- process.exitCode = code;
30
- }).catch((error) => {
31
- process.stderr.write(`Internal CLI error: ${boundedCliErrorMessage(error)}\n`);
32
- process.exitCode = 70;
33
- }).finally(() => {
34
- process.removeListener('SIGINT', interrupt);
35
- });
51
+ });
52
+ }
package/cli/arguments.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { parseInspectArchiveArguments, parseSignArtifactArguments, parseVerifySignatureArguments, } from "./evidence-command-arguments.mjs";
2
2
  import { CliUsageError } from "./errors.mjs";
3
+ import { INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
3
4
  const MAX_CLI_ARGUMENTS = 32;
4
5
  const MAX_CLI_ARGUMENT_LENGTH = 1024;
5
6
  const CLI_COMMANDS = [
@@ -16,7 +17,9 @@ const CLI_COMMANDS = [
16
17
  'http',
17
18
  'tls',
18
19
  'registry-support',
20
+ 'registry-doctor',
19
21
  'risk-calibrate',
22
+ 'lookalike-calibrate',
20
23
  'verify-artifact',
21
24
  'inspect-archive',
22
25
  'sign-artifact',
@@ -25,7 +28,13 @@ const CLI_COMMANDS = [
25
28
  'compare',
26
29
  'page-compare',
27
30
  'mail-review',
31
+ 'review-evidence',
32
+ 'domain-control',
33
+ 'assurance',
34
+ 'sharing-review',
35
+ 'workflow-plan',
28
36
  'diff',
37
+ 'reconcile',
29
38
  'timeline',
30
39
  'export',
31
40
  ];
@@ -128,8 +137,12 @@ function parseCliArgumentsCore(argv) {
128
137
  return parseTlsArguments(argv.slice(1));
129
138
  if (command === 'registry-support')
130
139
  return parseRegistrySupportArguments(argv.slice(1));
140
+ if (command === 'registry-doctor')
141
+ return parseRegistryDoctorArguments(argv.slice(1));
131
142
  if (command === 'risk-calibrate')
132
143
  return parseRiskCalibrateArguments(argv.slice(1));
144
+ if (command === 'lookalike-calibrate')
145
+ return parseLookalikeCalibrateArguments(argv.slice(1));
133
146
  if (command === 'verify-artifact')
134
147
  return parseVerifyArtifactArguments(argv.slice(1));
135
148
  if (command === 'inspect-archive')
@@ -146,8 +159,20 @@ function parseCliArgumentsCore(argv) {
146
159
  return parsePageCompareArguments(argv.slice(1));
147
160
  if (command === 'mail-review')
148
161
  return parseMailReviewArguments(argv.slice(1));
162
+ if (command === 'review-evidence')
163
+ return parseReviewEvidenceArguments(argv.slice(1));
164
+ if (command === 'domain-control')
165
+ return parseDomainControlArguments(argv.slice(1));
166
+ if (command === 'assurance')
167
+ return parseAssuranceArguments(argv.slice(1));
168
+ if (command === 'sharing-review')
169
+ return parseSharingReviewArguments(argv.slice(1));
170
+ if (command === 'workflow-plan')
171
+ return parseWorkflowPlanArguments(argv.slice(1));
149
172
  if (command === 'diff')
150
173
  return parseDiffArguments(argv.slice(1));
174
+ if (command === 'reconcile')
175
+ return parseReconcileArguments(argv.slice(1));
151
176
  if (command === 'timeline')
152
177
  return parseTimelineArguments(argv.slice(1));
153
178
  if (command === 'export')
@@ -163,7 +188,13 @@ function parseCliArgumentsCore(argv) {
163
188
  let strictExit = false;
164
189
  let events = false;
165
190
  let plan = false;
166
- for (const argument of argv.slice(1)) {
191
+ let observerLabel = null;
192
+ let vantageLabel = null;
193
+ const lookupArguments = argv.slice(1);
194
+ for (let index = 0; index < lookupArguments.length; index++) {
195
+ const argument = lookupArguments[index];
196
+ if (argument === undefined)
197
+ break;
167
198
  if (argument === '--json') {
168
199
  if (output !== 'terminal')
169
200
  throw new CliUsageError('Choose only one output format.');
@@ -207,6 +238,22 @@ function parseCliArgumentsCore(argv) {
207
238
  throw new CliUsageError('--plan may be supplied only once.');
208
239
  plan = true;
209
240
  }
241
+ else if (argument === '--observer' || argument === '--vantage') {
242
+ const isObserver = argument === '--observer';
243
+ if (isObserver ? observerLabel !== null : vantageLabel !== null) {
244
+ throw new CliUsageError(`${argument} may be supplied only once.`);
245
+ }
246
+ const value = lookupArguments[++index];
247
+ if (!value || value.startsWith('-'))
248
+ throw new CliUsageError(`${argument} requires one bounded label.`);
249
+ const normalized = value.replace(/\s+/gu, ' ').trim();
250
+ if (!normalized || normalized.length > 80)
251
+ throw new CliUsageError(`${argument} requires a label of at most 80 characters.`);
252
+ if (isObserver)
253
+ observerLabel = normalized;
254
+ else
255
+ vantageLabel = normalized;
256
+ }
210
257
  else if (argument === '--quiet')
211
258
  quiet = true;
212
259
  else if (argument === '--no-color')
@@ -227,7 +274,7 @@ function parseCliArgumentsCore(argv) {
227
274
  if (plan && (detailSet || strictExit || events || quiet)) {
228
275
  throw new CliUsageError('--plan cannot be combined with detail, strict-exit, event, or quiet options.');
229
276
  }
230
- return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, quiet, color };
277
+ return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, observerLabel, vantageLabel, quiet, color };
231
278
  }
232
279
  function parseCompletionArguments(argv) {
233
280
  if (argv.length !== 1 || !['bash', 'zsh', 'fish', 'powershell'].includes(argv[0] || '')) {
@@ -893,6 +940,200 @@ function parseMailReviewArguments(argv) {
893
940
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
894
941
  return { action: 'mail-review', source, output, quiet, color };
895
942
  }
943
+ function parseReviewEvidenceArguments(argv) {
944
+ let source = null;
945
+ let mmdbSource = null;
946
+ let output = 'terminal';
947
+ let quiet = false;
948
+ let color = true;
949
+ for (let index = 0; index < argv.length; index += 1) {
950
+ const argument = argv[index];
951
+ if (argument === undefined)
952
+ break;
953
+ if (argument === '--json') {
954
+ if (output !== 'terminal')
955
+ throw new CliUsageError('--json may be supplied only once.');
956
+ output = 'json';
957
+ }
958
+ else if (argument === '--mmdb') {
959
+ const value = argv[++index];
960
+ if (!value || value.startsWith('-') || mmdbSource !== null)
961
+ throw new CliUsageError('--mmdb requires one database file and may be supplied only once.');
962
+ mmdbSource = value;
963
+ }
964
+ else if (argument === '--quiet')
965
+ quiet = true;
966
+ else if (argument === '--no-color')
967
+ color = false;
968
+ else if (argument.startsWith('-'))
969
+ throw new CliUsageError(`Unknown option "${argument}".`);
970
+ else if (source === null)
971
+ source = argument;
972
+ else
973
+ throw new CliUsageError('review-evidence accepts one optional versioned JSON input file.');
974
+ }
975
+ if (quiet && output !== 'terminal')
976
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
977
+ return { action: 'review-evidence', source, mmdbSource, output, quiet, color };
978
+ }
979
+ function parseDomainControlArguments(argv) {
980
+ let source = null;
981
+ let output = 'terminal';
982
+ let quiet = false;
983
+ let color = true;
984
+ for (const argument of argv) {
985
+ if (argument === '--json') {
986
+ if (output !== 'terminal')
987
+ throw new CliUsageError('--json may be supplied only once.');
988
+ output = 'json';
989
+ }
990
+ else if (argument === '--quiet')
991
+ quiet = true;
992
+ else if (argument === '--no-color')
993
+ color = false;
994
+ else if (argument.startsWith('-'))
995
+ throw new CliUsageError(`Unknown option "${argument}".`);
996
+ else if (source === null)
997
+ source = argument;
998
+ else
999
+ throw new CliUsageError('domain-control accepts one optional versioned JSON input file.');
1000
+ }
1001
+ if (quiet && output !== 'terminal')
1002
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1003
+ return { action: 'domain-control', source, output, quiet, color };
1004
+ }
1005
+ function parseAssuranceArguments(argv) {
1006
+ let source = null;
1007
+ let output = 'terminal';
1008
+ let quiet = false;
1009
+ let color = true;
1010
+ for (const argument of argv) {
1011
+ if (argument === '--json') {
1012
+ if (output !== 'terminal')
1013
+ throw new CliUsageError('--json may be supplied only once.');
1014
+ output = 'json';
1015
+ }
1016
+ else if (argument === '--quiet')
1017
+ quiet = true;
1018
+ else if (argument === '--no-color')
1019
+ color = false;
1020
+ else if (argument.startsWith('-'))
1021
+ throw new CliUsageError(`Unknown option "${argument}".`);
1022
+ else if (source === null)
1023
+ source = argument;
1024
+ else
1025
+ throw new CliUsageError('assurance accepts one optional versioned JSON input file.');
1026
+ }
1027
+ if (quiet && output !== 'terminal')
1028
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1029
+ return { action: 'assurance', source, output, quiet, color };
1030
+ }
1031
+ function parseSharingReviewArguments(argv) {
1032
+ let source = null;
1033
+ let output = 'terminal';
1034
+ let marking = null;
1035
+ let recipientScope = null;
1036
+ let purpose = null;
1037
+ let humanReviewed = false;
1038
+ let personalDataReviewed = false;
1039
+ let redactionsConfirmed = false;
1040
+ let quiet = false;
1041
+ let color = true;
1042
+ for (let index = 0; index < argv.length; index++) {
1043
+ const argument = argv[index];
1044
+ if (argument === undefined)
1045
+ break;
1046
+ if (argument === '--json') {
1047
+ if (output !== 'terminal')
1048
+ throw new CliUsageError('--json may be supplied only once.');
1049
+ output = 'json';
1050
+ }
1051
+ else if (argument === '--marking') {
1052
+ if (marking !== null)
1053
+ throw new CliUsageError('--marking may be supplied only once.');
1054
+ const value = argv[++index];
1055
+ if (!['clear', 'green', 'amber', 'amber-strict', 'red'].includes(value || '')) {
1056
+ throw new CliUsageError('--marking requires clear, green, amber, amber-strict, or red.');
1057
+ }
1058
+ marking = value;
1059
+ }
1060
+ else if (argument === '--recipient-scope') {
1061
+ if (recipientScope !== null)
1062
+ throw new CliUsageError('--recipient-scope may be supplied only once.');
1063
+ const value = argv[++index];
1064
+ if (!['public', 'community', 'organization', 'named-recipients'].includes(value || '')) {
1065
+ throw new CliUsageError('--recipient-scope requires public, community, organization, or named-recipients.');
1066
+ }
1067
+ recipientScope = value;
1068
+ }
1069
+ else if (argument === '--purpose') {
1070
+ if (purpose !== null)
1071
+ throw new CliUsageError('--purpose may be supplied only once.');
1072
+ const value = argv[++index];
1073
+ if (!value || value.startsWith('-'))
1074
+ throw new CliUsageError('--purpose requires one bounded description.');
1075
+ const normalized = value.replace(/\s+/gu, ' ').trim();
1076
+ if (!normalized || normalized.length > 200)
1077
+ throw new CliUsageError('--purpose is limited to 200 characters.');
1078
+ purpose = normalized;
1079
+ }
1080
+ else if (argument === '--human-reviewed')
1081
+ humanReviewed = true;
1082
+ else if (argument === '--personal-data-reviewed')
1083
+ personalDataReviewed = true;
1084
+ else if (argument === '--redactions-confirmed')
1085
+ redactionsConfirmed = true;
1086
+ else if (argument === '--quiet')
1087
+ quiet = true;
1088
+ else if (argument === '--no-color')
1089
+ color = false;
1090
+ else if (argument.startsWith('-'))
1091
+ throw new CliUsageError(`Unknown option "${argument}".`);
1092
+ else if (source === null)
1093
+ source = argument;
1094
+ else
1095
+ throw new CliUsageError('sharing-review accepts one optional artefact JSON file.');
1096
+ }
1097
+ if (!marking || !recipientScope || !purpose) {
1098
+ throw new CliUsageError('sharing-review requires --marking, --recipient-scope, and --purpose.');
1099
+ }
1100
+ if (quiet && output !== 'terminal')
1101
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1102
+ return {
1103
+ action: 'sharing-review', source, output, marking, recipientScope, purpose,
1104
+ humanReviewed, personalDataReviewed, redactionsConfirmed, quiet, color,
1105
+ };
1106
+ }
1107
+ function parseWorkflowPlanArguments(argv) {
1108
+ const positional = [];
1109
+ let output = 'terminal';
1110
+ let quiet = false;
1111
+ let color = true;
1112
+ for (const argument of argv) {
1113
+ if (argument === '--json') {
1114
+ if (output !== 'terminal')
1115
+ throw new CliUsageError('--json may be supplied only once.');
1116
+ output = 'json';
1117
+ }
1118
+ else if (argument === '--quiet')
1119
+ quiet = true;
1120
+ else if (argument === '--no-color')
1121
+ color = false;
1122
+ else if (argument.startsWith('-'))
1123
+ throw new CliUsageError(`Unknown option "${argument}".`);
1124
+ else
1125
+ positional.push(argument);
1126
+ }
1127
+ if (positional.length !== 2)
1128
+ throw new CliUsageError('workflow-plan requires one fixed recipe and one subject.');
1129
+ const recipe = positional[0];
1130
+ if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1131
+ throw new CliUsageError(`workflow-plan recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1132
+ }
1133
+ if (quiet && output !== 'terminal')
1134
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1135
+ return { action: 'workflow-plan', recipe: recipe, subject: positional[1], output, quiet, color };
1136
+ }
896
1137
  function parseDiffArguments(argv) {
897
1138
  let leftSource = null;
898
1139
  let rightSource = null;
@@ -926,6 +1167,36 @@ function parseDiffArguments(argv) {
926
1167
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
927
1168
  return { action: 'diff', leftSource, rightSource, output, quiet, color };
928
1169
  }
1170
+ function parseReconcileArguments(argv) {
1171
+ const sources = [];
1172
+ let output = 'terminal';
1173
+ let quiet = false;
1174
+ let color = true;
1175
+ for (const argument of argv) {
1176
+ if (argument === '--json') {
1177
+ if (output !== 'terminal')
1178
+ throw new CliUsageError('--json may be supplied only once.');
1179
+ output = 'json';
1180
+ }
1181
+ else if (argument === '--quiet')
1182
+ quiet = true;
1183
+ else if (argument === '--no-color')
1184
+ color = false;
1185
+ else if (argument.startsWith('-'))
1186
+ throw new CliUsageError(`Unknown option "${argument}".`);
1187
+ else
1188
+ sources.push(argument);
1189
+ }
1190
+ if (sources.length < 2 || sources.length > 5) {
1191
+ throw new CliUsageError('reconcile requires from 2 to 5 saved lookup JSON files.');
1192
+ }
1193
+ if (new Set(sources).size !== sources.length) {
1194
+ throw new CliUsageError('reconcile input files must be different.');
1195
+ }
1196
+ if (quiet && output !== 'terminal')
1197
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1198
+ return { action: 'reconcile', sources, output, quiet, color };
1199
+ }
929
1200
  function parseTimelineArguments(argv) {
930
1201
  const sources = [];
931
1202
  let output = 'terminal';
@@ -982,6 +1253,32 @@ function parseRegistrySupportArguments(argv) {
982
1253
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
983
1254
  return { action: 'registry-support', target, output, quiet, color };
984
1255
  }
1256
+ function parseRegistryDoctorArguments(argv) {
1257
+ let source = null;
1258
+ let output = 'terminal';
1259
+ let quiet = false;
1260
+ let color = true;
1261
+ for (const argument of argv) {
1262
+ if (argument === '--json') {
1263
+ if (output !== 'terminal')
1264
+ throw new CliUsageError('--json may be supplied only once.');
1265
+ output = 'json';
1266
+ }
1267
+ else if (argument === '--quiet')
1268
+ quiet = true;
1269
+ else if (argument === '--no-color')
1270
+ color = false;
1271
+ else if (argument.startsWith('-'))
1272
+ throw new CliUsageError(`Unknown option "${argument}".`);
1273
+ else if (source === null)
1274
+ source = argument;
1275
+ else
1276
+ throw new CliUsageError('registry-doctor accepts one optional saved Lookup JSON file.');
1277
+ }
1278
+ if (quiet && output !== 'terminal')
1279
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1280
+ return { action: 'registry-doctor', source, output, quiet, color };
1281
+ }
985
1282
  function parseRiskCalibrateArguments(argv) {
986
1283
  let source = null;
987
1284
  let output = 'terminal';
@@ -1008,6 +1305,32 @@ function parseRiskCalibrateArguments(argv) {
1008
1305
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1009
1306
  return { action: 'risk-calibrate', source, output, quiet, color };
1010
1307
  }
1308
+ function parseLookalikeCalibrateArguments(argv) {
1309
+ let source = null;
1310
+ let output = 'terminal';
1311
+ let quiet = false;
1312
+ let color = true;
1313
+ for (const argument of argv) {
1314
+ if (argument === '--json') {
1315
+ if (output !== 'terminal')
1316
+ throw new CliUsageError('--json may be supplied only once.');
1317
+ output = 'json';
1318
+ }
1319
+ else if (argument === '--quiet')
1320
+ quiet = true;
1321
+ else if (argument === '--no-color')
1322
+ color = false;
1323
+ else if (argument.startsWith('-'))
1324
+ throw new CliUsageError(`Unknown option "${argument}".`);
1325
+ else if (source === null)
1326
+ source = argument;
1327
+ else
1328
+ throw new CliUsageError('lookalike-calibrate accepts one optional reviewed dataset JSON file.');
1329
+ }
1330
+ if (quiet && output !== 'terminal')
1331
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1332
+ return { action: 'lookalike-calibrate', source, output, quiet, color };
1333
+ }
1011
1334
  function parseVerifyArtifactArguments(argv) {
1012
1335
  let source = null;
1013
1336
  let passphraseSource = null;
@@ -1041,7 +1364,7 @@ function parseVerifyArtifactArguments(argv) {
1041
1364
  else if (source === null)
1042
1365
  source = argument;
1043
1366
  else
1044
- throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artifact on stdin.');
1367
+ throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1045
1368
  }
1046
1369
  if (quiet && output !== 'terminal')
1047
1370
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');