@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
@@ -1,12 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { open } from 'node:fs/promises';
2
+ import { lstat } from 'node:fs/promises';
3
3
  import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
4
4
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
5
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
6
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
7
  import { boundedCliInputError, CliUsageError } from "./errors.mjs";
6
8
  import { writePrivateFile } from "./output-file.mjs";
7
9
  export const CLI_BULK_CHECKPOINT_SCHEMA = 'whoisleuth.cli.bulk-checkpoint';
8
10
  export const CLI_BULK_CHECKPOINT_VERSION = 2;
9
- const SUPPORTED_BULK_CHECKPOINT_VERSIONS = new Set([1, CLI_BULK_CHECKPOINT_VERSION]);
10
11
  export const MAX_BULK_CHECKPOINT_BYTES = 16 * 1024 * 1024;
11
12
  const MAX_CHECKPOINT_JSON_DEPTH = 12;
12
13
  const MAX_CHECKPOINT_OBJECT_KEYS = 256;
@@ -19,8 +20,8 @@ function checkpointDigest(queries, deep) {
19
20
  .update(queries.join('\n'))
20
21
  .digest('hex');
21
22
  }
22
- function validTimestamp(value) {
23
- return typeof value === 'string' && value.length <= 64 && Number.isFinite(Date.parse(value));
23
+ function normalizedTimestamp(value) {
24
+ return normalizeExplicitIsoTimestamp(value);
24
25
  }
25
26
  function isBoundedCheckpointJson(value, depth = 0) {
26
27
  if (depth > MAX_CHECKPOINT_JSON_DEPTH)
@@ -53,7 +54,7 @@ function normalizeCompactResult(value) {
53
54
  return null;
54
55
  return result;
55
56
  }
56
- function normalizeCheckpointResult(value, queries, classifyQuery, version = CLI_BULK_CHECKPOINT_VERSION) {
57
+ function normalizeCheckpointResult(value, queries, classifyQuery) {
57
58
  const item = recordOrNull(value);
58
59
  const index = item?.index;
59
60
  if (!Number.isSafeInteger(index) || Number(index) < 0 || Number(index) >= queries.length)
@@ -61,11 +62,9 @@ function normalizeCheckpointResult(value, queries, classifyQuery, version = CLI_
61
62
  const query = queries[Number(index)];
62
63
  if (typeof item?.query !== 'string' || item.query !== query)
63
64
  return null;
64
- const observedAt = version >= 2
65
- ? item.observedAt === null
66
- ? null
67
- : validTimestamp(item.observedAt) ? new Date(item.observedAt).toISOString() : undefined
68
- : null;
65
+ const observedAt = item.observedAt === null
66
+ ? null
67
+ : normalizedTimestamp(item.observedAt) ?? undefined;
69
68
  if (observedAt === undefined)
70
69
  return null;
71
70
  if (item.ok === false) {
@@ -86,33 +85,37 @@ function parseBulkCheckpoint(text, options) {
86
85
  if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
87
86
  throw new CliUsageError(`Bulk checkpoint input is limited to ${MAX_BULK_CHECKPOINT_BYTES} bytes.`);
88
87
  }
88
+ const normalized = text.replace(/^\uFEFF/u, '');
89
89
  let parsed;
90
90
  try {
91
- parsed = JSON.parse(text.replace(/^\uFEFF/u, ''));
91
+ scanBoundedJson(normalized);
92
+ parsed = JSON.parse(normalized);
92
93
  }
93
94
  catch {
94
- throw new CliUsageError('Bulk checkpoint must be valid JSON.');
95
+ throw new CliUsageError('Bulk checkpoint must be valid bounded JSON without duplicate keys.');
95
96
  }
96
97
  const document = recordOrNull(parsed);
97
98
  const expectedDigest = checkpointDigest(options.queries, options.deep);
98
99
  const version = typeof document?.version === 'number' && Number.isSafeInteger(document.version)
99
100
  ? document.version
100
101
  : 0;
101
- if (document?.schema !== CLI_BULK_CHECKPOINT_SCHEMA || !SUPPORTED_BULK_CHECKPOINT_VERSIONS.has(version)) {
102
- throw new CliUsageError(`Bulk checkpoint must use ${CLI_BULK_CHECKPOINT_SCHEMA} version 1 or ${CLI_BULK_CHECKPOINT_VERSION}.`);
102
+ if (document?.schema !== CLI_BULK_CHECKPOINT_SCHEMA || version !== CLI_BULK_CHECKPOINT_VERSION) {
103
+ throw new CliUsageError(`Bulk checkpoint must use ${CLI_BULK_CHECKPOINT_SCHEMA} version ${CLI_BULK_CHECKPOINT_VERSION}.`);
103
104
  }
104
105
  if (document.mode !== (options.deep ? 'deep' : 'fast')
105
106
  || document.inputDigestSha256 !== expectedDigest
106
107
  || document.queryCount !== options.queries.length) {
107
108
  throw new CliUsageError('Bulk checkpoint does not match the current input or scan mode.');
108
109
  }
109
- if (!validTimestamp(document.startedAt) || !validTimestamp(document.updatedAt) || !Array.isArray(document.results)) {
110
+ const startedAt = normalizedTimestamp(document.startedAt);
111
+ const updatedAt = normalizedTimestamp(document.updatedAt);
112
+ if (!startedAt || !updatedAt || !Array.isArray(document.results)) {
110
113
  throw new CliUsageError('Bulk checkpoint metadata is invalid.');
111
114
  }
112
115
  const results = [];
113
116
  const seen = new Set();
114
117
  for (const candidate of document.results.slice(0, options.queries.length + 1)) {
115
- const result = normalizeCheckpointResult(candidate, options.queries, options.classifyQuery, version);
118
+ const result = normalizeCheckpointResult(candidate, options.queries, options.classifyQuery);
116
119
  if (!result || seen.has(result.index))
117
120
  throw new CliUsageError('Bulk checkpoint contains an invalid or duplicate result.');
118
121
  seen.add(result.index);
@@ -124,8 +127,8 @@ function parseBulkCheckpoint(text, options) {
124
127
  mode: options.deep ? 'deep' : 'fast',
125
128
  inputDigestSha256: expectedDigest,
126
129
  queryCount: options.queries.length,
127
- startedAt: new Date(document.startedAt).toISOString(),
128
- updatedAt: new Date(document.updatedAt).toISOString(),
130
+ startedAt,
131
+ updatedAt,
129
132
  results: results.sort((left, right) => left.index - right.index),
130
133
  };
131
134
  }
@@ -143,8 +146,7 @@ async function readCheckpointFile(path) {
143
146
  }
144
147
  async function checkpointFileExists(path) {
145
148
  try {
146
- const handle = await open(path, 'r');
147
- await handle.close();
149
+ await lstat(path);
148
150
  return true;
149
151
  }
150
152
  catch (error) {
@@ -165,14 +167,20 @@ async function createBulkCheckpointWriter(options) {
165
167
  const resumed = options.resume
166
168
  ? parseBulkCheckpoint(await readCheckpointFile(options.path), options)
167
169
  : null;
168
- const startedAt = resumed?.startedAt || now();
170
+ const currentTimestamp = () => {
171
+ const timestamp = normalizedTimestamp(now());
172
+ if (!timestamp)
173
+ throw new CliUsageError('Bulk checkpoint collection time is invalid.');
174
+ return timestamp;
175
+ };
176
+ const startedAt = resumed?.startedAt || currentTimestamp();
169
177
  const results = new Map((resumed?.results || []).map((item) => [item.index, item]));
170
178
  let activeWrite = null;
171
179
  let dirty = false;
172
180
  let writeFailure = null;
173
181
  let created = resumed !== null;
174
182
  function document() {
175
- const updatedAt = now();
183
+ const updatedAt = currentTimestamp();
176
184
  return {
177
185
  schema: CLI_BULK_CHECKPOINT_SCHEMA,
178
186
  version: CLI_BULK_CHECKPOINT_VERSION,
@@ -227,7 +235,11 @@ async function createBulkCheckpointWriter(options) {
227
235
  return Object.freeze({
228
236
  initialResults: Object.freeze([...results.values()].sort((left, right) => left.index - right.index)),
229
237
  record(result) {
230
- const observedAt = validTimestamp(result.observedAt) ? new Date(result.observedAt).toISOString() : now();
238
+ const observedAt = normalizedTimestamp(result.observedAt) ?? normalizedTimestamp(now());
239
+ if (!observedAt) {
240
+ writeFailure ||= new CliUsageError('Bulk checkpoint collection time is invalid.');
241
+ return;
242
+ }
231
243
  const normalized = normalizeCheckpointResult({ ...result, observedAt }, options.queries, options.classifyQuery);
232
244
  if (!normalized) {
233
245
  writeFailure ||= new CliUsageError('Bulk checkpoint result is not a bounded compact lookup result.');
@@ -1,8 +1,7 @@
1
- import { createReadStream } from 'node:fs';
2
1
  import { classifyQuery } from "../lib/classify.mjs";
3
2
  import { runUnifiedLookup } from "../lib/lookup.mjs";
4
3
  import { createBulkCheckpointWriter } from "./bulk-checkpoint.mjs";
5
- import { MAX_BULK_INPUT_BYTES, parseBulkQueries, readTextStreamBounded, runBulkLookups, } from "./bulk.mjs";
4
+ import { MAX_BULK_INPUT_BYTES, parseBulkQueries, runBulkLookups, } from "./bulk.mjs";
6
5
  import { formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems } from "./bulk-output.mjs";
7
6
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
8
7
  import EXIT_CODES from "./exit-codes.mjs";
@@ -24,9 +23,7 @@ async function runBulkCommand(args, dependencies, context) {
24
23
  try {
25
24
  input = dependencies.readBulkInput
26
25
  ? await dependencies.readBulkInput(args.source)
27
- : await readTextStreamBounded(args.source
28
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
29
- : dependencies.stdin || process.stdin, MAX_BULK_INPUT_BYTES, dependencies.signal);
26
+ : await context.readInput(args.source, MAX_BULK_INPUT_BYTES, 'Bulk input');
30
27
  }
31
28
  catch (error) {
32
29
  if (error instanceof CliUsageError)
package/cli/bulk.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { abortable } from "../lib/abort.mjs";
3
+ import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
3
4
  import { CliUsageError } from "./arguments.mjs";
5
+ import { hasUnsafeCliText } from "./errors.mjs";
4
6
  import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
5
7
  const MAX_BULK_INPUT_BYTES = 1024 * 1024;
6
8
  async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signal) {
@@ -26,7 +28,12 @@ async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signa
26
28
  finally {
27
29
  signal?.removeEventListener('abort', abort);
28
30
  }
29
- return Buffer.concat(chunks).toString('utf8');
31
+ try {
32
+ return decodeBoundedUtf8(Buffer.concat(chunks), 'Bulk input');
33
+ }
34
+ catch (cause) {
35
+ throw new CliUsageError(cause instanceof Error ? cause.message : 'Bulk input must contain valid UTF-8 text.');
36
+ }
30
37
  }
31
38
  function parseBulkQueries(text, { deep = false } = {}) {
32
39
  if (typeof text !== 'string')
@@ -43,7 +50,7 @@ function parseBulkQueries(text, { deep = false } = {}) {
43
50
  const query = line.trim();
44
51
  if (!query)
45
52
  continue;
46
- if (query.length > 1024 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(query)) {
53
+ if (query.length > 1024 || hasUnsafeCliText(query)) {
47
54
  throw new CliUsageError('Bulk input contains an overlong query or unsupported control character.');
48
55
  }
49
56
  const key = query.toLowerCase();