@slicedearth/whoisleuth-cli 1.45.0 → 1.47.4

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 (133) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +59 -6
  4. package/bin/whoisleuth.mjs +5 -3
  5. package/cli/arguments.mjs +256 -9
  6. package/cli/artifact-structure.mjs +1833 -0
  7. package/cli/artifact-verify.mjs +242 -49
  8. package/cli/bounded-json.mjs +1 -0
  9. package/cli/bulk-checkpoint.mjs +2 -3
  10. package/cli/bulk-command-runner.mjs +2 -5
  11. package/cli/case-pack.mjs +819 -34
  12. package/cli/command-reference.mjs +48 -24
  13. package/cli/completion.mjs +16 -8
  14. package/cli/ct-event-intake.mjs +32 -2
  15. package/cli/discovery-scan-command-runner.mjs +4 -5
  16. package/cli/discovery-workflow.mjs +2 -3
  17. package/cli/doctor.mjs +2 -2
  18. package/cli/domain-control-monitor.mjs +21 -15
  19. package/cli/domain-control-observations.mjs +25 -2
  20. package/cli/evidence-command-runner.mjs +17 -11
  21. package/cli/evidence-signing.mjs +73 -45
  22. package/cli/export-evidence.mjs +11 -4
  23. package/cli/formatters/html.mjs +3 -3
  24. package/cli/formatters/terminal.mjs +3 -0
  25. package/cli/input.mjs +57 -0
  26. package/cli/interchange-report.mjs +226 -0
  27. package/cli/investigation-manifest.mjs +16 -17
  28. package/cli/investigation-run.mjs +71 -23
  29. package/cli/local-mmdb-review.mjs +45 -14
  30. package/cli/lookup-command-runner.mjs +2 -2
  31. package/cli/lookup-diff.mjs +9 -0
  32. package/cli/network-command-runner.mjs +78 -0
  33. package/cli/offline-evidence-review.mjs +28 -8
  34. package/cli/page-compare.mjs +8 -5
  35. package/cli/registry-cohort.mjs +385 -61
  36. package/cli/registry-support.mjs +2 -1
  37. package/cli/retained-artifact-diff.mjs +376 -0
  38. package/cli/risk-calibration.mjs +7 -5
  39. package/cli/runner.mjs +147 -185
  40. package/cli/saved-lookup.mjs +26 -3
  41. package/cli/sharing-review.mjs +12 -6
  42. package/docs/cli-reference.md +444 -70
  43. package/docs/cli.md +24 -5
  44. package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
  45. package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
  46. package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
  47. package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
  48. package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
  49. package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
  50. package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
  51. package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
  52. package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
  53. package/frontend/src/lib/analysis/bulk-sort.js +4 -0
  54. package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
  55. package/frontend/src/lib/analysis/case-evidence-model.js +8 -0
  56. package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
  57. package/frontend/src/lib/analysis/case-migration-model.js +83 -31
  58. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  59. package/frontend/src/lib/analysis/case-record-model.js +2 -0
  60. package/frontend/src/lib/analysis/case-record-operations.js +42 -12
  61. package/frontend/src/lib/analysis/case-report.js +39 -6
  62. package/frontend/src/lib/analysis/case-response-model.js +66 -7
  63. package/frontend/src/lib/analysis/case-response-packet.js +15 -5
  64. package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
  65. package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
  66. package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
  67. package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
  68. package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
  69. package/frontend/src/lib/analysis/domain-control-records.js +88 -0
  70. package/frontend/src/lib/analysis/external-findings-import.js +70 -6
  71. package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
  72. package/frontend/src/lib/analysis/investigation-guide.js +52 -10
  73. package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
  74. package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
  75. package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
  76. package/frontend/src/lib/analysis/lookup-decision-support.js +61 -6
  77. package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
  78. package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
  79. package/frontend/src/lib/analysis/utils.js +26 -3
  80. package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
  81. package/frontend/src/lib/analysis/visualization-models.js +63 -94
  82. package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
  83. package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
  84. package/frontend/src/lib/analysis/workspace-archive.js +104 -14
  85. package/lib/application-version.mjs +3 -0
  86. package/lib/availability.mjs +14 -12
  87. package/lib/bounded-file.mjs +13 -0
  88. package/lib/bounded-json-response.mjs +17 -4
  89. package/lib/bounded-json.mjs +249 -0
  90. package/lib/certificate-extension-profile.mjs +2 -1
  91. package/lib/cryptographic-assurance.mjs +155 -0
  92. package/lib/ct-search.mjs +87 -47
  93. package/lib/dns-intelligence.mjs +3 -2
  94. package/lib/dns-resolver-selection.mjs +6 -4
  95. package/lib/dnssec-chain-validation.mjs +1053 -0
  96. package/lib/dnssec-evidence-validation.mjs +10 -3
  97. package/lib/domain-change-packet.mjs +7 -5
  98. package/lib/domain-control-manifest.mjs +17 -129
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/evidence-export.mjs +751 -78
  101. package/lib/evidence-report-markdown.mjs +3 -0
  102. package/lib/evidence-report.mjs +5 -0
  103. package/lib/favicon.mjs +8 -5
  104. package/lib/http-evidence-bounds.mjs +3 -0
  105. package/lib/http-intelligence.mjs +2 -5
  106. package/lib/interchange-fidelity-registry.mjs +122 -0
  107. package/lib/local-geoip-evidence.mjs +2 -1
  108. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  109. package/lib/lookup-response-contract.mjs +691 -3
  110. package/lib/lookup.mjs +2 -2
  111. package/lib/observation.mjs +7 -6
  112. package/lib/outbound-identity.mjs +2 -3
  113. package/lib/perceptual-hash.mjs +12 -4
  114. package/lib/portable-generator.mjs +21 -0
  115. package/lib/provider-policy-admission.mjs +9 -4
  116. package/lib/rdap-normalization.mjs +18 -5
  117. package/lib/rdap-registrar.mjs +5 -1
  118. package/lib/registry-capabilities.mjs +3 -0
  119. package/lib/registry-capability-catalogue.mjs +12 -2
  120. package/lib/registry-comparison.mjs +28 -11
  121. package/lib/risk-calibration-summary.mjs +338 -0
  122. package/lib/rpki-evidence.mjs +9 -8
  123. package/lib/safe-fetch.mjs +59 -10
  124. package/lib/security-txt.mjs +26 -3
  125. package/lib/service-binding-dns.mjs +6 -1
  126. package/lib/smtp-transport-review.mjs +784 -0
  127. package/lib/sslbl-certificates.generated.mjs +136 -134
  128. package/lib/threat-intelligence-runtime.mjs +3 -11
  129. package/lib/threat-intelligence-types.mjs +18 -1
  130. package/lib/tls-intelligence.mjs +11 -5
  131. package/lib/tlsa-evidence.mjs +37 -7
  132. package/package.json +1 -1
  133. package/third-party-notices.txt +95 -5817
package/DISCLOSURE CHANGED
@@ -8,9 +8,13 @@ can have both legitimate and potentially harmful uses.
8
8
  WHOISleuth is intended for authorised defensive investigation, abuse response,
9
9
  threat research, domain acquisition due diligence, asset review, and education.
10
10
  Network collection is initiated explicitly by the operator, uses bounded
11
- requests, and keeps source provenance and incomplete results visible. The tool
12
- does not exploit targets, bypass access controls, submit credentials, execute
13
- remote page scripts, or automate enforcement actions.
11
+ requests, and keeps source provenance and incomplete results visible. Hosted
12
+ and distributable collection does not exploit targets, bypass access controls,
13
+ submit credentials, execute remote page scripts, or automate enforcement
14
+ actions. The separate repo-local rendered-capture package is an explicit
15
+ authorised exception: it executes page JavaScript in a disposable,
16
+ network-bounded browser and writes only a local screenshot plus sanitised
17
+ digest and manifest artefacts for operator-controlled review and deletion.
14
18
 
15
19
  Operators are responsible for using WHOISleuth only on systems and data they
16
20
  are authorised to examine and in accordance with applicable laws, provider
package/NOTICE CHANGED
@@ -32,10 +32,11 @@ warning-lists are available under CC0 1.0 Universal or the BSD 2-Clause
32
32
  "Simplified" License. Source:
33
33
  https://github.com/MISP/misp-warninglists
34
34
 
35
- The exact locked production-dependency inventory and the licence documents
36
- distributed by those packages are retained in
37
- frontend/static/third-party-notices.txt. The same file is served by the static
38
- website at /third-party-notices.txt and is checked for lockfile drift in CI.
35
+ The exact locked production-dependency inventories and licence documents are
36
+ distributed as third-party-notices.txt. The website inventory is served at
37
+ /third-party-notices.txt; the installed CLI contains its own notice file for
38
+ only the CLI runtime dependency closure. Both are generated from the reviewed
39
+ lockfile, and the website inventory is checked for lockfile drift in CI.
39
40
 
40
41
  The generated Unicode confusable projection is derived from Unicode UTS #39
41
42
  data file confusables.txt, Unicode version 17.0.0.
package/README.md CHANGED
@@ -59,6 +59,8 @@ node bin/whoisleuth.mts discover example.test --dictionary private-terms.txt --s
59
59
  node bin/whoisleuth.mts discover-scan example.test --scan-limit 50 --checkpoint candidate-scan.json --json
60
60
  node bin/whoisleuth.mts page-compare official.json candidate.json --json
61
61
  node bin/whoisleuth.mts mail-review bulk.json --json
62
+ node bin/whoisleuth.mts dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json
63
+ node bin/whoisleuth.mts mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json
62
64
  node bin/whoisleuth.mts domain-control domain-control-input.json --json
63
65
  node bin/whoisleuth.mts assurance domain-assurance-input.json --json
64
66
  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
@@ -88,6 +90,21 @@ explicit and are never interpreted as evidence of safety or absence.
88
90
  `lookup --plan` classifies a target and explains the selected source families
89
91
  and disclosures without making a network request. `commands --json` exposes the
90
92
  installed command contracts for local wrappers without running collection.
93
+ The isolated `dnssec-validate` and `mail-transport` actions are not part of
94
+ Lookup, Bulk, monitoring, or recipes. Each run requires explicit
95
+ owned-or-authorised acknowledgement; mail transport also requires a separate
96
+ active-probe acknowledgement and never sends a message, authenticates, tests
97
+ relay, or enumerates recipients or mailboxes. Its output distinguishes selected,
98
+ public-revalidated, and connected addresses and explicitly reports that A, AAAA,
99
+ and CNAME authentication was not evaluated after a candidate is retained, or
100
+ that address authentication is unavailable when no candidate exists; only
101
+ confirmed connections can form address relationship leads. If a DANE-TA TLSA
102
+ usage 2 association is published, active collection retains only the observed
103
+ leaf certificate and leaves that comparison partial without certificate-path
104
+ construction and trust-anchor path validation. SMTP relay PKIX-TA usage 0 and
105
+ PKIX-EE usage 1 records are retained as unsupported and cannot complete SMTP
106
+ DANE assurance; a separate usage 3 match remains eligible. Saved
107
+ Lookup JSON is byte-, nesting-, entry-, and per-container-bounded before parsing and portable export.
91
108
  Focused command help and the generated manual label every operation as offline
92
109
  or networked and state its target, input, and concurrency boundaries. The
93
110
  packaged [dual-use disclosure](https://github.com/slicedearth/whoisleuth/blob/main/DISCLOSURE)
@@ -127,12 +144,48 @@ non-comparable evidence separate; the labels are context, not proof of
127
144
  independence. `registry-doctor`, `assurance`, `sharing-review`, and
128
145
  `lookalike-calibrate` add offline compatibility, change/recovery, sharing, and
129
146
  review-yield workflows without contacting the hosted application.
130
- `registry-cohort` aggregates target-free publication-quality counts from saved
131
- Lookups, while `registry-scaffold` produces a sanitised synthetic contributor
132
- fixture. `brief` and `case-pack` create bounded local handoffs, `monitor-once`
133
- runs one explicit control review rather than a daemon, and `workflow-run`
134
- executes only installed fixed-recipe steps with per-run network approval and
135
- analyst-selection pauses.
147
+ `registry-cohort` emits target-free publication-quality timelines from either
148
+ saved Lookups or one unmixed family of retained version-1/version-2 cohort
149
+ reports; it never sums overlapping retained samples into independent
150
+ consistency. `registry-scaffold` produces a sanitised synthetic contributor
151
+ fixture. `diff` preserves saved-Lookup output and also emits a bounded
152
+ comparison ledger for explicitly paired Bulk sessions or domain portfolios.
153
+ `verify-artifact --manifest ... --manifest-entry ...` separates exact retained
154
+ byte identity from canonical-only or mismatched identity. `brief` and
155
+ `case-pack` create bounded local handoffs, `monitor-once` runs one explicit
156
+ control review rather than a daemon, and `workflow-run` executes only installed
157
+ fixed-recipe steps with per-run network approval and analyst-selection pauses.
158
+ The current schema-26 JSON produced by `export` is accepted directly by
159
+ `verify-artifact` and reported as `structure_valid`. It has no embedded checksum
160
+ or signature, so exact retained bytes require a verified investigation manifest
161
+ entry. The export can contain a bounded privacy-projected registry RDAP
162
+ publication, normalised WHOIS values, and bounded contacts and should be
163
+ reviewed before sharing. Request and response headers, cookies, session and
164
+ credential fields, and credential-bearing or query-bearing URLs are excluded.
165
+ Schema 25 remains readable through its historical wrapper contract: retained
166
+ diagnostics are authoritative, unavailable wrapper data is suppressed during
167
+ replay, and other contradictions fail closed. Verification and browser replay share a 5 MiB,
168
+ 20,000-entry, 24-level portable boundary.
169
+
170
+ Lookup can also export a version-1 `whoisleuth.lookup-claim-passport` for one
171
+ selected readiness statement. `verify-artifact` checks its strict bounded
172
+ structure and sorted-json-v2 digest; `interchange-report` describes the exact
173
+ source-state fields it preserves and the raw payload, contact, page-value,
174
+ request-path, credential, browser-store, and signer-authentication fields it
175
+ excludes.
176
+
177
+ Current `case-pack` output keeps its version-1 envelope. Trusted and internal
178
+ audiences preserve exact analyst-selected Case-to-Brand Profile identifiers;
179
+ public output clears them from cases and embedded reports and discloses the
180
+ bounded `brandProfileReferencesOmitted` count. Verification also enforces the
181
+ actual audience projection after digest validation, rejects re-signed
182
+ sensitive-field leaks at expected or unexpected nested paths, and binds each
183
+ report to its canonical top-level Case and exact manifest counts. Current
184
+ schema-12 input must survive bounded normalisation exactly, provide safe unique
185
+ Case identities and a valid unique at-most-eight identifier list, and use
186
+ report v8. Schema 11 uses report v7; valid collections through schema 10 remain
187
+ compatible with supported reports through v6 and without later branch,
188
+ reference, or omission-count fields.
136
189
 
137
190
  Bulk also supports fixed-column CSV, domain-only or exact-query output,
138
191
  registered, inconclusive, and hard-failure output filters, and bounded A, AAAA,
@@ -28,12 +28,14 @@ else {
28
28
  import("../cli/runner.mjs"),
29
29
  ]);
30
30
  let resolvedArgv;
31
+ let profileResolutionFailed = false;
31
32
  try {
32
33
  resolvedArgv = await configModule.resolveCliProfileArguments(argv);
33
34
  }
34
35
  catch (error) {
35
36
  process.stderr.write(`Usage error: ${errorsModule.boundedCliErrorMessage(error, 'Invalid CLI profile')}\n`);
36
37
  process.exitCode = EXIT_CODES.USAGE;
38
+ profileResolutionFailed = true;
37
39
  resolvedArgv = [];
38
40
  }
39
41
  const cancellation = new AbortController();
@@ -54,9 +56,9 @@ else {
54
56
  const onSigterm = () => interrupt('SIGTERM');
55
57
  process.on('SIGINT', onSigint);
56
58
  process.on('SIGTERM', onSigterm);
57
- const execution = resolvedArgv.length
58
- ? runnerModule.runCli(resolvedArgv, { signal: cancellation.signal })
59
- : Promise.resolve(process.exitCode || EXIT_CODES.USAGE);
59
+ const execution = profileResolutionFailed
60
+ ? Promise.resolve(process.exitCode || EXIT_CODES.USAGE)
61
+ : runnerModule.runCli(resolvedArgv, { signal: cancellation.signal });
60
62
  execution.then((code) => {
61
63
  if (code === 130) {
62
64
  process.removeListener('SIGINT', onSigint);
package/cli/arguments.mjs CHANGED
@@ -21,6 +21,8 @@ const CLI_COMMANDS = [
21
21
  'posture',
22
22
  'http',
23
23
  'tls',
24
+ 'dnssec-validate',
25
+ 'mail-transport',
24
26
  'registry-support',
25
27
  'registry-doctor',
26
28
  'registry-cohort',
@@ -28,6 +30,7 @@ const CLI_COMMANDS = [
28
30
  'risk-calibrate',
29
31
  'lookalike-calibrate',
30
32
  'verify-artifact',
33
+ 'interchange-report',
31
34
  'inspect-archive',
32
35
  'sign-artifact',
33
36
  'verify-signature',
@@ -156,6 +159,10 @@ function parseCliArgumentsCore(argv) {
156
159
  return parseHttpArguments(argv.slice(1));
157
160
  if (command === 'tls')
158
161
  return parseTlsArguments(argv.slice(1));
162
+ if (command === 'dnssec-validate')
163
+ return parseDnssecValidateArguments(argv.slice(1));
164
+ if (command === 'mail-transport')
165
+ return parseMailTransportArguments(argv.slice(1));
159
166
  if (command === 'registry-support')
160
167
  return parseRegistrySupportArguments(argv.slice(1));
161
168
  if (command === 'registry-doctor')
@@ -170,6 +177,8 @@ function parseCliArgumentsCore(argv) {
170
177
  return parseLookalikeCalibrateArguments(argv.slice(1));
171
178
  if (command === 'verify-artifact')
172
179
  return parseVerifyArtifactArguments(argv.slice(1));
180
+ if (command === 'interchange-report')
181
+ return parseInterchangeReportArguments(argv.slice(1));
173
182
  if (command === 'inspect-archive')
174
183
  return parseInspectArchiveArguments(argv.slice(1));
175
184
  if (command === 'sign-artifact')
@@ -223,6 +232,7 @@ function parseCliArgumentsCore(argv) {
223
232
  let strictExit = false;
224
233
  let events = false;
225
234
  let plan = false;
235
+ let includeAttribution = true;
226
236
  let failOn = null;
227
237
  let observerLabel = null;
228
238
  let vantageLabel = null;
@@ -279,6 +289,11 @@ function parseCliArgumentsCore(argv) {
279
289
  throw new CliUsageError('--plan may be supplied only once.');
280
290
  plan = true;
281
291
  }
292
+ else if (argument === '--no-attribution') {
293
+ if (!includeAttribution)
294
+ throw new CliUsageError('--no-attribution may be supplied only once.');
295
+ includeAttribution = false;
296
+ }
282
297
  else if (argument === '--fail-on') {
283
298
  if (failOn !== null)
284
299
  throw new CliUsageError('--fail-on may be supplied only once.');
@@ -315,12 +330,15 @@ function parseCliArgumentsCore(argv) {
315
330
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
316
331
  if (detailSet && output !== 'terminal')
317
332
  throw new CliUsageError('--summary and --verbose apply only to terminal output.');
333
+ if (!includeAttribution && output !== 'markdown' && output !== 'html') {
334
+ throw new CliUsageError('--no-attribution applies only to Markdown or HTML reports.');
335
+ }
318
336
  if (plan && !['terminal', 'json'].includes(output))
319
337
  throw new CliUsageError('--plan supports terminal or JSON output only.');
320
338
  if (plan && (detailSet || strictExit || events || quiet || failOn !== null)) {
321
339
  throw new CliUsageError('--plan cannot be combined with detail, strict-exit, event, or quiet options.');
322
340
  }
323
- return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, observerLabel, vantageLabel, quiet, color, ...(failOn ? { failOn } : {}) };
341
+ return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, includeAttribution, observerLabel, vantageLabel, quiet, color, ...(failOn ? { failOn } : {}) };
324
342
  }
325
343
  function parseCompletionArguments(argv) {
326
344
  if (argv.length !== 1 || !['bash', 'zsh', 'fish', 'powershell'].includes(argv[0] || '')) {
@@ -963,6 +981,122 @@ function parseTlsArguments(argv) {
963
981
  const { source: hostname, ...options } = parseSingleJsonInput(argv, 'tls accepts one hostname.');
964
982
  return { action: 'tls', hostname, ...options };
965
983
  }
984
+ function parseDnssecValidateArguments(argv) {
985
+ let target = null;
986
+ let resolver = null;
987
+ let trustAnchorSource = null;
988
+ let ownedOrAuthorized = false;
989
+ let output = 'terminal';
990
+ let quiet = false;
991
+ let color = true;
992
+ for (let index = 0; index < argv.length; index += 1) {
993
+ const argument = argv[index];
994
+ if (argument === '--resolver' || argument === '--trust-anchor') {
995
+ const selected = argument === '--resolver' ? resolver : trustAnchorSource;
996
+ if (selected !== null)
997
+ throw new CliUsageError(`${argument} may be supplied only once.`);
998
+ const value = argv[++index];
999
+ if (!value || value.startsWith('-'))
1000
+ throw new CliUsageError(`${argument} requires one bounded value.`);
1001
+ if (argument === '--resolver')
1002
+ resolver = value;
1003
+ else
1004
+ trustAnchorSource = value;
1005
+ }
1006
+ else if (argument === '--owned-or-authorized') {
1007
+ if (ownedOrAuthorized)
1008
+ throw new CliUsageError('--owned-or-authorized may be supplied only once.');
1009
+ ownedOrAuthorized = true;
1010
+ }
1011
+ else 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 (argument && target === null)
1023
+ target = argument;
1024
+ else
1025
+ throw new CliUsageError('dnssec-validate accepts one domain target.');
1026
+ }
1027
+ if (!target)
1028
+ throw new CliUsageError('dnssec-validate requires one domain target.');
1029
+ if (!resolver)
1030
+ throw new CliUsageError('dnssec-validate requires --resolver <public-IP>.');
1031
+ if (!trustAnchorSource)
1032
+ throw new CliUsageError('dnssec-validate requires --trust-anchor <anchor.json>.');
1033
+ if (!ownedOrAuthorized)
1034
+ throw new CliUsageError('dnssec-validate requires --owned-or-authorized for every run.');
1035
+ if (quiet && output !== 'terminal')
1036
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1037
+ return { action: 'dnssec-validate', target, resolver, trustAnchorSource, ownedOrAuthorized: true, output, quiet, color };
1038
+ }
1039
+ function parseMailTransportArguments(argv) {
1040
+ let source = null;
1041
+ let resolver = null;
1042
+ let trustAnchorSource = null;
1043
+ let ownedOrAuthorized = false;
1044
+ let activeProbeAcknowledged = false;
1045
+ let output = 'terminal';
1046
+ let quiet = false;
1047
+ let color = true;
1048
+ for (let index = 0; index < argv.length; index += 1) {
1049
+ const argument = argv[index];
1050
+ if (argument === '--resolver' || argument === '--trust-anchor') {
1051
+ const selected = argument === '--resolver' ? resolver : trustAnchorSource;
1052
+ if (selected !== null)
1053
+ throw new CliUsageError(`${argument} may be supplied only once.`);
1054
+ const value = argv[++index];
1055
+ if (!value || value.startsWith('-'))
1056
+ throw new CliUsageError(`${argument} requires one bounded value.`);
1057
+ if (argument === '--resolver')
1058
+ resolver = value;
1059
+ else
1060
+ trustAnchorSource = value;
1061
+ }
1062
+ else if (argument === '--owned-or-authorized') {
1063
+ if (ownedOrAuthorized)
1064
+ throw new CliUsageError('--owned-or-authorized may be supplied only once.');
1065
+ ownedOrAuthorized = true;
1066
+ }
1067
+ else if (argument === '--active-probe') {
1068
+ if (activeProbeAcknowledged)
1069
+ throw new CliUsageError('--active-probe may be supplied only once.');
1070
+ activeProbeAcknowledged = true;
1071
+ }
1072
+ else if (argument === '--json') {
1073
+ if (output !== 'terminal')
1074
+ throw new CliUsageError('--json may be supplied only once.');
1075
+ output = 'json';
1076
+ }
1077
+ else if (argument === '--quiet')
1078
+ quiet = true;
1079
+ else if (argument === '--no-color')
1080
+ color = false;
1081
+ else if (argument?.startsWith('-'))
1082
+ throw new CliUsageError(`Unknown option "${argument}".`);
1083
+ else if (argument && source === null)
1084
+ source = argument;
1085
+ else
1086
+ throw new CliUsageError('mail-transport accepts one optional versioned JSON input file.');
1087
+ }
1088
+ if (!resolver)
1089
+ throw new CliUsageError('mail-transport requires --resolver <public-IP>.');
1090
+ if (!trustAnchorSource)
1091
+ throw new CliUsageError('mail-transport requires --trust-anchor <anchor.json>.');
1092
+ if (!ownedOrAuthorized)
1093
+ throw new CliUsageError('mail-transport requires --owned-or-authorized for every run.');
1094
+ if (!activeProbeAcknowledged)
1095
+ throw new CliUsageError('mail-transport requires --active-probe for every run.');
1096
+ if (quiet && output !== 'terminal')
1097
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1098
+ return { action: 'mail-transport', source, resolver, trustAnchorSource, ownedOrAuthorized: true, activeProbeAcknowledged: true, output, quiet, color };
1099
+ }
966
1100
  function parseCompareArguments(argv) {
967
1101
  const parsed = parseSingleJsonInput(argv, 'compare accepts one optional lookup JSON file. Otherwise pipe one lookup document on stdin.');
968
1102
  return { action: 'compare', ...parsed };
@@ -1325,15 +1459,32 @@ function parseWorkflowRunArguments(argv) {
1325
1459
  function parseDiffArguments(argv) {
1326
1460
  let leftSource = null;
1327
1461
  let rightSource = null;
1462
+ let leftSessionId = null;
1463
+ let rightSessionId = null;
1328
1464
  let output = 'terminal';
1329
1465
  let quiet = false;
1330
1466
  let color = true;
1331
- for (const argument of argv) {
1467
+ for (let index = 0; index < argv.length; index++) {
1468
+ const argument = argv[index];
1469
+ if (argument === undefined)
1470
+ break;
1332
1471
  if (argument === '--json') {
1333
1472
  if (output !== 'terminal')
1334
1473
  throw new CliUsageError('--json may be supplied only once.');
1335
1474
  output = 'json';
1336
1475
  }
1476
+ else if (argument === '--left-session' || argument === '--right-session') {
1477
+ const current = argument === '--left-session' ? leftSessionId : rightSessionId;
1478
+ if (current !== null)
1479
+ throw new CliUsageError(`${argument} may be supplied only once.`);
1480
+ const value = argv[++index];
1481
+ if (!value || !/^[A-Za-z0-9_-]{1,128}$/u.test(value))
1482
+ throw new CliUsageError(`${argument} requires one bounded saved-session ID.`);
1483
+ if (argument === '--left-session')
1484
+ leftSessionId = value;
1485
+ else
1486
+ rightSessionId = value;
1487
+ }
1337
1488
  else if (argument === '--quiet')
1338
1489
  quiet = true;
1339
1490
  else if (argument === '--no-color')
@@ -1345,15 +1496,15 @@ function parseDiffArguments(argv) {
1345
1496
  else if (rightSource === null)
1346
1497
  rightSource = argument;
1347
1498
  else
1348
- throw new CliUsageError('diff accepts exactly two saved lookup JSON files.');
1499
+ throw new CliUsageError('diff accepts exactly two compatible retained JSON files.');
1349
1500
  }
1350
1501
  if (!leftSource || !rightSource)
1351
- throw new CliUsageError('diff requires exactly two saved lookup JSON files.');
1502
+ throw new CliUsageError('diff requires exactly two compatible retained JSON files.');
1352
1503
  if (leftSource === rightSource)
1353
1504
  throw new CliUsageError('diff requires two different input files.');
1354
1505
  if (quiet && output !== 'terminal')
1355
1506
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1356
- return { action: 'diff', leftSource, rightSource, output, quiet, color };
1507
+ return { action: 'diff', leftSource, rightSource, leftSessionId, rightSessionId, output, quiet, color };
1357
1508
  }
1358
1509
  function parseReconcileArguments(argv) {
1359
1510
  const sources = [];
@@ -1451,8 +1602,30 @@ function parseRegistryScaffoldArguments(argv) {
1451
1602
  return { action: 'registry-scaffold', profile, suffix, scenario: scenario };
1452
1603
  }
1453
1604
  function parseRiskCalibrateArguments(argv) {
1454
- const parsed = parseSingleJsonInput(argv, 'risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1455
- return { action: 'risk-calibrate', ...parsed };
1605
+ let source = null;
1606
+ let output = 'terminal';
1607
+ let quiet = false;
1608
+ let color = true;
1609
+ for (const argument of argv) {
1610
+ if (argument === '--json' || argument === '--summary-json') {
1611
+ if (output !== 'terminal')
1612
+ throw new CliUsageError('--json and --summary-json are mutually exclusive and may be supplied only once.');
1613
+ output = argument === '--json' ? 'json' : 'summary_json';
1614
+ }
1615
+ else if (argument === '--quiet')
1616
+ quiet = true;
1617
+ else if (argument === '--no-color')
1618
+ color = false;
1619
+ else if (argument.startsWith('-'))
1620
+ throw new CliUsageError(`Unknown option "${argument}".`);
1621
+ else if (source === null)
1622
+ source = argument;
1623
+ else
1624
+ throw new CliUsageError('risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1625
+ }
1626
+ if (quiet && output !== 'terminal')
1627
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1628
+ return { action: 'risk-calibrate', source, output, quiet, color };
1456
1629
  }
1457
1630
  function parseLookalikeCalibrateArguments(argv) {
1458
1631
  const parsed = parseSingleJsonInput(argv, 'lookalike-calibrate accepts one optional reviewed dataset JSON file.');
@@ -1461,7 +1634,10 @@ function parseLookalikeCalibrateArguments(argv) {
1461
1634
  function parseVerifyArtifactArguments(argv) {
1462
1635
  let source = null;
1463
1636
  let passphraseSource = null;
1637
+ let manifestSource = null;
1638
+ let manifestEntryId = null;
1464
1639
  let output = 'terminal';
1640
+ let strictExit = false;
1465
1641
  let quiet = false;
1466
1642
  let color = true;
1467
1643
  for (let index = 0; index < argv.length; index++) {
@@ -1482,6 +1658,28 @@ function parseVerifyArtifactArguments(argv) {
1482
1658
  }
1483
1659
  passphraseSource = value;
1484
1660
  }
1661
+ else if (argument === '--manifest') {
1662
+ if (manifestSource !== null)
1663
+ throw new CliUsageError('--manifest may be supplied only once.');
1664
+ const value = argv[++index];
1665
+ if (!value || value.startsWith('-'))
1666
+ throw new CliUsageError('--manifest requires one bounded investigation manifest file.');
1667
+ manifestSource = value;
1668
+ }
1669
+ else if (argument === '--manifest-entry') {
1670
+ if (manifestEntryId !== null)
1671
+ throw new CliUsageError('--manifest-entry may be supplied only once.');
1672
+ const value = argv[++index];
1673
+ if (!value || !/^artifact-(?:[1-9]|1[0-6])$/u.test(value)) {
1674
+ throw new CliUsageError('--manifest-entry requires an ID from artifact-1 through artifact-16.');
1675
+ }
1676
+ manifestEntryId = value;
1677
+ }
1678
+ else if (argument === '--strict-exit') {
1679
+ if (strictExit)
1680
+ throw new CliUsageError('--strict-exit may be supplied only once.');
1681
+ strictExit = true;
1682
+ }
1485
1683
  else if (argument === '--quiet')
1486
1684
  quiet = true;
1487
1685
  else if (argument === '--no-color')
@@ -1493,9 +1691,50 @@ function parseVerifyArtifactArguments(argv) {
1493
1691
  else
1494
1692
  throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1495
1693
  }
1694
+ if ((manifestSource === null) !== (manifestEntryId === null)) {
1695
+ throw new CliUsageError('--manifest and --manifest-entry must be supplied together.');
1696
+ }
1697
+ if (quiet && output !== 'terminal')
1698
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1699
+ return { action: 'verify-artifact', source, passphraseSource, manifestSource, manifestEntryId, output, strictExit, quiet, color };
1700
+ }
1701
+ function parseInterchangeReportArguments(argv) {
1702
+ let source = null;
1703
+ let passphraseSource = null;
1704
+ let output = 'terminal';
1705
+ let quiet = false;
1706
+ let color = true;
1707
+ for (let index = 0; index < argv.length; index++) {
1708
+ const argument = argv[index];
1709
+ if (argument === undefined)
1710
+ break;
1711
+ if (argument === '--json') {
1712
+ if (output !== 'terminal')
1713
+ throw new CliUsageError('--json may be supplied only once.');
1714
+ output = 'json';
1715
+ }
1716
+ else if (argument === '--passphrase-file') {
1717
+ if (passphraseSource !== null)
1718
+ throw new CliUsageError('--passphrase-file may be supplied only once.');
1719
+ const value = argv[++index];
1720
+ if (!value || value.startsWith('-'))
1721
+ throw new CliUsageError('--passphrase-file requires one bounded UTF-8 file.');
1722
+ passphraseSource = value;
1723
+ }
1724
+ else if (argument === '--quiet')
1725
+ quiet = true;
1726
+ else if (argument === '--no-color')
1727
+ color = false;
1728
+ else if (argument.startsWith('-'))
1729
+ throw new CliUsageError(`Unknown option "${argument}".`);
1730
+ else if (source === null)
1731
+ source = argument;
1732
+ else
1733
+ throw new CliUsageError('interchange-report accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1734
+ }
1496
1735
  if (quiet && output !== 'terminal')
1497
1736
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1498
- return { action: 'verify-artifact', source, passphraseSource, output, quiet, color };
1737
+ return { action: 'interchange-report', source, passphraseSource, output, quiet, color };
1499
1738
  }
1500
1739
  function parseSourceReportArguments(argv) {
1501
1740
  const parsed = parseSingleJsonInput(argv, 'source-report accepts one optional JSON file. Otherwise pipe lookup documents on stdin.');
@@ -1504,6 +1743,7 @@ function parseSourceReportArguments(argv) {
1504
1743
  function parseExportArguments(argv) {
1505
1744
  let source = null;
1506
1745
  let compact = false;
1746
+ let includeAttribution = true;
1507
1747
  let format = 'json';
1508
1748
  for (const argument of argv) {
1509
1749
  if (argument === '--compact') {
@@ -1511,6 +1751,11 @@ function parseExportArguments(argv) {
1511
1751
  throw new CliUsageError('--compact may be supplied only once.');
1512
1752
  compact = true;
1513
1753
  }
1754
+ else if (argument === '--no-attribution') {
1755
+ if (!includeAttribution)
1756
+ throw new CliUsageError('--no-attribution may be supplied only once.');
1757
+ includeAttribution = false;
1758
+ }
1514
1759
  else if (argument === '--markdown' || argument === '--html') {
1515
1760
  if (format !== 'json')
1516
1761
  throw new CliUsageError('Choose only one evidence export format.');
@@ -1525,6 +1770,8 @@ function parseExportArguments(argv) {
1525
1770
  }
1526
1771
  if (compact && format !== 'json')
1527
1772
  throw new CliUsageError('--compact applies to JSON export and cannot be combined with --markdown or --html.');
1528
- return { action: 'export', source, format, compact };
1773
+ if (!includeAttribution && format === 'json')
1774
+ throw new CliUsageError('--no-attribution applies only to Markdown or HTML reports.');
1775
+ return { action: 'export', source, format, compact, includeAttribution };
1529
1776
  }
1530
1777
  export { CLI_COMMANDS, CliUsageError, MAX_CLI_ARGUMENTS, MAX_CLI_ARGUMENT_LENGTH, parseCliArguments };