@slicedearth/whoisleuth-cli 2.3.0 → 2.4.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.
Files changed (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +83 -61
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -0,0 +1,168 @@
1
+ import { StringDecoder } from 'node:string_decoder';
2
+ import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
3
+ export function canReadInteractiveLine(input, output, environment = process.env) {
4
+ return input?.isTTY === true && output?.isTTY === true
5
+ && typeof input.setRawMode === 'function' && typeof input.resume === 'function'
6
+ && typeof input.pause === 'function' && typeof input.on === 'function'
7
+ && typeof input.off === 'function' && typeof output.write === 'function'
8
+ && !environment.CI;
9
+ }
10
+ const MAX_INTERACTIVE_ANSWER_SCALARS = 1_024;
11
+ const MAX_INTERACTIVE_ANSWER_BYTES = 4_096;
12
+ function boundedInteractiveAnswer(value) {
13
+ const supplied = typeof value === 'string' ? value : '';
14
+ if (Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES
15
+ || Array.from(supplied).length > MAX_INTERACTIVE_ANSWER_SCALARS
16
+ || hasUnsafeCliText(supplied)) {
17
+ throw new CliUsageError('Interactive input must be bounded text without control characters.');
18
+ }
19
+ return supplied.trim();
20
+ }
21
+ function abortError() {
22
+ return new DOMException('Aborted', 'AbortError');
23
+ }
24
+ function readBoundedInteractiveLine(prompt, options) {
25
+ const { input, output, signal } = options;
26
+ if (signal?.aborted)
27
+ return Promise.reject(signal.reason || abortError());
28
+ const previousRaw = input.isRaw === true;
29
+ const wasPaused = input.isPaused?.() === true;
30
+ let receivedBytes = 0;
31
+ let receivedScalars = 0;
32
+ let settled = false;
33
+ let characters = [];
34
+ const decoder = new StringDecoder('utf8');
35
+ return new Promise((resolve, reject) => {
36
+ const cleanup = () => {
37
+ let failure = null;
38
+ const attempt = (operation) => {
39
+ try {
40
+ operation();
41
+ }
42
+ catch (error) {
43
+ failure ||= error instanceof Error ? error : new Error('Interactive terminal cleanup failed.');
44
+ }
45
+ };
46
+ attempt(() => input.off?.('data', onData));
47
+ attempt(() => input.off?.('end', onEnd));
48
+ attempt(() => signal?.removeEventListener('abort', onAbort));
49
+ attempt(() => input.setRawMode?.(previousRaw));
50
+ if (wasPaused)
51
+ attempt(() => input.pause?.());
52
+ return failure;
53
+ };
54
+ const finish = (value, error) => {
55
+ if (settled)
56
+ return;
57
+ settled = true;
58
+ const cleanupFailure = cleanup();
59
+ characters = [];
60
+ if (error && cleanupFailure) {
61
+ reject(new AggregateError([error, cleanupFailure], `${error.message}; terminal cleanup also failed: ${cleanupFailure.message}`));
62
+ }
63
+ else if (error || cleanupFailure)
64
+ reject(error || cleanupFailure);
65
+ else
66
+ resolve(value || '');
67
+ };
68
+ const rejectUsage = () => finish(undefined, new CliUsageError('Interactive input must be bounded text without control characters.'));
69
+ const onData = (chunk) => {
70
+ if (settled)
71
+ return;
72
+ let rawChunk;
73
+ if (Buffer.isBuffer(chunk)) {
74
+ if (chunk.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
75
+ rejectUsage();
76
+ return;
77
+ }
78
+ rawChunk = chunk;
79
+ }
80
+ else {
81
+ const supplied = String(chunk ?? '');
82
+ if (supplied.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes
83
+ || Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
84
+ rejectUsage();
85
+ return;
86
+ }
87
+ rawChunk = Buffer.from(supplied, 'utf8');
88
+ }
89
+ receivedBytes += rawChunk.length;
90
+ const value = decoder.write(rawChunk);
91
+ receivedScalars += Array.from(value).length;
92
+ if (receivedBytes > MAX_INTERACTIVE_ANSWER_BYTES
93
+ || receivedScalars > MAX_INTERACTIVE_ANSWER_SCALARS
94
+ || value.includes('\ufffd')) {
95
+ rejectUsage();
96
+ return;
97
+ }
98
+ for (const character of value) {
99
+ if (character === '\r' || character === '\n') {
100
+ try {
101
+ output.write('\n');
102
+ }
103
+ catch (error) {
104
+ finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
105
+ return;
106
+ }
107
+ finish(characters.join(''));
108
+ return;
109
+ }
110
+ if (character === '\u0003' || character === '\u0004' || character === '\u001b') {
111
+ finish(undefined, abortError());
112
+ return;
113
+ }
114
+ if (character === '\u0008' || character === '\u007f') {
115
+ if (characters.length) {
116
+ characters.pop();
117
+ try {
118
+ output.write('\b \b');
119
+ }
120
+ catch (error) {
121
+ finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
122
+ return;
123
+ }
124
+ }
125
+ continue;
126
+ }
127
+ if (hasUnsafeCliText(character)) {
128
+ rejectUsage();
129
+ return;
130
+ }
131
+ characters.push(character);
132
+ try {
133
+ output.write(character);
134
+ }
135
+ catch (error) {
136
+ finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
137
+ return;
138
+ }
139
+ }
140
+ };
141
+ const onEnd = () => {
142
+ const remainder = decoder.end();
143
+ if (remainder)
144
+ rejectUsage();
145
+ else
146
+ finish(undefined, abortError());
147
+ };
148
+ const onAbort = () => finish(undefined, signal?.reason instanceof Error ? signal.reason : abortError());
149
+ try {
150
+ output.write(prompt);
151
+ input.on?.('data', onData);
152
+ input.on?.('end', onEnd);
153
+ signal?.addEventListener('abort', onAbort, { once: true });
154
+ if (signal?.aborted) {
155
+ onAbort();
156
+ return;
157
+ }
158
+ input.setRawMode?.(true);
159
+ if (settled)
160
+ return;
161
+ input.resume?.();
162
+ }
163
+ catch (error) {
164
+ finish(undefined, error instanceof Error ? error : new Error('Interactive terminal input failed.'));
165
+ }
166
+ });
167
+ }
168
+ export { MAX_INTERACTIVE_ANSWER_BYTES, MAX_INTERACTIVE_ANSWER_SCALARS, boundedInteractiveAnswer, readBoundedInteractiveLine };
@@ -1,15 +1,39 @@
1
1
  import { formatCliJunit } from "./ci-report.mjs";
2
2
  import { formatDomainControlMonitor, runDomainControlMonitor } from "./domain-control-monitor.mjs";
3
- import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
3
+ import { boundedCliErrorMessage, createCliDiagnosticOutput, CliUsageError } from "./errors.mjs";
4
4
  import EXIT_CODES from "./exit-codes.mjs";
5
5
  import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
6
6
  import { formatJsonDocument } from "./formatters/json.mjs";
7
7
  import { buildInvestigationPlan, buildWorkflowRecipeCatalogue, formatInvestigationPlan, formatWorkflowRecipeCatalogue, } from "./investigation-plan.mjs";
8
- import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, runInvestigationRecipe, } from "./investigation-run.mjs";
8
+ import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, investigationRunExitCode, runInvestigationRecipe, } from "./investigation-run.mjs";
9
9
  import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES } from "./offline-evidence-review.mjs";
10
+ import { MAX_OFFLINE_ARTIFACT_BYTES } from "./artifact-verify.mjs";
11
+ import { MAX_RETAINED_ARTIFACT_DIFF_BYTES } from "./retained-artifact-diff.mjs";
12
+ import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
13
+ import { MAX_COMPARE_INPUT_BYTES } from "./compare.mjs";
14
+ import { MAX_SOURCE_RELIABILITY_INPUT_BYTES } from "./source-reliability.mjs";
15
+ import { MAX_SHARING_REVIEW_BYTES } from "./sharing-review.mjs";
10
16
  import { createBufferedOutput } from "./output-file.mjs";
17
+ import { boundedInteractiveAnswer, canReadInteractiveLine, readBoundedInteractiveLine } from "./terminal-input.mjs";
11
18
  import { WORKFLOW_INLINE_COMMANDS } from "./inline-command-families.mjs";
12
19
  import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
20
+ function boundWorkflowInputs(command, inputs, dependencies, context) {
21
+ if (!inputs.size)
22
+ return {};
23
+ const read = (source, fallback, maximumBytes, label) => typeof source === 'string' && inputs.has(source)
24
+ ? inputs.get(source)
25
+ : fallback ? fallback(source) : context.readInput(source, maximumBytes, label);
26
+ switch (command) {
27
+ case 'export': return { readExportInput: (source) => read(source, dependencies.readExportInput, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Evidence export input') };
28
+ case 'verify-artifact': return { readArtifactInput: (source) => read(source, dependencies.readArtifactInput, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input') };
29
+ case 'sharing-review': return { readArtifactInput: (source) => read(source, dependencies.readArtifactInput, MAX_SHARING_REVIEW_BYTES, 'Sharing review input') };
30
+ case 'compare': return { readCompareInput: (source) => read(source, dependencies.readCompareInput, MAX_COMPARE_INPUT_BYTES, 'Comparison input') };
31
+ case 'source-report': return { readSourceReliabilityInput: (source) => read(source, dependencies.readSourceReliabilityInput, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input') };
32
+ case 'diff':
33
+ case 'timeline': return { readDiffInput: (source) => read(source, dependencies.readDiffInput, command === 'diff' ? MAX_RETAINED_ARTIFACT_DIFF_BYTES : MAX_SAVED_LOOKUP_INPUT_BYTES, command === 'diff' ? 'Retained diff input' : 'Lookup timeline input') };
34
+ default: throw new CliUsageError('This fixed-workflow command does not accept retained artefacts.');
35
+ }
36
+ }
13
37
  async function runMonitorOnceCommand(args, dependencies, context) {
14
38
  context.setFailureLabel('One-shot domain control review');
15
39
  let manifestInput;
@@ -82,12 +106,18 @@ async function runWorkflowPlanCommand(args, dependencies, context) {
82
106
  }
83
107
  async function runWorkflowRecipeCommand(args, dependencies, context) {
84
108
  context.setFailureLabel('Investigation workflow');
109
+ const input = (dependencies.stdin ?? process.stdin);
110
+ if (args.interactive && !canReadInteractiveLine(input, context.stderr, dependencies.environment)) {
111
+ throw new CliUsageError('--interactive requires terminal input and terminal stderr. Use --select for unattended workflows.');
112
+ }
85
113
  let resumeInput = null;
86
114
  if (args.resumeSource) {
87
115
  try {
88
- resumeInput = dependencies.readDiffInput
89
- ? await dependencies.readDiffInput(args.resumeSource)
90
- : await context.readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
116
+ resumeInput = dependencies.workflowResumeInput !== undefined
117
+ ? dependencies.workflowResumeInput
118
+ : dependencies.readDiffInput
119
+ ? await dependencies.readDiffInput(args.resumeSource)
120
+ : await context.readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
91
121
  }
92
122
  catch (error) {
93
123
  if (error instanceof CliUsageError)
@@ -98,17 +128,42 @@ async function runWorkflowRecipeCommand(args, dependencies, context) {
98
128
  const document = await runInvestigationRecipe(args.recipe, args.subject, {
99
129
  approveNetwork: args.approveNetwork,
100
130
  selections: args.selections,
131
+ artifactBindings: args.artifactBindings,
132
+ confirmedReviews: args.confirmedReviews,
133
+ ...(args.interactive ? { selectInputs: async (request) => {
134
+ context.writeStderr(`${request.label}\nEnter literal input paths or values. Leave blank to pause; no approval is implied.\n`);
135
+ const answers = [];
136
+ for (const placeholder of request.inputs) {
137
+ const prompt = `${request.stepId} ${placeholder}: `;
138
+ const answer = boundedInteractiveAnswer(await (dependencies.workflowQuestion
139
+ ? dependencies.workflowQuestion(prompt)
140
+ : readBoundedInteractiveLine(prompt, { input: input, output: context.stderr,
141
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}) })));
142
+ if (!answer)
143
+ break;
144
+ answers.push(answer);
145
+ }
146
+ return answers;
147
+ } } : {}),
101
148
  resumeInput,
102
149
  generatedAt: context.now(),
103
150
  ...(dependencies.signal ? { signal: dependencies.signal } : {}),
104
- execute: async (command, stepArguments) => {
151
+ execute: async (command, stepArguments, inputs) => {
105
152
  const stepStdout = createBufferedOutput();
106
- const stepStderr = createBufferedOutput();
107
- const exitCode = await context.executeCli([command, ...stepArguments], {
108
- stdout: stepStdout.stream,
109
- stderr: stepStderr.stream,
110
- });
111
- return { exitCode, stdout: stepStdout.value() };
153
+ const stepStderr = createCliDiagnosticOutput();
154
+ try {
155
+ const exitCode = await context.executeCli([command, ...stepArguments], {
156
+ stdout: stepStdout.stream,
157
+ stderr: stepStderr.stream,
158
+ ...boundWorkflowInputs(command, inputs, dependencies, context),
159
+ });
160
+ return { exitCode, stdout: stepStdout.value() };
161
+ }
162
+ finally {
163
+ const diagnostic = stepStderr.value();
164
+ if (diagnostic)
165
+ context.writeStderr(`${command}: ${diagnostic}\n`);
166
+ }
112
167
  },
113
168
  });
114
169
  if (!args.quiet) {
@@ -116,7 +171,7 @@ async function runWorkflowRecipeCommand(args, dependencies, context) {
116
171
  ? formatJsonDocument(document)
117
172
  : context.terminal(formatInvestigationRun(document), args.color));
118
173
  }
119
- return document.state === 'step_failed' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
174
+ return investigationRunExitCode(document);
120
175
  }
121
176
  const WORKFLOW_COMMAND_HANDLERS = Object.freeze({
122
177
  'monitor-once': runMonitorOnceCommand,
@@ -12,20 +12,8 @@ contains the exhaustive recipient, retention and export metadata.
12
12
 
13
13
  ## Installation
14
14
 
15
- Public releases require Node.js 24 or later. Release verification uses the
16
- exact Node.js 24 maintainer runtime and separately exercises the installed
17
- package on Node.js 26:
18
-
19
- ```bash
20
- npm exec --yes --ignore-scripts --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
21
- npm install --global --ignore-scripts @slicedearth/whoisleuth-cli
22
- whoisleuth doctor
23
- ```
24
-
25
- The scoped package and application share one semantic version. The package
26
- requires no dependency lifecycle scripts and does not call the hosted
27
- WHOISleuth deployment. From a repository checkout, replace `whoisleuth` with
28
- `node bin/whoisleuth.mts`.
15
+ Use the [installation guide](cli.md#installation) for runtime requirements,
16
+ installation and updates. The CLI runs locally without a hosted account.
29
17
 
30
18
  ## Find and inspect commands
31
19
 
@@ -46,6 +34,13 @@ command position as Lookup shorthand. Use explicit `lookup` where input needs
46
34
  supported URL-like normalisation. Credentials, paths, queries, fragments, ports
47
35
  and unsupported special-use targets are rejected by the shorthand.
48
36
 
37
+ Explicit `lookup` normally uses only a pasted URL's hostname. Add
38
+ `--deep --exact-url` to collect that page's path and query instead of the
39
+ homepage; fragments are not sent. `--plan` discloses this scope without making
40
+ requests or copying the URL into the plan. Use stdin to keep a sensitive URL
41
+ out of shell history. Saved request provenance omits queries, but paths and
42
+ page-derived text still require review before sharing.
43
+
49
44
  `registry-scaffold` has a separate fixture contract: its `--profile` selects one
50
45
  fixed fixture profile and shared `--config` profiles are rejected. It produces
51
46
  sanitised local fixture material and makes no registry request.
@@ -63,12 +58,8 @@ disclosures without collecting. Fast is the Lookup default. Deep adds the
63
58
  applicable registration, DNS, HTTP, TLS, page, technology and network context.
64
59
  Optional browser providers are not implicit CLI actions.
65
60
 
66
- `mail-headers` is an offline review of one analyst-selected RFC-style header
67
- block. It reports domain-only identity, header-reported authentication,
68
- exact-domain alignment, and bounded `Received` routing. It retains no address
69
- local parts, display names, subject, body, attachment, or raw header value in
70
- its output. It does not independently validate SPF, DKIM, DMARC, or ARC and
71
- does not treat divergence as proof of abuse.
61
+ The [message-header review](cli.md#message-header-review) is offline and reports
62
+ publisher claims, not independent DNS or cryptographic validation.
72
63
 
73
64
  `dnssec-validate` and `mail-transport` are isolated authorised actions. Both
74
65
  require a selected literal public resolver, a local trust-anchor document and
@@ -77,6 +68,8 @@ transport handles at most three selected MX hosts sequentially, sends `EHLO`
77
68
  and uses `STARTTLS` only when advertised. It does not send mail, authenticate,
78
69
  test relay, enumerate recipients or retry automatically. DNSSEC, TLSA/DANE,
79
70
  PKIX, STARTTLS and SMTP transport remain separate evidence states.
71
+ STARTTLS detection uses the full bounded reply. If the retained capability
72
+ inventory is shortened, the report remains partial and names the omission.
80
73
 
81
74
  The repository-only rendered-capture package is outside hosted and distributable
82
75
  collection. It executes remote page JavaScript only for an explicitly
@@ -91,11 +84,20 @@ refused. Output goes to the terminal unless a command supports `--output`.
91
84
  Private output is written atomically and an existing path is refused unless
92
85
  replacement is explicit.
93
86
 
87
+ `--` ends option processing. All later arguments are literal positional inputs,
88
+ including names such as `--help`. Use `./-evidence.json` for a hyphen-prefixed
89
+ filename when completing paths across shells. Options must precede `--`.
90
+
94
91
  Terminal text is the default. JSON, JSONL, CSV, Markdown, HTML and domain-only
95
92
  formats are available only where the installed command declares them.
96
93
  Redirected and machine output contains no ANSI or progress text. Diagnostics
97
94
  and optional target-free `--events` output use standard error.
98
95
 
96
+ For `bulk` and `discover-scan`, `--csv-with-metadata` adds the source schema and
97
+ version, observation and report times, collection origin, scan mode, diagnostic
98
+ version and source-health states. Missing clocks remain `unknown`; a null
99
+ source state is unmeasured, not a negative result. `--csv` keeps the compact columns.
100
+
99
101
  `--fail-on` and `--strict-exit` expose selected evidence states to automation
100
102
  without changing the result document. Review focused help for the policies a
101
103
  command supports.
@@ -116,16 +118,18 @@ Readers accept only their declared public and current versions. Unknown,
116
118
  unreleased historical and future schemas fail before partial interpretation;
117
119
  an invalid import is not treated as an empty document.
118
120
 
119
- `inspect-archive` reads current workspace archive v8 and exact versions 5, 6 and 7. It
121
+ `inspect-archive` reads current workspace archive v9 and exact versions 5–8. It
120
122
  reports section metadata and digest-only search results unless `--reveal` is
121
123
  explicitly selected. It never searches notes, contacts or arbitrary raw fields.
122
124
 
123
125
  `export` reads supported saved Lookup v1 or v2 and writes current Lookup
124
- evidence schema 28. Published v2 schema 27 and exact v1 schema 26 remain
125
- readable. Versions 27 and 28 exclude raw registration payloads, expanded
126
+ evidence schema 29. Published v2 schemas 27 and 28 and exact v1 schema 26 remain
127
+ readable. Versions 27–29 exclude raw registration payloads, expanded
126
128
  contacts, credentials, complete query-bearing URLs and provider payloads;
127
- schema 28 can also retain the bounded, separately attributed registrar-standing
128
- projection shown by Lookup.
129
+ schemas 28 and 29 also retain the bounded registrar-standing projection.
130
+ Schema 29 identifies the hostname used for DNS, TLS and web observations,
131
+ separately from the registrable domain used by registration sources. Older
132
+ evidence retains its original collection scope.
129
133
 
130
134
  `verify-artifact` checks a recognised structure and its applicable integrity
131
135
  contract. `interchange-report` describes retained and omitted fields.
@@ -133,6 +137,45 @@ contract. `interchange-report` describes retained and omitted fields.
133
137
  validity and signer trust separate. None of these checks establishes that
134
138
  evidence is accurate, current, safe to share or attributable to a person.
135
139
 
140
+ ### Signer trust
141
+
142
+ Use `verify-signature package.json --trust-store-file trust.json --json` to
143
+ check an explicitly selected fingerprint policy offline. The result contains
144
+ the ordinary signature verification and a separate current trust decision.
145
+ Only a matching `trusted` entry succeeds; unknown, `retired`, `revoked` or
146
+ future-reviewed entries return exit code 4, including with `--quiet`.
147
+ Malformed files return 2. Without this option, the existing verification
148
+ output is unchanged. A supplied `--public-key-file` must also match.
149
+
150
+ The trust file contains no keys. For example:
151
+
152
+ ```json
153
+ {
154
+ "schema": "whoisleuth.evidence-signer-trust-store",
155
+ "version": 1,
156
+ "entries": [{
157
+ "keyIdSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
158
+ "label": "Evidence reviewer",
159
+ "status": "trusted",
160
+ "updatedAt": "2026-09-01T00:00:00.000Z",
161
+ "note": "Fingerprint confirmed through the established contact channel."
162
+ }]
163
+ }
164
+ ```
165
+
166
+ Replace the example fingerprint with the SHA-256 fingerprint of the signer's
167
+ SPKI DER public-key bytes, confirmed through an authenticated channel—not
168
+ merely copied from the package being checked. Review rotation and revocation
169
+ updates through that channel. On rotation, mark the old entry `retired` and
170
+ optionally record `successorKeyIdSha256`; independently confirm and add the
171
+ replacement entry. Use `revoked` for a withdrawn trust decision. Neither a
172
+ successor link nor a claimed signing date overrides current status.
173
+
174
+ Files support 1,024 distinct entries within 4 MiB. Labels and single-line notes
175
+ allow 160 and 2,048 characters respectively. Reports include only the matching
176
+ entry and the exact file digest. Keep the file and any saved reports under your
177
+ own retention policy. No automatic key discovery, key storage or trust refresh occurs.
178
+
136
179
  ## Command-family boundaries
137
180
 
138
181
  - Evidence collection commands retain separate source states. A failed or
@@ -144,17 +187,20 @@ evidence is accurate, current, safe to share or attributable to a person.
144
187
  - Bulk applies one declared collection contract per target. Fast accepts up to
145
188
  500 targets and Deep up to 50; each target remains a separate request.
146
189
  - Respond commands package browser-created Cases and prepare local packets,
147
- reports and sharing reviews. They do not create durable Cases, submit,
148
- publish, notify or grant recipient authorisation.
190
+ reports and sharing reviews. The offline [`case` workflow](cli.md#local-case-files)
191
+ also creates and updates ordinary local Case files through explicit, conflict-checked
192
+ output. Neither workflow submits, publishes, notifies or grants recipient authorisation.
149
193
  - Assurance, comparison and calibration commands describe supplied or retained
150
194
  evidence. They do not tune the running model, change infrastructure or turn an
151
195
  analyst label into observed truth.
152
196
  - `workflow-plan` lists fixed installed recipes without executing them.
153
197
  `workflow-run` executes only installed steps, requires approval for network
154
198
  work and pauses at unresolved analyst selections. Repeat
155
- `--select <step-id>=<path-or-value>` in placeholder order to resume a selected
156
- step; version-1 checkpoints remain readable and version 2 retains the exact
157
- selections in the local checkpoint.
199
+ `--select <step-id>=<path-or-value>` for remaining placeholders in order, or bind
200
+ a compatible earlier output with `--use-artifact <step-id>:<input-number>=<earlier-step-id>`.
201
+ Checkpoints retain exact selections and bindings and distinguish incomplete
202
+ collection from retryable failures; see [resuming a fixed workflow](cli.md#resuming-a-fixed-workflow)
203
+ for the supported versions and resume behaviour.
158
204
 
159
205
  Use the installed focused help for positional inputs, exact ceilings, options,
160
206
  network effects, outputs and command-specific exit behaviour.