@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -0,0 +1,18 @@
1
+ import { SUPPORT_COMMAND_DEFINITIONS } from "./support-command-definitions.mjs";
2
+ import { ASSURANCE_COMMAND_DEFINITIONS } from "./assurance-command-definitions.mjs";
3
+ import { COLLECTION_COMMAND_DEFINITIONS } from "./collection-command-definitions.mjs";
4
+ import { NETWORK_COMMAND_DEFINITIONS } from "./network-command-definitions.mjs";
5
+ import { REVIEW_COMMAND_DEFINITIONS } from "./review-command-definitions.mjs";
6
+ import { EVIDENCE_COMMAND_DEFINITIONS } from "./evidence-command-definitions.mjs";
7
+ import { WORKFLOW_COMMAND_DEFINITIONS } from "./workflow-command-definitions.mjs";
8
+ import { HISTORY_COMMAND_DEFINITIONS } from "./history-command-definitions.mjs";
9
+ export const COMMAND_SEEDS = Object.freeze({
10
+ ...SUPPORT_COMMAND_DEFINITIONS,
11
+ ...ASSURANCE_COMMAND_DEFINITIONS,
12
+ ...COLLECTION_COMMAND_DEFINITIONS,
13
+ ...NETWORK_COMMAND_DEFINITIONS,
14
+ ...REVIEW_COMMAND_DEFINITIONS,
15
+ ...EVIDENCE_COMMAND_DEFINITIONS,
16
+ ...WORKFLOW_COMMAND_DEFINITIONS,
17
+ ...HISTORY_COMMAND_DEFINITIONS,
18
+ });
package/cli/compare.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { CliUsageError } from "./arguments.mjs";
1
+ import { CliUsageError } from "./errors.mjs";
2
2
  import { isRdapRegistryAccessProfile, isWhoisRegistryAccessProfile, } from "./registry-access.mjs";
3
3
  import { MAX_SAVED_LOOKUP_INPUT_BYTES, MAX_SAVED_LOOKUP_STRING_LENGTH, SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, readSavedLookupInputBounded, } from "./saved-lookup.mjs";
4
4
  const MAX_COMPARE_INPUT_BYTES = MAX_SAVED_LOOKUP_INPUT_BYTES;
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path';
4
4
  import { CliUsageError } from "./errors.mjs";
5
5
  import { cliInvocationOptionIndices, cliMetaActionForInvocation } from "./command-reference.mjs";
6
6
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
7
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
7
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
8
8
  export const CLI_CONFIG_SCHEMA = 'whoisleuth.cli.config';
9
9
  export const CLI_CONFIG_VERSION = 1;
10
10
  export const MAX_CLI_CONFIG_BYTES = 64 * 1024;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { domainToASCII } from 'node:url';
3
- import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
+ import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
4
4
  import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
5
5
  export const CT_EVENT_BATCH_SCHEMA = 'whoisleuth.ct-event-batch';
6
6
  export const CT_EVENT_BATCH_VERSION = 1;
@@ -118,9 +118,9 @@ export function buildCtEventFindings(inputRaw) {
118
118
  });
119
119
  const seen = new Set();
120
120
  const unique = candidates
121
- .sort((left, right) => left.domain.localeCompare(right.domain)
122
- || left.observedAt.localeCompare(right.observedAt)
123
- || left.structuredObservation.value.localeCompare(right.structuredObservation.value))
121
+ .sort((left, right) => compareCodeUnits(left.domain, right.domain)
122
+ || compareCodeUnits(left.observedAt, right.observedAt)
123
+ || compareCodeUnits(left.structuredObservation.value, right.structuredObservation.value))
124
124
  .filter((item) => {
125
125
  const key = `${item.domain}\u0000${item.observedAt}\u0000${item.structuredObservation.value}\u0000${item.structuredObservation.eventId}`;
126
126
  if (seen.has(key))
package/cli/discover.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
3
- import { CliUsageError } from "./arguments.mjs";
4
- import { normalizeDiscoverySuffix } from "../lib/registrable-domain.mjs";
3
+ import { CliUsageError } from "./errors.mjs";
4
+ import { normalizeDiscoverySuffix } from "../packages/analysis/registrable-domain.mjs";
5
5
  const DEFAULT_DISCOVERY_TLDS = Object.freeze(['com', 'net', 'org']);
6
6
  const MAX_DISCOVERY_TLD_TEXT_LENGTH = 1024;
7
7
  const MAX_DISCOVERY_TLD_TOKENS_INSPECTED = 80;
@@ -2,9 +2,9 @@ import { createHash } from 'node:crypto';
2
2
  import { availabilityState, bulkDnsSummary } from "./bulk-output.mjs";
3
3
  import { boundedCliInputError, CliUsageError } from "./errors.mjs";
4
4
  import { writePrivateFile } from "./output-file.mjs";
5
- import { isValidAsciiDomainName } from "../lib/hostname.mjs";
5
+ import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
6
6
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
7
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
7
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
8
8
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
9
9
  export const CLI_DISCOVERY_OBSERVATION_SCHEMA = 'whoisleuth.cli.discovery-observation-snapshot';
10
10
  export const CLI_DISCOVERY_OBSERVATION_VERSION = 2;
@@ -23,7 +23,7 @@ async function readAllowlist(source, dependencies, context, classify) {
23
23
  catch (error) {
24
24
  if (error instanceof CliUsageError)
25
25
  throw error;
26
- throw new CliUsageError(`Could not read discovery scan allowlist: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
26
+ throw new CliUsageError(`Could not read discovery scan allowlist: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
27
27
  }
28
28
  }
29
29
  async function runDiscoveryScanCommand(args, dependencies, context) {
@@ -52,7 +52,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
52
52
  const queries = candidates.map((candidate) => String(candidate.domain));
53
53
  if (args.plan) {
54
54
  const document = buildCollectionPreflight({
55
- command: 'discover-scan', targetCount: queries.length, targetLimit: args.deep ? 50 : 500,
55
+ command: 'discover-scan', targetCount: queries.length,
56
56
  deep: args.deep, concurrency: args.concurrency, output: args.output, checkpoint: false,
57
57
  customResolvers: Boolean(args.resolverText), allowlist: Boolean(args.allowlistSource),
58
58
  });
@@ -86,7 +86,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
86
86
  throw new CliUsageError('Bulk checkpoint collection context does not match the current DNS resolver selection.');
87
87
  }
88
88
  if (item.observedAt === null || item.observedAt === undefined) {
89
- throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.');
89
+ throw new CliUsageError('Discovery resume requires an observation time for every retained checkpoint result.', 'missing_input');
90
90
  }
91
91
  }
92
92
  const indicator = context.beginProgress(`Collecting 0 of ${queries.length} generated candidates`);
@@ -135,6 +135,10 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
135
135
  }
136
136
  catch (error) {
137
137
  checkpointFailure = error;
138
+ eventProgress.emit({ event: 'warning', state: 'checkpoint_unavailable' });
139
+ if (!eventProgress.enabled) {
140
+ context.writeStderr(`Checkpoint warning: ${boundedCliErrorMessage(error, 'Checkpoint could not be written')}. The latest progress may not be saved.\n`);
141
+ }
138
142
  }
139
143
  }
140
144
  const snapshot = args.observationSnapshot
@@ -170,12 +174,6 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
170
174
  else
171
175
  context.writeStdout(context.terminal(formatTerminalDiscoveryScan(document), args.color));
172
176
  }
173
- if (checkpointFailure) {
174
- eventProgress.emit({ event: 'warning', state: 'checkpoint_unavailable' });
175
- if (!eventProgress.enabled) {
176
- context.writeStderr(`Checkpoint warning: ${boundedCliErrorMessage(checkpointFailure, 'Checkpoint could not be written')}. Completed output is still available.\n`);
177
- }
178
- }
179
177
  const policyFindings = evaluateCliFailPolicies(document, args.failOn || []);
180
178
  if (policyFindings.length && !eventProgress.enabled)
181
179
  context.writeStderr(formatFailPolicyNotice(policyFindings));
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { Buffer } from 'node:buffer';
3
3
  import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
4
- import { MAX_DISCOVERY_SUFFIX_LENGTH } from "../lib/registrable-domain.mjs";
4
+ import { MAX_DISCOVERY_SUFFIX_LENGTH } from "../packages/analysis/registrable-domain.mjs";
5
5
  import { runBulkLookups } from "./bulk.mjs";
6
6
  import { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkCsvMetadataValues, bulkDnsSummary } from "./bulk-output.mjs";
7
7
  import { cliCsvCell } from "./csv.mjs";
@@ -1,9 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { boundedCliInputError, CliUsageError } from "./errors.mjs";
3
3
  import { writePrivateFile } from "./output-file.mjs";
4
- import { isValidAsciiDomainName } from "../lib/hostname.mjs";
4
+ import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
5
5
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
6
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
6
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
7
7
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
8
8
  export const CLI_DISCOVERY_SNAPSHOT_SCHEMA = 'whoisleuth.cli.discovery-snapshot';
9
9
  export const CLI_DISCOVERY_SNAPSHOT_VERSION = 1;
@@ -4,7 +4,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
4
4
  async function generateDiscoveryCandidates(args, dependencies, context) {
5
5
  const seed = args.seed || await context.readSingleInput();
6
6
  if (!seed)
7
- throw new CliUsageError('discover requires one brand label or domain as an argument or on stdin.');
7
+ throw new CliUsageError('discover requires one brand label or domain as an argument or on stdin.', 'missing_input');
8
8
  const loadGenerator = dependencies.loadTyposquatGenerator || (() => import("../lib/typosquat-generator.mjs"));
9
9
  const generator = await loadGenerator();
10
10
  const tlds = normalizeDiscoveryTlds(args.tldText || DEFAULT_DISCOVERY_TLDS.join(','), generator.MAX_GENERATION_TLDS);
@@ -23,7 +23,7 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
23
23
  if (args.preset === 'custom'
24
24
  && !mutationFamilies.includes('dictionary')
25
25
  && !mutationFamilies.includes('dictionary_token_replacement')) {
26
- throw new CliUsageError('--dictionary requires a dictionary mutation family.');
26
+ throw new CliUsageError('--dictionary requires a dictionary mutation family.', 'conflicting_options');
27
27
  }
28
28
  try {
29
29
  dictionaryText = dependencies.readDiscoveryDictionary
@@ -33,11 +33,11 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
33
33
  catch (error) {
34
34
  if (error instanceof CliUsageError)
35
35
  throw error;
36
- throw new CliUsageError(`Could not read discovery dictionary: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
36
+ throw new CliUsageError(`Could not read discovery dictionary: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
37
37
  }
38
38
  const normalizedDictionary = generator.normalizeCustomDictionaryTerms(dictionaryText);
39
39
  if (!normalizedDictionary.values.length) {
40
- throw new CliUsageError('The discovery dictionary did not contain any valid terms.');
40
+ throw new CliUsageError('The discovery dictionary did not contain any valid terms.', 'missing_input');
41
41
  }
42
42
  }
43
43
  const result = generator.generateTyposquatCandidateSet(seed, tlds, {
@@ -1,9 +1,9 @@
1
1
  import { classifyQuery } from "../lib/classify.mjs";
2
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
2
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
3
3
  import { buildDomainControlFlightRecorder, validateDomainControlFlightRecorderDocument, normalizeDomainControlFlightRecorderObservations, } from "../lib/domain-control-flight-recorder.mjs";
4
- import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, } from "../packages/contracts/domain-control-flight-recorder.mjs";
5
- import { DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION, PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, } from "../packages/contracts/domain-control-review.mjs";
6
- import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
4
+ import { DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, } from "../packages/contracts/domain-control-flight-recorder.mjs";
5
+ import { DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, } from "../packages/contracts/domain-control-review.mjs";
6
+ import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS, DOMAIN_CONTROL_MONITOR_COLLECTION_KEYS, DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS, DOMAIN_CONTROL_MONITOR_FAILURE_CATEGORIES, DOMAIN_CONTROL_MONITOR_FAILURE_KEYS, DOMAIN_CONTROL_MONITOR_LIMITATIONS, DOMAIN_CONTROL_MONITOR_MANIFEST_KEYS, DOMAIN_CONTROL_MONITOR_ROOT_KEYS, MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMAINS as CONTRACT_MAX_DOMAIN_CONTROL_MONITOR_DOMAINS, MAX_DOMAIN_CONTROL_MONITOR_ERROR_LENGTH, MAX_DOMAIN_CONTROL_MONITOR_FAILURES, MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES, MAX_DOMAIN_CONTROL_MONITOR_JSON_CONTAINER_ITEMS, MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH, MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS, MAX_DOMAIN_CONTROL_MONITOR_JSON_VALUES, MIN_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MIN_DOMAIN_CONTROL_MONITOR_DOMAINS, } from "../packages/contracts/domain-control-monitor.mjs";
7
7
  import { validateDomainControlReviewDocument, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
8
8
  import { requireDomainName, requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
9
9
  import { buildCliLookupDocument } from "./formatters/json.mjs";
@@ -62,8 +62,7 @@ function previousSnapshot(input, currentGeneratedAt) {
62
62
  throw new CliUsageError(`Previous monitor snapshot must use ${CLI_DOMAIN_CONTROL_MONITOR_SCHEMA} supported version ${SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS.join(' or ')}.`);
63
63
  }
64
64
  const legacy = root.version === PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION;
65
- const reviewVersion = legacy ? PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION : DOMAIN_CONTROL_REVIEW_VERSION;
66
- const flightVersion = legacy ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION : DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION;
65
+ const { review: reviewVersion, flightRecorder: flightVersion } = DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS[root.version];
67
66
  let previousGeneratedAt;
68
67
  try {
69
68
  previousGeneratedAt = requireIsoTimestamp(root.generatedAt, 'Previous monitor snapshot generatedAt');
@@ -307,6 +306,7 @@ export function formatDomainControlMonitor(document) {
307
306
  `Succeeded ${document.collection.succeeded}`,
308
307
  `Failed ${document.collection.failed}`,
309
308
  `Review ${document.review.state}`,
309
+ `Due ${document.review.counts.due ?? 0}`,
310
310
  `Unexpected ${document.flightRecorder.summary.unexpectedChanges}`,
311
311
  '',
312
312
  ].join('\n');
@@ -4,7 +4,7 @@ import { canonicalCaaRecord, canonicalDsRecord, canonicalMxRecord, canonicalDoma
4
4
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
5
  import { postureTransferRestriction } from "../packages/evidence/domain-posture-context.mjs";
6
6
  import { MAX_DOMAIN_CONTROL_INPUT_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION } from "../packages/contracts/domain-control-manifest.mjs";
7
- import { scanBoundedJson, } from "../lib/bounded-json.mjs";
7
+ import { scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
8
8
  import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS, CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_LIMITATIONS, CLI_DOMAIN_CONTROL_REVIEW_SCHEMA, CLI_DOMAIN_CONTROL_REVIEW_VERSION, PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS, DOMAIN_CONTROL_REVIEW_VERSION, MAX_DOMAIN_CONTROL_REVIEW_INPUT_BYTES, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, MAX_DOMAIN_CONTROL_REVIEW_LOOKUPS, MIN_DOMAIN_CONTROL_REVIEW_LOOKUPS, } from "../packages/contracts/domain-control-review.mjs";
9
9
  import { MAX_FLIGHT_RECORDER_VALUES, mergeConcurrentDomainControlFields, } from "../lib/domain-control-flight-recorder.mjs";
10
10
  import { DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, reviewDomainControlManifest, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
@@ -289,6 +289,7 @@ export function formatCliDomainControlReview(document) {
289
289
  `Domains ${review.domains.length}`,
290
290
  `Lookups ${document.input.lookupsReceived}`,
291
291
  `Drift ${review.counts.drift ?? 0}`,
292
+ `Due ${review.counts.due ?? 0}`,
292
293
  `Incomplete ${(review.counts.partial ?? 0) + (review.counts.unavailable ?? 0) + (review.counts.unsupported ?? 0)}`,
293
294
  '',
294
295
  ...review.domains.map((item) => `${item.domain} ${item.state}`),
@@ -0,0 +1,61 @@
1
+ import { constants } from 'node:fs';
2
+ import { open } from 'node:fs/promises';
3
+ import { DOMAIN_FEED_LIMITS, normalizeDomainFeedSelection } from "../packages/monitoring/domain-feed.mjs";
4
+ import { CliUsageError } from "./errors.mjs";
5
+ export function parseDomainFeedSelectors(values) {
6
+ const hosts = [], terms = [], negativeTerms = [];
7
+ for (const value of values) {
8
+ if (value.startsWith('host:'))
9
+ hosts.push(value.slice(5));
10
+ else if (value.startsWith('term:'))
11
+ terms.push(value.slice(5));
12
+ else if (value.startsWith('exclude:'))
13
+ negativeTerms.push(value.slice(8));
14
+ else
15
+ throw new CliUsageError('--select requires host:<plain-hostname>, term:<literal-text> or exclude:<literal-text>.');
16
+ }
17
+ const selection = normalizeDomainFeedSelection({ hosts, terms, ...(negativeTerms.length ? { negativeTerms } : {}) });
18
+ if (!selection.hosts.length && !selection.terms.length)
19
+ throw new CliUsageError('Select at least one exact host or literal term.');
20
+ return selection;
21
+ }
22
+ /** Stream one descriptor-bound local regular file; URLs, pipes and symlinks are not inputs. */
23
+ export async function* readDomainFeedFile(source, signal) {
24
+ if (!source || /^[a-z][a-z0-9+.-]*:\/\//iu.test(source))
25
+ throw new CliUsageError('Domain feed review requires an explicit local regular file, not a URL.');
26
+ if (signal?.aborted)
27
+ throw new DOMException('Feed reading was cancelled.', 'AbortError');
28
+ const handle = await open(source, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
29
+ let stream;
30
+ const abort = () => stream?.destroy(new DOMException('Feed reading was cancelled.', 'AbortError'));
31
+ try {
32
+ if (signal?.aborted)
33
+ throw new DOMException('Feed reading was cancelled.', 'AbortError');
34
+ const before = await handle.stat();
35
+ if (!before.isFile())
36
+ throw new CliUsageError('The domain feed must be a regular file.');
37
+ if (before.size > DOMAIN_FEED_LIMITS.bytes)
38
+ throw new CliUsageError('The domain feed exceeds its 256 MiB raw-byte ceiling.');
39
+ stream = handle.createReadStream({ autoClose: false, highWaterMark: 64 * 1024 });
40
+ signal?.addEventListener('abort', abort, { once: true });
41
+ if (signal?.aborted)
42
+ abort();
43
+ let bytes = 0;
44
+ for await (const chunk of stream) {
45
+ if (signal?.aborted)
46
+ throw new DOMException('Feed reading was cancelled.', 'AbortError');
47
+ if (!(chunk instanceof Uint8Array) || chunk.byteLength > DOMAIN_FEED_LIMITS.bytes - bytes)
48
+ throw new CliUsageError('The domain feed exceeds its raw-byte ceiling.');
49
+ bytes += chunk.byteLength;
50
+ yield chunk;
51
+ }
52
+ const after = await handle.stat();
53
+ if (bytes !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
54
+ throw new CliUsageError('The domain feed changed while it was being read; no review was produced.');
55
+ }
56
+ finally {
57
+ signal?.removeEventListener('abort', abort);
58
+ stream?.destroy();
59
+ await handle.close();
60
+ }
61
+ }
package/cli/errors.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
+ import { hasUnsafeCliText } from "../packages/contracts/cli-grammar.mjs";
2
3
  const MAX_CLI_ERROR_MESSAGE_LENGTH = 300;
3
4
  const MAX_CLI_DIAGNOSTIC_BYTES = 4_096;
4
- const CLI_DEFAULT_IGNORABLE_RE = /\p{Default_Ignorable_Code_Point}/u;
5
5
  const CLI_DEFAULT_IGNORABLE_GLOBAL_RE = /\p{Default_Ignorable_Code_Point}/gu;
6
6
  class CliUsageError extends Error {
7
- constructor(message) {
7
+ reason;
8
+ constructor(message, reason = 'invalid_input') {
8
9
  super(message);
9
10
  this.name = 'CliUsageError';
11
+ this.reason = reason;
10
12
  }
11
13
  }
12
14
  function errorMessage(error) {
@@ -22,9 +24,6 @@ function boundedCliErrorMessage(error, fallback = 'Unexpected command failure')
22
24
  .trim()
23
25
  .slice(0, MAX_CLI_ERROR_MESSAGE_LENGTH) || fallback;
24
26
  }
25
- function hasUnsafeCliText(value) {
26
- return /[\x00-\x1f\x7f-\x9f]/u.test(value) || CLI_DEFAULT_IGNORABLE_RE.test(value);
27
- }
28
27
  // A transient stderr preview, never a checkpoint field. Bound before collecting
29
28
  // and redact request URLs and credential-shaped fields before shortening text.
30
29
  function createCliDiagnosticOutput() {
@@ -59,6 +58,6 @@ function boundedCliInputError(error, label) {
59
58
  if (error instanceof CliUsageError)
60
59
  return error;
61
60
  const safeLabel = label.replace(/[^A-Za-z0-9 ()_-]+/gu, '').trim().slice(0, 80) || 'Input';
62
- return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`);
61
+ return new CliUsageError(`${safeLabel} could not be read as a bounded regular file.`, 'input_unavailable');
63
62
  }
64
63
  export { CliUsageError, MAX_CLI_ERROR_MESSAGE_LENGTH, boundedCliErrorMessage, boundedCliInputError, createCliDiagnosticOutput, hasUnsafeCliText, };
@@ -0,0 +1,46 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
4
+ import { terminalOptions } from "./argument-values.mjs";
5
+ function parseInspectArchiveArguments(parsed) {
6
+ const expectedContentDigest = parsed.optionValue('--expect-content-digest');
7
+ try {
8
+ parseArchiveContentDigest(expectedContentDigest);
9
+ }
10
+ catch {
11
+ throw new CliUsageError('--expect-content-digest requires sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
12
+ }
13
+ return {
14
+ action: 'inspect-archive',
15
+ source: parsed.positionalValue('source'),
16
+ passphraseSource: parsed.optionValue('--passphrase-file'),
17
+ search: parsed.optionValue('--search'),
18
+ reveal: parsed.hasOption('--reveal'),
19
+ requireMatch: parsed.hasOption('--require-match'),
20
+ expectedContentDigest,
21
+ output: parsed.hasOption('--json') ? 'json' : 'terminal',
22
+ ...terminalOptions(parsed),
23
+ };
24
+ }
25
+ function parseSignArtifactArguments(parsed) {
26
+ return {
27
+ action: 'sign-artifact',
28
+ source: parsed.positionalValue('source'),
29
+ privateKeySource: parsed.optionValue('--private-key-file'),
30
+ };
31
+ }
32
+ function parseVerifySignatureArguments(parsed) {
33
+ return {
34
+ action: 'verify-signature',
35
+ source: parsed.positionalValue('source'),
36
+ publicKeySource: parsed.optionValue('--public-key-file'),
37
+ trustStoreSource: parsed.optionValue('--trust-store-file'),
38
+ output: parsed.hasOption('--json') ? 'json' : 'terminal',
39
+ ...terminalOptions(parsed),
40
+ };
41
+ }
42
+ export const EVIDENCE_ARGUMENT_PARSERS = Object.freeze({
43
+ 'inspect-archive': parseInspectArchiveArguments,
44
+ 'sign-artifact': parseSignArtifactArguments,
45
+ 'verify-signature': parseVerifySignatureArguments,
46
+ });
@@ -0,0 +1,69 @@
1
+ import { WORKSPACE_ARCHIVE_VERSION } from "../packages/contracts/case-portability.mjs";
2
+ import { LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION, LEGACY_WORKSPACE_ARCHIVE_SCOPE, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
3
+ export const EVIDENCE_COMMAND_DEFINITIONS = Object.freeze({
4
+ "inspect-archive": commandSeed({
5
+ reference: {
6
+ description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
7
+ example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
8
+ boundary: 'Exact values require --reveal. New content comparisons use the reported sorted-json-v2:sha256 identity with --expect-content-digest; bare sha256 hashes retain their legacy locale-sensitive meaning. Retired and future archives are rejected. The archive is read locally and is never uploaded.',
9
+ },
10
+ collection: { scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
11
+ summary: 'Inspect an archive locally',
12
+ options: ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
13
+ positionals: OPTIONAL_FILE_POSITIONAL,
14
+ constraints: Object.freeze([
15
+ constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
16
+ constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
17
+ ]),
18
+ handlerOwner: 'evidence',
19
+ networkEffect: 'offline',
20
+ common: false,
21
+ schemaIdentifiers: Object.freeze(['whoisleuth.workspace-archive-inspection']),
22
+ primaryArtefacts: Object.freeze([]),
23
+ planSupport: false,
24
+ additionalOutputFormats: Object.freeze([]),
25
+ bootstrapProfile: 'allowed',
26
+ }),
27
+ "sign-artifact": commandSeed({
28
+ reference: {
29
+ description: 'Sign one reviewed response packet or supported manifest with a local private key.',
30
+ example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
31
+ boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
32
+ },
33
+ collection: { scope: 'Reads one selected artefact and one local private key without transmitting either.' },
34
+ summary: 'Sign a reviewed artefact locally',
35
+ options: ['--private-key-file'],
36
+ positionals: OPTIONAL_FILE_POSITIONAL,
37
+ constraints: Object.freeze([
38
+ constraint({ kind: 'required', options: ['--private-key-file'] }),
39
+ ]),
40
+ handlerOwner: 'evidence',
41
+ networkEffect: 'offline',
42
+ common: false,
43
+ schemaIdentifiers: Object.freeze(['whoisleuth.signed-evidence-package']),
44
+ primaryArtefacts: Object.freeze([]),
45
+ planSupport: false,
46
+ additionalOutputFormats: Object.freeze([]),
47
+ bootstrapProfile: 'allowed',
48
+ }),
49
+ "verify-signature": commandSeed({
50
+ reference: {
51
+ description: 'Verify one signed package and report embedded-artefact assurance separately. --trust-store-file also checks an explicit local fingerprint policy and emits a signer-trust report; unknown, retired, revoked or future-reviewed entries exit 4.',
52
+ example: 'whoisleuth verify-signature packet.signed.json --trust-store-file trust.json --json',
53
+ boundary: 'A valid signature proves package consistency for the embedded key, not identity, authority or evidence accuracy. With --trust-store-file, unknown, retired, revoked or future-reviewed entries exit 4 even if --public-key-file matches. Replacement fingerprints need their own trusted entry; no signing date overrides current revocation.',
54
+ },
55
+ collection: { scope: 'Reads one selected signed package, optional local public key and explicit fingerprint trust file. No automatic trust discovery or network requests.' },
56
+ summary: 'Verify a signed evidence package',
57
+ options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
58
+ positionals: OPTIONAL_FILE_POSITIONAL,
59
+ constraints: EMPTY_CONSTRAINTS,
60
+ handlerOwner: 'evidence',
61
+ networkEffect: 'offline',
62
+ common: false,
63
+ schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification', 'whoisleuth.evidence-signer-trust-store', 'whoisleuth.evidence-signer-trust-report']),
64
+ primaryArtefacts: Object.freeze([]),
65
+ planSupport: false,
66
+ additionalOutputFormats: Object.freeze([]),
67
+ bootstrapProfile: 'allowed',
68
+ }),
69
+ });
@@ -1,6 +1,7 @@
1
1
  import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, } from "./artifact-verify.mjs";
2
2
  import { formatArchiveInspection, inspectWorkspaceArchive, } from "./archive-inspect.mjs";
3
3
  import { MAX_SIGNING_KEY_FILE_BYTES, formatEvidenceSignatureVerification, signEvidencePackage, verifyEvidencePackageSignature, } from "./evidence-signing.mjs";
4
+ import { commandHasOwner } from "./command-reference.mjs";
4
5
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
5
6
  import EXIT_CODES from "./exit-codes.mjs";
6
7
  import { formatJsonDocument } from "./formatters/json.mjs";
@@ -8,9 +9,7 @@ import { readCliTextInput } from "./input.mjs";
8
9
  import { assessEvidenceSignerTrust, formatSignerTrustReport, MAX_SIGNER_TRUST_STORE_BYTES } from "./signer-trust.mjs";
9
10
  import { runDiscriminatedCommandHandler } from "./discriminated-command-handlers.mjs";
10
11
  export function isEvidenceCommand(args) {
11
- return args.action === 'inspect-archive'
12
- || args.action === 'sign-artifact'
13
- || args.action === 'verify-signature';
12
+ return commandHasOwner(args.action, 'evidence');
14
13
  }
15
14
  export function evidenceCommandFailureLabel(action) {
16
15
  if (action === 'inspect-archive')
@@ -169,7 +169,8 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
169
169
  }
170
170
  catch { /* Signature validity remains independently reportable. */ }
171
171
  let publicKeyMatched = null;
172
- if (trustedPublicKeyPem) {
172
+ const trustedKeySupplied = trustedPublicKeyPem !== undefined && trustedPublicKeyPem !== null;
173
+ if (trustedKeySupplied) {
173
174
  const trustedDer = spkiDer(importPublicKey(trustedPublicKeyPem, 'Trusted public key file'));
174
175
  publicKeyMatched = trustedDer.equals(publicDer);
175
176
  if (!publicKeyMatched)
@@ -181,7 +182,7 @@ export async function verifyEvidencePackageSignature(raw, trustedPublicKeyPem) {
181
182
  state: 'signature_valid',
182
183
  signature: Object.freeze({
183
184
  state: 'valid',
184
- signerTrust: trustedPublicKeyPem ? 'trusted_key' : 'embedded_key_only',
185
+ signerTrust: trustedKeySupplied ? 'trusted_key' : 'embedded_key_only',
185
186
  signedAt,
186
187
  keyIdSha256: signature.keyIdSha256,
187
188
  publicKeyMatched,
@@ -1,5 +1,5 @@
1
1
  import { domainToASCII } from 'node:url';
2
- import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
2
+ import { compareCodeUnits, exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
3
  import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, EXTERNAL_FINDING_CATEGORIES, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, parseExternalFindingsDocument, retainExternalFindingLimitations, } from "../packages/interchange/external-findings-import.mjs";
4
4
  export const EXTERNAL_OBSERVATION_MAPPING_SCHEMA = 'whoisleuth.external-observation-mapping';
5
5
  export const EXTERNAL_OBSERVATION_MAPPING_VERSION = 1;
@@ -112,7 +112,7 @@ export function mapExternalObservations(inputRaw) {
112
112
  : null,
113
113
  structuredObservation: null,
114
114
  });
115
- }).sort((left, right) => left.domain.localeCompare(right.domain) || left.observedAt.localeCompare(right.observedAt) || left.summary.localeCompare(right.summary));
115
+ }).sort((left, right) => compareCodeUnits(left.domain, right.domain) || compareCodeUnits(left.observedAt, right.observedAt) || compareCodeUnits(left.summary, right.summary));
116
116
  const selected = [];
117
117
  const seen = new Set();
118
118
  const domainCounts = new Map();
@@ -1,6 +1,7 @@
1
1
  import { bulkDnsSummary } from "../bulk-output.mjs";
2
2
  import { SAVED_LOOKUP_SCHEMA_VERSION, buildCliLookupDocument, } from "../saved-lookup.mjs";
3
3
  import { CLI_LOOKUP_SCHEMA } from "../../packages/contracts/cli-lookup.mjs";
4
+ import { terminalSafeJson } from "../../packages/interchange/json-output.mjs";
4
5
  const CLI_BULK_SCHEMA = 'whoisleuth.cli.bulk';
5
6
  const CLI_BULK_ITEM_SCHEMA = 'whoisleuth.cli.bulk.item';
6
7
  const CLI_CT_SEARCH_SCHEMA = 'whoisleuth.cli.ct-search';
@@ -18,19 +19,6 @@ const CLI_POSTURE_SCHEMA_VERSION = 1;
18
19
  const CLI_HTTP_SCHEMA_VERSION = 3;
19
20
  const CLI_TLS_SCHEMA_VERSION = 1;
20
21
  const CLI_COMPARE_SCHEMA_VERSION = 3;
21
- const TERMINAL_UNSAFE_JSON_RE = /[\u007f-\u009f\u2028\u2029]|\p{Default_Ignorable_Code_Point}/gu;
22
- function jsonUnicodeEscape(value) {
23
- const codePoint = value.codePointAt(0);
24
- if (codePoint <= 0xffff)
25
- return `\\u${codePoint.toString(16).padStart(4, '0')}`;
26
- const offset = codePoint - 0x10000;
27
- const high = 0xd800 + (offset >> 10);
28
- const low = 0xdc00 + (offset & 0x3ff);
29
- return `\\u${high.toString(16).padStart(4, '0')}\\u${low.toString(16).padStart(4, '0')}`;
30
- }
31
- function terminalSafeJson(value, space) {
32
- return JSON.stringify(value, null, space).replace(TERMINAL_UNSAFE_JSON_RE, jsonUnicodeEscape);
33
- }
34
22
  function formatJsonDocument(document) {
35
23
  return `${terminalSafeJson(document, 2)}\n`;
36
24
  }
@@ -168,6 +168,12 @@ function formatTerminalPosture(document) {
168
168
  `Dependencies ${dependencies.length} observed · ${dmarcAuthorizations.length} DMARC reporting destination${dmarcAuthorizations.length === 1 ? '' : 's'} checked`,
169
169
  '',
170
170
  ];
171
+ for (const value of dmarcAuthorizations.slice(0, 10)) {
172
+ const item = terminalRecord(value);
173
+ lines.push(`Reporting ${safeTerminalValue(item.destination)} · ${safeTerminalValue(item.reportType)} · ${item.state === 'self' ? 'same organisational scope' : safeTerminalValue(item.state).replaceAll('_', ' ')}`);
174
+ if (item.error)
175
+ lines.push(` Detail ${safeTerminalValue(item.error)}`);
176
+ }
171
177
  for (const value of checks) {
172
178
  const item = terminalRecord(value);
173
179
  lines.push(`[${safeTerminalValue(item.status, 'info').toUpperCase()}] ${safeTerminalValue(item.label)}: ${safeTerminalValue(item.summary)}`);
@@ -1,4 +1,5 @@
1
1
  import { registryAccessProfileLabel } from "../registry-access.mjs";
2
+ import { dnsAddressFamilyEvidence } from "../../packages/evidence/dns-query-outcome.mjs";
2
3
  import { appendTechnologyLines } from "./terminal-technology.mjs";
3
4
  import { appendDeliveryMetadataLines, appendPublicationMetadataLines } from "./terminal-metadata.mjs";
4
5
  import { securityPostureReview, summarizeSecurityPostureReview } from "../../lib/website-security-posture.mjs";
@@ -319,6 +320,11 @@ function formatTerminalLookup(document, { detail = 'standard' } = {}) {
319
320
  if (Object.keys(dns).length) {
320
321
  dnsLines.push(`Evidence ${titleCase(dns.status)}`);
321
322
  dnsLines.push(`Completeness ${dns.complete === true ? 'Complete' : 'Incomplete'}`);
323
+ for (const family of ['a', 'aaaa']) {
324
+ const evidence = dnsAddressFamilyEvidence(dns, family);
325
+ if (evidence.value)
326
+ dnsLines.push(`${family.toUpperCase().padEnd(14)} ${safeTerminalValue(evidence.value)}`);
327
+ }
322
328
  const recordTypes = [
323
329
  ['a', 'A'], ['aaaa', 'AAAA'], ['cname', 'CNAME'], ['ns', 'NS'],
324
330
  ['mx', 'MX'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['caa', 'CAA'],
@@ -0,0 +1,39 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { terminalOptions, jsonOutput, parseTwoFileComparisonArguments, uniqueSources, } from "./argument-values.mjs";
4
+ function parseDiffArguments(parsed) {
5
+ const compared = parseTwoFileComparisonArguments(parsed, 'diff');
6
+ const leftSessionId = parsed.optionValue('--left-session');
7
+ const rightSessionId = parsed.optionValue('--right-session');
8
+ for (const [option, value] of [
9
+ ['--left-session', leftSessionId],
10
+ ['--right-session', rightSessionId],
11
+ ]) {
12
+ if (value !== null && !/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
13
+ throw new CliUsageError(`${option} requires one bounded saved-session ID.`);
14
+ }
15
+ }
16
+ return { action: 'diff', ...compared, leftSessionId, rightSessionId };
17
+ }
18
+ export const HISTORY_ARGUMENT_PARSERS = Object.freeze({
19
+ diff: parseDiffArguments,
20
+ reconcile: (parsed) => ({
21
+ action: 'reconcile',
22
+ sources: uniqueSources(parsed, 'reconcile'),
23
+ output: jsonOutput(parsed),
24
+ ...terminalOptions(parsed),
25
+ }),
26
+ timeline: (parsed) => ({
27
+ action: 'timeline',
28
+ sources: uniqueSources(parsed, 'timeline'),
29
+ output: jsonOutput(parsed),
30
+ ...terminalOptions(parsed),
31
+ }),
32
+ export: (parsed) => ({
33
+ action: 'export',
34
+ source: parsed.positionalValue('source'),
35
+ format: parsed.hasOption('--markdown') ? 'markdown' : parsed.hasOption('--html') ? 'html' : 'json',
36
+ compact: parsed.hasOption('--compact'),
37
+ includeAttribution: !parsed.hasOption('--no-attribution'),
38
+ }),
39
+ });