@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
@@ -1,35 +1,23 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { canonicalArtifactJson } from "../packages/evidence/artifact-integrity.mjs";
3
3
  import { scanBoundedJson } from "../lib/bounded-json.mjs";
4
+ import { LOOKUP_SOURCE_STATES, lookupDiagnosticStates } from "../lib/lookup-diagnostics.mjs";
4
5
  import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, } from "../packages/evidence/observation.mjs";
5
6
  import { CLI_LOOKUP_SCHEMA, MAX_CLI_LOOKUP_BYTES, } from "../packages/contracts/cli-lookup.mjs";
6
7
  import { normalizeCliLookupDocument } from "./saved-lookup.mjs";
8
+ import { CLI_BULK_SCHEMA, CLI_BULK_ITEM_SCHEMA, CLI_BULK_SCHEMA_VERSION } from "./formatters/json.mjs";
9
+ import { LOOKUP_THREAT_INTELLIGENCE_PROVIDERS } from "../lib/lookup-threat-provider-inventory.mjs";
10
+ import { MAX_THREAT_INTELLIGENCE_PROVIDERS } from "../lib/lookup-response-contract.mjs";
11
+ import { THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_CONTRACT_VERSION } from "../lib/threat-intelligence-types.mjs";
7
12
  export const SOURCE_RELIABILITY_REPORT_SCHEMA = 'whoisleuth.source-reliability-report';
8
13
  export const SOURCE_RELIABILITY_REPORT_VERSION = 1;
9
14
  export const MAX_SOURCE_RELIABILITY_INPUT_BYTES = 12 * 1024 * 1024;
10
15
  export const MAX_SOURCE_RELIABILITY_DOCUMENTS = 100;
11
16
  export const MAX_SOURCE_RELIABILITY_SOURCES = 64;
12
- export const MAX_SOURCE_RELIABILITY_TRAVERSAL_NODES = 25_000;
13
17
  const SOURCE_RE = /^[a-z][a-z0-9_-]{0,39}$/u;
14
- const STATES = new Set([
15
- 'success',
16
- 'partial',
17
- 'not_found',
18
- 'skipped',
19
- 'error',
20
- 'unsupported',
21
- 'not_applicable',
22
- 'unavailable',
23
- 'rate_limited',
24
- 'complete',
25
- 'disabled',
26
- 'stale',
27
- ]);
28
- const LOOKUP_SCHEMAS = new Set([
29
- CLI_LOOKUP_SCHEMA,
30
- 'whoisleuth.cli.bulk',
31
- 'whoisleuth.cli.bulk.item',
32
- ]);
18
+ const STATES = new Set(LOOKUP_SOURCE_STATES);
19
+ const BULK_HEALTH_VERSIONS = new Set([1, 2, CLI_BULK_SCHEMA_VERSION]);
20
+ const PROVIDER_IDS = new Set(LOOKUP_THREAT_INTELLIGENCE_PROVIDERS.map((provider) => provider.id));
33
21
  const REPORT_KEYS = new Set([
34
22
  'schema',
35
23
  'version',
@@ -175,9 +163,9 @@ function parseLookupDocuments(values) {
175
163
  throw new TypeError('Source reliability input requires a supported bounded CLI Lookup document.');
176
164
  }
177
165
  }
178
- const supported = document.version === 1;
179
- if (!LOOKUP_SCHEMAS.has(schema) || !supported) {
180
- throw new TypeError('Source reliability input requires CLI Lookup version 1 or 2, version 1 Bulk or Bulk-item documents, or only source reliability reports.');
166
+ if ((schema !== CLI_BULK_SCHEMA && schema !== CLI_BULK_ITEM_SCHEMA)
167
+ || typeof document.version !== 'number' || !BULK_HEALTH_VERSIONS.has(document.version)) {
168
+ throw new TypeError('Source reliability input requires supported CLI Lookup, Bulk or Bulk-item documents, or only source reliability reports.');
181
169
  }
182
170
  return document;
183
171
  });
@@ -232,35 +220,21 @@ function collectTiming(document, store) {
232
220
  }
233
221
  }
234
222
  function collectDiagnostics(document, store) {
235
- const diagnostics = record(document.diagnostics);
236
- if (!diagnostics)
237
- return;
238
- const fixed = [
239
- ['rdap', record(diagnostics.rdap)?.status],
240
- ['registrar_rdap', record(record(diagnostics.rdap)?.registrar)?.status],
241
- ['whois', record(diagnostics.whois)?.status],
242
- ['availability', record(diagnostics.availability)?.status],
243
- ['reverse_dns', record(diagnostics.reverseDns)?.status],
244
- ['network_context', record(diagnostics.network)?.status],
245
- ['security_txt', record(diagnostics.securityTxt)?.status],
246
- ['sslbl', record(diagnostics.sslbl)?.status],
247
- ];
248
- for (const [source, value] of fixed) {
249
- const state = safeState(value);
223
+ for (const [source, state] of Object.entries(lookupDiagnosticStates(document.diagnostics))) {
250
224
  if (state)
251
225
  addState(store, source, state);
252
226
  }
253
227
  }
254
- function collectObservation(value, store) {
255
- if (value.version !== 1
228
+ function collectObservation(raw, source, store) {
229
+ const value = record(raw);
230
+ if (!value || value.version !== 1 || value.source !== source
256
231
  || typeof value.observedAt !== 'string'
257
232
  || typeof value.complete !== 'boolean'
258
233
  || typeof value.truncated !== 'boolean'
259
234
  || !Array.isArray(value.limitations))
260
235
  return;
261
- const source = safeSource(value.source);
262
236
  const state = safeState(value.status);
263
- if (!source || !state)
237
+ if (!state)
264
238
  return;
265
239
  const item = accumulator(store, source);
266
240
  item.observationSamples += 1;
@@ -271,48 +245,65 @@ function collectObservation(value, store) {
271
245
  if (value.truncated === true)
272
246
  item.truncated += 1;
273
247
  }
274
- function traverse(document, store) {
275
- let visited = 0;
276
- const stack = [{ value: document, depth: 0 }];
277
- while (stack.length) {
278
- const current = stack.pop();
279
- if (!current)
248
+ function collectLookupHealth(document, store) {
249
+ collectDiagnostics(document, store);
250
+ collectTiming(document, store);
251
+ const availability = record(document.availability);
252
+ const dns = record(availability?.dns);
253
+ const technology = record(availability?.technologyProfile);
254
+ // Only collector-owned envelopes have health semantics. Raw publications,
255
+ // parsed registration extensions and nested provider findings remain opaque.
256
+ const observations = [
257
+ ['dns', dns], ['dns', dns?.caaPolicy], ['dns_delegation', dns?.delegation],
258
+ ['http', availability?.http], ['tls', availability?.tls],
259
+ ['html', availability?.pageIdentity],
260
+ ];
261
+ const fullLookup = document.schema === CLI_LOOKUP_SCHEMA;
262
+ if (fullLookup)
263
+ observations.push(['html', availability?.credentialSurfaceProfile], ['html', availability?.structuredDataIdentity], ['derived', technology], ['derived', technology?.browserLibraryProfile], ['derived', availability?.pageRoleProfile], ['derived', availability?.clientBehaviorProfile], ['derived', availability?.securityPosture], ['reverse_dns', document.reverseDns], ['ip_rdap', document.networkContext], ['security_txt', document.securityTxt], ['sslbl', document.sslbl]);
264
+ for (const [source, observation] of observations)
265
+ collectObservation(observation, source, store);
266
+ if (!fullLookup)
267
+ return;
268
+ const providers = record(document.threatIntelligence)?.providers;
269
+ if (providers === undefined)
270
+ return;
271
+ if (!Array.isArray(providers) || providers.length > MAX_THREAT_INTELLIGENCE_PROVIDERS) {
272
+ throw new TypeError(`Source reliability input exceeds the ${MAX_THREAT_INTELLIGENCE_PROVIDERS}-provider health boundary.`);
273
+ }
274
+ const seen = new Set();
275
+ for (const rawProvider of providers) {
276
+ const provider = record(rawProvider);
277
+ const source = record(provider?.provider)?.id;
278
+ if (provider?.schema !== THREAT_INTELLIGENCE_SCHEMA || provider.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION
279
+ || typeof source !== 'string' || !PROVIDER_IDS.has(source)
280
+ || record(provider.observation)?.source !== source)
280
281
  continue;
281
- if (current.depth > 12) {
282
- throw new TypeError('Source reliability input exceeds the 12-level traversal limit.');
283
- }
284
- if (visited >= MAX_SOURCE_RELIABILITY_TRAVERSAL_NODES) {
285
- throw new TypeError(`Source reliability input exceeds the ${MAX_SOURCE_RELIABILITY_TRAVERSAL_NODES}-node traversal limit.`);
286
- }
287
- visited += 1;
288
- const object = record(current.value);
289
- if (object) {
290
- collectDiagnostics(object, store);
291
- collectTiming(object, store);
292
- collectObservation(object, store);
293
- if (object.state === 'rate_limited') {
294
- const source = safeSource(record(object.observation)?.source);
295
- if (source)
296
- addState(store, source, 'rate_limited');
297
- }
298
- const children = Object.values(object);
299
- if (children.length > 200) {
300
- throw new TypeError('Source reliability input contains an object with more than 200 fields.');
301
- }
302
- for (const child of children) {
303
- if (child && typeof child === 'object')
304
- stack.push({ value: child, depth: current.depth + 1 });
305
- }
306
- }
307
- else if (Array.isArray(current.value)) {
308
- if (current.value.length > 1_000) {
309
- throw new TypeError('Source reliability input contains an array with more than 1000 items.');
310
- }
311
- for (const child of current.value) {
312
- if (child && typeof child === 'object')
313
- stack.push({ value: child, depth: current.depth + 1 });
314
- }
282
+ if (seen.has(source))
283
+ throw new TypeError('Source reliability input contains duplicate provider health identities.');
284
+ seen.add(source);
285
+ collectObservation(provider?.observation, source, store);
286
+ if (provider?.state === 'rate_limited')
287
+ addState(store, source, 'rate_limited');
288
+ }
289
+ }
290
+ function collectDocumentHealth(document, store) {
291
+ if (document.schema === CLI_LOOKUP_SCHEMA) {
292
+ collectLookupHealth(document, store);
293
+ return;
294
+ }
295
+ const items = document.schema === CLI_BULK_SCHEMA ? document.results : [document];
296
+ if (!Array.isArray(items) || items.length > 1_000) {
297
+ throw new TypeError('Source reliability Bulk input requires at most 1000 result items.');
298
+ }
299
+ for (const rawItem of items) {
300
+ const item = record(rawItem);
301
+ if (!item || item.schema !== CLI_BULK_ITEM_SCHEMA || item.version !== document.version
302
+ || typeof item.ok !== 'boolean') {
303
+ throw new TypeError('Source reliability Bulk input contains an invalid result envelope.');
315
304
  }
305
+ if (item.ok)
306
+ collectLookupHealth(item, store);
316
307
  }
317
308
  }
318
309
  function percentile(sorted, ratio) {
@@ -663,7 +654,7 @@ export function buildSourceReliabilityReport(raw, generatedAt = new Date().toISO
663
654
  const sampleDate = lookupDocumentTimestamp(document);
664
655
  if (sampleDate)
665
656
  sampleDates.push(sampleDate);
666
- traverse(document, store);
657
+ collectDocumentHealth(document, store);
667
658
  }
668
659
  }
669
660
  sampleDates.sort();
@@ -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,