@slicedearth/whoisleuth-cli 2.2.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -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 +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  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 +112 -72
  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-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -0,0 +1,114 @@
1
+ import { MAX_EVIDENCE_PER_TECHNOLOGY, MAX_LIBRARY_FINDINGS, MAX_TECHNOLOGY_FINDINGS, } from "../../lib/lookup-child-profile-contract.mjs";
2
+ import { TECHNOLOGY_EVIDENCE_ROLE_ORDER, technologyEvidenceRoles, } from "../../lib/technology-evidence-role.mjs";
3
+ import { MAX_OBSERVATION_LIMITATIONS, readObservationTime } from "../../packages/evidence/observation.mjs";
4
+ import { boundedTerminalList, safeTerminalValue, terminalCount, terminalRecord, titleCase } from "./terminal-shared.mjs";
5
+ const ROLE_LABELS = {
6
+ observed_edge: 'Observed edge',
7
+ application_platform: 'App platform',
8
+ framework_runtime: 'Framework/run',
9
+ embedded_dependency: 'Embedded deps',
10
+ };
11
+ const SUMMARY_FINDINGS = 6;
12
+ const SUMMARY_NAMESERVERS = 5;
13
+ function observationLine(label, value, generatedAt) {
14
+ const time = readObservationTime(value, generatedAt);
15
+ if (!time.observedAt)
16
+ return `${label.padEnd(15)}Observed time unavailable`;
17
+ const age = time.ageDays === null ? 'age unavailable'
18
+ : time.ageDays === 0 ? 'less than a day old at output generation'
19
+ : `${time.ageDays} day${time.ageDays === 1 ? '' : 's'} old at output generation`;
20
+ return `${label.padEnd(15)}${time.observedAt} · ${age}`;
21
+ }
22
+ function appendLimitations(lines, value) {
23
+ if (!Array.isArray(value))
24
+ return;
25
+ for (const limitation of value.slice(0, MAX_OBSERVATION_LIMITATIONS)) {
26
+ lines.push(` Limit ${safeTerminalValue(limitation)}`);
27
+ }
28
+ }
29
+ /** Presents admitted source records; it never collects or infers a hidden origin. */
30
+ export function appendTechnologyLines(lines, input) {
31
+ const { technology, libraries, nameservers, generatedAt, detail } = input;
32
+ if (!technology.status && technology.source !== 'derived')
33
+ return;
34
+ const suppliedFindings = Array.isArray(technology.findings) ? technology.findings : [];
35
+ const findings = suppliedFindings.slice(0, MAX_TECHNOLOGY_FINDINGS).map((value) => {
36
+ const finding = terminalRecord(value);
37
+ return { finding, roles: technologyEvidenceRoles(finding) };
38
+ });
39
+ lines.push(`Technology ${titleCase(technology.status)} · ${findings.length} indicator${findings.length === 1 ? '' : 's'}`);
40
+ if (suppliedFindings.length > findings.length)
41
+ lines.push('Technology Additional findings exceed the admitted profile limit');
42
+ if (detail === 'summary')
43
+ return;
44
+ lines.push(observationLine('Tech observed', technology.observedAt, generatedAt));
45
+ if (detail === 'standard' && findings.length) {
46
+ const visible = findings.slice(0, SUMMARY_FINDINGS).map(({ finding }) => {
47
+ const qualifiers = [finding.category, finding.confidence ? `${safeTerminalValue(finding.confidence)} signature strength` : null]
48
+ .filter(Boolean).map((value) => safeTerminalValue(value));
49
+ return `${safeTerminalValue(finding.name, 'Unnamed indicator')}${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`;
50
+ });
51
+ lines.push(`Indicators ${boundedTerminalList(visible, findings.length - visible.length)}`);
52
+ }
53
+ const retainedNameservers = Array.isArray(nameservers) ? nameservers : [];
54
+ const visibleNameservers = retainedNameservers.slice(0, SUMMARY_NAMESERVERS).map((value) => safeTerminalValue(value));
55
+ lines.push(`Nameservers ${visibleNameservers.length ? boundedTerminalList(visibleNameservers, retainedNameservers.length - visibleNameservers.length) : 'Unavailable'} · identity does not establish operator or web-host ownership`);
56
+ for (const role of TECHNOLOGY_EVIDENCE_ROLE_ORDER) {
57
+ const matching = findings.filter((item) => item.roles.includes(role));
58
+ const names = matching.slice(0, SUMMARY_FINDINGS).map(({ finding }) => safeTerminalValue(finding.name, 'Unnamed indicator'));
59
+ lines.push(`${ROLE_LABELS[role].padEnd(15)}${names.length ? boundedTerminalList(names, matching.length - names.length) : 'None retained'}`);
60
+ }
61
+ lines.push('Origin host Not established from retained evidence');
62
+ if (detail === 'verbose') {
63
+ for (const { finding, roles } of findings) {
64
+ lines.push(`Indicator ${safeTerminalValue(finding.name, 'Unnamed indicator')}`);
65
+ lines.push(` Role ${roles.map((role) => ROLE_LABELS[role]).join(', ') || 'Not retained'}`);
66
+ lines.push(` Strength ${safeTerminalValue(finding.confidence, 'Unknown')} signature strength`);
67
+ const suppliedEvidence = Array.isArray(finding.evidence) ? finding.evidence : [];
68
+ const evidence = suppliedEvidence.slice(0, MAX_EVIDENCE_PER_TECHNOLOGY);
69
+ if (!evidence.length)
70
+ lines.push(' Signal No detailed signal was retained');
71
+ for (const value of evidence) {
72
+ const item = terminalRecord(value);
73
+ if (typeof item.source !== 'string' || !item.source.trim()
74
+ || typeof item.description !== 'string' || !item.description.trim()) {
75
+ lines.push(' Signal Incomplete retained signal record');
76
+ continue;
77
+ }
78
+ const role = TECHNOLOGY_EVIDENCE_ROLE_ORDER.find((candidate) => candidate === item.role);
79
+ lines.push(` Source ${safeTerminalValue(item.source, 'Not retained')} · ${role ? ROLE_LABELS[role] : 'Role not retained'}`);
80
+ lines.push(` Signal ${safeTerminalValue(item.description, 'Not retained')}`);
81
+ }
82
+ if (suppliedEvidence.length > evidence.length)
83
+ lines.push(' Limit Additional signals exceed the admitted profile limit');
84
+ }
85
+ appendLimitations(lines, technology.limitations);
86
+ }
87
+ if (libraries.profileVersion !== 1 && libraries.profileVersion !== 2 && libraries.source !== 'derived')
88
+ return;
89
+ const suppliedLibraries = Array.isArray(libraries.findings) ? libraries.findings : [];
90
+ const libraryFindings = suppliedLibraries.slice(0, MAX_LIBRARY_FINDINGS).map(terminalRecord);
91
+ const advisoryMatches = libraryFindings.filter((finding) => terminalCount(finding.advisoryCount) > 0).length;
92
+ lines.push(`JS libraries ${titleCase(libraries.status)} · ${libraryFindings.length} apparent · ${advisoryMatches} with catalogue advisory match${advisoryMatches === 1 ? '' : 'es'}`);
93
+ lines.push(observationLine('Lib observed', libraries.observedAt, generatedAt));
94
+ if (suppliedLibraries.length > libraryFindings.length)
95
+ lines.push('JS libraries Additional findings exceed the admitted profile limit');
96
+ if (detail !== 'verbose')
97
+ return;
98
+ const catalogue = terminalRecord(libraries.catalog);
99
+ lines.push(`Catalogue ${safeTerminalValue(catalogue.name, 'Component catalogue')} · ${safeTerminalValue(catalogue.version, 'Version unavailable')}`);
100
+ for (const library of libraryFindings) {
101
+ lines.push(`Library ${safeTerminalValue(library.name, 'Unnamed library')} · ${safeTerminalValue(library.apparentVersion, 'Version unavailable')}`);
102
+ const methods = Array.isArray(library.detectionMethods) ? library.detectionMethods.slice(0, 4).map((value) => safeTerminalValue(value)) : [];
103
+ lines.push(` Detected by ${methods.join(', ') || 'Not retained'}`);
104
+ lines.push(` Advisories ${terminalCount(library.advisoryCount)} catalogue matches`);
105
+ if (library.highestSeverity)
106
+ lines.push(` Severity ${safeTerminalValue(library.highestSeverity)}`);
107
+ for (const [field, label] of [['advisoryIdentifiers', 'Identifier'], ['knownExploitedIdentifiers', 'Known exploited']]) {
108
+ const values = Array.isArray(library[field]) ? library[field] : [];
109
+ for (const value of values.slice(0, 16))
110
+ lines.push(` ${label.padEnd(15)} ${safeTerminalValue(value)}`);
111
+ }
112
+ }
113
+ appendLimitations(lines, libraries.limitations);
114
+ }
@@ -17,8 +17,10 @@ const REVIEW_INLINE_COMMANDS = Object.freeze([
17
17
  'compare',
18
18
  'page-compare',
19
19
  'mail-review',
20
+ 'mail-headers',
20
21
  'review-evidence',
21
22
  'brief',
23
+ 'case',
22
24
  'case-pack',
23
25
  ]);
24
26
  const ASSURANCE_INLINE_COMMANDS = Object.freeze([
package/cli/input.mjs CHANGED
@@ -1,6 +1,46 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { constants } from 'node:fs';
3
+ import { open } from 'node:fs/promises';
2
4
  import { decodeBoundedUtf8, readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
3
5
  import { CliUsageError } from "./errors.mjs";
6
+ function headerEnd(bytes) {
7
+ for (let index = 0; index < bytes.length - 1; index += 1) {
8
+ if (bytes[index] === 0x0a && bytes[index + 1] === 0x0a)
9
+ return index;
10
+ if (index < bytes.length - 3
11
+ && bytes[index] === 0x0d && bytes[index + 1] === 0x0a
12
+ && bytes[index + 2] === 0x0d && bytes[index + 3] === 0x0a)
13
+ return index;
14
+ }
15
+ return -1;
16
+ }
17
+ function decodeHeaderStorage(storage, length, maximumBytes, label) {
18
+ const end = headerEnd(storage.subarray(0, length));
19
+ if ((end < 0 && length > maximumBytes) || end > maximumBytes) {
20
+ throw new CliUsageError(`${label} is limited to ${maximumBytes} bytes before the message body.`);
21
+ }
22
+ return decodeBoundedUtf8(storage.subarray(0, end < 0 ? length : end), label);
23
+ }
24
+ function appendUntilHeaderEnd(storage, offset, input) {
25
+ let nextOffset = offset;
26
+ for (const byte of input) {
27
+ if (nextOffset >= storage.length)
28
+ return Object.freeze({ offset: nextOffset, complete: false });
29
+ storage[nextOffset] = byte;
30
+ nextOffset += 1;
31
+ const lfBoundary = nextOffset >= 2
32
+ && storage[nextOffset - 2] === 0x0a
33
+ && storage[nextOffset - 1] === 0x0a;
34
+ const crlfBoundary = nextOffset >= 4
35
+ && storage[nextOffset - 4] === 0x0d
36
+ && storage[nextOffset - 3] === 0x0a
37
+ && storage[nextOffset - 2] === 0x0d
38
+ && storage[nextOffset - 1] === 0x0a;
39
+ if (lfBoundary || crlfBoundary)
40
+ return Object.freeze({ offset: nextOffset, complete: true });
41
+ }
42
+ return Object.freeze({ offset: nextOffset, complete: false });
43
+ }
4
44
  function abortError(signal) {
5
45
  return signal.reason instanceof Error
6
46
  ? signal.reason
@@ -60,3 +100,79 @@ export async function readCliTextInput(source, stdin, options) {
60
100
  options.signal?.removeEventListener('abort', onAbort);
61
101
  }
62
102
  }
103
+ /**
104
+ * Read only the RFC-style header block from a selected regular file or stdin.
105
+ * The fixed retained allocation includes delimiter look-ahead but accumulates
106
+ * only bytes through the first header/body boundary. A regular file may be
107
+ * larger than the header limit while parsing stays tied to the admitted prefix.
108
+ */
109
+ export async function readCliHeaderInput(source, stdin, options) {
110
+ if (!Number.isSafeInteger(options.maximumBytes) || options.maximumBytes < 1 || options.maximumBytes > 1024 * 1024) {
111
+ throw new TypeError('Header input has an invalid byte boundary.');
112
+ }
113
+ if (options.signal?.aborted)
114
+ throw abortError(options.signal);
115
+ const storage = Buffer.allocUnsafe(options.maximumBytes + 4);
116
+ if (source) {
117
+ let handle;
118
+ try {
119
+ handle = await open(source, constants.O_RDONLY | constants.O_NONBLOCK);
120
+ const before = await handle.stat();
121
+ if (!before.isFile())
122
+ throw new CliUsageError(`${options.label} must be a regular file.`);
123
+ let offset = 0;
124
+ let complete = false;
125
+ const chunk = Buffer.allocUnsafe(Math.min(4 * 1024, storage.length));
126
+ while (offset < storage.length && !complete) {
127
+ if (options.signal?.aborted)
128
+ throw abortError(options.signal);
129
+ const result = await handle.read(chunk, 0, Math.min(chunk.length, storage.length - offset), null);
130
+ if (result.bytesRead === 0)
131
+ break;
132
+ ({ offset, complete } = appendUntilHeaderEnd(storage, offset, chunk.subarray(0, result.bytesRead)));
133
+ }
134
+ chunk.fill(0);
135
+ const after = await handle.stat();
136
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
137
+ throw new CliUsageError(`${options.label} changed while it was being read.`);
138
+ }
139
+ return decodeHeaderStorage(storage, offset, options.maximumBytes, options.label);
140
+ }
141
+ catch (error) {
142
+ if (error instanceof CliUsageError || options.signal?.aborted)
143
+ throw error;
144
+ const message = error instanceof Error ? error.message : `${options.label} could not be read.`;
145
+ throw new CliUsageError(message);
146
+ }
147
+ finally {
148
+ await handle?.close();
149
+ }
150
+ }
151
+ if (!stdin || stdin.isTTY)
152
+ return '';
153
+ let offset = 0;
154
+ const onAbort = () => {
155
+ if ('destroy' in stdin && typeof stdin.destroy === 'function')
156
+ stdin.destroy(abortError(options.signal));
157
+ };
158
+ options.signal?.addEventListener('abort', onAbort, { once: true });
159
+ try {
160
+ for await (const chunk of stdin) {
161
+ if (options.signal?.aborted)
162
+ throw abortError(options.signal);
163
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
164
+ const retained = appendUntilHeaderEnd(storage, offset, bytes);
165
+ offset = retained.offset;
166
+ if (retained.complete)
167
+ break;
168
+ if (offset >= storage.length)
169
+ break;
170
+ }
171
+ if (options.signal?.aborted)
172
+ throw abortError(options.signal);
173
+ return decodeHeaderStorage(storage, offset, options.maximumBytes, options.label);
174
+ }
175
+ finally {
176
+ options.signal?.removeEventListener('abort', onAbort);
177
+ }
178
+ }
@@ -1,171 +1,13 @@
1
1
  import { classifyQuery, isDirectLookupTarget } from "../lib/classify.mjs";
2
- import { StringDecoder } from 'node:string_decoder';
3
- import { boundedCliErrorMessage, CliUsageError, hasUnsafeCliText } from "./errors.mjs";
2
+ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
4
3
  import { INVESTIGATION_PLAN_RECIPE_LABELS, INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
5
4
  import { buildCliLookupPlan, formatCliLookupPlan } from "./lookup-plan.mjs";
6
5
  import { canBrowseLookup, } from "./lookup-browser.mjs";
7
- const MAX_INTERACTIVE_ANSWER_SCALARS = 1_024;
8
- const MAX_INTERACTIVE_ANSWER_BYTES = 4_096;
6
+ import { MAX_INTERACTIVE_ANSWER_BYTES, MAX_INTERACTIVE_ANSWER_SCALARS, boundedInteractiveAnswer, readBoundedInteractiveLine } from "./terminal-input.mjs";
9
7
  function canLaunchInteractiveCli(input, output, environment = process.env) {
10
8
  return input?.isTTY === true
11
9
  && canBrowseLookup(input, output, environment);
12
10
  }
13
- function boundedInteractiveAnswer(value) {
14
- const supplied = typeof value === 'string' ? value : '';
15
- if (Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES
16
- || Array.from(supplied).length > MAX_INTERACTIVE_ANSWER_SCALARS
17
- || hasUnsafeCliText(supplied)) {
18
- throw new CliUsageError('Interactive input must be bounded text without control characters.');
19
- }
20
- return supplied.trim();
21
- }
22
- function abortError() {
23
- return new DOMException('Aborted', 'AbortError');
24
- }
25
- function readBoundedInteractiveLine(prompt, options) {
26
- const { input, output, signal } = options;
27
- if (signal?.aborted)
28
- return Promise.reject(signal.reason || abortError());
29
- const previousRaw = input.isRaw === true;
30
- const wasPaused = input.isPaused?.() === true;
31
- let receivedBytes = 0;
32
- let receivedScalars = 0;
33
- let settled = false;
34
- let characters = [];
35
- const decoder = new StringDecoder('utf8');
36
- return new Promise((resolve, reject) => {
37
- const cleanup = () => {
38
- let failure = null;
39
- const attempt = (operation) => {
40
- try {
41
- operation();
42
- }
43
- catch (error) {
44
- failure ||= error instanceof Error ? error : new Error('Interactive terminal cleanup failed.');
45
- }
46
- };
47
- attempt(() => input.off?.('data', onData));
48
- attempt(() => input.off?.('end', onEnd));
49
- attempt(() => signal?.removeEventListener('abort', onAbort));
50
- attempt(() => input.setRawMode?.(previousRaw));
51
- if (wasPaused)
52
- attempt(() => input.pause?.());
53
- return failure;
54
- };
55
- const finish = (value, error) => {
56
- if (settled)
57
- return;
58
- settled = true;
59
- const cleanupFailure = cleanup();
60
- characters = [];
61
- if (error && cleanupFailure) {
62
- reject(new AggregateError([error, cleanupFailure], `${error.message}; terminal cleanup also failed: ${cleanupFailure.message}`));
63
- }
64
- else if (error || cleanupFailure)
65
- reject(error || cleanupFailure);
66
- else
67
- resolve(value || '');
68
- };
69
- const rejectUsage = () => finish(undefined, new CliUsageError('Interactive input must be bounded text without control characters.'));
70
- const onData = (chunk) => {
71
- if (settled)
72
- return;
73
- let rawChunk;
74
- if (Buffer.isBuffer(chunk)) {
75
- if (chunk.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
76
- rejectUsage();
77
- return;
78
- }
79
- rawChunk = chunk;
80
- }
81
- else {
82
- const supplied = String(chunk ?? '');
83
- if (supplied.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes
84
- || Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
85
- rejectUsage();
86
- return;
87
- }
88
- rawChunk = Buffer.from(supplied, 'utf8');
89
- }
90
- receivedBytes += rawChunk.length;
91
- const value = decoder.write(rawChunk);
92
- receivedScalars += Array.from(value).length;
93
- if (receivedBytes > MAX_INTERACTIVE_ANSWER_BYTES
94
- || receivedScalars > MAX_INTERACTIVE_ANSWER_SCALARS
95
- || value.includes('\ufffd')) {
96
- rejectUsage();
97
- return;
98
- }
99
- for (const character of value) {
100
- if (character === '\r' || character === '\n') {
101
- try {
102
- output.write('\n');
103
- }
104
- catch (error) {
105
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
106
- return;
107
- }
108
- finish(characters.join(''));
109
- return;
110
- }
111
- if (character === '\u0003' || character === '\u0004' || character === '\u001b') {
112
- finish(undefined, abortError());
113
- return;
114
- }
115
- if (character === '\u0008' || character === '\u007f') {
116
- if (characters.length) {
117
- characters.pop();
118
- try {
119
- output.write('\b \b');
120
- }
121
- catch (error) {
122
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
123
- return;
124
- }
125
- }
126
- continue;
127
- }
128
- if (hasUnsafeCliText(character)) {
129
- rejectUsage();
130
- return;
131
- }
132
- characters.push(character);
133
- try {
134
- output.write(character);
135
- }
136
- catch (error) {
137
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
138
- return;
139
- }
140
- }
141
- };
142
- const onEnd = () => {
143
- const remainder = decoder.end();
144
- if (remainder)
145
- rejectUsage();
146
- else
147
- finish(undefined, abortError());
148
- };
149
- const onAbort = () => finish(undefined, signal?.reason instanceof Error ? signal.reason : abortError());
150
- try {
151
- output.write(prompt);
152
- input.on?.('data', onData);
153
- input.on?.('end', onEnd);
154
- signal?.addEventListener('abort', onAbort, { once: true });
155
- if (signal?.aborted) {
156
- onAbort();
157
- return;
158
- }
159
- input.setRawMode?.(true);
160
- if (settled)
161
- return;
162
- input.resume?.();
163
- }
164
- catch (error) {
165
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal input failed.'));
166
- }
167
- });
168
- }
169
11
  async function launchInteractiveCli(options) {
170
12
  const environment = options.environment || process.env;
171
13
  if (!canLaunchInteractiveCli(options.input, options.output, environment)) {
@@ -1,12 +1,11 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { INTERCHANGE_ARTIFACT_CONTRACTS, } from "../lib/interchange-fidelity-registry.mjs";
3
- import { offlineArtifactSatisfiesAssurance, verifyOfflineArtifact, } from "./artifact-verify.mjs";
3
+ import { offlineArtifactSatisfiesAssurance, MAX_OFFLINE_ARTIFACT_BYTES, parseOfflineArtifactJson, verifyOfflineArtifact, } from "./artifact-verify.mjs";
4
4
  import { mergeBrandProfiles, } from "../packages/workspace/brand-profile-model.mjs";
5
- import { parseBoundedJsonObject } from "./bounded-json.mjs";
6
5
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
7
6
  export const INTERCHANGE_FIDELITY_REPORT_SCHEMA = 'whoisleuth.interchange-fidelity-report';
8
7
  export const INTERCHANGE_FIDELITY_REPORT_VERSION = 2;
9
- export const MAX_INTERCHANGE_REPORT_BYTES = 15 * 1024 * 1024;
8
+ export const MAX_INTERCHANGE_REPORT_BYTES = MAX_OFFLINE_ARTIFACT_BYTES;
10
9
  function record(value) {
11
10
  return value && typeof value === 'object' && !Array.isArray(value)
12
11
  ? value
@@ -19,10 +18,7 @@ function parseInput(raw) {
19
18
  if (bytes < 1 || bytes > MAX_INTERCHANGE_REPORT_BYTES) {
20
19
  throw new TypeError(`Interchange input must be between 1 byte and ${MAX_INTERCHANGE_REPORT_BYTES} bytes.`);
21
20
  }
22
- return parseBoundedJsonObject(raw.replace(/^\uFEFF/u, ''), {
23
- label: 'Interchange input',
24
- maximumBytes: MAX_INTERCHANGE_REPORT_BYTES,
25
- });
21
+ return parseOfflineArtifactJson(raw.replace(/^\uFEFF/u, ''));
26
22
  }
27
23
  function nestedRecord(value, path) {
28
24
  let current = value;
@@ -123,7 +119,7 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
123
119
  }
124
120
  else {
125
121
  const verification = await verifyOfflineArtifact(raw, { passphrase: options.passphrase ?? null });
126
- verificationState = verification.state;
122
+ verificationState = verification.state === 'partial' ? 'not_verified' : verification.state;
127
123
  assuranceSatisfied = contract.requiredAssurance === 'whole_integrity'
128
124
  ? offlineArtifactSatisfiesAssurance(verification, 'whole_integrity')
129
125
  : contract.requiredAssurance === 'applicable_integrity'
@@ -198,6 +194,19 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
198
194
  export function formatInterchangeFidelityReport(report) {
199
195
  const browser = report.compatibility.browser;
200
196
  const cli = report.compatibility.cli;
197
+ const nextAction = !report.recognised
198
+ ? 'Choose a supported WHOISleuth export and run verify-artifact before trying to import it.'
199
+ : !report.artifact.versionSupported
200
+ ? 'Use a WHOISleuth release that supports this exact version, or re-export it with its last supported release.'
201
+ : !report.verification.assuranceSatisfied
202
+ ? 'Do not import or share this artefact yet; run verify-artifact and resolve the reported structure or integrity failure.'
203
+ : report.compatibility.fullyImportable === false
204
+ ? 'Review unsupported, blocked, skipped or pruned records before selecting any import.'
205
+ : browser?.import === 'supported'
206
+ ? 'Open the browser import preview and review the proposed additions and updates before applying them.'
207
+ : cli?.read === 'supported'
208
+ ? 'Use the declared offline CLI reader; browser import is not supported for this artefact.'
209
+ : 'Retain the verification result and use only the operations declared above.';
201
210
  return [
202
211
  'Interchange fidelity report',
203
212
  `Recognised ${report.recognised ? 'yes' : 'no'}`,
@@ -219,6 +228,7 @@ export function formatInterchangeFidelityReport(report) {
219
228
  '',
220
229
  `Preserved ${report.compatibility.preservedFieldGroups.join(', ') || 'none declared'}`,
221
230
  `Excluded ${report.compatibility.excludedFieldGroups.join(', ') || 'none declared'}`,
231
+ `Next action ${nextAction}`,
222
232
  '',
223
233
  ...report.limitations.map((item) => `Limitation: ${item}`),
224
234
  '',
@@ -0,0 +1,176 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { CLI_LOOKUP_SCHEMA, CLI_LOOKUP_VERSION } from "../packages/contracts/cli-lookup.mjs";
4
+ import { DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION } from "../packages/contracts/domain-control-review.mjs";
5
+ import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
6
+ import { validateDomainControlReviewDocument } from "../lib/domain-control-manifest.mjs";
7
+ import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, serializeLookupEvidence } from "../lib/evidence-export.mjs";
8
+ import { OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, OFFLINE_ARTIFACT_VERIFICATION_VERSION } from "./artifact-verify.mjs";
9
+ import { validateLookupEvidenceArtifactStructure } from "./artifact-validation/lookup-evidence.mjs";
10
+ import { CLI_DISCOVERY_SCAN_SCHEMA, CLI_DISCOVERY_SCAN_VERSION } from "./discovery-scan.mjs";
11
+ import { CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, } from "./formatters/json.mjs";
12
+ import { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION } from "./lookup-brief.mjs";
13
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, parseExternalFindingsDocument } from "../packages/interchange/external-findings-import.mjs";
14
+ import { SOURCE_RELIABILITY_REPORT_SCHEMA, SOURCE_RELIABILITY_REPORT_VERSION } from "./source-reliability.mjs";
15
+ import { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, verifyCliCasePack } from "./case-pack.mjs";
16
+ import { SHARING_REVIEW_SCHEMA, SHARING_REVIEW_VERSION } from "./sharing-review.mjs";
17
+ import { DOMAIN_ASSURANCE_SCHEMA, DOMAIN_ASSURANCE_VERSION } from "../lib/domain-assurance.mjs";
18
+ import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION } from "../lib/domain-change-packet.mjs";
19
+ import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION } from "../packages/contracts/domain-control-monitor.mjs";
20
+ import { CLI_LOOKUP_DIFF_SCHEMA, CLI_LOOKUP_DIFF_VERSION } from "./lookup-diff.mjs";
21
+ import { CLI_LOOKUP_TIMELINE_SCHEMA, CLI_LOOKUP_TIMELINE_VERSION } from "./lookup-timeline.mjs";
22
+ import { normalizeCliLookupDocument } from "./saved-lookup.mjs";
23
+ import { CliUsageError } from "./errors.mjs";
24
+ import { MAX_INVESTIGATION_RUN_SELECTIONS } from "../packages/contracts/investigation-run.mjs";
25
+ // This is a fixed-recipe boundary, not a general command or format registry.
26
+ // Non-reusable reports have their envelope checked; reusable evidence also
27
+ // passes its existing input validator before it can enter another command.
28
+ export function validateWorkflowResult(step, result, retained) {
29
+ if (!result || typeof result !== 'object' || Array.isArray(result))
30
+ throw new TypeError('Expected a JSON document.');
31
+ const value = result;
32
+ let schema;
33
+ let version;
34
+ switch (step.command) {
35
+ case 'lookup':
36
+ schema = CLI_LOOKUP_SCHEMA;
37
+ version = CLI_LOOKUP_VERSION;
38
+ normalizeCliLookupDocument(value, { label: 'Workflow Lookup result' });
39
+ break;
40
+ case 'export':
41
+ schema = LOOKUP_EVIDENCE_SCHEMA;
42
+ version = LOOKUP_EVIDENCE_SCHEMA_VERSION;
43
+ validateLookupEvidenceArtifactStructure(value);
44
+ serializeLookupEvidence(value);
45
+ break;
46
+ case 'domain-control':
47
+ schema = DOMAIN_CONTROL_REVIEW_SCHEMA;
48
+ version = DOMAIN_CONTROL_REVIEW_VERSION;
49
+ validateDomainControlReviewDocument(value);
50
+ break;
51
+ case 'case-pack':
52
+ schema = CLI_CASE_PACK_SCHEMA;
53
+ version = CLI_CASE_PACK_VERSION;
54
+ verifyCliCasePack(value);
55
+ break;
56
+ case 'ct-search':
57
+ schema = CLI_CT_SEARCH_SCHEMA;
58
+ version = CLI_CT_SEARCH_SCHEMA_VERSION;
59
+ break;
60
+ case 'ct-intake':
61
+ schema = EXTERNAL_FINDINGS_SCHEMA;
62
+ version = EXTERNAL_FINDINGS_VERSION;
63
+ parseExternalFindingsDocument(value);
64
+ break;
65
+ case 'brief':
66
+ schema = CLI_LOOKUP_BRIEF_SCHEMA;
67
+ version = CLI_LOOKUP_BRIEF_VERSION;
68
+ break;
69
+ case 'compare':
70
+ schema = CLI_COMPARE_SCHEMA;
71
+ version = CLI_COMPARE_SCHEMA_VERSION;
72
+ break;
73
+ case 'source-report':
74
+ schema = SOURCE_RELIABILITY_REPORT_SCHEMA;
75
+ version = SOURCE_RELIABILITY_REPORT_VERSION;
76
+ break;
77
+ case 'sharing-review':
78
+ schema = SHARING_REVIEW_SCHEMA;
79
+ version = SHARING_REVIEW_VERSION;
80
+ break;
81
+ case 'assurance':
82
+ schema = DOMAIN_ASSURANCE_SCHEMA;
83
+ version = DOMAIN_ASSURANCE_VERSION;
84
+ break;
85
+ case 'change-packet':
86
+ schema = DOMAIN_CHANGE_PACKET_SCHEMA;
87
+ version = DOMAIN_CHANGE_PACKET_VERSION;
88
+ break;
89
+ case 'monitor-once':
90
+ schema = CLI_DOMAIN_CONTROL_MONITOR_SCHEMA;
91
+ version = CLI_DOMAIN_CONTROL_MONITOR_VERSION;
92
+ break;
93
+ case 'discover':
94
+ schema = CLI_DISCOVER_SCHEMA;
95
+ version = CLI_DISCOVER_SCHEMA_VERSION;
96
+ break;
97
+ case 'discover-scan':
98
+ schema = CLI_DISCOVERY_SCAN_SCHEMA;
99
+ version = CLI_DISCOVERY_SCAN_VERSION;
100
+ break;
101
+ case 'posture':
102
+ schema = CLI_POSTURE_SCHEMA;
103
+ version = CLI_POSTURE_SCHEMA_VERSION;
104
+ break;
105
+ case 'verify-artifact':
106
+ schema = OFFLINE_ARTIFACT_VERIFICATION_SCHEMA;
107
+ version = OFFLINE_ARTIFACT_VERIFICATION_VERSION;
108
+ break;
109
+ case 'diff':
110
+ schema = CLI_LOOKUP_DIFF_SCHEMA;
111
+ version = CLI_LOOKUP_DIFF_VERSION;
112
+ break;
113
+ case 'timeline':
114
+ schema = CLI_LOOKUP_TIMELINE_SCHEMA;
115
+ version = CLI_LOOKUP_TIMELINE_VERSION;
116
+ break;
117
+ default: throw new TypeError('This command has no fixed-workflow output contract.');
118
+ }
119
+ const envelope = step.command === 'case-pack' ? value.packet : value;
120
+ const actualVersion = step.command === 'export' || step.command === 'ct-intake' ? value.schemaVersion : envelope.version;
121
+ // Only existing format validators admit historical versions. Report-only
122
+ // steps use the same output versions as the latest public checkpoint writer.
123
+ const historicalReader = retained && ['lookup', 'export', 'domain-control'].includes(step.command);
124
+ if (envelope.schema !== schema || schema !== step.produces
125
+ || (!historicalReader && actualVersion !== version) || !Number.isSafeInteger(actualVersion)) {
126
+ throw new TypeError('Unsupported workflow output schema or version.');
127
+ }
128
+ const id = `sha256:${createHash('sha256').update(canonicalArtifactJsonV2(value)).digest('hex')}`;
129
+ return Object.freeze({ id, schema, version: Number(actualVersion) });
130
+ }
131
+ export function normalizeWorkflowBindings(plan, value) {
132
+ if (!Array.isArray(value) || value.length > MAX_INVESTIGATION_RUN_SELECTIONS) {
133
+ throw new CliUsageError(`Workflow artefact bindings are limited to ${MAX_INVESTIGATION_RUN_SELECTIONS}.`);
134
+ }
135
+ const slots = new Set();
136
+ const bindings = value.map((raw) => {
137
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)
138
+ || !isDeepStrictEqual(Object.keys(raw).sort(), ['input', 'sourceStepId', 'stepId'])) {
139
+ throw new CliUsageError('Workflow artefact bindings must identify a step, input number and earlier source step.');
140
+ }
141
+ const item = raw;
142
+ const destinationIndex = plan.steps.findIndex((step) => step.id === item.stepId);
143
+ const sourceIndex = plan.steps.findIndex((step) => step.id === item.sourceStepId);
144
+ const step = plan.steps[destinationIndex];
145
+ const source = plan.steps[sourceIndex];
146
+ const input = Number(item.input);
147
+ const expected = step ? workflowInputSchemas(step.command) : [];
148
+ const slot = `${destinationIndex}:${input}`;
149
+ if (!step || !source || sourceIndex >= destinationIndex || !expected.includes(source.produces)
150
+ || typeof item.input !== 'number' || !Number.isSafeInteger(input) || input < 1
151
+ || input > step.arguments.filter((argument) => /^<[^>]+>$/u.test(argument)).length || slots.has(slot)) {
152
+ throw new CliUsageError('Workflow artefact binding does not match a compatible earlier output and file input in the fixed recipe.');
153
+ }
154
+ slots.add(slot);
155
+ return Object.freeze({ stepId: step.id, input, sourceStepId: source.id });
156
+ });
157
+ return Object.freeze(bindings.sort((left, right) => plan.steps.findIndex((step) => step.id === left.stepId) - plan.steps.findIndex((step) => step.id === right.stepId)
158
+ || left.input - right.input));
159
+ }
160
+ /** Compatible file inputs in the installed recipes, not general CLI pipelines. */
161
+ export function workflowInputSchemas(command) {
162
+ switch (command) {
163
+ case 'export':
164
+ case 'diff':
165
+ case 'timeline':
166
+ case 'compare':
167
+ case 'source-report':
168
+ case 'brief': return [CLI_LOOKUP_SCHEMA];
169
+ case 'verify-artifact': return [LOOKUP_EVIDENCE_SCHEMA];
170
+ case 'sharing-review': return [CLI_CASE_PACK_SCHEMA];
171
+ default: return [];
172
+ }
173
+ }
174
+ export function workflowArtifactReference(artifact) {
175
+ return `workflow-artifact:${artifact.id}`;
176
+ }