@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -2,12 +2,14 @@ import { Buffer } from 'node:buffer';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { CliUsageError } from "./errors.mjs";
5
+ import { cliMetaActionForInvocation } from "./command-reference.mjs";
5
6
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
7
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
6
8
  export const CLI_CONFIG_SCHEMA = 'whoisleuth.cli.config';
7
9
  export const CLI_CONFIG_VERSION = 1;
8
10
  export const MAX_CLI_CONFIG_BYTES = 64 * 1024;
9
11
  const SAFE_BOOLEAN_DEFAULTS = new Set(['--no-color', '--summary', '--verbose', '--fast']);
10
- const SAFE_VALUE_DEFAULTS = new Set(['--concurrency', '--observer', '--vantage']);
12
+ const SAFE_VALUE_DEFAULTS = new Set(['--concurrency', '--observer', '--palette', '--vantage']);
11
13
  const OPTION_GROUPS = Object.freeze({
12
14
  '--no-color': 'colour',
13
15
  '--summary': 'detail',
@@ -15,6 +17,7 @@ const OPTION_GROUPS = Object.freeze({
15
17
  '--fast': 'scan',
16
18
  '--concurrency': 'concurrency',
17
19
  '--observer': 'observer',
20
+ '--palette': 'colour',
18
21
  '--vantage': 'vantage',
19
22
  });
20
23
  function validProfileName(value) {
@@ -54,6 +57,9 @@ function validateProfileArguments(value) {
54
57
  if (argument === '--concurrency' && (!/^\d+$/u.test(next) || Number(next) < 1 || Number(next) > 8)) {
55
58
  throw new CliUsageError('CLI profile concurrency must be an integer from 1 to 8.');
56
59
  }
60
+ if (argument === '--palette' && !['auto', 'light', 'dark'].includes(next)) {
61
+ throw new CliUsageError('CLI profile palette must be auto, light, or dark.');
62
+ }
57
63
  groups.add(group);
58
64
  validated.push(argument, next);
59
65
  }
@@ -62,12 +68,14 @@ function validateProfileArguments(value) {
62
68
  function parseProfileDocument(input) {
63
69
  if (Buffer.byteLength(input, 'utf8') > MAX_CLI_CONFIG_BYTES)
64
70
  throw new CliUsageError('CLI configuration is limited to 64 KiB.');
71
+ const normalized = input.replace(/^\uFEFF/u, '');
65
72
  let parsed;
66
73
  try {
67
- parsed = JSON.parse(input.replace(/^\uFEFF/u, ''));
74
+ scanBoundedJson(normalized);
75
+ parsed = JSON.parse(normalized);
68
76
  }
69
77
  catch {
70
- throw new CliUsageError('CLI configuration must be valid JSON.');
78
+ throw new CliUsageError('CLI configuration must be valid bounded JSON without duplicate keys.');
71
79
  }
72
80
  const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
73
81
  if (root.schema !== CLI_CONFIG_SCHEMA || root.version !== CLI_CONFIG_VERSION || !root.profiles || typeof root.profiles !== 'object' || Array.isArray(root.profiles)) {
@@ -109,6 +117,8 @@ function suppliedGroups(argumentsList) {
109
117
  return groups;
110
118
  }
111
119
  export async function resolveCliProfileArguments(argv, options = {}) {
120
+ if (cliMetaActionForInvocation(argv))
121
+ return [...argv];
112
122
  // registry-scaffold owns --profile as the capability template to generate.
113
123
  // Do not reinterpret it as a global CLI-default profile.
114
124
  if (argv[0] === 'registry-scaffold') {
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { domainToASCII } from 'node:url';
2
3
  import { exactKeys, requireBoundedString, requireIsoTimestamp, requireRecord, } from "../lib/bounded-contract-normalizers.mjs";
3
4
  export const CT_EVENT_BATCH_SCHEMA = 'whoisleuth.ct-event-batch';
@@ -21,6 +22,15 @@ const EVENT_KEYS = new Set([
21
22
  'limitations',
22
23
  ]);
23
24
  const SHA256_RE = /^[a-f0-9]{64}$/u;
25
+ function eventId(logId, certificateSha256, observedAt) {
26
+ return createHash('sha256')
27
+ .update(logId)
28
+ .update('\u0000')
29
+ .update(certificateSha256)
30
+ .update('\u0000')
31
+ .update(observedAt)
32
+ .digest('hex');
33
+ }
24
34
  function optionalText(value, label, maximum) {
25
35
  if (value === undefined || value === null || value === '')
26
36
  return null;
@@ -80,6 +90,7 @@ export function buildCtEventFindings(inputRaw) {
80
90
  throw new TypeError(`events[${eventIndex}].completeness must be complete or partial.`);
81
91
  }
82
92
  const limitations = boundedLimitations(event.limitations, `events[${eventIndex}].limitations`);
93
+ const retainedEventId = eventId(logId, certificateSha256, observedAt);
83
94
  return names.map((name) => ({
84
95
  domain: name,
85
96
  category: 'certificate',
@@ -96,6 +107,11 @@ export function buildCtEventFindings(inputRaw) {
96
107
  value: certificateSha256,
97
108
  issuer,
98
109
  notAfter,
110
+ eventId: retainedEventId,
111
+ logId,
112
+ certificateSha256,
113
+ dnsNameCount: names.length,
114
+ namesComplete: completeness === 'complete',
99
115
  },
100
116
  }));
101
117
  });
@@ -105,7 +121,7 @@ export function buildCtEventFindings(inputRaw) {
105
121
  || left.observedAt.localeCompare(right.observedAt)
106
122
  || left.structuredObservation.value.localeCompare(right.structuredObservation.value))
107
123
  .filter((item) => {
108
- const key = `${item.domain}\u0000${item.observedAt}\u0000${item.structuredObservation.value}`;
124
+ const key = `${item.domain}\u0000${item.observedAt}\u0000${item.structuredObservation.value}\u0000${item.structuredObservation.eventId}`;
109
125
  if (seen.has(key))
110
126
  return false;
111
127
  seen.add(key);
@@ -127,17 +143,31 @@ export function buildCtEventFindings(inputRaw) {
127
143
  break;
128
144
  }
129
145
  const truncated = selected.length < unique.length;
146
+ const selectedNamesByEvent = new Map();
147
+ for (const finding of selected) {
148
+ const retainedEventId = finding.structuredObservation.eventId;
149
+ selectedNamesByEvent.set(retainedEventId, (selectedNamesByEvent.get(retainedEventId) ?? 0) + 1);
150
+ }
130
151
  const findings = selected.map((finding) => Object.freeze({
131
152
  ...finding,
153
+ structuredObservation: Object.freeze({
154
+ ...finding.structuredObservation,
155
+ namesComplete: finding.structuredObservation.namesComplete
156
+ && selectedNamesByEvent.get(finding.structuredObservation.eventId) === finding.structuredObservation.dnsNameCount,
157
+ }),
132
158
  limitations: Object.freeze([
133
159
  ...finding.limitations,
134
160
  'The supplied event is an observation, not proof that the certificate was served by the named domain or that the domain operator requested it.',
161
+ ...(finding.structuredObservation.namesComplete
162
+ && selectedNamesByEvent.get(finding.structuredObservation.eventId) !== finding.structuredObservation.dnsNameCount
163
+ ? ['The bounded import did not retain every DNS name from this certificate event.']
164
+ : []),
135
165
  ...(truncated ? [`The batch contained ${unique.length} unique domain observations; this import preserves the first ${MAX_CT_EXTERNAL_FINDINGS} in deterministic order.`] : []),
136
166
  ].slice(0, 8)),
137
167
  }));
138
168
  return Object.freeze({
139
169
  schema: 'whoisleuth.external-findings',
140
- schemaVersion: 3,
170
+ schemaVersion: 4,
141
171
  source,
142
172
  findings: Object.freeze(findings),
143
173
  });
package/cli/discover.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
2
3
  import { CliUsageError } from "./arguments.mjs";
3
4
  const DEFAULT_DISCOVERY_TLDS = Object.freeze(['com', 'net', 'org']);
4
5
  const MAX_DISCOVERY_TLD_TEXT_LENGTH = 1024;
@@ -16,7 +17,12 @@ async function readDiscoveryDictionaryBounded(stream, limit = MAX_DISCOVERY_DICT
16
17
  throw new CliUsageError(`Discovery dictionary input is limited to ${limit} bytes.`);
17
18
  chunks.push(buffer);
18
19
  }
19
- return Buffer.concat(chunks).toString('utf8');
20
+ try {
21
+ return decodeBoundedUtf8(Buffer.concat(chunks), 'Discovery dictionary input');
22
+ }
23
+ catch (cause) {
24
+ throw new CliUsageError(cause instanceof Error ? cause.message : 'Discovery dictionary input must contain valid UTF-8 text.');
25
+ }
20
26
  }
21
27
  function normalizeDiscoveryTlds(raw, maximum) {
22
28
  if (!Number.isSafeInteger(maximum) || maximum < 1) {
@@ -4,11 +4,12 @@ import { boundedCliInputError, CliUsageError } from "./errors.mjs";
4
4
  import { writePrivateFile } from "./output-file.mjs";
5
5
  import { isValidAsciiDomainName } from "../lib/hostname.mjs";
6
6
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
7
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
8
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
7
9
  export const CLI_DISCOVERY_OBSERVATION_SCHEMA = 'whoisleuth.cli.discovery-observation-snapshot';
8
10
  export const CLI_DISCOVERY_OBSERVATION_VERSION = 2;
9
11
  export const MAX_DISCOVERY_OBSERVATION_BYTES = 8 * 1024 * 1024;
10
12
  const MAX_DISCOVERY_OBSERVATIONS = 500;
11
- const SUPPORTED_DISCOVERY_OBSERVATION_VERSIONS = new Set([1, CLI_DISCOVERY_OBSERVATION_VERSION]);
12
13
  function record(value) {
13
14
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
14
15
  }
@@ -25,10 +26,10 @@ function normalizedTimestamp(value, label, optional = false) {
25
26
  if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value)) {
26
27
  throw new CliUsageError(`${label} is invalid.`);
27
28
  }
28
- const parsed = Date.parse(value);
29
- if (!Number.isFinite(parsed))
30
- throw new CliUsageError(`${label} is invalid.`);
31
- return new Date(parsed).toISOString();
29
+ const normalized = normalizeExplicitIsoTimestamp(value);
30
+ if (normalized)
31
+ return normalized;
32
+ throw new CliUsageError(`${label} is invalid.`);
32
33
  }
33
34
  function latestObservedAt(registrationObservedAt, dnsObservedAt) {
34
35
  if (!registrationObservedAt)
@@ -64,32 +65,25 @@ function normalizeDns(value) {
64
65
  hasDmarc: nullableBoolean(dns.hasDmarc),
65
66
  };
66
67
  }
67
- function normalizeObservation(value, version) {
68
+ function normalizeObservation(value) {
68
69
  const item = record(value);
69
70
  const dns = normalizeDns(item.dns);
70
71
  try {
71
- const validAttemptStates = version === 1 ? ['error', 'success'] : ['error', 'partial', 'success'];
72
72
  if (!dns
73
- || !validAttemptStates.includes(String(item.latestAttemptState))
73
+ || !['error', 'partial', 'success'].includes(String(item.latestAttemptState))
74
74
  || typeof item.availabilityState !== 'string' || !item.availabilityState || item.availabilityState.length > 40
75
75
  || /[\u0000-\u001f\u007f]/u.test(item.availabilityState)
76
76
  || typeof item.confidence !== 'string' || !item.confidence || item.confidence.length > 40
77
77
  || /[\u0000-\u001f\u007f]/u.test(item.confidence))
78
78
  return null;
79
- const legacyObservedAt = normalizedTimestamp(item.observedAt, 'Discovery observation time', version >= 2);
80
- const registrationObservedAt = version === 1
81
- ? item.availabilityState === 'unknown' ? null : legacyObservedAt
82
- : normalizedTimestamp(item.registrationObservedAt, 'Discovery registration observation time', true);
83
- const dnsObservedAt = version === 1
84
- ? ['partial', 'success'].includes(dns.status) ? legacyObservedAt : null
85
- : normalizedTimestamp(item.dnsObservedAt, 'Discovery DNS observation time', true);
79
+ const declaredObservedAt = normalizedTimestamp(item.observedAt, 'Discovery observation time', true);
80
+ const registrationObservedAt = normalizedTimestamp(item.registrationObservedAt, 'Discovery registration observation time', true);
81
+ const dnsObservedAt = normalizedTimestamp(item.dnsObservedAt, 'Discovery DNS observation time', true);
86
82
  const observedAt = latestObservedAt(registrationObservedAt, dnsObservedAt);
87
- if (version >= 2 && legacyObservedAt !== observedAt)
83
+ if (declaredObservedAt !== observedAt)
88
84
  return null;
89
- const latestRegistrationState = version === 1
90
- ? registrationObservedAt ? 'observed' : 'unavailable'
91
- : item.latestRegistrationState;
92
- const latestDnsState = version === 1 ? dns.status : item.latestDnsState;
85
+ const latestRegistrationState = item.latestRegistrationState;
86
+ const latestDnsState = item.latestDnsState;
93
87
  if (!['observed', 'unavailable'].includes(String(latestRegistrationState))
94
88
  || typeof latestDnsState !== 'string' || !latestDnsState || latestDnsState.length > 40
95
89
  || /[\u0000-\u001f\u007f]/u.test(latestDnsState))
@@ -118,28 +112,30 @@ function parseObservationSnapshot(text, expectedDigest) {
118
112
  if (Buffer.byteLength(text, 'utf8') > MAX_DISCOVERY_OBSERVATION_BYTES) {
119
113
  throw new CliUsageError(`Discovery observation snapshots are limited to ${MAX_DISCOVERY_OBSERVATION_BYTES} bytes.`);
120
114
  }
115
+ const normalizedInput = text.replace(/^\uFEFF/u, '');
121
116
  let parsed;
122
117
  try {
123
- parsed = JSON.parse(text.replace(/^\uFEFF/u, ''));
118
+ scanBoundedJson(normalizedInput);
119
+ parsed = JSON.parse(normalizedInput);
124
120
  }
125
121
  catch {
126
- throw new CliUsageError('Discovery observation snapshot must be valid JSON.');
122
+ throw new CliUsageError('Discovery observation snapshot must be valid bounded JSON without duplicate keys.');
127
123
  }
128
124
  const document = record(parsed);
129
125
  const version = typeof document.version === 'number' && Number.isSafeInteger(document.version)
130
126
  ? document.version
131
127
  : 0;
132
- if (document.schema !== CLI_DISCOVERY_OBSERVATION_SCHEMA || !SUPPORTED_DISCOVERY_OBSERVATION_VERSIONS.has(version)) {
133
- throw new CliUsageError(`Discovery observation snapshot must use ${CLI_DISCOVERY_OBSERVATION_SCHEMA} version 1 or ${CLI_DISCOVERY_OBSERVATION_VERSION}.`);
128
+ if (document.schema !== CLI_DISCOVERY_OBSERVATION_SCHEMA || version !== CLI_DISCOVERY_OBSERVATION_VERSION) {
129
+ throw new CliUsageError(`Discovery observation snapshot must use ${CLI_DISCOVERY_OBSERVATION_SCHEMA} version ${CLI_DISCOVERY_OBSERVATION_VERSION}.`);
134
130
  }
135
131
  if (document.configurationDigestSha256 !== expectedDigest) {
136
132
  throw new CliUsageError('Discovery observation snapshot does not match this candidate set, scan mode, or resolver selection.');
137
133
  }
138
- if (typeof document.generatedAt !== 'string' || !Number.isFinite(Date.parse(document.generatedAt))
139
- || !Array.isArray(document.observations) || document.observations.length > MAX_DISCOVERY_OBSERVATIONS) {
134
+ if (!Array.isArray(document.observations) || document.observations.length > MAX_DISCOVERY_OBSERVATIONS) {
140
135
  throw new CliUsageError('Discovery observation snapshot metadata is invalid.');
141
136
  }
142
- const observations = document.observations.map((item) => normalizeObservation(item, version));
137
+ const generatedAt = normalizedTimestamp(document.generatedAt, 'Discovery snapshot generation time') ?? '';
138
+ const observations = document.observations.map((item) => normalizeObservation(item));
143
139
  if (observations.some((item) => item === null) || document.observationCount !== observations.length) {
144
140
  throw new CliUsageError('Discovery observation snapshot contains invalid evidence.');
145
141
  }
@@ -150,7 +146,7 @@ function parseObservationSnapshot(text, expectedDigest) {
150
146
  return {
151
147
  schema: CLI_DISCOVERY_OBSERVATION_SCHEMA,
152
148
  version: CLI_DISCOVERY_OBSERVATION_VERSION,
153
- generatedAt: normalizedTimestamp(document.generatedAt, 'Discovery snapshot generation time') ?? '',
149
+ generatedAt,
154
150
  configurationDigestSha256: expectedDigest,
155
151
  observationCount: normalized.length,
156
152
  observations: normalized,
@@ -1,9 +1,8 @@
1
- import { createReadStream } from 'node:fs';
2
1
  import { classifyQuery } from "../lib/classify.mjs";
3
2
  import { normalizeSelectedDnsResolvers } from "../lib/dns-resolver-selection.mjs";
4
3
  import { runUnifiedLookup } from "../lib/lookup.mjs";
5
4
  import { createBulkCheckpointWriter } from "./bulk-checkpoint.mjs";
6
- import { buildDiscoveryScanDocument, formatDiscoveryScanCsv, formatDiscoveryScanDomains, formatDiscoveryScanJsonLines, formatTerminalDiscoveryScan, parseDiscoveryScanAllowlist, readDiscoveryScanListBounded, runDiscoveryScanChunks, } from "./discovery-scan.mjs";
5
+ import { buildDiscoveryScanDocument, formatDiscoveryScanCsv, formatDiscoveryScanDomains, formatDiscoveryScanJsonLines, formatTerminalDiscoveryScan, parseDiscoveryScanAllowlist, runDiscoveryScanChunks, } from "./discovery-scan.mjs";
7
6
  import { updateDiscoveryObservationSnapshot } from "./discovery-observation-snapshot.mjs";
8
7
  import { generateDiscoveryCandidates } from "./discovery-workflow.mjs";
9
8
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
@@ -12,13 +11,13 @@ import { formatJsonDocument } from "./formatters/json.mjs";
12
11
  import { createCliProgressEvents } from "./progress-events.mjs";
13
12
  import { buildCollectionPreflight, formatCollectionPreflight } from "./collection-preflight.mjs";
14
13
  import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
15
- async function readAllowlist(source, dependencies, classify) {
14
+ async function readAllowlist(source, dependencies, context, classify) {
16
15
  if (!source)
17
16
  return new Set();
18
17
  try {
19
18
  const text = dependencies.readDiscoveryAllowlist
20
19
  ? await dependencies.readDiscoveryAllowlist(source)
21
- : await readDiscoveryScanListBounded(createReadStream(source, { highWaterMark: 64 * 1024 }));
20
+ : await context.readInput(source, 64 * 1024, 'Discovery scan allowlist');
22
21
  return parseDiscoveryScanAllowlist(text, classify);
23
22
  }
24
23
  catch (error) {
@@ -35,6 +34,15 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
35
34
  });
36
35
  context.setEventProgress(eventProgress);
37
36
  eventProgress.emit({ event: 'started' });
37
+ let resolverServers = [];
38
+ if (args.resolverText) {
39
+ try {
40
+ resolverServers = normalizeSelectedDnsResolvers(args.resolverText);
41
+ }
42
+ catch (error) {
43
+ throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid DNS resolver selection'));
44
+ }
45
+ }
38
46
  const { metadata: generationMetadata, result } = await generateDiscoveryCandidates(args, dependencies, context);
39
47
  const candidates = result.candidates
40
48
  .filter((candidate) => typeof candidate.domain === 'string' && candidate.domain.length > 0)
@@ -52,16 +60,7 @@ async function runDiscoveryScanCommand(args, dependencies, context) {
52
60
  return EXIT_CODES.SUCCESS;
53
61
  }
54
62
  const classify = dependencies.classifyQuery || classifyQuery;
55
- let resolverServers = [];
56
- if (args.resolverText) {
57
- try {
58
- resolverServers = normalizeSelectedDnsResolvers(args.resolverText);
59
- }
60
- catch (error) {
61
- throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid DNS resolver selection'));
62
- }
63
- }
64
- const allowlist = await readAllowlist(args.allowlistSource, dependencies, classify);
63
+ const allowlist = await readAllowlist(args.allowlistSource, dependencies, context, classify);
65
64
  const checkpointWriter = dependencies.createBulkCheckpointWriter || createBulkCheckpointWriter;
66
65
  const checkpoint = args.checkpoint
67
66
  ? await checkpointWriter({
@@ -1,9 +1,11 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { Buffer } from 'node:buffer';
3
+ import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
3
4
  import { runBulkLookups } from "./bulk.mjs";
4
5
  import { REGISTERED_STATES, availabilityState, bulkDnsSummary } from "./bulk-output.mjs";
5
6
  import { cliCsvCell } from "./csv.mjs";
6
7
  import { CliUsageError } from "./errors.mjs";
8
+ import { terminalSafeJson } from "./formatters/json.mjs";
7
9
  export const CLI_DISCOVERY_SCAN_SCHEMA = 'whoisleuth.cli.discovery-scan';
8
10
  export const CLI_DISCOVERY_SCAN_ITEM_SCHEMA = 'whoisleuth.cli.discovery-scan.item';
9
11
  export const CLI_DISCOVERY_SCAN_VERSION = 1;
@@ -30,7 +32,12 @@ async function readDiscoveryScanListBounded(stream, limit = MAX_DISCOVERY_SCAN_A
30
32
  throw new CliUsageError(`Discovery scan allowlists are limited to ${limit} bytes.`);
31
33
  chunks.push(buffer);
32
34
  }
33
- return Buffer.concat(chunks).toString('utf8');
35
+ try {
36
+ return decodeBoundedUtf8(Buffer.concat(chunks), 'Discovery scan allowlist');
37
+ }
38
+ catch (cause) {
39
+ throw new CliUsageError(cause instanceof Error ? cause.message : 'Discovery scan allowlist must contain valid UTF-8 text.');
40
+ }
34
41
  }
35
42
  function parseDiscoveryScanAllowlist(text, classifyQuery) {
36
43
  if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_DISCOVERY_SCAN_ALLOWLIST_BYTES) {
@@ -232,7 +239,10 @@ function buildDiscoveryScanDocument(candidates, items, metadata, allowlist, snap
232
239
  collection: {
233
240
  chunkSize: metadata.chunkSize,
234
241
  concurrency: metadata.concurrency,
235
- resolver: metadata.resolverServers.length ? 'analyst_selected' : 'system_default',
242
+ resolver: metadata.resolverServers.length ? 'mixed' : 'system_default',
243
+ dnsResolver: metadata.resolverServers.length ? 'analyst_selected' : 'system_default',
244
+ registryEndpointResolution: 'system_default',
245
+ serviceAddressResolution: 'system_default',
236
246
  resolverServers: [...metadata.resolverServers],
237
247
  },
238
248
  filter: metadata.filter,
@@ -258,6 +268,9 @@ function buildDiscoveryScanDocument(candidates, items, metadata, allowlist, snap
258
268
  'Failed, partial, rate-limited, and unavailable sources remain inconclusive and are not interpreted as absence or safety.',
259
269
  'The allowlist changes review priority only. It does not remove collected evidence or assert that a domain is benign.',
260
270
  'Relationship groups use exact bounded DNS observations from this run and can include common shared infrastructure.',
271
+ ...(metadata.resolverServers.length
272
+ ? ['The analyst-selected resolver applies to DNS record and delegation evidence only. Registry, WHOIS, HTTP, redirect, favicon, and TLS endpoint address resolution remains system-default and is reported separately.']
273
+ : []),
261
274
  ],
262
275
  };
263
276
  }
@@ -286,7 +299,7 @@ async function runDiscoveryScanChunks(queries, options) {
286
299
  }
287
300
  function formatDiscoveryScanJsonLines(document) {
288
301
  return document.results.length
289
- ? `${document.results.map((item) => JSON.stringify({ ...item, generatedAt: document.generatedAt })).join('\n')}\n`
302
+ ? `${document.results.map((item) => terminalSafeJson({ ...item, generatedAt: document.generatedAt })).join('\n')}\n`
290
303
  : '';
291
304
  }
292
305
  function formatDiscoveryScanCsv(document) {
@@ -311,6 +324,7 @@ function formatTerminalDiscoveryScan(document) {
311
324
  `Matched ${document.summary.matched}`,
312
325
  `Source failures ${document.summary.failed}`,
313
326
  `Relationships ${document.relationships.length}`,
327
+ `Resolution ${document.collection.resolver === 'mixed' ? 'mixed (selected DNS; system service addresses)' : 'system default'}`,
314
328
  '',
315
329
  'Review queue',
316
330
  ];
@@ -3,6 +3,8 @@ import { boundedCliInputError, CliUsageError } from "./errors.mjs";
3
3
  import { writePrivateFile } from "./output-file.mjs";
4
4
  import { isValidAsciiDomainName } from "../lib/hostname.mjs";
5
5
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
6
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
7
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
6
8
  export const CLI_DISCOVERY_SNAPSHOT_SCHEMA = 'whoisleuth.cli.discovery-snapshot';
7
9
  export const CLI_DISCOVERY_SNAPSHOT_VERSION = 1;
8
10
  export const MAX_DISCOVERY_SNAPSHOT_BYTES = 4 * 1024 * 1024;
@@ -30,16 +32,24 @@ function normalizedDomains(value) {
30
32
  }
31
33
  return [...output].sort();
32
34
  }
35
+ function normalizedTimestamp(value) {
36
+ const normalized = normalizeExplicitIsoTimestamp(value);
37
+ if (!normalized)
38
+ throw new CliUsageError('Discovery snapshot collection time is invalid.');
39
+ return normalized;
40
+ }
33
41
  function parseDiscoverySnapshot(text, expectedConfigurationDigest) {
34
42
  if (Buffer.byteLength(text, 'utf8') > MAX_DISCOVERY_SNAPSHOT_BYTES) {
35
43
  throw new CliUsageError(`Discovery snapshot input is limited to ${MAX_DISCOVERY_SNAPSHOT_BYTES} bytes.`);
36
44
  }
45
+ const normalized = text.replace(/^\uFEFF/u, '');
37
46
  let parsed;
38
47
  try {
39
- parsed = JSON.parse(text.replace(/^\uFEFF/u, ''));
48
+ scanBoundedJson(normalized);
49
+ parsed = JSON.parse(normalized);
40
50
  }
41
51
  catch {
42
- throw new CliUsageError('Discovery snapshot must be valid JSON.');
52
+ throw new CliUsageError('Discovery snapshot must be valid bounded JSON without duplicate keys.');
43
53
  }
44
54
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
45
55
  throw new CliUsageError('Discovery snapshot must be one JSON object.');
@@ -51,16 +61,14 @@ function parseDiscoverySnapshot(text, expectedConfigurationDigest) {
51
61
  if (value.configurationDigestSha256 !== expectedConfigurationDigest) {
52
62
  throw new CliUsageError('Discovery snapshot configuration does not match this run. Choose another state file or restore the original discovery controls.');
53
63
  }
54
- if (typeof value.generatedAt !== 'string' || !Number.isFinite(Date.parse(value.generatedAt))) {
55
- throw new CliUsageError('Discovery snapshot collection time is invalid.');
56
- }
64
+ const generatedAt = normalizedTimestamp(value.generatedAt);
57
65
  const candidates = normalizedDomains(value.candidates);
58
66
  if (value.candidateCount !== candidates.length)
59
67
  throw new CliUsageError('Discovery snapshot candidate count is inconsistent.');
60
68
  return {
61
69
  schema: CLI_DISCOVERY_SNAPSHOT_SCHEMA,
62
70
  version: CLI_DISCOVERY_SNAPSHOT_VERSION,
63
- generatedAt: new Date(value.generatedAt).toISOString(),
71
+ generatedAt,
64
72
  configurationDigestSha256: expectedConfigurationDigest,
65
73
  candidateCount: candidates.length,
66
74
  candidates,
@@ -95,7 +103,7 @@ async function updateDiscoverySnapshot(path, candidatesValue, configuration, gen
95
103
  const document = {
96
104
  schema: CLI_DISCOVERY_SNAPSHOT_SCHEMA,
97
105
  version: CLI_DISCOVERY_SNAPSHOT_VERSION,
98
- generatedAt: new Date(generatedAt).toISOString(),
106
+ generatedAt: normalizedTimestamp(generatedAt),
99
107
  configurationDigestSha256: digest,
100
108
  candidateCount: candidates.length,
101
109
  candidates,
@@ -1,6 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { createReadStream } from 'node:fs';
3
- import { DEFAULT_DISCOVERY_TLDS, MAX_DISCOVERY_DICTIONARY_BYTES, normalizeDiscoveryTlds, readDiscoveryDictionaryBounded, } from "./discover.mjs";
2
+ import { DEFAULT_DISCOVERY_TLDS, MAX_DISCOVERY_DICTIONARY_BYTES, normalizeDiscoveryTlds, } from "./discover.mjs";
4
3
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
5
4
  async function generateDiscoveryCandidates(args, dependencies, context) {
6
5
  const seed = args.seed || await context.readSingleInput();
@@ -29,7 +28,7 @@ async function generateDiscoveryCandidates(args, dependencies, context) {
29
28
  try {
30
29
  dictionaryText = dependencies.readDiscoveryDictionary
31
30
  ? await dependencies.readDiscoveryDictionary(args.dictionarySource)
32
- : await readDiscoveryDictionaryBounded(createReadStream(args.dictionarySource, { highWaterMark: MAX_DISCOVERY_DICTIONARY_BYTES }), MAX_DISCOVERY_DICTIONARY_BYTES);
31
+ : await context.readInput(args.dictionarySource, MAX_DISCOVERY_DICTIONARY_BYTES, 'Discovery dictionary');
33
32
  }
34
33
  catch (error) {
35
34
  if (error instanceof CliUsageError)
package/cli/doctor.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { withTimeout } from "../lib/abort.mjs";
2
- const DOCTOR_SCHEMA = 'whoisleuth.cli.doctor';
3
- const DOCTOR_VERSION = 1;
2
+ export const DOCTOR_SCHEMA = 'whoisleuth.cli.doctor';
3
+ export const DOCTOR_VERSION = 1;
4
4
  const MINIMUM_NODE_MAJOR = 24;
5
5
  const NETWORK_HOST = 'whois.iana.org';
6
6
  const NETWORK_QUERY = 'example.com';