@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

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 (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. 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`);
@@ -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}`),
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();
@@ -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
+ });
@@ -0,0 +1,88 @@
1
+ import { LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION } from "../packages/contracts/lookup-evidence.mjs";
2
+ import { PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS, positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
3
+ export const HISTORY_COMMAND_DEFINITIONS = Object.freeze({
4
+ diff: commandSeed({
5
+ reference: {
6
+ description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
7
+ example: 'whoisleuth diff earlier.json later.json --json',
8
+ boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. Saved Lookups can describe the same or different domains; same-domain comparisons preserve observation times and collection uncertainty. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
9
+ },
10
+ collection: { scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
11
+ summary: 'Compare two compatible retained artefacts',
12
+ options: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
13
+ positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
14
+ constraints: EMPTY_CONSTRAINTS,
15
+ handlerOwner: 'history',
16
+ networkEffect: 'offline',
17
+ common: true,
18
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-diff']),
19
+ primaryArtefacts: Object.freeze(['Retained-evidence comparison']),
20
+ planSupport: false,
21
+ additionalOutputFormats: Object.freeze([]),
22
+ bootstrapProfile: 'allowed',
23
+ }),
24
+ reconcile: commandSeed({
25
+ reference: {
26
+ description: 'Reconcile bounded values across independently labelled observations of one domain.',
27
+ example: 'whoisleuth reconcile office.json mobile.json external.json --json',
28
+ boundary: 'The command is offline, accepts 2 to 5 saved observations for one domain, and never treats labels as proof of network independence or majority agreement as truth.',
29
+ },
30
+ collection: { scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
31
+ summary: 'Reconcile independently labelled observations',
32
+ options: ['--json', '--quiet', '--no-color'],
33
+ positionals: Object.freeze([positional('sources', 'file', 2, 5)]),
34
+ constraints: EMPTY_CONSTRAINTS,
35
+ handlerOwner: 'history',
36
+ networkEffect: 'offline',
37
+ common: false,
38
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-reconciliation']),
39
+ primaryArtefacts: Object.freeze([]),
40
+ planSupport: false,
41
+ additionalOutputFormats: Object.freeze([]),
42
+ bootstrapProfile: 'allowed',
43
+ }),
44
+ timeline: commandSeed({
45
+ reference: {
46
+ description: 'Build an ordered same-domain history from saved Lookup observations.',
47
+ example: 'whoisleuth timeline first.json second.json latest.json --json',
48
+ boundary: 'The command is offline, accepts 2 to 20 bounded inputs for one domain, retains no filenames or raw registry payloads, and does not treat changed collection conditions as a domain change.',
49
+ },
50
+ collection: { scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
51
+ summary: 'Build same-domain history from saved lookups',
52
+ options: ['--json', '--quiet', '--no-color'],
53
+ positionals: Object.freeze([positional('sources', 'file', 2, 20)]),
54
+ constraints: EMPTY_CONSTRAINTS,
55
+ handlerOwner: 'history',
56
+ networkEffect: 'offline',
57
+ common: false,
58
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-timeline']),
59
+ primaryArtefacts: Object.freeze(['Bounded retained-observation timeline']),
60
+ planSupport: false,
61
+ additionalOutputFormats: Object.freeze([]),
62
+ bootstrapProfile: 'allowed',
63
+ }),
64
+ export: commandSeed({
65
+ reference: {
66
+ description: 'Convert one saved lookup into a versioned evidence report.',
67
+ example: 'whoisleuth export lookup.json --markdown',
68
+ boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schemas ${PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
69
+ },
70
+ collection: { scope: 'Reads one saved Lookup and writes one bounded report.' },
71
+ summary: 'Convert a lookup to an evidence report',
72
+ options: ['--markdown', '--html', '--compact', '--no-attribution'],
73
+ positionals: OPTIONAL_FILE_POSITIONAL,
74
+ constraints: Object.freeze([
75
+ constraint({ kind: 'mutually_exclusive', options: ['--markdown', '--html'] }),
76
+ constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
77
+ constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
78
+ ]),
79
+ handlerOwner: 'history',
80
+ networkEffect: 'offline',
81
+ common: true,
82
+ schemaIdentifiers: Object.freeze(['whoisleuth.lookup-evidence']),
83
+ primaryArtefacts: Object.freeze(['Portable evidence report']),
84
+ planSupport: false,
85
+ additionalOutputFormats: Object.freeze(['JSON']),
86
+ bootstrapProfile: 'allowed',
87
+ }),
88
+ });